[
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: github-actions\n    directory: /\n    schedule:\n      interval: daily\n  - package-ecosystem: pip\n    directory: /\n    schedule:\n      interval: daily\n"
  },
  {
    "path": ".github/workflows/publish.yaml",
    "content": "name: publish\n\non:\n  release:\n    types:\n      - created\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n    environment: release\n    permissions:\n      id-token: write\n      contents: write\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-python@v6\n      - uses: astral-sh/setup-uv@v5\n        with:\n          enable-cache: true\n      - run: uv build\n      - uses: pypa/gh-action-pypi-publish@release/v1\n      - run: gh release upload $GITHUB_REF_NAME dist/*\n        env:\n          GH_TOKEN: ${{ github.token }}\n"
  },
  {
    "path": ".github/workflows/test.yaml",
    "content": "name: test\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - uses: astral-sh/setup-uv@v5\n        with:\n          enable-cache: true\n      - run: uvx hatch test -acp\n        if: ${{ always() }}\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-python@v6\n      - uses: astral-sh/setup-uv@v5\n        with:\n          enable-cache: true\n      - name: check formatting\n        run: uvx hatch fmt --check -f\n      - name: check linting\n        run: uvx hatch fmt --check -l --output-format=github\n      - name: check uv.lock is up-to-date\n        run: uv lock --check\n      - name: check requirements.txt is up-to-date\n        run: |\n          uv export >requirements.txt\n          git diff --exit-code requirements.txt\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# poetry\n#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.\n#   This is especially recommended for binary packages to ensure reproducibility, and is more\n#   commonly ignored for libraries.\n#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control\n#poetry.lock\n\n# pdm\n#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.\n#pdm.lock\n#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it\n#   in version control.\n#   https://pdm.fming.dev/#use-with-ide\n.pdm.toml\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# PyCharm\n#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can\n#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore\n#  and can be added to the global gitignore or merged into this file.  For a more nuclear\n#  option (not recommended) you can uncomment the following to ignore the entire idea folder.\n#.idea/\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) Ollama\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Ollama Python Library\n\nThe Ollama Python library provides the easiest way to integrate Python 3.8+ projects with [Ollama](https://github.com/ollama/ollama).\n\n## Prerequisites\n\n- [Ollama](https://ollama.com/download) should be installed and running\n- Pull a model to use with the library: `ollama pull <model>` e.g. `ollama pull gemma3`\n  - See [Ollama.com](https://ollama.com/search) for more information on the models available.\n\n## Install\n\n```sh\npip install ollama\n```\n\n## Usage\n\n```python\nfrom ollama import chat\nfrom ollama import ChatResponse\n\nresponse: ChatResponse = chat(model='gemma3', messages=[\n  {\n    'role': 'user',\n    'content': 'Why is the sky blue?',\n  },\n])\nprint(response['message']['content'])\n# or access fields directly from the response object\nprint(response.message.content)\n```\n\nSee [_types.py](ollama/_types.py) for more information on the response types.\n\n## Streaming responses\n\nResponse streaming can be enabled by setting `stream=True`.\n\n```python\nfrom ollama import chat\n\nstream = chat(\n    model='gemma3',\n    messages=[{'role': 'user', 'content': 'Why is the sky blue?'}],\n    stream=True,\n)\n\nfor chunk in stream:\n  print(chunk['message']['content'], end='', flush=True)\n```\n\n## Cloud Models\n\nRun larger models by offloading to Ollama’s cloud while keeping your local workflow.\n\n- Supported models: `deepseek-v3.1:671b-cloud`, `gpt-oss:20b-cloud`, `gpt-oss:120b-cloud`, `kimi-k2:1t-cloud`, `qwen3-coder:480b-cloud`, `kimi-k2-thinking` See [Ollama Models - Cloud](https://ollama.com/search?c=cloud) for more information\n\n### Run via local Ollama\n\n1) Sign in (one-time):\n\n```\nollama signin\n```\n\n2) Pull a cloud model:\n\n```\nollama pull gpt-oss:120b-cloud\n```\n\n3) Make a request:\n\n```python\nfrom ollama import Client\n\nclient = Client()\n\nmessages = [\n  {\n    'role': 'user',\n    'content': 'Why is the sky blue?',\n  },\n]\n\nfor part in client.chat('gpt-oss:120b-cloud', messages=messages, stream=True):\n  print(part.message.content, end='', flush=True)\n```\n\n### Cloud API (ollama.com)\n\nAccess cloud models directly by pointing the client at `https://ollama.com`.\n\n1) Create an API key from [ollama.com](https://ollama.com/settings/keys) , then set:\n\n```\nexport OLLAMA_API_KEY=your_api_key\n```\n\n2) (Optional) List models available via the API:\n\n```\ncurl https://ollama.com/api/tags\n```\n\n3) Generate a response via the cloud API:\n\n```python\nimport os\nfrom ollama import Client\n\nclient = Client(\n    host='https://ollama.com',\n    headers={'Authorization': 'Bearer ' + os.environ.get('OLLAMA_API_KEY')}\n)\n\nmessages = [\n  {\n    'role': 'user',\n    'content': 'Why is the sky blue?',\n  },\n]\n\nfor part in client.chat('gpt-oss:120b', messages=messages, stream=True):\n  print(part.message.content, end='', flush=True)\n```\n\n## Custom client\nA custom client can be created by instantiating `Client` or `AsyncClient` from `ollama`.\n\nAll extra keyword arguments are passed into the [`httpx.Client`](https://www.python-httpx.org/api/#client).\n\n```python\nfrom ollama import Client\nclient = Client(\n  host='http://localhost:11434',\n  headers={'x-some-header': 'some-value'}\n)\nresponse = client.chat(model='gemma3', messages=[\n  {\n    'role': 'user',\n    'content': 'Why is the sky blue?',\n  },\n])\n```\n\n## Async client\n\nThe `AsyncClient` class is used to make asynchronous requests. It can be configured with the same fields as the `Client` class.\n\n```python\nimport asyncio\nfrom ollama import AsyncClient\n\nasync def chat():\n  message = {'role': 'user', 'content': 'Why is the sky blue?'}\n  response = await AsyncClient().chat(model='gemma3', messages=[message])\n\nasyncio.run(chat())\n```\n\nSetting `stream=True` modifies functions to return a Python asynchronous generator:\n\n```python\nimport asyncio\nfrom ollama import AsyncClient\n\nasync def chat():\n  message = {'role': 'user', 'content': 'Why is the sky blue?'}\n  async for part in await AsyncClient().chat(model='gemma3', messages=[message], stream=True):\n    print(part['message']['content'], end='', flush=True)\n\nasyncio.run(chat())\n```\n\n## API\n\nThe Ollama Python library's API is designed around the [Ollama REST API](https://github.com/ollama/ollama/blob/main/docs/api.md)\n\n### Chat\n\n```python\nollama.chat(model='gemma3', messages=[{'role': 'user', 'content': 'Why is the sky blue?'}])\n```\n\n### Generate\n\n```python\nollama.generate(model='gemma3', prompt='Why is the sky blue?')\n```\n\n### List\n\n```python\nollama.list()\n```\n\n### Show\n\n```python\nollama.show('gemma3')\n```\n\n### Create\n\n```python\nollama.create(model='example', from_='gemma3', system=\"You are Mario from Super Mario Bros.\")\n```\n\n### Copy\n\n```python\nollama.copy('gemma3', 'user/gemma3')\n```\n\n### Delete\n\n```python\nollama.delete('gemma3')\n```\n\n### Pull\n\n```python\nollama.pull('gemma3')\n```\n\n### Push\n\n```python\nollama.push('user/gemma3')\n```\n\n### Embed\n\n```python\nollama.embed(model='gemma3', input='The sky is blue because of rayleigh scattering')\n```\n\n### Embed (batch)\n\n```python\nollama.embed(model='gemma3', input=['The sky is blue because of rayleigh scattering', 'Grass is green because of chlorophyll'])\n```\n\n### Ps\n\n```python\nollama.ps()\n```\n\n## Errors\n\nErrors are raised if requests return an error status or if an error is detected while streaming.\n\n```python\nmodel = 'does-not-yet-exist'\n\ntry:\n  ollama.chat(model)\nexcept ollama.ResponseError as e:\n  print('Error:', e.error)\n  if e.status_code == 404:\n    ollama.pull(model)\n```\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security\n\nThe Ollama maintainer team takes security seriously and will actively work to resolve security issues.\n\n## Reporting a vulnerability\n\nIf you discover a security vulnerability, please do not open a public issue. Instead, please report it by emailing hello@ollama.com. We ask that you give us sufficient time to investigate and address the vulnerability before disclosing it publicly.\n\nPlease include the following details in your report:\n- A description of the vulnerability\n- Steps to reproduce the issue\n- Your assessment of the potential impact\n- Any possible mitigations\n\n## Security best practices\n\nWhile the maintainer team does their best to secure Ollama, users are encouraged to implement their own security best practices, such as:\n\n- Regularly updating to the latest version of Ollama\n- Securing access to hosted instances of Ollama\n- Monitoring systems for unusual activity\n\n## Contact\n\nFor any other questions or concerns related to security, please contact us at hello@ollama.com\n"
  },
  {
    "path": "examples/README.md",
    "content": "# Running Examples\n\nRun the examples in this directory with:\n\n```sh\n# Run example\npython3 examples/<example>.py\n\n# or with uv\nuv run examples/<example>.py\n```\n\nSee [ollama/docs/api.md](https://github.com/ollama/ollama/blob/main/docs/api.md) for full API documentation\n\n### Chat - Chat with a model\n\n- [chat.py](chat.py)\n- [async-chat.py](async-chat.py)\n- [chat-stream.py](chat-stream.py) - Streamed outputs\n- [chat-with-history.py](chat-with-history.py) - Chat with model and maintain history of the conversation\n\n### Generate - Generate text with a model\n\n- [generate.py](generate.py)\n- [async-generate.py](async-generate.py)\n- [generate-stream.py](generate-stream.py) - Streamed outputs\n- [fill-in-middle.py](fill-in-middle.py) - Given a prefix and suffix, fill in the middle\n\n### Tools/Function Calling - Call a function with a model\n\n- [tools.py](tools.py) - Simple example of Tools/Function Calling\n- [async-tools.py](async-tools.py)\n- [multi-tool.py](multi-tool.py) - Using multiple tools, with thinking enabled\n\n#### gpt-oss\n\n- [gpt-oss-tools.py](gpt-oss-tools.py)\n- [gpt-oss-tools-stream.py](gpt-oss-tools-stream.py)\n\n### Web search\n\nAn API key from Ollama's cloud service is required. You can create one [here](https://ollama.com/settings/keys).\n\n```shell\nexport OLLAMA_API_KEY=\"your_api_key_here\"\n```\n\n- [web-search.py](web-search.py)\n- [web-search-gpt-oss.py](web-search-gpt-oss.py) - Using browser research tools with gpt-oss\n\n#### MCP server\n\nThe MCP server can be used with an MCP client like Cursor, Cline, Codex, Open WebUI, Goose, and more.\n\n```sh\nuv run examples/web-search-mcp.py\n```\n\nConfiguration to use with an MCP client:\n\n```json\n{\n  \"mcpServers\": {\n    \"web_search\": {\n      \"type\": \"stdio\",\n      \"command\": \"uv\",\n      \"args\": [\"run\", \"path/to/ollama-python/examples/web-search-mcp.py\"],\n      \"env\": { \"OLLAMA_API_KEY\": \"your_api_key_here\" }\n    }\n  }\n}\n```\n\n- [web-search-mcp.py](web-search-mcp.py)\n\n### Multimodal with Images - Chat with a multimodal (image chat) model\n\n- [multimodal-chat.py](multimodal-chat.py)\n- [multimodal-generate.py](multimodal-generate.py)\n\n### Image Generation (Experimental) - Generate images with a model\n\n> **Note:** Image generation is experimental and currently only available on macOS.\n\n- [generate-image.py](generate-image.py)\n\n### Structured Outputs - Generate structured outputs with a model\n\n- [structured-outputs.py](structured-outputs.py)\n- [async-structured-outputs.py](async-structured-outputs.py)\n- [structured-outputs-image.py](structured-outputs-image.py)\n\n### Ollama List - List all downloaded models and their properties\n\n- [list.py](list.py)\n\n### Ollama Show - Display model properties and capabilities\n\n- [show.py](show.py)\n\n### Ollama ps - Show model status with CPU/GPU usage\n\n- [ps.py](ps.py)\n\n### Ollama Pull - Pull a model from Ollama\n\nRequirement: `pip install tqdm`\n\n- [pull.py](pull.py)\n\n### Ollama Create - Create a model from a Modelfile\n\n- [create.py](create.py)\n\n### Ollama Embed - Generate embeddings with a model\n\n- [embed.py](embed.py)\n\n### Thinking - Enable thinking mode for a model\n\n- [thinking.py](thinking.py)\n\n### Thinking (generate) - Enable thinking mode for a model\n\n- [thinking-generate.py](thinking-generate.py)\n\n### Thinking (levels) - Choose the thinking level\n\n- [thinking-levels.py](thinking-levels.py)\n"
  },
  {
    "path": "examples/async-chat.py",
    "content": "import asyncio\n\nfrom ollama import AsyncClient\n\n\nasync def main():\n  messages = [\n    {\n      'role': 'user',\n      'content': 'Why is the sky blue?',\n    },\n  ]\n\n  client = AsyncClient()\n  response = await client.chat('gemma3', messages=messages)\n  print(response['message']['content'])\n\n\nif __name__ == '__main__':\n  asyncio.run(main())\n"
  },
  {
    "path": "examples/async-generate.py",
    "content": "import asyncio\n\nimport ollama\n\n\nasync def main():\n  client = ollama.AsyncClient()\n  response = await client.generate('gemma3', 'Why is the sky blue?')\n  print(response['response'])\n\n\nif __name__ == '__main__':\n  try:\n    asyncio.run(main())\n  except KeyboardInterrupt:\n    print('\\nGoodbye!')\n"
  },
  {
    "path": "examples/async-structured-outputs.py",
    "content": "import asyncio\n\nfrom pydantic import BaseModel\n\nfrom ollama import AsyncClient\n\n\n# Define the schema for the response\nclass FriendInfo(BaseModel):\n  name: str\n  age: int\n  is_available: bool\n\n\nclass FriendList(BaseModel):\n  friends: list[FriendInfo]\n\n\nasync def main():\n  client = AsyncClient()\n  response = await client.chat(\n    model='llama3.1:8b',\n    messages=[{'role': 'user', 'content': 'I have two friends. The first is Ollama 22 years old busy saving the world, and the second is Alonso 23 years old and wants to hang out. Return a list of friends in JSON format'}],\n    format=FriendList.model_json_schema(),  # Use Pydantic to generate the schema\n    options={'temperature': 0},  # Make responses more deterministic\n  )\n\n  # Use Pydantic to validate the response\n  friends_response = FriendList.model_validate_json(response.message.content)\n  print(friends_response)\n\n\nif __name__ == '__main__':\n  asyncio.run(main())\n"
  },
  {
    "path": "examples/async-tools.py",
    "content": "import asyncio\n\nimport ollama\nfrom ollama import ChatResponse\n\n\ndef add_two_numbers(a: int, b: int) -> int:\n  \"\"\"\n  Add two numbers\n\n  Args:\n    a (int): The first number\n    b (int): The second number\n\n  Returns:\n    int: The sum of the two numbers\n  \"\"\"\n  return a + b\n\n\ndef subtract_two_numbers(a: int, b: int) -> int:\n  \"\"\"\n  Subtract two numbers\n  \"\"\"\n  return a - b\n\n\n# Tools can still be manually defined and passed into chat\nsubtract_two_numbers_tool = {\n  'type': 'function',\n  'function': {\n    'name': 'subtract_two_numbers',\n    'description': 'Subtract two numbers',\n    'parameters': {\n      'type': 'object',\n      'required': ['a', 'b'],\n      'properties': {\n        'a': {'type': 'integer', 'description': 'The first number'},\n        'b': {'type': 'integer', 'description': 'The second number'},\n      },\n    },\n  },\n}\n\nmessages = [{'role': 'user', 'content': 'What is three plus one?'}]\nprint('Prompt:', messages[0]['content'])\n\navailable_functions = {\n  'add_two_numbers': add_two_numbers,\n  'subtract_two_numbers': subtract_two_numbers,\n}\n\n\nasync def main():\n  client = ollama.AsyncClient()\n\n  response: ChatResponse = await client.chat(\n    'llama3.1',\n    messages=messages,\n    tools=[add_two_numbers, subtract_two_numbers_tool],\n  )\n\n  if response.message.tool_calls:\n    # There may be multiple tool calls in the response\n    for tool in response.message.tool_calls:\n      # Ensure the function is available, and then call it\n      if function_to_call := available_functions.get(tool.function.name):\n        print('Calling function:', tool.function.name)\n        print('Arguments:', tool.function.arguments)\n        output = function_to_call(**tool.function.arguments)\n        print('Function output:', output)\n      else:\n        print('Function', tool.function.name, 'not found')\n\n  # Only needed to chat with the model using the tool call results\n  if response.message.tool_calls:\n    # Add the function response to messages for the model to use\n    messages.append(response.message)\n    messages.append({'role': 'tool', 'content': str(output), 'tool_name': tool.function.name})\n\n    # Get final response from model with function outputs\n    final_response = await client.chat('llama3.1', messages=messages)\n    print('Final response:', final_response.message.content)\n\n  else:\n    print('No tool calls returned from model')\n\n\nif __name__ == '__main__':\n  try:\n    asyncio.run(main())\n  except KeyboardInterrupt:\n    print('\\nGoodbye!')\n"
  },
  {
    "path": "examples/chat-logprobs.py",
    "content": "from typing import Iterable\n\nimport ollama\n\n\ndef print_logprobs(logprobs: Iterable[dict], label: str) -> None:\n  print(f'\\n{label}:')\n  for entry in logprobs:\n    token = entry.get('token', '')\n    logprob = entry.get('logprob')\n    print(f'  token={token!r:<12} logprob={logprob:.3f}')\n    for alt in entry.get('top_logprobs', []):\n      if alt['token'] != token:\n        print(f'    alt -> {alt[\"token\"]!r:<12} ({alt[\"logprob\"]:.3f})')\n\n\nmessages = [\n  {\n    'role': 'user',\n    'content': 'hi! be concise.',\n  },\n]\n\nresponse = ollama.chat(\n  model='gemma3',\n  messages=messages,\n  logprobs=True,\n  top_logprobs=3,\n)\nprint('Chat response:', response['message']['content'])\nprint_logprobs(response.get('logprobs', []), 'chat logprobs')\n"
  },
  {
    "path": "examples/chat-stream.py",
    "content": "from ollama import chat\n\nmessages = [\n  {\n    'role': 'user',\n    'content': 'Why is the sky blue?',\n  },\n]\n\nfor part in chat('gemma3', messages=messages, stream=True):\n  print(part['message']['content'], end='', flush=True)\n"
  },
  {
    "path": "examples/chat-with-history.py",
    "content": "from ollama import chat\n\nmessages = [\n  {\n    'role': 'user',\n    'content': 'Why is the sky blue?',\n  },\n  {\n    'role': 'assistant',\n    'content': \"The sky is blue because of the way the Earth's atmosphere scatters sunlight.\",\n  },\n  {\n    'role': 'user',\n    'content': 'What is the weather in Tokyo?',\n  },\n  {\n    'role': 'assistant',\n    'content': \"\"\"The weather in Tokyo is typically warm and humid during the summer months, with temperatures often exceeding 30°C (86°F). The city experiences a rainy season from June to September, with heavy rainfall and occasional typhoons. Winter is mild, with temperatures\n    rarely dropping below freezing. The city is known for its high-tech and vibrant culture, with many popular tourist attractions such as the Tokyo Tower, Senso-ji Temple, and the bustling Shibuya district.\"\"\",\n  },\n]\n\nwhile True:\n  user_input = input('Chat with history: ')\n  response = chat(\n    'gemma3',\n    messages=[*messages, {'role': 'user', 'content': user_input}],\n  )\n\n  # Add the response to the messages to maintain the history\n  messages += [\n    {'role': 'user', 'content': user_input},\n    {'role': 'assistant', 'content': response.message.content},\n  ]\n  print(response.message.content + '\\n')\n"
  },
  {
    "path": "examples/chat.py",
    "content": "from ollama import chat\n\nmessages = [\n  {\n    'role': 'user',\n    'content': 'Why is the sky blue?',\n  },\n]\n\nresponse = chat('gemma3', messages=messages)\nprint(response['message']['content'])\n"
  },
  {
    "path": "examples/create.py",
    "content": "from ollama import Client\n\nclient = Client()\nresponse = client.create(\n  model='my-assistant',\n  from_='gemma3',\n  system='You are mario from Super Mario Bros.',\n  stream=False,\n)\nprint(response.status)\n"
  },
  {
    "path": "examples/embed.py",
    "content": "from ollama import embed\n\nresponse = embed(model='llama3.2', input='Hello, world!')\nprint(response['embeddings'])\n"
  },
  {
    "path": "examples/fill-in-middle.py",
    "content": "from ollama import generate\n\nprompt = '''def remove_non_ascii(s: str) -> str:\n    \"\"\" '''\n\nsuffix = \"\"\"\n    return result\n\"\"\"\n\nresponse = generate(\n  model='codellama:7b-code',\n  prompt=prompt,\n  suffix=suffix,\n  options={\n    'num_predict': 128,\n    'temperature': 0,\n    'top_p': 0.9,\n    'stop': ['<EOT>'],\n  },\n)\n\nprint(response['response'])\n"
  },
  {
    "path": "examples/generate-image.py",
    "content": "# Image generation is experimental and currently only available on macOS\n\nimport base64\n\nfrom ollama import generate\n\nprompt = 'a sunset over mountains'\nprint(f'Prompt: {prompt}')\n\nfor response in generate(model='x/z-image-turbo', prompt=prompt, stream=True):\n  if response.image:\n    # Final response contains the image\n    with open('output.png', 'wb') as f:\n      f.write(base64.b64decode(response.image))\n    print('\\nImage saved to output.png')\n  elif response.total:\n    # Progress update\n    print(f'Progress: {response.completed or 0}/{response.total}', end='\\r')\n"
  },
  {
    "path": "examples/generate-logprobs.py",
    "content": "from typing import Iterable\n\nimport ollama\n\n\ndef print_logprobs(logprobs: Iterable[dict], label: str) -> None:\n  print(f'\\n{label}:')\n  for entry in logprobs:\n    token = entry.get('token', '')\n    logprob = entry.get('logprob')\n    print(f'  token={token!r:<12} logprob={logprob:.3f}')\n    for alt in entry.get('top_logprobs', []):\n      if alt['token'] != token:\n        print(f'    alt -> {alt[\"token\"]!r:<12} ({alt[\"logprob\"]:.3f})')\n\n\nresponse = ollama.generate(\n  model='gemma3',\n  prompt='hi! be concise.',\n  logprobs=True,\n  top_logprobs=3,\n)\nprint('Generate response:', response['response'])\nprint_logprobs(response.get('logprobs', []), 'generate logprobs')\n"
  },
  {
    "path": "examples/generate-stream.py",
    "content": "from ollama import generate\n\nfor part in generate('gemma3', 'Why is the sky blue?', stream=True):\n  print(part['response'], end='', flush=True)\n"
  },
  {
    "path": "examples/generate.py",
    "content": "from ollama import generate\n\nresponse = generate('gemma3', 'Why is the sky blue?')\nprint(response['response'])\n"
  },
  {
    "path": "examples/gpt-oss-tools-stream.py",
    "content": "# /// script\n# requires-python = \">=3.11\"\n# dependencies = [\n#     \"gpt-oss\",\n#     \"ollama\",\n#     \"rich\",\n# ]\n# ///\nimport random\nfrom typing import Iterator\n\nfrom rich import print\n\nfrom ollama import Client\nfrom ollama._types import ChatResponse\n\n\ndef get_weather(city: str) -> str:\n  \"\"\"\n  Get the current temperature for a city\n\n  Args:\n      city (str): The name of the city\n\n  Returns:\n      str: The current temperature\n  \"\"\"\n  temperatures = list(range(-10, 35))\n\n  temp = random.choice(temperatures)\n\n  return f'The temperature in {city} is {temp}°C'\n\n\ndef get_weather_conditions(city: str) -> str:\n  \"\"\"\n  Get the weather conditions for a city\n\n  Args:\n      city (str): The name of the city\n\n  Returns:\n      str: The current weather conditions\n  \"\"\"\n  conditions = ['sunny', 'cloudy', 'rainy', 'snowy', 'foggy']\n  return random.choice(conditions)\n\n\navailable_tools = {'get_weather': get_weather, 'get_weather_conditions': get_weather_conditions}\n\nmessages = [{'role': 'user', 'content': 'What is the weather like in London? What are the conditions in Toronto?'}]\n\nclient = Client(\n  # Ollama Turbo\n  # host=\"https://ollama.com\", headers={'Authorization': (os.getenv('OLLAMA_API_KEY'))}\n)\n\nmodel = 'gpt-oss:20b'\n# gpt-oss can call tools while \"thinking\"\n# a loop is needed to call the tools and get the results\nfinal = True\nwhile True:\n  response_stream: Iterator[ChatResponse] = client.chat(model=model, messages=messages, tools=[get_weather, get_weather_conditions], stream=True)\n  tool_calls = []\n  thinking = ''\n  content = ''\n\n  for chunk in response_stream:\n    if chunk.message.tool_calls:\n      tool_calls.extend(chunk.message.tool_calls)\n\n    if chunk.message.content:\n      if not (chunk.message.thinking or chunk.message.thinking == '') and final:\n        print('\\n\\n' + '=' * 10)\n        print('Final result: ')\n        final = False\n      print(chunk.message.content, end='', flush=True)\n\n    if chunk.message.thinking:\n      # accumulate thinking\n      thinking += chunk.message.thinking\n      print(chunk.message.thinking, end='', flush=True)\n\n  if thinking != '' or content != '' or len(tool_calls) > 0:\n    messages.append({'role': 'assistant', 'thinking': thinking, 'content': content, 'tool_calls': tool_calls})\n\n  print()\n\n  if tool_calls:\n    for tool_call in tool_calls:\n      function_to_call = available_tools.get(tool_call.function.name)\n      if function_to_call:\n        print('\\nCalling tool:', tool_call.function.name, 'with arguments: ', tool_call.function.arguments)\n        result = function_to_call(**tool_call.function.arguments)\n        print('Tool result: ', result + '\\n')\n\n        result_message = {'role': 'tool', 'content': result, 'tool_name': tool_call.function.name}\n        messages.append(result_message)\n      else:\n        print(f'Tool {tool_call.function.name} not found')\n        messages.append({'role': 'tool', 'content': f'Tool {tool_call.function.name} not found', 'tool_name': tool_call.function.name})\n\n  else:\n    # no more tool calls, we can stop the loop\n    break\n"
  },
  {
    "path": "examples/gpt-oss-tools.py",
    "content": "# /// script\n# requires-python = \">=3.11\"\n# dependencies = [\n#     \"gpt-oss\",\n#     \"ollama\",\n#     \"rich\",\n# ]\n# ///\nimport random\n\nfrom rich import print\n\nfrom ollama import Client\nfrom ollama._types import ChatResponse\n\n\ndef get_weather(city: str) -> str:\n  \"\"\"\n  Get the current temperature for a city\n\n  Args:\n      city (str): The name of the city\n\n  Returns:\n      str: The current temperature\n  \"\"\"\n  temperatures = list(range(-10, 35))\n\n  temp = random.choice(temperatures)\n\n  return f'The temperature in {city} is {temp}°C'\n\n\ndef get_weather_conditions(city: str) -> str:\n  \"\"\"\n  Get the weather conditions for a city\n\n  Args:\n      city (str): The name of the city\n\n  Returns:\n      str: The current weather conditions\n  \"\"\"\n  conditions = ['sunny', 'cloudy', 'rainy', 'snowy', 'foggy']\n  return random.choice(conditions)\n\n\navailable_tools = {'get_weather': get_weather, 'get_weather_conditions': get_weather_conditions}\n\nmessages = [{'role': 'user', 'content': 'What is the weather like in London? What are the conditions in Toronto?'}]\n\n\nclient = Client(\n  # Ollama Turbo\n  # host=\"https://ollama.com\", headers={'Authorization': (os.getenv('OLLAMA_API_KEY'))}\n)\nmodel = 'gpt-oss:20b'\n# gpt-oss can call tools while \"thinking\"\n# a loop is needed to call the tools and get the results\nwhile True:\n  response: ChatResponse = client.chat(model=model, messages=messages, tools=[get_weather, get_weather_conditions])\n\n  if response.message.content:\n    print('Content: ')\n    print(response.message.content + '\\n')\n  if response.message.thinking:\n    print('Thinking: ')\n    print(response.message.thinking + '\\n')\n\n  messages.append(response.message)\n\n  if response.message.tool_calls:\n    for tool_call in response.message.tool_calls:\n      function_to_call = available_tools.get(tool_call.function.name)\n      if function_to_call:\n        result = function_to_call(**tool_call.function.arguments)\n        print('Result from tool call name: ', tool_call.function.name, 'with arguments: ', tool_call.function.arguments, 'result: ', result + '\\n')\n        messages.append({'role': 'tool', 'content': result, 'tool_name': tool_call.function.name})\n      else:\n        print(f'Tool {tool_call.function.name} not found')\n        messages.append({'role': 'tool', 'content': f'Tool {tool_call.function.name} not found', 'tool_name': tool_call.function.name})\n  else:\n    # no more tool calls, we can stop the loop\n    break\n"
  },
  {
    "path": "examples/list.py",
    "content": "from ollama import ListResponse, list\n\nresponse: ListResponse = list()\n\nfor model in response.models:\n  print('Name:', model.model)\n  print('  Size (MB):', f'{(model.size.real / 1024 / 1024):.2f}')\n  if model.details:\n    print('  Format:', model.details.format)\n    print('  Family:', model.details.family)\n    print('  Parameter Size:', model.details.parameter_size)\n    print('  Quantization Level:', model.details.quantization_level)\n  print('\\n')\n"
  },
  {
    "path": "examples/multi-tool.py",
    "content": "import random\nfrom typing import Iterator\n\nfrom ollama import ChatResponse, Client\n\n\ndef get_temperature(city: str) -> int:\n  \"\"\"\n  Get the temperature for a city in Celsius\n\n  Args:\n    city (str): The name of the city\n\n  Returns:\n    int: The current temperature in Celsius\n  \"\"\"\n  # This is a mock implementation - would need to use a real weather API\n  import random\n\n  if city not in ['London', 'Paris', 'New York', 'Tokyo', 'Sydney']:\n    return 'Unknown city'\n\n  return str(random.randint(0, 35)) + ' degrees Celsius'\n\n\ndef get_conditions(city: str) -> str:\n  \"\"\"\n  Get the weather conditions for a city\n  \"\"\"\n  if city not in ['London', 'Paris', 'New York', 'Tokyo', 'Sydney']:\n    return 'Unknown city'\n  # This is a mock implementation - would need to use a real weather API\n  conditions = ['sunny', 'cloudy', 'rainy', 'snowy']\n  return random.choice(conditions)\n\n\navailable_functions = {\n  'get_temperature': get_temperature,\n  'get_conditions': get_conditions,\n}\n\n\ncities = ['London', 'Paris', 'New York', 'Tokyo', 'Sydney']\ncity = random.choice(cities)\ncity2 = random.choice(cities)\nmessages = [{'role': 'user', 'content': f'What is the temperature in {city}? and what are the weather conditions in {city2}?'}]\nprint('----- Prompt:', messages[0]['content'], '\\n')\n\nmodel = 'qwen3'\nclient = Client()\nresponse: Iterator[ChatResponse] = client.chat(model, stream=True, messages=messages, tools=[get_temperature, get_conditions], think=True)\n\nfor chunk in response:\n  if chunk.message.thinking:\n    print(chunk.message.thinking, end='', flush=True)\n  if chunk.message.content:\n    print(chunk.message.content, end='', flush=True)\n  if chunk.message.tool_calls:\n    for tool in chunk.message.tool_calls:\n      if function_to_call := available_functions.get(tool.function.name):\n        print('\\nCalling function:', tool.function.name, 'with arguments:', tool.function.arguments)\n        output = function_to_call(**tool.function.arguments)\n        print('> Function output:', output, '\\n')\n\n        # Add the assistant message and tool call result to the messages\n        messages.append(chunk.message)\n        messages.append({'role': 'tool', 'content': str(output), 'tool_name': tool.function.name})\n      else:\n        print('Function', tool.function.name, 'not found')\n\nprint('----- Sending result back to model \\n')\nif any(msg.get('role') == 'tool' for msg in messages):\n  res = client.chat(model, stream=True, tools=[get_temperature, get_conditions], messages=messages, think=True)\n  done_thinking = False\n  for chunk in res:\n    if chunk.message.thinking:\n      print(chunk.message.thinking, end='', flush=True)\n    if chunk.message.content:\n      if not done_thinking:\n        print('\\n----- Final result:')\n        done_thinking = True\n      print(chunk.message.content, end='', flush=True)\n    if chunk.message.tool_calls:\n      # Model should be explaining the tool calls and the results in this output\n      print('Model returned tool calls:')\n      print(chunk.message.tool_calls)\nelse:\n  print('No tool calls returned')\n"
  },
  {
    "path": "examples/multimodal-chat.py",
    "content": "from ollama import chat\n\n# from pathlib import Path\n\n# Pass in the path to the image\npath = input('Please enter the path to the image: ')\n\n# You can also pass in base64 encoded image data\n# img = base64.b64encode(Path(path).read_bytes()).decode()\n# or the raw bytes\n# img = Path(path).read_bytes()\n\nresponse = chat(\n  model='gemma3',\n  messages=[\n    {\n      'role': 'user',\n      'content': 'What is in this image? Be concise.',\n      'images': [path],\n    }\n  ],\n)\n\nprint(response.message.content)\n"
  },
  {
    "path": "examples/multimodal-generate.py",
    "content": "import random\nimport sys\n\nimport httpx\n\nfrom ollama import generate\n\nlatest = httpx.get('https://xkcd.com/info.0.json')\nlatest.raise_for_status()\n\nnum = int(sys.argv[1]) if len(sys.argv) > 1 else random.randint(1, latest.json().get('num'))\n\ncomic = httpx.get(f'https://xkcd.com/{num}/info.0.json')\ncomic.raise_for_status()\n\nprint(f'xkcd #{comic.json().get(\"num\")}: {comic.json().get(\"alt\")}')\nprint(f'link: https://xkcd.com/{num}')\nprint('---')\n\nraw = httpx.get(comic.json().get('img'))\nraw.raise_for_status()\n\nfor response in generate('llava', 'explain this comic:', images=[raw.content], stream=True):\n  print(response['response'], end='', flush=True)\n\nprint()\n"
  },
  {
    "path": "examples/ps.py",
    "content": "from ollama import ProcessResponse, chat, ps, pull\n\n# Ensure at least one model is loaded\nresponse = pull('gemma3', stream=True)\nprogress_states = set()\nfor progress in response:\n  if progress.get('status') in progress_states:\n    continue\n  progress_states.add(progress.get('status'))\n  print(progress.get('status'))\n\nprint('\\n')\n\nprint('Waiting for model to load... \\n')\nchat(model='gemma3', messages=[{'role': 'user', 'content': 'Why is the sky blue?'}])\n\n\nresponse: ProcessResponse = ps()\nfor model in response.models:\n  print('Model: ', model.model)\n  print('  Digest: ', model.digest)\n  print('  Expires at: ', model.expires_at)\n  print('  Size: ', model.size)\n  print('  Size vram: ', model.size_vram)\n  print('  Details: ', model.details)\n  print('  Context length: ', model.context_length)\n  print('\\n')\n"
  },
  {
    "path": "examples/pull.py",
    "content": "from tqdm import tqdm\n\nfrom ollama import pull\n\ncurrent_digest, bars = '', {}\nfor progress in pull('gemma3', stream=True):\n  digest = progress.get('digest', '')\n  if digest != current_digest and current_digest in bars:\n    bars[current_digest].close()\n\n  if not digest:\n    print(progress.get('status'))\n    continue\n\n  if digest not in bars and (total := progress.get('total')):\n    bars[digest] = tqdm(total=total, desc=f'pulling {digest[7:19]}', unit='B', unit_scale=True)\n\n  if completed := progress.get('completed'):\n    bars[digest].update(completed - bars[digest].n)\n\n  current_digest = digest\n"
  },
  {
    "path": "examples/show.py",
    "content": "from ollama import ShowResponse, show\n\nresponse: ShowResponse = show('gemma3')\nprint('Model Information:')\nprint(f'Modified at:   {response.modified_at}')\nprint(f'Template:      {response.template}')\nprint(f'Modelfile:     {response.modelfile}')\nprint(f'License:       {response.license}')\nprint(f'Details:       {response.details}')\nprint(f'Model Info:    {response.modelinfo}')\nprint(f'Parameters:    {response.parameters}')\nprint(f'Capabilities:  {response.capabilities}')\n"
  },
  {
    "path": "examples/structured-outputs-image.py",
    "content": "from pathlib import Path\nfrom typing import Literal\n\nfrom pydantic import BaseModel\n\nfrom ollama import chat\n\n\n# Define the schema for image objects\nclass Object(BaseModel):\n  name: str\n  confidence: float\n  attributes: str\n\n\nclass ImageDescription(BaseModel):\n  summary: str\n  objects: list[Object]\n  scene: str\n  colors: list[str]\n  time_of_day: Literal['Morning', 'Afternoon', 'Evening', 'Night']\n  setting: Literal['Indoor', 'Outdoor', 'Unknown']\n  text_content: str | None = None\n\n\n# Get path from user input\npath = input('Enter the path to your image: ')\npath = Path(path)\n\n# Verify the file exists\nif not path.exists():\n  raise FileNotFoundError(f'Image not found at: {path}')\n\n# Set up chat as usual\nresponse = chat(\n  model='gemma3',\n  format=ImageDescription.model_json_schema(),  # Pass in the schema for the response\n  messages=[\n    {\n      'role': 'user',\n      'content': 'Analyze this image and return a detailed JSON description including objects, scene, colors and any text detected. If you cannot determine certain details, leave those fields empty.',\n      'images': [path],\n    },\n  ],\n  options={'temperature': 0},  # Set temperature to 0 for more deterministic output\n)\n\n\n# Convert received content to the schema\nimage_analysis = ImageDescription.model_validate_json(response.message.content)\nprint(image_analysis)\n"
  },
  {
    "path": "examples/structured-outputs.py",
    "content": "from pydantic import BaseModel\n\nfrom ollama import chat\n\n\n# Define the schema for the response\nclass FriendInfo(BaseModel):\n  name: str\n  age: int\n  is_available: bool\n\n\nclass FriendList(BaseModel):\n  friends: list[FriendInfo]\n\n\n# schema = {'type': 'object', 'properties': {'friends': {'type': 'array', 'items': {'type': 'object', 'properties': {'name': {'type': 'string'}, 'age': {'type': 'integer'}, 'is_available': {'type': 'boolean'}}, 'required': ['name', 'age', 'is_available']}}}, 'required': ['friends']}\nresponse = chat(\n  model='llama3.1:8b',\n  messages=[{'role': 'user', 'content': 'I have two friends. The first is Ollama 22 years old busy saving the world, and the second is Alonso 23 years old and wants to hang out. Return a list of friends in JSON format'}],\n  format=FriendList.model_json_schema(),  # Use Pydantic to generate the schema or format=schema\n  options={'temperature': 0},  # Make responses more deterministic\n)\n\n# Use Pydantic to validate the response\nfriends_response = FriendList.model_validate_json(response.message.content)\nprint(friends_response)\n"
  },
  {
    "path": "examples/thinking-generate.py",
    "content": "from ollama import generate\n\nresponse = generate('deepseek-r1', 'why is the sky blue', think=True)\n\nprint('Thinking:\\n========\\n\\n' + response.thinking)\nprint('\\nResponse:\\n========\\n\\n' + response.response)\n"
  },
  {
    "path": "examples/thinking-levels.py",
    "content": "from ollama import chat\n\n\ndef heading(text):\n  print(text)\n  print('=' * len(text))\n\n\nmessages = [\n  {'role': 'user', 'content': 'What is 10 + 23?'},\n]\n\n# gpt-oss supports 'low', 'medium', 'high'\nlevels = ['low', 'medium', 'high']\nfor i, level in enumerate(levels):\n  response = chat('gpt-oss:20b', messages=messages, think=level)\n\n  heading(f'Thinking ({level})')\n  print(response.message.thinking)\n  print('\\n')\n  heading('Response')\n  print(response.message.content)\n  print('\\n')\n  if i < len(levels) - 1:\n    print('-' * 20)\n    print('\\n')\n"
  },
  {
    "path": "examples/thinking.py",
    "content": "from ollama import chat\n\nmessages = [\n  {\n    'role': 'user',\n    'content': 'What is 10 + 23?',\n  },\n]\n\nresponse = chat('deepseek-r1', messages=messages, think=True)\n\nprint('Thinking:\\n========\\n\\n' + response.message.thinking)\nprint('\\nResponse:\\n========\\n\\n' + response.message.content)\n"
  },
  {
    "path": "examples/tools.py",
    "content": "from ollama import ChatResponse, chat\n\n\ndef add_two_numbers(a: int, b: int) -> int:\n  \"\"\"\n  Add two numbers\n\n  Args:\n    a (int): The first number\n    b (int): The second number\n\n  Returns:\n    int: The sum of the two numbers\n  \"\"\"\n\n  # The cast is necessary as returned tool call arguments don't always conform exactly to schema\n  # E.g. this would prevent \"what is 30 + 12\" to produce '3012' instead of 42\n  return int(a) + int(b)\n\n\ndef subtract_two_numbers(a: int, b: int) -> int:\n  \"\"\"\n  Subtract two numbers\n  \"\"\"\n\n  # The cast is necessary as returned tool call arguments don't always conform exactly to schema\n  return int(a) - int(b)\n\n\n# Tools can still be manually defined and passed into chat\nsubtract_two_numbers_tool = {\n  'type': 'function',\n  'function': {\n    'name': 'subtract_two_numbers',\n    'description': 'Subtract two numbers',\n    'parameters': {\n      'type': 'object',\n      'required': ['a', 'b'],\n      'properties': {\n        'a': {'type': 'integer', 'description': 'The first number'},\n        'b': {'type': 'integer', 'description': 'The second number'},\n      },\n    },\n  },\n}\n\nmessages = [{'role': 'user', 'content': 'What is three plus one?'}]\nprint('Prompt:', messages[0]['content'])\n\navailable_functions = {\n  'add_two_numbers': add_two_numbers,\n  'subtract_two_numbers': subtract_two_numbers,\n}\n\nresponse: ChatResponse = chat(\n  'llama3.1',\n  messages=messages,\n  tools=[add_two_numbers, subtract_two_numbers_tool],\n)\n\nif response.message.tool_calls:\n  # There may be multiple tool calls in the response\n  for tool in response.message.tool_calls:\n    # Ensure the function is available, and then call it\n    if function_to_call := available_functions.get(tool.function.name):\n      print('Calling function:', tool.function.name)\n      print('Arguments:', tool.function.arguments)\n      output = function_to_call(**tool.function.arguments)\n      print('Function output:', output)\n    else:\n      print('Function', tool.function.name, 'not found')\n\n# Only needed to chat with the model using the tool call results\nif response.message.tool_calls:\n  # Add the function response to messages for the model to use\n  messages.append(response.message)\n  messages.append({'role': 'tool', 'content': str(output), 'tool_name': tool.function.name})\n\n  # Get final response from model with function outputs\n  final_response = chat('llama3.1', messages=messages)\n  print('Final response:', final_response.message.content)\n\nelse:\n  print('No tool calls returned from model')\n"
  },
  {
    "path": "examples/web-search-gpt-oss.py",
    "content": "# /// script\n# requires-python = \">=3.11\"\n# dependencies = [\n#     \"ollama\",\n# ]\n# ///\nfrom typing import Any, Dict, List\n\nfrom web_search_gpt_oss_helper import Browser\n\nfrom ollama import Client\n\n\ndef main() -> None:\n  client = Client()\n  browser = Browser(initial_state=None, client=client)\n\n  def browser_search(query: str, topn: int = 10) -> str:\n    return browser.search(query=query, topn=topn)['pageText']\n\n  def browser_open(id: int | str | None = None, cursor: int = -1, loc: int = -1, num_lines: int = -1) -> str:\n    return browser.open(id=id, cursor=cursor, loc=loc, num_lines=num_lines)['pageText']\n\n  def browser_find(pattern: str, cursor: int = -1, **_: Any) -> str:\n    return browser.find(pattern=pattern, cursor=cursor)['pageText']\n\n  browser_search_schema = {\n    'type': 'function',\n    'function': {\n      'name': 'browser.search',\n    },\n  }\n\n  browser_open_schema = {\n    'type': 'function',\n    'function': {\n      'name': 'browser.open',\n    },\n  }\n\n  browser_find_schema = {\n    'type': 'function',\n    'function': {\n      'name': 'browser.find',\n    },\n  }\n\n  available_tools = {\n    'browser.search': browser_search,\n    'browser.open': browser_open,\n    'browser.find': browser_find,\n  }\n\n  query = \"what is ollama's new engine\"\n  print('Prompt:', query, '\\n')\n\n  messages: List[Dict[str, Any]] = [{'role': 'user', 'content': query}]\n\n  while True:\n    resp = client.chat(\n      model='gpt-oss:120b-cloud',\n      messages=messages,\n      tools=[browser_search_schema, browser_open_schema, browser_find_schema],\n      think=True,\n    )\n\n    if resp.message.thinking:\n      print('Thinking:\\n========\\n')\n      print(resp.message.thinking + '\\n')\n\n    if resp.message.content:\n      print('Response:\\n========\\n')\n      print(resp.message.content + '\\n')\n\n    messages.append(resp.message)\n\n    if not resp.message.tool_calls:\n      break\n\n    for tc in resp.message.tool_calls:\n      tool_name = tc.function.name\n      args = tc.function.arguments or {}\n      print(f'Tool name: {tool_name}, args: {args}')\n      fn = available_tools.get(tool_name)\n      if not fn:\n        messages.append({'role': 'tool', 'content': f'Tool {tool_name} not found', 'tool_name': tool_name})\n        continue\n\n      try:\n        result_text = fn(**args)\n        print('Result: ', result_text[:200] + '...')\n      except Exception as e:\n        result_text = f'Error from {tool_name}: {e}'\n\n      messages.append({'role': 'tool', 'content': result_text, 'tool_name': tool_name})\n\n\nif __name__ == '__main__':\n  main()\n"
  },
  {
    "path": "examples/web-search-mcp.py",
    "content": "# /// script\n# requires-python = \">=3.11\"\n# dependencies = [\n#   \"mcp\",\n#   \"rich\",\n#   \"ollama\",\n# ]\n# ///\n\"\"\"\nMCP stdio server exposing Ollama web_search and web_fetch as tools.\n\nEnvironment:\n- OLLAMA_API_KEY (required): if set, will be used as Authorization header.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom typing import Any, Dict\n\nfrom ollama import Client\n\ntry:\n  # Preferred high-level API (if available)\n  from mcp.server.fastmcp import FastMCP  # type: ignore\n\n  _FASTMCP_AVAILABLE = True\nexcept Exception:\n  _FASTMCP_AVAILABLE = False\n\nif not _FASTMCP_AVAILABLE:\n  # Fallback to the low-level stdio server API\n  from mcp.server import Server  # type: ignore\n  from mcp.server.stdio import stdio_server  # type: ignore\n\n\nclient = Client()\n\n\ndef _web_search_impl(query: str, max_results: int = 3) -> Dict[str, Any]:\n  res = client.web_search(query=query, max_results=max_results)\n  return res.model_dump()\n\n\ndef _web_fetch_impl(url: str) -> Dict[str, Any]:\n  res = client.web_fetch(url=url)\n  return res.model_dump()\n\n\nif _FASTMCP_AVAILABLE:\n  app = FastMCP('ollama-search-fetch')\n\n  @app.tool()\n  def web_search(query: str, max_results: int = 3) -> Dict[str, Any]:\n    \"\"\"\n    Perform a web search using Ollama's hosted search API.\n\n    Args:\n      query: The search query to run.\n      max_results: Maximum results to return (default: 3).\n\n    Returns:\n      JSON-serializable dict matching ollama.WebSearchResponse.model_dump()\n    \"\"\"\n\n    return _web_search_impl(query=query, max_results=max_results)\n\n  @app.tool()\n  def web_fetch(url: str) -> Dict[str, Any]:\n    \"\"\"\n    Fetch the content of a web page for the provided URL.\n\n    Args:\n      url: The absolute URL to fetch.\n\n    Returns:\n      JSON-serializable dict matching ollama.WebFetchResponse.model_dump()\n    \"\"\"\n\n    return _web_fetch_impl(url=url)\n\n  if __name__ == '__main__':\n    app.run()\n\nelse:\n  server = Server('ollama-search-fetch')  # type: ignore[name-defined]\n\n  @server.tool()  # type: ignore[attr-defined]\n  async def web_search(query: str, max_results: int = 3) -> Dict[str, Any]:\n    \"\"\"\n    Perform a web search using Ollama's hosted search API.\n\n    Args:\n      query: The search query to run.\n      max_results: Maximum results to return (default: 3).\n    \"\"\"\n\n    return await asyncio.to_thread(_web_search_impl, query, max_results)\n\n  @server.tool()  # type: ignore[attr-defined]\n  async def web_fetch(url: str) -> Dict[str, Any]:\n    \"\"\"\n    Fetch the content of a web page for the provided URL.\n\n    Args:\n      url: The absolute URL to fetch.\n    \"\"\"\n\n    return await asyncio.to_thread(_web_fetch_impl, url)\n\n  async def _main() -> None:\n    async with stdio_server() as (read, write):  # type: ignore[name-defined]\n      await server.run(read, write)  # type: ignore[attr-defined]\n\n  if __name__ == '__main__':\n    asyncio.run(_main())\n"
  },
  {
    "path": "examples/web-search.py",
    "content": "# /// script\n# requires-python = \">=3.11\"\n# dependencies = [\n#     \"rich\",\n#     \"ollama\",\n# ]\n# ///\nfrom typing import Union\n\nfrom rich import print\n\nfrom ollama import WebFetchResponse, WebSearchResponse, chat, web_fetch, web_search\n\n\ndef format_tool_results(\n  results: Union[WebSearchResponse, WebFetchResponse],\n  user_search: str,\n):\n  output = []\n  if isinstance(results, WebSearchResponse):\n    output.append(f'Search results for \"{user_search}\":')\n    for result in results.results:\n      output.append(f'{result.title}' if result.title else f'{result.content}')\n      output.append(f'   URL: {result.url}')\n      output.append(f'   Content: {result.content}')\n      output.append('')\n    return '\\n'.join(output).rstrip()\n\n  elif isinstance(results, WebFetchResponse):\n    output.append(f'Fetch results for \"{user_search}\":')\n    output.extend(\n      [\n        f'Title: {results.title}',\n        f'URL: {user_search}' if user_search else '',\n        f'Content: {results.content}',\n      ]\n    )\n    if results.links:\n      output.append(f'Links: {\", \".join(results.links)}')\n    output.append('')\n    return '\\n'.join(output).rstrip()\n\n\n# client = Client(headers={'Authorization': f\"Bearer {os.getenv('OLLAMA_API_KEY')}\"} if api_key else None)\navailable_tools = {'web_search': web_search, 'web_fetch': web_fetch}\n\nquery = \"what is ollama's new engine\"\nprint('Query: ', query)\n\nmessages = [{'role': 'user', 'content': query}]\nwhile True:\n  response = chat(model='qwen3', messages=messages, tools=[web_search, web_fetch], think=True)\n  if response.message.thinking:\n    print('Thinking: ')\n    print(response.message.thinking + '\\n\\n')\n  if response.message.content:\n    print('Content: ')\n    print(response.message.content + '\\n')\n\n  messages.append(response.message)\n\n  if response.message.tool_calls:\n    for tool_call in response.message.tool_calls:\n      function_to_call = available_tools.get(tool_call.function.name)\n      if function_to_call:\n        args = tool_call.function.arguments\n        result: Union[WebSearchResponse, WebFetchResponse] = function_to_call(**args)\n        print('Result from tool call name:', tool_call.function.name, 'with arguments:')\n        print(args)\n        print()\n\n        user_search = args.get('query', '') or args.get('url', '')\n        formatted_tool_results = format_tool_results(result, user_search=user_search)\n\n        print(formatted_tool_results[:300])\n        print()\n\n        # caps the result at ~2000 tokens\n        messages.append({'role': 'tool', 'content': formatted_tool_results[: 2000 * 4], 'tool_name': tool_call.function.name})\n      else:\n        print(f'Tool {tool_call.function.name} not found')\n        messages.append({'role': 'tool', 'content': f'Tool {tool_call.function.name} not found', 'tool_name': tool_call.function.name})\n  else:\n    # no more tool calls, we can stop the loop\n    break\n"
  },
  {
    "path": "examples/web_search_gpt_oss_helper.py",
    "content": "from __future__ import annotations\n\nimport re\nfrom dataclasses import dataclass, field\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional, Protocol, Tuple\nfrom urllib.parse import urlparse\n\nfrom ollama import Client\n\n\n@dataclass\nclass Page:\n  url: str\n  title: str\n  text: str\n  lines: List[str]\n  links: Dict[int, str]\n  fetched_at: datetime\n\n\n@dataclass\nclass BrowserStateData:\n  page_stack: List[str] = field(default_factory=list)\n  view_tokens: int = 1024\n  url_to_page: Dict[str, Page] = field(default_factory=dict)\n\n\n@dataclass\nclass WebSearchResult:\n  title: str\n  url: str\n  content: Dict[str, str]\n\n\nclass SearchClient(Protocol):\n  def search(self, queries: List[str], max_results: Optional[int] = None): ...\n\n\nclass CrawlClient(Protocol):\n  def crawl(self, urls: List[str]): ...\n\n\n# ---- Constants ---------------------------------------------------------------\n\nDEFAULT_VIEW_TOKENS = 1024\nCAPPED_TOOL_CONTENT_LEN = 8000\n\n# ---- Helpers ----------------------------------------------------------------\n\n\ndef cap_tool_content(text: str) -> str:\n  if not text:\n    return text\n  if len(text) <= CAPPED_TOOL_CONTENT_LEN:\n    return text\n  if CAPPED_TOOL_CONTENT_LEN <= 1:\n    return text[:CAPPED_TOOL_CONTENT_LEN]\n  return text[: CAPPED_TOOL_CONTENT_LEN - 1] + '…'\n\n\ndef _safe_domain(u: str) -> str:\n  try:\n    parsed = urlparse(u)\n    host = parsed.netloc or u\n    return host.replace('www.', '') if host else u\n  except Exception:\n    return u\n\n\n# ---- BrowserState ------------------------------------------------------------\n\n\nclass BrowserState:\n  def __init__(self, initial_state: Optional[BrowserStateData] = None):\n    self._data = initial_state or BrowserStateData(view_tokens=DEFAULT_VIEW_TOKENS)\n\n  def get_data(self) -> BrowserStateData:\n    return self._data\n\n  def set_data(self, data: BrowserStateData) -> None:\n    self._data = data\n\n\n# ---- Browser ----------------------------------------------------------------\n\n\nclass Browser:\n  def __init__(\n    self,\n    initial_state: Optional[BrowserStateData] = None,\n    client: Optional[Client] = None,\n  ):\n    self.state = BrowserState(initial_state)\n    self._client: Optional[Client] = client\n\n  def set_client(self, client: Client) -> None:\n    self._client = client\n\n  def get_state(self) -> BrowserStateData:\n    return self.state.get_data()\n\n  # ---- internal utils ----\n\n  def _save_page(self, page: Page) -> None:\n    data = self.state.get_data()\n    data.url_to_page[page.url] = page\n    data.page_stack.append(page.url)\n    self.state.set_data(data)\n\n  def _page_from_stack(self, url: str) -> Page:\n    data = self.state.get_data()\n    page = data.url_to_page.get(url)\n    if not page:\n      raise ValueError(f'Page not found for url {url}')\n    return page\n\n  def _join_lines_with_numbers(self, lines: List[str]) -> str:\n    result = []\n    for i, line in enumerate(lines):\n      result.append(f'L{i}: {line}')\n    return '\\n'.join(result)\n\n  def _wrap_lines(self, text: str, width: int = 80) -> List[str]:\n    if width <= 0:\n      width = 80\n    src_lines = text.split('\\n')\n    wrapped: List[str] = []\n    for line in src_lines:\n      if line == '':\n        wrapped.append('')\n      elif len(line) <= width:\n        wrapped.append(line)\n      else:\n        words = re.split(r'\\s+', line)\n        if not words:\n          wrapped.append(line)\n          continue\n        curr = ''\n        for w in words:\n          test = (curr + ' ' + w) if curr else w\n          if len(test) > width and curr:\n            wrapped.append(curr)\n            curr = w\n          else:\n            curr = test\n        if curr:\n          wrapped.append(curr)\n    return wrapped\n\n  def _process_markdown_links(self, text: str) -> Tuple[str, Dict[int, str]]:\n    links: Dict[int, str] = {}\n    link_id = 0\n\n    multiline_pattern = re.compile(r'\\[([^\\]]+)\\]\\s*\\n\\s*\\(([^)]+)\\)')\n    text = multiline_pattern.sub(lambda m: f'[{m.group(1)}]({m.group(2)})', text)\n    text = re.sub(r'\\s+', ' ', text)\n\n    link_pattern = re.compile(r'\\[([^\\]]+)\\]\\(([^)]+)\\)')\n\n    def _repl(m: re.Match) -> str:\n      nonlocal link_id\n      link_text = m.group(1).strip()\n      link_url = m.group(2).strip()\n      domain = _safe_domain(link_url)\n      formatted = f'【{link_id}†{link_text}†{domain}】'\n      links[link_id] = link_url\n      link_id += 1\n      return formatted\n\n    processed = link_pattern.sub(_repl, text)\n    return processed, links\n\n  def _get_end_loc(self, loc: int, num_lines: int, total_lines: int, lines: List[str]) -> int:\n    if num_lines <= 0:\n      txt = self._join_lines_with_numbers(lines[loc:])\n      data = self.state.get_data()\n      chars_per_token = 4\n      max_chars = min(data.view_tokens * chars_per_token, len(txt))\n      num_lines = txt[:max_chars].count('\\n') + 1\n    return min(loc + num_lines, total_lines)\n\n  def _display_page(self, page: Page, cursor: int, loc: int, num_lines: int) -> str:\n    total_lines = len(page.lines) or 0\n    if total_lines == 0:\n      page.lines = ['']\n      total_lines = 1\n\n    if loc != loc or loc < 0:\n      loc = 0\n    elif loc >= total_lines:\n      loc = max(0, total_lines - 1)\n\n    end_loc = self._get_end_loc(loc, num_lines, total_lines, page.lines)\n\n    header = f'[{cursor}] {page.title}'\n    header += f'({page.url})\\n' if page.url else '\\n'\n    header += f'**viewing lines [{loc} - {end_loc - 1}] of {total_lines - 1}**\\n\\n'\n\n    body_lines = []\n    for i in range(loc, end_loc):\n      body_lines.append(f'L{i}: {page.lines[i]}')\n\n    return header + '\\n'.join(body_lines)\n\n  # ---- page builders ----\n\n  def _build_search_results_page_collection(self, query: str, results: Dict[str, Any]) -> Page:\n    page = Page(\n      url=f'search_results_{query}',\n      title=query,\n      text='',\n      lines=[],\n      links={},\n      fetched_at=datetime.utcnow(),\n    )\n\n    tb = []\n    tb.append('')\n    tb.append('# Search Results')\n    tb.append('')\n\n    link_idx = 0\n    for query_results in results.get('results', {}).values():\n      for result in query_results:\n        domain = _safe_domain(result.get('url', ''))\n        link_fmt = f'* 【{link_idx}†{result.get(\"title\", \"\")}†{domain}】'\n        tb.append(link_fmt)\n\n        raw_snip = result.get('content') or ''\n        capped = (raw_snip[:400] + '…') if len(raw_snip) > 400 else raw_snip\n        cleaned = re.sub(r'\\d{40,}', lambda m: m.group(0)[:40] + '…', capped)\n        cleaned = re.sub(r'\\s{3,}', ' ', cleaned)\n        tb.append(cleaned)\n        page.links[link_idx] = result.get('url', '')\n        link_idx += 1\n\n    page.text = '\\n'.join(tb)\n    page.lines = self._wrap_lines(page.text, 80)\n    return page\n\n  def _build_search_result_page(self, result: WebSearchResult, link_idx: int) -> Page:\n    page = Page(\n      url=result.url,\n      title=result.title,\n      text='',\n      lines=[],\n      links={},\n      fetched_at=datetime.utcnow(),\n    )\n\n    link_fmt = f'【{link_idx}†{result.title}】\\n'\n    preview = link_fmt + f'URL: {result.url}\\n'\n    full_text = result.content.get('fullText', '') if result.content else ''\n    preview += full_text[:300] + '\\n\\n'\n\n    if not full_text:\n      page.links[link_idx] = result.url\n\n    if full_text:\n      raw = f'URL: {result.url}\\n{full_text}'\n      processed, links = self._process_markdown_links(raw)\n      page.text = processed\n      page.links = links\n    else:\n      page.text = preview\n\n    page.lines = self._wrap_lines(page.text, 80)\n    return page\n\n  def _build_page_from_fetch(self, requested_url: str, fetch_response: Dict[str, Any]) -> Page:\n    page = Page(\n      url=requested_url,\n      title=requested_url,\n      text='',\n      lines=[],\n      links={},\n      fetched_at=datetime.utcnow(),\n    )\n\n    for url, url_results in fetch_response.get('results', {}).items():\n      if url_results:\n        r0 = url_results[0]\n        if r0.get('content'):\n          page.text = r0['content']\n        if r0.get('title'):\n          page.title = r0['title']\n        page.url = url\n        break\n\n    if not page.text:\n      page.text = 'No content could be extracted from this page.'\n    else:\n      page.text = f'URL: {page.url}\\n{page.text}'\n\n    processed, links = self._process_markdown_links(page.text)\n    page.text = processed\n    page.links = links\n    page.lines = self._wrap_lines(page.text, 80)\n    return page\n\n  def _build_find_results_page(self, pattern: str, page: Page) -> Page:\n    find_page = Page(\n      url=f'find_results_{pattern}',\n      title=f'Find results for text: `{pattern}` in `{page.title}`',\n      text='',\n      lines=[],\n      links={},\n      fetched_at=datetime.utcnow(),\n    )\n\n    max_results = 50\n    num_show_lines = 4\n    pattern_lower = pattern.lower()\n\n    result_chunks: List[str] = []\n    line_idx = 0\n    while line_idx < len(page.lines):\n      line = page.lines[line_idx]\n      if pattern_lower not in line.lower():\n        line_idx += 1\n        continue\n\n      end_line = min(line_idx + num_show_lines, len(page.lines))\n      snippet = '\\n'.join(page.lines[line_idx:end_line])\n      link_fmt = f'【{len(result_chunks)}†match at L{line_idx}】'\n      result_chunks.append(f'{link_fmt}\\n{snippet}')\n\n      if len(result_chunks) >= max_results:\n        break\n      line_idx += num_show_lines\n\n    if not result_chunks:\n      find_page.text = f'No `find` results for pattern: `{pattern}`'\n    else:\n      find_page.text = '\\n\\n'.join(result_chunks)\n\n    find_page.lines = self._wrap_lines(find_page.text, 80)\n    return find_page\n\n  # ---- public API: search / open / find ------------------------------------\n\n  def search(self, *, query: str, topn: int = 5) -> Dict[str, Any]:\n    if not self._client:\n      raise RuntimeError('Client not provided')\n\n    resp = self._client.web_search(query, max_results=topn)\n\n    normalized: Dict[str, Any] = {'results': {}}\n    rows: List[Dict[str, str]] = []\n    for item in resp.results:\n      content = item.content or ''\n      rows.append(\n        {\n          'title': item.title,\n          'url': item.url,\n          'content': content,\n        }\n      )\n    normalized['results'][query] = rows\n\n    search_page = self._build_search_results_page_collection(query, normalized)\n    self._save_page(search_page)\n    cursor = len(self.get_state().page_stack) - 1\n\n    for query_results in normalized.get('results', {}).values():\n      for i, r in enumerate(query_results):\n        ws = WebSearchResult(\n          title=r.get('title', ''),\n          url=r.get('url', ''),\n          content={'fullText': r.get('content', '') or ''},\n        )\n        result_page = self._build_search_result_page(ws, i + 1)\n        data = self.get_state()\n        data.url_to_page[result_page.url] = result_page\n        self.state.set_data(data)\n\n    page_text = self._display_page(search_page, cursor, loc=0, num_lines=-1)\n    return {'state': self.get_state(), 'pageText': cap_tool_content(page_text)}\n\n  def open(\n    self,\n    *,\n    id: Optional[str | int] = None,\n    cursor: int = -1,\n    loc: int = 0,\n    num_lines: int = -1,\n  ) -> Dict[str, Any]:\n    if not self._client:\n      raise RuntimeError('Client not provided')\n\n    state = self.get_state()\n\n    if isinstance(id, str):\n      url = id\n      if url in state.url_to_page:\n        self._save_page(state.url_to_page[url])\n        cursor = len(self.get_state().page_stack) - 1\n        page_text = self._display_page(state.url_to_page[url], cursor, loc, num_lines)\n        return {'state': self.get_state(), 'pageText': cap_tool_content(page_text)}\n\n      fetch_response = self._client.web_fetch(url)\n      normalized: Dict[str, Any] = {\n        'results': {\n          url: [\n            {\n              'title': fetch_response.title or url,\n              'url': url,\n              'content': fetch_response.content or '',\n            }\n          ]\n        }\n      }\n      new_page = self._build_page_from_fetch(url, normalized)\n      self._save_page(new_page)\n      cursor = len(self.get_state().page_stack) - 1\n      page_text = self._display_page(new_page, cursor, loc, num_lines)\n      return {'state': self.get_state(), 'pageText': cap_tool_content(page_text)}\n\n    # Resolve current page from stack only if needed (int id or no id)\n    page: Optional[Page] = None\n    if cursor >= 0:\n      if state.page_stack:\n        if cursor >= len(state.page_stack):\n          cursor = max(0, len(state.page_stack) - 1)\n        page = self._page_from_stack(state.page_stack[cursor])\n      else:\n        page = None\n    else:\n      if state.page_stack:\n        page = self._page_from_stack(state.page_stack[-1])\n\n    if isinstance(id, int):\n      if not page:\n        raise RuntimeError('No current page to resolve link from')\n\n      link_url = page.links.get(id)\n      if not link_url:\n        err = Page(\n          url=f'invalid_link_{id}',\n          title=f'No link with id {id} on `{page.title}`',\n          text='',\n          lines=[],\n          links={},\n          fetched_at=datetime.utcnow(),\n        )\n        available = sorted(page.links.keys())\n        available_list = ', '.join(map(str, available)) if available else '(none)'\n        err.text = '\\n'.join(\n          [\n            f'Requested link id: {id}',\n            f'Current page: {page.title}',\n            f'Available link ids on this page: {available_list}',\n            '',\n            'Tips:',\n            '- To scroll this page, call browser_open with { loc, num_lines } (no id).',\n            '- To open a result from a search results page, pass the correct { cursor, id }.',\n          ]\n        )\n        err.lines = self._wrap_lines(err.text, 80)\n        self._save_page(err)\n        cursor = len(self.get_state().page_stack) - 1\n        page_text = self._display_page(err, cursor, 0, -1)\n        return {'state': self.get_state(), 'pageText': cap_tool_content(page_text)}\n\n      new_page = state.url_to_page.get(link_url)\n      if not new_page:\n        fetch_response = self._client.web_fetch(link_url)\n        normalized: Dict[str, Any] = {\n          'results': {\n            link_url: [\n              {\n                'title': fetch_response.title or link_url,\n                'url': link_url,\n                'content': fetch_response.content or '',\n              }\n            ]\n          }\n        }\n        new_page = self._build_page_from_fetch(link_url, normalized)\n\n      self._save_page(new_page)\n      cursor = len(self.get_state().page_stack) - 1\n      page_text = self._display_page(new_page, cursor, loc, num_lines)\n      return {'state': self.get_state(), 'pageText': cap_tool_content(page_text)}\n\n    if not page:\n      raise RuntimeError('No current page to display')\n\n    cur = self.get_state()\n    cur.page_stack.append(page.url)\n    self.state.set_data(cur)\n    cursor = len(cur.page_stack) - 1\n    page_text = self._display_page(page, cursor, loc, num_lines)\n    return {'state': self.get_state(), 'pageText': cap_tool_content(page_text)}\n\n  def find(self, *, pattern: str, cursor: int = -1) -> Dict[str, Any]:\n    state = self.get_state()\n    if cursor == -1:\n      if not state.page_stack:\n        raise RuntimeError('No pages to search in')\n      page = self._page_from_stack(state.page_stack[-1])\n      cursor = len(state.page_stack) - 1\n    else:\n      if cursor < 0 or cursor >= len(state.page_stack):\n        cursor = max(0, min(cursor, len(state.page_stack) - 1))\n      page = self._page_from_stack(state.page_stack[cursor])\n\n    find_page = self._build_find_results_page(pattern, page)\n    self._save_page(find_page)\n    new_cursor = len(self.get_state().page_stack) - 1\n\n    page_text = self._display_page(find_page, new_cursor, 0, -1)\n    return {'state': self.get_state(), 'pageText': cap_tool_content(page_text)}\n"
  },
  {
    "path": "ollama/__init__.py",
    "content": "from ollama._client import AsyncClient, Client\nfrom ollama._types import (\n  ChatResponse,\n  EmbeddingsResponse,\n  EmbedResponse,\n  GenerateResponse,\n  Image,\n  ListResponse,\n  Message,\n  Options,\n  ProcessResponse,\n  ProgressResponse,\n  RequestError,\n  ResponseError,\n  ShowResponse,\n  StatusResponse,\n  Tool,\n  WebFetchResponse,\n  WebSearchResponse,\n)\n\n__all__ = [\n  'AsyncClient',\n  'ChatResponse',\n  'Client',\n  'EmbedResponse',\n  'EmbeddingsResponse',\n  'GenerateResponse',\n  'Image',\n  'ListResponse',\n  'Message',\n  'Options',\n  'ProcessResponse',\n  'ProgressResponse',\n  'RequestError',\n  'ResponseError',\n  'ShowResponse',\n  'StatusResponse',\n  'Tool',\n  'WebFetchResponse',\n  'WebSearchResponse',\n]\n\n_client = Client()\n\ngenerate = _client.generate\nchat = _client.chat\nembed = _client.embed\nembeddings = _client.embeddings\npull = _client.pull\npush = _client.push\ncreate = _client.create\ndelete = _client.delete\nlist = _client.list\ncopy = _client.copy\nshow = _client.show\nps = _client.ps\nweb_search = _client.web_search\nweb_fetch = _client.web_fetch\n"
  },
  {
    "path": "ollama/_client.py",
    "content": "import contextlib\nimport ipaddress\nimport json\nimport os\nimport platform\nimport sys\nimport urllib.parse\nfrom hashlib import sha256\nfrom os import PathLike\nfrom pathlib import Path\nfrom typing import (\n  Any,\n  Callable,\n  Dict,\n  List,\n  Literal,\n  Mapping,\n  Optional,\n  Sequence,\n  Type,\n  TypeVar,\n  Union,\n  overload,\n)\n\nimport anyio\nfrom pydantic.json_schema import JsonSchemaValue\n\nfrom ollama._utils import convert_function_to_tool\n\nif sys.version_info < (3, 9):\n  from typing import AsyncIterator, Iterator\nelse:\n  from collections.abc import AsyncIterator, Iterator\n\nfrom importlib import metadata\n\ntry:\n  __version__ = metadata.version('ollama')\nexcept metadata.PackageNotFoundError:\n  __version__ = '0.0.0'\n\nimport httpx\n\nfrom ollama._types import (\n  ChatRequest,\n  ChatResponse,\n  CopyRequest,\n  CreateRequest,\n  DeleteRequest,\n  EmbeddingsRequest,\n  EmbeddingsResponse,\n  EmbedRequest,\n  EmbedResponse,\n  GenerateRequest,\n  GenerateResponse,\n  Image,\n  ListResponse,\n  Message,\n  Options,\n  ProcessResponse,\n  ProgressResponse,\n  PullRequest,\n  PushRequest,\n  ResponseError,\n  ShowRequest,\n  ShowResponse,\n  StatusResponse,\n  Tool,\n  WebFetchRequest,\n  WebFetchResponse,\n  WebSearchRequest,\n  WebSearchResponse,\n)\n\nT = TypeVar('T')\n\n\nclass BaseClient(contextlib.AbstractContextManager, contextlib.AbstractAsyncContextManager):\n  def __init__(\n    self,\n    client,\n    host: Optional[str] = None,\n    *,\n    follow_redirects: bool = True,\n    timeout: Any = None,\n    headers: Optional[Mapping[str, str]] = None,\n    **kwargs,\n  ) -> None:\n    \"\"\"\n    Creates a httpx client. Default parameters are the same as those defined in httpx\n    except for the following:\n    - `follow_redirects`: True\n    - `timeout`: None\n    `kwargs` are passed to the httpx client.\n    \"\"\"\n\n    headers = {\n      k.lower(): v\n      for k, v in {\n        **(headers or {}),\n        'Content-Type': 'application/json',\n        'Accept': 'application/json',\n        'User-Agent': f'ollama-python/{__version__} ({platform.machine()} {platform.system().lower()}) Python/{platform.python_version()}',\n      }.items()\n      if v is not None\n    }\n    api_key = os.getenv('OLLAMA_API_KEY', None)\n    if not headers.get('authorization') and api_key:\n      headers['authorization'] = f'Bearer {api_key}'\n\n    self._client = client(\n      base_url=_parse_host(host or os.getenv('OLLAMA_HOST')),\n      follow_redirects=follow_redirects,\n      timeout=timeout,\n      headers=headers,\n      **kwargs,\n    )\n\n  def __exit__(self, exc_type, exc_val, exc_tb):\n    self.close()\n\n  async def __aexit__(self, exc_type, exc_val, exc_tb):\n    await self.close()\n\n\nCONNECTION_ERROR_MESSAGE = 'Failed to connect to Ollama. Please check that Ollama is downloaded, running and accessible. https://ollama.com/download'\n\n\nclass Client(BaseClient):\n  def __init__(self, host: Optional[str] = None, **kwargs) -> None:\n    super().__init__(httpx.Client, host, **kwargs)\n\n  def close(self):\n    self._client.close()\n\n  def _request_raw(self, *args, **kwargs):\n    try:\n      r = self._client.request(*args, **kwargs)\n      r.raise_for_status()\n      return r\n    except httpx.HTTPStatusError as e:\n      raise ResponseError(e.response.text, e.response.status_code) from None\n    except httpx.ConnectError:\n      raise ConnectionError(CONNECTION_ERROR_MESSAGE) from None\n\n  @overload\n  def _request(\n    self,\n    cls: Type[T],\n    *args,\n    stream: Literal[False] = False,\n    **kwargs,\n  ) -> T: ...\n\n  @overload\n  def _request(\n    self,\n    cls: Type[T],\n    *args,\n    stream: Literal[True] = True,\n    **kwargs,\n  ) -> Iterator[T]: ...\n\n  @overload\n  def _request(\n    self,\n    cls: Type[T],\n    *args,\n    stream: bool = False,\n    **kwargs,\n  ) -> Union[T, Iterator[T]]: ...\n\n  def _request(\n    self,\n    cls: Type[T],\n    *args,\n    stream: bool = False,\n    **kwargs,\n  ) -> Union[T, Iterator[T]]:\n    if stream:\n\n      def inner():\n        with self._client.stream(*args, **kwargs) as r:\n          try:\n            r.raise_for_status()\n          except httpx.HTTPStatusError as e:\n            e.response.read()\n            raise ResponseError(e.response.text, e.response.status_code) from None\n\n          for line in r.iter_lines():\n            part = json.loads(line)\n            if err := part.get('error'):\n              raise ResponseError(err)\n            yield cls(**part)\n\n      return inner()\n\n    return cls(**self._request_raw(*args, **kwargs).json())\n\n  @overload\n  def generate(\n    self,\n    model: str = '',\n    prompt: str = '',\n    suffix: str = '',\n    *,\n    system: str = '',\n    template: str = '',\n    context: Optional[Sequence[int]] = None,\n    stream: Literal[False] = False,\n    think: Optional[bool] = None,\n    logprobs: Optional[bool] = None,\n    top_logprobs: Optional[int] = None,\n    raw: bool = False,\n    format: Optional[Union[Literal['', 'json'], JsonSchemaValue]] = None,\n    images: Optional[Sequence[Union[str, bytes, Image]]] = None,\n    options: Optional[Union[Mapping[str, Any], Options]] = None,\n    keep_alive: Optional[Union[float, str]] = None,\n    width: Optional[int] = None,\n    height: Optional[int] = None,\n    steps: Optional[int] = None,\n  ) -> GenerateResponse: ...\n\n  @overload\n  def generate(\n    self,\n    model: str = '',\n    prompt: str = '',\n    suffix: str = '',\n    *,\n    system: str = '',\n    template: str = '',\n    context: Optional[Sequence[int]] = None,\n    stream: Literal[True] = True,\n    think: Optional[bool] = None,\n    logprobs: Optional[bool] = None,\n    top_logprobs: Optional[int] = None,\n    raw: bool = False,\n    format: Optional[Union[Literal['', 'json'], JsonSchemaValue]] = None,\n    images: Optional[Sequence[Union[str, bytes, Image]]] = None,\n    options: Optional[Union[Mapping[str, Any], Options]] = None,\n    keep_alive: Optional[Union[float, str]] = None,\n    width: Optional[int] = None,\n    height: Optional[int] = None,\n    steps: Optional[int] = None,\n  ) -> Iterator[GenerateResponse]: ...\n\n  def generate(\n    self,\n    model: str = '',\n    prompt: Optional[str] = None,\n    suffix: Optional[str] = None,\n    *,\n    system: Optional[str] = None,\n    template: Optional[str] = None,\n    context: Optional[Sequence[int]] = None,\n    stream: bool = False,\n    think: Optional[bool] = None,\n    logprobs: Optional[bool] = None,\n    top_logprobs: Optional[int] = None,\n    raw: Optional[bool] = None,\n    format: Optional[Union[Literal['', 'json'], JsonSchemaValue]] = None,\n    images: Optional[Sequence[Union[str, bytes, Image]]] = None,\n    options: Optional[Union[Mapping[str, Any], Options]] = None,\n    keep_alive: Optional[Union[float, str]] = None,\n    width: Optional[int] = None,\n    height: Optional[int] = None,\n    steps: Optional[int] = None,\n  ) -> Union[GenerateResponse, Iterator[GenerateResponse]]:\n    \"\"\"\n    Create a response using the requested model.\n\n    Raises `RequestError` if a model is not provided.\n\n    Raises `ResponseError` if the request could not be fulfilled.\n\n    Returns `GenerateResponse` if `stream` is `False`, otherwise returns a `GenerateResponse` generator.\n    \"\"\"\n\n    return self._request(\n      GenerateResponse,\n      'POST',\n      '/api/generate',\n      json=GenerateRequest(\n        model=model,\n        prompt=prompt,\n        suffix=suffix,\n        system=system,\n        template=template,\n        context=context,\n        stream=stream,\n        think=think,\n        logprobs=logprobs,\n        top_logprobs=top_logprobs,\n        raw=raw,\n        format=format,\n        images=list(_copy_images(images)) if images else None,\n        options=options,\n        keep_alive=keep_alive,\n        width=width,\n        height=height,\n        steps=steps,\n      ).model_dump(exclude_none=True),\n      stream=stream,\n    )\n\n  @overload\n  def chat(\n    self,\n    model: str = '',\n    messages: Optional[Sequence[Union[Mapping[str, Any], Message]]] = None,\n    *,\n    tools: Optional[Sequence[Union[Mapping[str, Any], Tool, Callable]]] = None,\n    stream: Literal[False] = False,\n    think: Optional[Union[bool, Literal['low', 'medium', 'high']]] = None,\n    logprobs: Optional[bool] = None,\n    top_logprobs: Optional[int] = None,\n    format: Optional[Union[Literal['', 'json'], JsonSchemaValue]] = None,\n    options: Optional[Union[Mapping[str, Any], Options]] = None,\n    keep_alive: Optional[Union[float, str]] = None,\n  ) -> ChatResponse: ...\n\n  @overload\n  def chat(\n    self,\n    model: str = '',\n    messages: Optional[Sequence[Union[Mapping[str, Any], Message]]] = None,\n    *,\n    tools: Optional[Sequence[Union[Mapping[str, Any], Tool, Callable]]] = None,\n    stream: Literal[True] = True,\n    think: Optional[Union[bool, Literal['low', 'medium', 'high']]] = None,\n    logprobs: Optional[bool] = None,\n    top_logprobs: Optional[int] = None,\n    format: Optional[Union[Literal['', 'json'], JsonSchemaValue]] = None,\n    options: Optional[Union[Mapping[str, Any], Options]] = None,\n    keep_alive: Optional[Union[float, str]] = None,\n  ) -> Iterator[ChatResponse]: ...\n\n  def chat(\n    self,\n    model: str = '',\n    messages: Optional[Sequence[Union[Mapping[str, Any], Message]]] = None,\n    *,\n    tools: Optional[Sequence[Union[Mapping[str, Any], Tool, Callable]]] = None,\n    stream: bool = False,\n    think: Optional[Union[bool, Literal['low', 'medium', 'high']]] = None,\n    logprobs: Optional[bool] = None,\n    top_logprobs: Optional[int] = None,\n    format: Optional[Union[Literal['', 'json'], JsonSchemaValue]] = None,\n    options: Optional[Union[Mapping[str, Any], Options]] = None,\n    keep_alive: Optional[Union[float, str]] = None,\n  ) -> Union[ChatResponse, Iterator[ChatResponse]]:\n    \"\"\"\n    Create a chat response using the requested model.\n\n    Args:\n      tools:\n        A JSON schema as a dict, an Ollama Tool or a Python Function.\n        Python functions need to follow Google style docstrings to be converted to an Ollama Tool.\n        For more information, see: https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings\n      stream: Whether to stream the response.\n      format: The format of the response.\n\n    Example:\n      def add_two_numbers(a: int, b: int) -> int:\n        '''\n        Add two numbers together.\n\n        Args:\n          a: First number to add\n          b: Second number to add\n\n        Returns:\n          int: The sum of a and b\n        '''\n        return a + b\n\n      client.chat(model='llama3.2', tools=[add_two_numbers], messages=[...])\n\n    Raises `RequestError` if a model is not provided.\n\n    Raises `ResponseError` if the request could not be fulfilled.\n\n    Returns `ChatResponse` if `stream` is `False`, otherwise returns a `ChatResponse` generator.\n    \"\"\"\n    return self._request(\n      ChatResponse,\n      'POST',\n      '/api/chat',\n      json=ChatRequest(\n        model=model,\n        messages=list(_copy_messages(messages)),\n        tools=list(_copy_tools(tools)),\n        stream=stream,\n        think=think,\n        logprobs=logprobs,\n        top_logprobs=top_logprobs,\n        format=format,\n        options=options,\n        keep_alive=keep_alive,\n      ).model_dump(exclude_none=True),\n      stream=stream,\n    )\n\n  def embed(\n    self,\n    model: str = '',\n    input: Union[str, Sequence[str]] = '',\n    truncate: Optional[bool] = None,\n    options: Optional[Union[Mapping[str, Any], Options]] = None,\n    keep_alive: Optional[Union[float, str]] = None,\n    dimensions: Optional[int] = None,\n  ) -> EmbedResponse:\n    return self._request(\n      EmbedResponse,\n      'POST',\n      '/api/embed',\n      json=EmbedRequest(\n        model=model,\n        input=input,\n        truncate=truncate,\n        options=options,\n        keep_alive=keep_alive,\n        dimensions=dimensions,\n      ).model_dump(exclude_none=True),\n    )\n\n  def embeddings(\n    self,\n    model: str = '',\n    prompt: Optional[str] = None,\n    options: Optional[Union[Mapping[str, Any], Options]] = None,\n    keep_alive: Optional[Union[float, str]] = None,\n  ) -> EmbeddingsResponse:\n    \"\"\"\n    Deprecated in favor of `embed`.\n    \"\"\"\n    return self._request(\n      EmbeddingsResponse,\n      'POST',\n      '/api/embeddings',\n      json=EmbeddingsRequest(\n        model=model,\n        prompt=prompt,\n        options=options,\n        keep_alive=keep_alive,\n      ).model_dump(exclude_none=True),\n    )\n\n  @overload\n  def pull(\n    self,\n    model: str,\n    *,\n    insecure: bool = False,\n    stream: Literal[False] = False,\n  ) -> ProgressResponse: ...\n\n  @overload\n  def pull(\n    self,\n    model: str,\n    *,\n    insecure: bool = False,\n    stream: Literal[True] = True,\n  ) -> Iterator[ProgressResponse]: ...\n\n  def pull(\n    self,\n    model: str,\n    *,\n    insecure: bool = False,\n    stream: bool = False,\n  ) -> Union[ProgressResponse, Iterator[ProgressResponse]]:\n    \"\"\"\n    Raises `ResponseError` if the request could not be fulfilled.\n\n    Returns `ProgressResponse` if `stream` is `False`, otherwise returns a `ProgressResponse` generator.\n    \"\"\"\n    return self._request(\n      ProgressResponse,\n      'POST',\n      '/api/pull',\n      json=PullRequest(\n        model=model,\n        insecure=insecure,\n        stream=stream,\n      ).model_dump(exclude_none=True),\n      stream=stream,\n    )\n\n  @overload\n  def push(\n    self,\n    model: str,\n    *,\n    insecure: bool = False,\n    stream: Literal[False] = False,\n  ) -> ProgressResponse: ...\n\n  @overload\n  def push(\n    self,\n    model: str,\n    *,\n    insecure: bool = False,\n    stream: Literal[True] = True,\n  ) -> Iterator[ProgressResponse]: ...\n\n  def push(\n    self,\n    model: str,\n    *,\n    insecure: bool = False,\n    stream: bool = False,\n  ) -> Union[ProgressResponse, Iterator[ProgressResponse]]:\n    \"\"\"\n    Raises `ResponseError` if the request could not be fulfilled.\n\n    Returns `ProgressResponse` if `stream` is `False`, otherwise returns a `ProgressResponse` generator.\n    \"\"\"\n    return self._request(\n      ProgressResponse,\n      'POST',\n      '/api/push',\n      json=PushRequest(\n        model=model,\n        insecure=insecure,\n        stream=stream,\n      ).model_dump(exclude_none=True),\n      stream=stream,\n    )\n\n  @overload\n  def create(\n    self,\n    model: str,\n    quantize: Optional[str] = None,\n    from_: Optional[str] = None,\n    files: Optional[Dict[str, str]] = None,\n    adapters: Optional[Dict[str, str]] = None,\n    template: Optional[str] = None,\n    license: Optional[Union[str, List[str]]] = None,\n    system: Optional[str] = None,\n    parameters: Optional[Union[Mapping[str, Any], Options]] = None,\n    messages: Optional[Sequence[Union[Mapping[str, Any], Message]]] = None,\n    *,\n    stream: Literal[False] = False,\n  ) -> ProgressResponse: ...\n\n  @overload\n  def create(\n    self,\n    model: str,\n    quantize: Optional[str] = None,\n    from_: Optional[str] = None,\n    files: Optional[Dict[str, str]] = None,\n    adapters: Optional[Dict[str, str]] = None,\n    template: Optional[str] = None,\n    license: Optional[Union[str, List[str]]] = None,\n    system: Optional[str] = None,\n    parameters: Optional[Union[Mapping[str, Any], Options]] = None,\n    messages: Optional[Sequence[Union[Mapping[str, Any], Message]]] = None,\n    *,\n    stream: Literal[True] = True,\n  ) -> Iterator[ProgressResponse]: ...\n\n  def create(\n    self,\n    model: str,\n    quantize: Optional[str] = None,\n    from_: Optional[str] = None,\n    files: Optional[Dict[str, str]] = None,\n    adapters: Optional[Dict[str, str]] = None,\n    template: Optional[str] = None,\n    license: Optional[Union[str, List[str]]] = None,\n    system: Optional[str] = None,\n    parameters: Optional[Union[Mapping[str, Any], Options]] = None,\n    messages: Optional[Sequence[Union[Mapping[str, Any], Message]]] = None,\n    *,\n    stream: bool = False,\n  ) -> Union[ProgressResponse, Iterator[ProgressResponse]]:\n    \"\"\"\n    Raises `ResponseError` if the request could not be fulfilled.\n\n    Returns `ProgressResponse` if `stream` is `False`, otherwise returns a `ProgressResponse` generator.\n    \"\"\"\n    return self._request(\n      ProgressResponse,\n      'POST',\n      '/api/create',\n      json=CreateRequest(\n        model=model,\n        stream=stream,\n        quantize=quantize,\n        from_=from_,\n        files=files,\n        adapters=adapters,\n        license=license,\n        template=template,\n        system=system,\n        parameters=parameters,\n        messages=messages,\n      ).model_dump(exclude_none=True),\n      stream=stream,\n    )\n\n  def create_blob(self, path: Union[str, Path]) -> str:\n    sha256sum = sha256()\n    with open(path, 'rb') as r:\n      while True:\n        chunk = r.read(32 * 1024)\n        if not chunk:\n          break\n        sha256sum.update(chunk)\n\n    digest = f'sha256:{sha256sum.hexdigest()}'\n\n    with open(path, 'rb') as r:\n      self._request_raw('POST', f'/api/blobs/{digest}', content=r)\n\n    return digest\n\n  def list(self) -> ListResponse:\n    return self._request(\n      ListResponse,\n      'GET',\n      '/api/tags',\n    )\n\n  def delete(self, model: str) -> StatusResponse:\n    r = self._request_raw(\n      'DELETE',\n      '/api/delete',\n      json=DeleteRequest(\n        model=model,\n      ).model_dump(exclude_none=True),\n    )\n    return StatusResponse(\n      status='success' if r.status_code == 200 else 'error',\n    )\n\n  def copy(self, source: str, destination: str) -> StatusResponse:\n    r = self._request_raw(\n      'POST',\n      '/api/copy',\n      json=CopyRequest(\n        source=source,\n        destination=destination,\n      ).model_dump(exclude_none=True),\n    )\n    return StatusResponse(\n      status='success' if r.status_code == 200 else 'error',\n    )\n\n  def show(self, model: str) -> ShowResponse:\n    return self._request(\n      ShowResponse,\n      'POST',\n      '/api/show',\n      json=ShowRequest(\n        model=model,\n      ).model_dump(exclude_none=True),\n    )\n\n  def ps(self) -> ProcessResponse:\n    return self._request(\n      ProcessResponse,\n      'GET',\n      '/api/ps',\n    )\n\n  def web_search(self, query: str, max_results: int = 3) -> WebSearchResponse:\n    \"\"\"\n    Performs a web search\n\n    Args:\n      query: The query to search for\n      max_results: The maximum number of results to return (default: 3)\n\n    Returns:\n      WebSearchResponse with the search results\n    Raises:\n      ValueError: If OLLAMA_API_KEY environment variable is not set\n    \"\"\"\n    if not self._client.headers.get('authorization', '').startswith('Bearer '):\n      raise ValueError('Authorization header with Bearer token is required for web search')\n\n    return self._request(\n      WebSearchResponse,\n      'POST',\n      'https://ollama.com/api/web_search',\n      json=WebSearchRequest(\n        query=query,\n        max_results=max_results,\n      ).model_dump(exclude_none=True),\n    )\n\n  def web_fetch(self, url: str) -> WebFetchResponse:\n    \"\"\"\n    Fetches the content of a web page for the provided URL.\n\n    Args:\n      url: The URL to fetch\n\n    Returns:\n      WebFetchResponse with the fetched result\n    \"\"\"\n    if not self._client.headers.get('authorization', '').startswith('Bearer '):\n      raise ValueError('Authorization header with Bearer token is required for web fetch')\n\n    return self._request(\n      WebFetchResponse,\n      'POST',\n      'https://ollama.com/api/web_fetch',\n      json=WebFetchRequest(\n        url=url,\n      ).model_dump(exclude_none=True),\n    )\n\n\nclass AsyncClient(BaseClient):\n  def __init__(self, host: Optional[str] = None, **kwargs) -> None:\n    super().__init__(httpx.AsyncClient, host, **kwargs)\n\n  async def close(self):\n    await self._client.aclose()\n\n  async def _request_raw(self, *args, **kwargs):\n    try:\n      r = await self._client.request(*args, **kwargs)\n      r.raise_for_status()\n      return r\n    except httpx.HTTPStatusError as e:\n      raise ResponseError(e.response.text, e.response.status_code) from None\n    except httpx.ConnectError:\n      raise ConnectionError(CONNECTION_ERROR_MESSAGE) from None\n\n  @overload\n  async def _request(\n    self,\n    cls: Type[T],\n    *args,\n    stream: Literal[False] = False,\n    **kwargs,\n  ) -> T: ...\n\n  @overload\n  async def _request(\n    self,\n    cls: Type[T],\n    *args,\n    stream: Literal[True] = True,\n    **kwargs,\n  ) -> AsyncIterator[T]: ...\n\n  @overload\n  async def _request(\n    self,\n    cls: Type[T],\n    *args,\n    stream: bool = False,\n    **kwargs,\n  ) -> Union[T, AsyncIterator[T]]: ...\n\n  async def _request(\n    self,\n    cls: Type[T],\n    *args,\n    stream: bool = False,\n    **kwargs,\n  ) -> Union[T, AsyncIterator[T]]:\n    if stream:\n\n      async def inner():\n        async with self._client.stream(*args, **kwargs) as r:\n          try:\n            r.raise_for_status()\n          except httpx.HTTPStatusError as e:\n            await e.response.aread()\n            raise ResponseError(e.response.text, e.response.status_code) from None\n\n          async for line in r.aiter_lines():\n            part = json.loads(line)\n            if err := part.get('error'):\n              raise ResponseError(err)\n            yield cls(**part)\n\n      return inner()\n\n    return cls(**(await self._request_raw(*args, **kwargs)).json())\n\n  async def web_search(self, query: str, max_results: int = 3) -> WebSearchResponse:\n    \"\"\"\n    Performs a web search\n\n    Args:\n      query: The query to search for\n      max_results: The maximum number of results to return (default: 3)\n\n    Returns:\n      WebSearchResponse with the search results\n    \"\"\"\n    return await self._request(\n      WebSearchResponse,\n      'POST',\n      'https://ollama.com/api/web_search',\n      json=WebSearchRequest(\n        query=query,\n        max_results=max_results,\n      ).model_dump(exclude_none=True),\n    )\n\n  async def web_fetch(self, url: str) -> WebFetchResponse:\n    \"\"\"\n    Fetches the content of a web page for the provided URL.\n\n    Args:\n      url: The URL to fetch\n\n    Returns:\n      WebFetchResponse with the fetched result\n    \"\"\"\n    return await self._request(\n      WebFetchResponse,\n      'POST',\n      'https://ollama.com/api/web_fetch',\n      json=WebFetchRequest(\n        url=url,\n      ).model_dump(exclude_none=True),\n    )\n\n  @overload\n  async def generate(\n    self,\n    model: str = '',\n    prompt: str = '',\n    suffix: str = '',\n    *,\n    system: str = '',\n    template: str = '',\n    context: Optional[Sequence[int]] = None,\n    stream: Literal[False] = False,\n    think: Optional[Union[bool, Literal['low', 'medium', 'high']]] = None,\n    logprobs: Optional[bool] = None,\n    top_logprobs: Optional[int] = None,\n    raw: bool = False,\n    format: Optional[Union[Literal['', 'json'], JsonSchemaValue]] = None,\n    images: Optional[Sequence[Union[str, bytes, Image]]] = None,\n    options: Optional[Union[Mapping[str, Any], Options]] = None,\n    keep_alive: Optional[Union[float, str]] = None,\n    width: Optional[int] = None,\n    height: Optional[int] = None,\n    steps: Optional[int] = None,\n  ) -> GenerateResponse: ...\n\n  @overload\n  async def generate(\n    self,\n    model: str = '',\n    prompt: str = '',\n    suffix: str = '',\n    *,\n    system: str = '',\n    template: str = '',\n    context: Optional[Sequence[int]] = None,\n    stream: Literal[True] = True,\n    think: Optional[Union[bool, Literal['low', 'medium', 'high']]] = None,\n    logprobs: Optional[bool] = None,\n    top_logprobs: Optional[int] = None,\n    raw: bool = False,\n    format: Optional[Union[Literal['', 'json'], JsonSchemaValue]] = None,\n    images: Optional[Sequence[Union[str, bytes, Image]]] = None,\n    options: Optional[Union[Mapping[str, Any], Options]] = None,\n    keep_alive: Optional[Union[float, str]] = None,\n    width: Optional[int] = None,\n    height: Optional[int] = None,\n    steps: Optional[int] = None,\n  ) -> AsyncIterator[GenerateResponse]: ...\n\n  async def generate(\n    self,\n    model: str = '',\n    prompt: Optional[str] = None,\n    suffix: Optional[str] = None,\n    *,\n    system: Optional[str] = None,\n    template: Optional[str] = None,\n    context: Optional[Sequence[int]] = None,\n    stream: bool = False,\n    think: Optional[Union[bool, Literal['low', 'medium', 'high']]] = None,\n    logprobs: Optional[bool] = None,\n    top_logprobs: Optional[int] = None,\n    raw: Optional[bool] = None,\n    format: Optional[Union[Literal['', 'json'], JsonSchemaValue]] = None,\n    images: Optional[Sequence[Union[str, bytes, Image]]] = None,\n    options: Optional[Union[Mapping[str, Any], Options]] = None,\n    keep_alive: Optional[Union[float, str]] = None,\n    width: Optional[int] = None,\n    height: Optional[int] = None,\n    steps: Optional[int] = None,\n  ) -> Union[GenerateResponse, AsyncIterator[GenerateResponse]]:\n    \"\"\"\n    Create a response using the requested model.\n\n    Raises `RequestError` if a model is not provided.\n\n    Raises `ResponseError` if the request could not be fulfilled.\n\n    Returns `GenerateResponse` if `stream` is `False`, otherwise returns an asynchronous `GenerateResponse` generator.\n    \"\"\"\n    return await self._request(\n      GenerateResponse,\n      'POST',\n      '/api/generate',\n      json=GenerateRequest(\n        model=model,\n        prompt=prompt,\n        suffix=suffix,\n        system=system,\n        template=template,\n        context=context,\n        stream=stream,\n        think=think,\n        logprobs=logprobs,\n        top_logprobs=top_logprobs,\n        raw=raw,\n        format=format,\n        images=list(_copy_images(images)) if images else None,\n        options=options,\n        keep_alive=keep_alive,\n        width=width,\n        height=height,\n        steps=steps,\n      ).model_dump(exclude_none=True),\n      stream=stream,\n    )\n\n  @overload\n  async def chat(\n    self,\n    model: str = '',\n    messages: Optional[Sequence[Union[Mapping[str, Any], Message]]] = None,\n    *,\n    tools: Optional[Sequence[Union[Mapping[str, Any], Tool, Callable]]] = None,\n    stream: Literal[False] = False,\n    think: Optional[Union[bool, Literal['low', 'medium', 'high']]] = None,\n    logprobs: Optional[bool] = None,\n    top_logprobs: Optional[int] = None,\n    format: Optional[Union[Literal['', 'json'], JsonSchemaValue]] = None,\n    options: Optional[Union[Mapping[str, Any], Options]] = None,\n    keep_alive: Optional[Union[float, str]] = None,\n  ) -> ChatResponse: ...\n\n  @overload\n  async def chat(\n    self,\n    model: str = '',\n    messages: Optional[Sequence[Union[Mapping[str, Any], Message]]] = None,\n    *,\n    tools: Optional[Sequence[Union[Mapping[str, Any], Tool, Callable]]] = None,\n    stream: Literal[True] = True,\n    think: Optional[Union[bool, Literal['low', 'medium', 'high']]] = None,\n    logprobs: Optional[bool] = None,\n    top_logprobs: Optional[int] = None,\n    format: Optional[Union[Literal['', 'json'], JsonSchemaValue]] = None,\n    options: Optional[Union[Mapping[str, Any], Options]] = None,\n    keep_alive: Optional[Union[float, str]] = None,\n  ) -> AsyncIterator[ChatResponse]: ...\n\n  async def chat(\n    self,\n    model: str = '',\n    messages: Optional[Sequence[Union[Mapping[str, Any], Message]]] = None,\n    *,\n    tools: Optional[Sequence[Union[Mapping[str, Any], Tool, Callable]]] = None,\n    stream: bool = False,\n    think: Optional[Union[bool, Literal['low', 'medium', 'high']]] = None,\n    logprobs: Optional[bool] = None,\n    top_logprobs: Optional[int] = None,\n    format: Optional[Union[Literal['', 'json'], JsonSchemaValue]] = None,\n    options: Optional[Union[Mapping[str, Any], Options]] = None,\n    keep_alive: Optional[Union[float, str]] = None,\n  ) -> Union[ChatResponse, AsyncIterator[ChatResponse]]:\n    \"\"\"\n    Create a chat response using the requested model.\n\n    Args:\n      tools:\n        A JSON schema as a dict, an Ollama Tool or a Python Function.\n        Python functions need to follow Google style docstrings to be converted to an Ollama Tool.\n        For more information, see: https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings\n      stream: Whether to stream the response.\n      format: The format of the response.\n\n    Example:\n      def add_two_numbers(a: int, b: int) -> int:\n        '''\n        Add two numbers together.\n\n        Args:\n          a: First number to add\n          b: Second number to add\n\n        Returns:\n          int: The sum of a and b\n        '''\n        return a + b\n\n      await client.chat(model='llama3.2', tools=[add_two_numbers], messages=[...])\n\n    Raises `RequestError` if a model is not provided.\n\n    Raises `ResponseError` if the request could not be fulfilled.\n\n    Returns `ChatResponse` if `stream` is `False`, otherwise returns an asynchronous `ChatResponse` generator.\n    \"\"\"\n\n    return await self._request(\n      ChatResponse,\n      'POST',\n      '/api/chat',\n      json=ChatRequest(\n        model=model,\n        messages=list(_copy_messages(messages)),\n        tools=list(_copy_tools(tools)),\n        stream=stream,\n        think=think,\n        logprobs=logprobs,\n        top_logprobs=top_logprobs,\n        format=format,\n        options=options,\n        keep_alive=keep_alive,\n      ).model_dump(exclude_none=True),\n      stream=stream,\n    )\n\n  async def embed(\n    self,\n    model: str = '',\n    input: Union[str, Sequence[str]] = '',\n    truncate: Optional[bool] = None,\n    options: Optional[Union[Mapping[str, Any], Options]] = None,\n    keep_alive: Optional[Union[float, str]] = None,\n    dimensions: Optional[int] = None,\n  ) -> EmbedResponse:\n    return await self._request(\n      EmbedResponse,\n      'POST',\n      '/api/embed',\n      json=EmbedRequest(\n        model=model,\n        input=input,\n        truncate=truncate,\n        options=options,\n        keep_alive=keep_alive,\n        dimensions=dimensions,\n      ).model_dump(exclude_none=True),\n    )\n\n  async def embeddings(\n    self,\n    model: str = '',\n    prompt: Optional[str] = None,\n    options: Optional[Union[Mapping[str, Any], Options]] = None,\n    keep_alive: Optional[Union[float, str]] = None,\n  ) -> EmbeddingsResponse:\n    \"\"\"\n    Deprecated in favor of `embed`.\n    \"\"\"\n    return await self._request(\n      EmbeddingsResponse,\n      'POST',\n      '/api/embeddings',\n      json=EmbeddingsRequest(\n        model=model,\n        prompt=prompt,\n        options=options,\n        keep_alive=keep_alive,\n      ).model_dump(exclude_none=True),\n    )\n\n  @overload\n  async def pull(\n    self,\n    model: str,\n    *,\n    insecure: bool = False,\n    stream: Literal[False] = False,\n  ) -> ProgressResponse: ...\n\n  @overload\n  async def pull(\n    self,\n    model: str,\n    *,\n    insecure: bool = False,\n    stream: Literal[True] = True,\n  ) -> AsyncIterator[ProgressResponse]: ...\n\n  async def pull(\n    self,\n    model: str,\n    *,\n    insecure: bool = False,\n    stream: bool = False,\n  ) -> Union[ProgressResponse, AsyncIterator[ProgressResponse]]:\n    \"\"\"\n    Raises `ResponseError` if the request could not be fulfilled.\n\n    Returns `ProgressResponse` if `stream` is `False`, otherwise returns a `ProgressResponse` generator.\n    \"\"\"\n    return await self._request(\n      ProgressResponse,\n      'POST',\n      '/api/pull',\n      json=PullRequest(\n        model=model,\n        insecure=insecure,\n        stream=stream,\n      ).model_dump(exclude_none=True),\n      stream=stream,\n    )\n\n  @overload\n  async def push(\n    self,\n    model: str,\n    *,\n    insecure: bool = False,\n    stream: Literal[False] = False,\n  ) -> ProgressResponse: ...\n\n  @overload\n  async def push(\n    self,\n    model: str,\n    *,\n    insecure: bool = False,\n    stream: Literal[True] = True,\n  ) -> AsyncIterator[ProgressResponse]: ...\n\n  async def push(\n    self,\n    model: str,\n    *,\n    insecure: bool = False,\n    stream: bool = False,\n  ) -> Union[ProgressResponse, AsyncIterator[ProgressResponse]]:\n    \"\"\"\n    Raises `ResponseError` if the request could not be fulfilled.\n\n    Returns `ProgressResponse` if `stream` is `False`, otherwise returns a `ProgressResponse` generator.\n    \"\"\"\n    return await self._request(\n      ProgressResponse,\n      'POST',\n      '/api/push',\n      json=PushRequest(\n        model=model,\n        insecure=insecure,\n        stream=stream,\n      ).model_dump(exclude_none=True),\n      stream=stream,\n    )\n\n  @overload\n  async def create(\n    self,\n    model: str,\n    quantize: Optional[str] = None,\n    from_: Optional[str] = None,\n    files: Optional[Dict[str, str]] = None,\n    adapters: Optional[Dict[str, str]] = None,\n    template: Optional[str] = None,\n    license: Optional[Union[str, List[str]]] = None,\n    system: Optional[str] = None,\n    parameters: Optional[Union[Mapping[str, Any], Options]] = None,\n    messages: Optional[Sequence[Union[Mapping[str, Any], Message]]] = None,\n    *,\n    stream: Literal[False] = False,\n  ) -> ProgressResponse: ...\n\n  @overload\n  async def create(\n    self,\n    model: str,\n    quantize: Optional[str] = None,\n    from_: Optional[str] = None,\n    files: Optional[Dict[str, str]] = None,\n    adapters: Optional[Dict[str, str]] = None,\n    template: Optional[str] = None,\n    license: Optional[Union[str, List[str]]] = None,\n    system: Optional[str] = None,\n    parameters: Optional[Union[Mapping[str, Any], Options]] = None,\n    messages: Optional[Sequence[Union[Mapping[str, Any], Message]]] = None,\n    *,\n    stream: Literal[True] = True,\n  ) -> AsyncIterator[ProgressResponse]: ...\n\n  async def create(\n    self,\n    model: str,\n    quantize: Optional[str] = None,\n    from_: Optional[str] = None,\n    files: Optional[Dict[str, str]] = None,\n    adapters: Optional[Dict[str, str]] = None,\n    template: Optional[str] = None,\n    license: Optional[Union[str, List[str]]] = None,\n    system: Optional[str] = None,\n    parameters: Optional[Union[Mapping[str, Any], Options]] = None,\n    messages: Optional[Sequence[Union[Mapping[str, Any], Message]]] = None,\n    *,\n    stream: bool = False,\n  ) -> Union[ProgressResponse, AsyncIterator[ProgressResponse]]:\n    \"\"\"\n    Raises `ResponseError` if the request could not be fulfilled.\n\n    Returns `ProgressResponse` if `stream` is `False`, otherwise returns a `ProgressResponse` generator.\n    \"\"\"\n\n    return await self._request(\n      ProgressResponse,\n      'POST',\n      '/api/create',\n      json=CreateRequest(\n        model=model,\n        stream=stream,\n        quantize=quantize,\n        from_=from_,\n        files=files,\n        adapters=adapters,\n        license=license,\n        template=template,\n        system=system,\n        parameters=parameters,\n        messages=messages,\n      ).model_dump(exclude_none=True),\n      stream=stream,\n    )\n\n  async def create_blob(self, path: Union[str, Path]) -> str:\n    sha256sum = sha256()\n    async with await anyio.open_file(path, 'rb') as r:\n      while True:\n        chunk = await r.read(32 * 1024)\n        if not chunk:\n          break\n        sha256sum.update(chunk)\n\n    digest = f'sha256:{sha256sum.hexdigest()}'\n\n    async def upload_bytes():\n      async with await anyio.open_file(path, 'rb') as r:\n        while True:\n          chunk = await r.read(32 * 1024)\n          if not chunk:\n            break\n          yield chunk\n\n    await self._request_raw('POST', f'/api/blobs/{digest}', content=upload_bytes())\n\n    return digest\n\n  async def list(self) -> ListResponse:\n    return await self._request(\n      ListResponse,\n      'GET',\n      '/api/tags',\n    )\n\n  async def delete(self, model: str) -> StatusResponse:\n    r = await self._request_raw(\n      'DELETE',\n      '/api/delete',\n      json=DeleteRequest(\n        model=model,\n      ).model_dump(exclude_none=True),\n    )\n    return StatusResponse(\n      status='success' if r.status_code == 200 else 'error',\n    )\n\n  async def copy(self, source: str, destination: str) -> StatusResponse:\n    r = await self._request_raw(\n      'POST',\n      '/api/copy',\n      json=CopyRequest(\n        source=source,\n        destination=destination,\n      ).model_dump(exclude_none=True),\n    )\n    return StatusResponse(\n      status='success' if r.status_code == 200 else 'error',\n    )\n\n  async def show(self, model: str) -> ShowResponse:\n    return await self._request(\n      ShowResponse,\n      'POST',\n      '/api/show',\n      json=ShowRequest(\n        model=model,\n      ).model_dump(exclude_none=True),\n    )\n\n  async def ps(self) -> ProcessResponse:\n    return await self._request(\n      ProcessResponse,\n      'GET',\n      '/api/ps',\n    )\n\n\ndef _copy_images(images: Optional[Sequence[Union[Image, Any]]]) -> Iterator[Image]:\n  for image in images or []:\n    yield image if isinstance(image, Image) else Image(value=image)\n\n\ndef _copy_messages(messages: Optional[Sequence[Union[Mapping[str, Any], Message]]]) -> Iterator[Message]:\n  for message in messages or []:\n    yield Message.model_validate(\n      {k: list(_copy_images(v)) if k == 'images' else v for k, v in dict(message).items() if v},\n    )\n\n\ndef _copy_tools(tools: Optional[Sequence[Union[Mapping[str, Any], Tool, Callable]]] = None) -> Iterator[Tool]:\n  for unprocessed_tool in tools or []:\n    yield convert_function_to_tool(unprocessed_tool) if callable(unprocessed_tool) else Tool.model_validate(unprocessed_tool)\n\n\ndef _as_path(s: Optional[Union[str, PathLike]]) -> Union[Path, None]:\n  if isinstance(s, (str, Path)):\n    try:\n      if (p := Path(s)).exists():\n        return p\n    except Exception:\n      ...\n  return None\n\n\ndef _parse_host(host: Optional[str]) -> str:\n  \"\"\"\n  >>> _parse_host(None)\n  'http://127.0.0.1:11434'\n  >>> _parse_host('')\n  'http://127.0.0.1:11434'\n  >>> _parse_host('1.2.3.4')\n  'http://1.2.3.4:11434'\n  >>> _parse_host(':56789')\n  'http://127.0.0.1:56789'\n  >>> _parse_host('1.2.3.4:56789')\n  'http://1.2.3.4:56789'\n  >>> _parse_host('http://1.2.3.4')\n  'http://1.2.3.4:80'\n  >>> _parse_host('https://1.2.3.4')\n  'https://1.2.3.4:443'\n  >>> _parse_host('https://1.2.3.4:56789')\n  'https://1.2.3.4:56789'\n  >>> _parse_host('example.com')\n  'http://example.com:11434'\n  >>> _parse_host('example.com:56789')\n  'http://example.com:56789'\n  >>> _parse_host('http://example.com')\n  'http://example.com:80'\n  >>> _parse_host('https://example.com')\n  'https://example.com:443'\n  >>> _parse_host('https://example.com:56789')\n  'https://example.com:56789'\n  >>> _parse_host('example.com/')\n  'http://example.com:11434'\n  >>> _parse_host('example.com:56789/')\n  'http://example.com:56789'\n  >>> _parse_host('example.com/path')\n  'http://example.com:11434/path'\n  >>> _parse_host('example.com:56789/path')\n  'http://example.com:56789/path'\n  >>> _parse_host('https://example.com:56789/path')\n  'https://example.com:56789/path'\n  >>> _parse_host('example.com:56789/path/')\n  'http://example.com:56789/path'\n  >>> _parse_host('[0001:002:003:0004::1]')\n  'http://[0001:002:003:0004::1]:11434'\n  >>> _parse_host('[0001:002:003:0004::1]:56789')\n  'http://[0001:002:003:0004::1]:56789'\n  >>> _parse_host('http://[0001:002:003:0004::1]')\n  'http://[0001:002:003:0004::1]:80'\n  >>> _parse_host('https://[0001:002:003:0004::1]')\n  'https://[0001:002:003:0004::1]:443'\n  >>> _parse_host('https://[0001:002:003:0004::1]:56789')\n  'https://[0001:002:003:0004::1]:56789'\n  >>> _parse_host('[0001:002:003:0004::1]/')\n  'http://[0001:002:003:0004::1]:11434'\n  >>> _parse_host('[0001:002:003:0004::1]:56789/')\n  'http://[0001:002:003:0004::1]:56789'\n  >>> _parse_host('[0001:002:003:0004::1]/path')\n  'http://[0001:002:003:0004::1]:11434/path'\n  >>> _parse_host('[0001:002:003:0004::1]:56789/path')\n  'http://[0001:002:003:0004::1]:56789/path'\n  >>> _parse_host('https://[0001:002:003:0004::1]:56789/path')\n  'https://[0001:002:003:0004::1]:56789/path'\n  >>> _parse_host('[0001:002:003:0004::1]:56789/path/')\n  'http://[0001:002:003:0004::1]:56789/path'\n  \"\"\"\n\n  host, port = host or '', 11434\n  scheme, _, hostport = host.partition('://')\n  if not hostport:\n    scheme, hostport = 'http', host\n  elif scheme == 'http':\n    port = 80\n  elif scheme == 'https':\n    port = 443\n\n  split = urllib.parse.urlsplit(f'{scheme}://{hostport}')\n  host = split.hostname or '127.0.0.1'\n  port = split.port or port\n\n  try:\n    if isinstance(ipaddress.ip_address(host), ipaddress.IPv6Address):\n      # Fix missing square brackets for IPv6 from urlsplit\n      host = f'[{host}]'\n  except ValueError:\n    ...\n\n  if path := split.path.strip('/'):\n    return f'{scheme}://{host}:{port}/{path}'\n\n  return f'{scheme}://{host}:{port}'\n"
  },
  {
    "path": "ollama/_types.py",
    "content": "import contextlib\nimport json\nfrom base64 import b64decode, b64encode\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Any, Dict, List, Mapping, Optional, Sequence, Union\n\nfrom pydantic import (\n  BaseModel,\n  ByteSize,\n  ConfigDict,\n  Field,\n  model_serializer,\n)\nfrom pydantic.json_schema import JsonSchemaValue\nfrom typing_extensions import Annotated, Literal\n\n\nclass SubscriptableBaseModel(BaseModel):\n  def __getitem__(self, key: str) -> Any:\n    \"\"\"\n    >>> msg = Message(role='user')\n    >>> msg['role']\n    'user'\n    >>> msg = Message(role='user')\n    >>> msg['nonexistent']\n    Traceback (most recent call last):\n    KeyError: 'nonexistent'\n    \"\"\"\n    if key in self:\n      return getattr(self, key)\n\n    raise KeyError(key)\n\n  def __setitem__(self, key: str, value: Any) -> None:\n    \"\"\"\n    >>> msg = Message(role='user')\n    >>> msg['role'] = 'assistant'\n    >>> msg['role']\n    'assistant'\n    >>> tool_call = Message.ToolCall(function=Message.ToolCall.Function(name='foo', arguments={}))\n    >>> msg = Message(role='user', content='hello')\n    >>> msg['tool_calls'] = [tool_call]\n    >>> msg['tool_calls'][0]['function']['name']\n    'foo'\n    \"\"\"\n    setattr(self, key, value)\n\n  def __contains__(self, key: str) -> bool:\n    \"\"\"\n    >>> msg = Message(role='user')\n    >>> 'nonexistent' in msg\n    False\n    >>> 'role' in msg\n    True\n    >>> 'content' in msg\n    False\n    >>> msg.content = 'hello!'\n    >>> 'content' in msg\n    True\n    >>> msg = Message(role='user', content='hello!')\n    >>> 'content' in msg\n    True\n    >>> 'tool_calls' in msg\n    False\n    >>> msg['tool_calls'] = []\n    >>> 'tool_calls' in msg\n    True\n    >>> msg['tool_calls'] = [Message.ToolCall(function=Message.ToolCall.Function(name='foo', arguments={}))]\n    >>> 'tool_calls' in msg\n    True\n    >>> msg['tool_calls'] = None\n    >>> 'tool_calls' in msg\n    True\n    >>> tool = Tool()\n    >>> 'type' in tool\n    True\n    \"\"\"\n    if key in self.model_fields_set:\n      return True\n\n    if value := self.__class__.model_fields.get(key):\n      return value.default is not None\n\n    return False\n\n  def get(self, key: str, default: Any = None) -> Any:\n    \"\"\"\n    >>> msg = Message(role='user')\n    >>> msg.get('role')\n    'user'\n    >>> msg = Message(role='user')\n    >>> msg.get('nonexistent')\n    >>> msg = Message(role='user')\n    >>> msg.get('nonexistent', 'default')\n    'default'\n    >>> msg = Message(role='user', tool_calls=[ Message.ToolCall(function=Message.ToolCall.Function(name='foo', arguments={}))])\n    >>> msg.get('tool_calls')[0]['function']['name']\n    'foo'\n    \"\"\"\n    return getattr(self, key) if hasattr(self, key) else default\n\n\nclass Options(SubscriptableBaseModel):\n  # load time options\n  numa: Optional[bool] = None\n  num_ctx: Optional[int] = None\n  num_batch: Optional[int] = None\n  num_gpu: Optional[int] = None\n  main_gpu: Optional[int] = None\n  low_vram: Optional[bool] = None\n  f16_kv: Optional[bool] = None\n  logits_all: Optional[bool] = None\n  vocab_only: Optional[bool] = None\n  use_mmap: Optional[bool] = None\n  use_mlock: Optional[bool] = None\n  embedding_only: Optional[bool] = None\n  num_thread: Optional[int] = None\n\n  # runtime options\n  num_keep: Optional[int] = None\n  seed: Optional[int] = None\n  num_predict: Optional[int] = None\n  top_k: Optional[int] = None\n  top_p: Optional[float] = None\n  tfs_z: Optional[float] = None\n  typical_p: Optional[float] = None\n  repeat_last_n: Optional[int] = None\n  temperature: Optional[float] = None\n  repeat_penalty: Optional[float] = None\n  presence_penalty: Optional[float] = None\n  frequency_penalty: Optional[float] = None\n  mirostat: Optional[int] = None\n  mirostat_tau: Optional[float] = None\n  mirostat_eta: Optional[float] = None\n  penalize_newline: Optional[bool] = None\n  stop: Optional[Sequence[str]] = None\n\n\nclass BaseRequest(SubscriptableBaseModel):\n  model: Annotated[str, Field(min_length=1)]\n  'Model to use for the request.'\n\n\nclass BaseStreamableRequest(BaseRequest):\n  stream: Optional[bool] = None\n  'Stream response.'\n\n\nclass BaseGenerateRequest(BaseStreamableRequest):\n  options: Optional[Union[Mapping[str, Any], Options]] = None\n  'Options to use for the request.'\n\n  format: Optional[Union[Literal['', 'json'], JsonSchemaValue]] = None\n  'Format of the response.'\n\n  keep_alive: Optional[Union[float, str]] = None\n  'Keep model alive for the specified duration.'\n\n\nclass Image(BaseModel):\n  value: Union[str, bytes, Path]\n\n  @model_serializer\n  def serialize_model(self):\n    if isinstance(self.value, (Path, bytes)):\n      return b64encode(self.value.read_bytes() if isinstance(self.value, Path) else self.value).decode()\n\n    if isinstance(self.value, str):\n      try:\n        if Path(self.value).exists():\n          return b64encode(Path(self.value).read_bytes()).decode()\n      except Exception:\n        # Long base64 string can't be wrapped in Path, so try to treat as base64 string\n        pass\n\n      # String might be a file path, but might not exist\n      if self.value.split('.')[-1] in ('png', 'jpg', 'jpeg', 'webp'):\n        raise ValueError(f'File {self.value} does not exist')\n\n      try:\n        # Try to decode to check if it's already base64\n        b64decode(self.value)\n        return self.value\n      except Exception:\n        raise ValueError('Invalid image data, expected base64 string or path to image file') from Exception\n\n\nclass GenerateRequest(BaseGenerateRequest):\n  prompt: Optional[str] = None\n  'Prompt to generate response from.'\n\n  suffix: Optional[str] = None\n  'Suffix to append to the response.'\n\n  system: Optional[str] = None\n  'System prompt to prepend to the prompt.'\n\n  template: Optional[str] = None\n  'Template to use for the response.'\n\n  context: Optional[Sequence[int]] = None\n  'Tokenized history to use for the response.'\n\n  raw: Optional[bool] = None\n\n  images: Optional[Sequence[Image]] = None\n  'Image data for multimodal models.'\n\n  think: Optional[Union[bool, Literal['low', 'medium', 'high']]] = None\n  'Enable thinking mode (for thinking models).'\n\n  logprobs: Optional[bool] = None\n  'Return log probabilities for generated tokens.'\n\n  top_logprobs: Optional[int] = None\n  'Number of alternative tokens and log probabilities to include per position (0-20).'\n\n  # Experimental image generation parameters\n  width: Optional[int] = None\n  'Width of the generated image in pixels (for image generation models).'\n\n  height: Optional[int] = None\n  'Height of the generated image in pixels (for image generation models).'\n\n  steps: Optional[int] = None\n  'Number of diffusion steps (for image generation models).'\n\n\nclass BaseGenerateResponse(SubscriptableBaseModel):\n  model: Optional[str] = None\n  'Model used to generate response.'\n\n  created_at: Optional[str] = None\n  'Time when the request was created.'\n\n  done: Optional[bool] = None\n  'True if response is complete, otherwise False. Useful for streaming to detect the final response.'\n\n  done_reason: Optional[str] = None\n  'Reason for completion. Only present when done is True.'\n\n  total_duration: Optional[int] = None\n  'Total duration in nanoseconds.'\n\n  load_duration: Optional[int] = None\n  'Load duration in nanoseconds.'\n\n  prompt_eval_count: Optional[int] = None\n  'Number of tokens evaluated in the prompt.'\n\n  prompt_eval_duration: Optional[int] = None\n  'Duration of evaluating the prompt in nanoseconds.'\n\n  eval_count: Optional[int] = None\n  'Number of tokens evaluated in inference.'\n\n  eval_duration: Optional[int] = None\n  'Duration of evaluating inference in nanoseconds.'\n\n\nclass TokenLogprob(SubscriptableBaseModel):\n  token: str\n  'Token text.'\n\n  logprob: float\n  'Log probability for the token.'\n\n\nclass Logprob(TokenLogprob):\n  top_logprobs: Optional[Sequence[TokenLogprob]] = None\n  'Most likely tokens and their log probabilities.'\n\n\nclass GenerateResponse(BaseGenerateResponse):\n  \"\"\"\n  Response returned by generate requests.\n  \"\"\"\n\n  response: Optional[str] = None\n  'Response content. When streaming, this contains a fragment of the response.'\n\n  thinking: Optional[str] = None\n  'Thinking content. Only present when thinking is enabled.'\n\n  context: Optional[Sequence[int]] = None\n  'Tokenized history up to the point of the response.'\n\n  logprobs: Optional[Sequence[Logprob]] = None\n  'Log probabilities for generated tokens.'\n\n  # Image generation response fields\n  image: Optional[str] = None\n  'Base64-encoded generated image data (for image generation models).'\n\n  # Streaming progress fields (for image generation)\n  completed: Optional[int] = None\n  'Number of completed steps (for image generation streaming).'\n\n  total: Optional[int] = None\n  'Total number of steps (for image generation streaming).'\n\n\nclass Message(SubscriptableBaseModel):\n  \"\"\"\n  Chat message.\n  \"\"\"\n\n  role: str\n  \"Assumed role of the message. Response messages has role 'assistant' or 'tool'.\"\n\n  content: Optional[str] = None\n  'Content of the message. Response messages contains message fragments when streaming.'\n\n  thinking: Optional[str] = None\n  'Thinking content. Only present when thinking is enabled.'\n\n  images: Optional[Sequence[Image]] = None\n  \"\"\"\n  Optional list of image data for multimodal models.\n\n  Valid input types are:\n\n  - `str` or path-like object: path to image file\n  - `bytes` or bytes-like object: raw image data\n\n  Valid image formats depend on the model. See the model card for more information.\n  \"\"\"\n\n  tool_name: Optional[str] = None\n  'Name of the executed tool.'\n\n  class ToolCall(SubscriptableBaseModel):\n    \"\"\"\n    Model tool calls.\n    \"\"\"\n\n    class Function(SubscriptableBaseModel):\n      \"\"\"\n      Tool call function.\n      \"\"\"\n\n      name: str\n      'Name of the function.'\n\n      arguments: Mapping[str, Any]\n      'Arguments of the function.'\n\n    function: Function\n    'Function to be called.'\n\n  tool_calls: Optional[Sequence[ToolCall]] = None\n  \"\"\"\n  Tools calls to be made by the model.\n  \"\"\"\n\n\nclass Tool(SubscriptableBaseModel):\n  type: Optional[str] = 'function'\n\n  class Function(SubscriptableBaseModel):\n    name: Optional[str] = None\n    description: Optional[str] = None\n\n    class Parameters(SubscriptableBaseModel):\n      model_config = ConfigDict(populate_by_name=True)\n      type: Optional[Literal['object']] = 'object'\n      defs: Optional[Any] = Field(None, alias='$defs')\n      items: Optional[Any] = None\n      required: Optional[Sequence[str]] = None\n\n      class Property(SubscriptableBaseModel):\n        model_config = ConfigDict(arbitrary_types_allowed=True)\n\n        type: Optional[Union[str, Sequence[str]]] = None\n        items: Optional[Any] = None\n        description: Optional[str] = None\n        enum: Optional[Sequence[Any]] = None\n\n      properties: Optional[Mapping[str, Property]] = None\n\n    parameters: Optional[Parameters] = None\n\n  function: Optional[Function] = None\n\n\nclass ChatRequest(BaseGenerateRequest):\n  @model_serializer(mode='wrap')\n  def serialize_model(self, nxt):\n    output = nxt(self)\n    if output.get('tools'):\n      for tool in output['tools']:\n        if 'function' in tool and 'parameters' in tool['function'] and 'defs' in tool['function']['parameters']:\n          tool['function']['parameters']['$defs'] = tool['function']['parameters'].pop('defs')\n    return output\n\n  messages: Optional[Sequence[Union[Mapping[str, Any], Message]]] = None\n  'Messages to chat with.'\n\n  tools: Optional[Sequence[Tool]] = None\n  'Tools to use for the chat.'\n\n  think: Optional[Union[bool, Literal['low', 'medium', 'high']]] = None\n  'Enable thinking mode (for thinking models).'\n\n  logprobs: Optional[bool] = None\n  'Return log probabilities for generated tokens.'\n\n  top_logprobs: Optional[int] = None\n  'Number of alternative tokens and log probabilities to include per position (0-20).'\n\n\nclass ChatResponse(BaseGenerateResponse):\n  \"\"\"\n  Response returned by chat requests.\n  \"\"\"\n\n  message: Message\n  'Response message.'\n\n  logprobs: Optional[Sequence[Logprob]] = None\n  'Log probabilities for generated tokens if requested.'\n\n\nclass EmbedRequest(BaseRequest):\n  input: Union[str, Sequence[str]]\n  'Input text to embed.'\n\n  truncate: Optional[bool] = None\n  'Truncate the input to the maximum token length.'\n\n  options: Optional[Union[Mapping[str, Any], Options]] = None\n  'Options to use for the request.'\n\n  keep_alive: Optional[Union[float, str]] = None\n\n  dimensions: Optional[int] = None\n  'Dimensions truncates the output embedding to the specified dimension.'\n\n\nclass EmbedResponse(BaseGenerateResponse):\n  \"\"\"\n  Response returned by embed requests.\n  \"\"\"\n\n  embeddings: Sequence[Sequence[float]]\n  'Embeddings of the inputs.'\n\n\nclass EmbeddingsRequest(BaseRequest):\n  prompt: Optional[str] = None\n  'Prompt to generate embeddings from.'\n\n  options: Optional[Union[Mapping[str, Any], Options]] = None\n  'Options to use for the request.'\n\n  keep_alive: Optional[Union[float, str]] = None\n\n\nclass EmbeddingsResponse(SubscriptableBaseModel):\n  \"\"\"\n  Response returned by embeddings requests.\n  \"\"\"\n\n  embedding: Sequence[float]\n  'Embedding of the prompt.'\n\n\nclass PullRequest(BaseStreamableRequest):\n  \"\"\"\n  Request to pull the model.\n  \"\"\"\n\n  insecure: Optional[bool] = None\n  'Allow insecure (HTTP) connections.'\n\n\nclass PushRequest(BaseStreamableRequest):\n  \"\"\"\n  Request to pull the model.\n  \"\"\"\n\n  insecure: Optional[bool] = None\n  'Allow insecure (HTTP) connections.'\n\n\nclass CreateRequest(BaseStreamableRequest):\n  @model_serializer(mode='wrap')\n  def serialize_model(self, nxt):\n    output = nxt(self)\n    if 'from_' in output:\n      output['from'] = output.pop('from_')\n    return output\n\n  \"\"\"\n  Request to create a new model.\n  \"\"\"\n  quantize: Optional[str] = None\n  from_: Optional[str] = None\n  files: Optional[Dict[str, str]] = None\n  adapters: Optional[Dict[str, str]] = None\n  template: Optional[str] = None\n  license: Optional[Union[str, List[str]]] = None\n  system: Optional[str] = None\n  parameters: Optional[Union[Mapping[str, Any], Options]] = None\n  messages: Optional[Sequence[Union[Mapping[str, Any], Message]]] = None\n\n\nclass ModelDetails(SubscriptableBaseModel):\n  parent_model: Optional[str] = None\n  format: Optional[str] = None\n  family: Optional[str] = None\n  families: Optional[Sequence[str]] = None\n  parameter_size: Optional[str] = None\n  quantization_level: Optional[str] = None\n\n\nclass ListResponse(SubscriptableBaseModel):\n  class Model(SubscriptableBaseModel):\n    model: Optional[str] = None\n    modified_at: Optional[datetime] = None\n    digest: Optional[str] = None\n    size: Optional[ByteSize] = None\n    details: Optional[ModelDetails] = None\n\n  models: Sequence[Model]\n  'List of models.'\n\n\nclass DeleteRequest(BaseRequest):\n  \"\"\"\n  Request to delete a model.\n  \"\"\"\n\n\nclass CopyRequest(BaseModel):\n  \"\"\"\n  Request to copy a model.\n  \"\"\"\n\n  source: str\n  'Source model to copy.'\n\n  destination: str\n  'Destination model to copy to.'\n\n\nclass StatusResponse(SubscriptableBaseModel):\n  status: Optional[str] = None\n\n\nclass ProgressResponse(StatusResponse):\n  completed: Optional[int] = None\n  total: Optional[int] = None\n  digest: Optional[str] = None\n\n\nclass ShowRequest(BaseRequest):\n  \"\"\"\n  Request to show model information.\n  \"\"\"\n\n\nclass ShowResponse(SubscriptableBaseModel):\n  modified_at: Optional[datetime] = None\n\n  template: Optional[str] = None\n\n  modelfile: Optional[str] = None\n\n  license: Optional[str] = None\n\n  details: Optional[ModelDetails] = None\n\n  modelinfo: Optional[Mapping[str, Any]] = Field(alias='model_info')\n\n  parameters: Optional[str] = None\n\n  capabilities: Optional[List[str]] = None\n\n\nclass ProcessResponse(SubscriptableBaseModel):\n  class Model(SubscriptableBaseModel):\n    model: Optional[str] = None\n    name: Optional[str] = None\n    digest: Optional[str] = None\n    expires_at: Optional[datetime] = None\n    size: Optional[ByteSize] = None\n    size_vram: Optional[ByteSize] = None\n    details: Optional[ModelDetails] = None\n    context_length: Optional[int] = None\n\n  models: Sequence[Model]\n\n\nclass WebSearchRequest(SubscriptableBaseModel):\n  query: str\n  max_results: Optional[int] = None\n\n\nclass WebSearchResult(SubscriptableBaseModel):\n  content: Optional[str] = None\n  title: Optional[str] = None\n  url: Optional[str] = None\n\n\nclass WebFetchRequest(SubscriptableBaseModel):\n  url: str\n\n\nclass WebSearchResponse(SubscriptableBaseModel):\n  results: Sequence[WebSearchResult]\n\n\nclass WebFetchResponse(SubscriptableBaseModel):\n  title: Optional[str] = None\n  content: Optional[str] = None\n  links: Optional[Sequence[str]] = None\n\n\nclass RequestError(Exception):\n  \"\"\"\n  Common class for request errors.\n  \"\"\"\n\n  def __init__(self, error: str):\n    super().__init__(error)\n    self.error = error\n    'Reason for the error.'\n\n\nclass ResponseError(Exception):\n  \"\"\"\n  Common class for response errors.\n  \"\"\"\n\n  def __init__(self, error: str, status_code: int = -1):\n    # try to parse content as JSON and extract 'error'\n    # fallback to raw content if JSON parsing fails\n    with contextlib.suppress(json.JSONDecodeError):\n      error = json.loads(error).get('error', error)\n\n    super().__init__(error)\n    self.error = error\n    'Reason for the error.'\n\n    self.status_code = status_code\n    'HTTP status code of the response.'\n\n  def __str__(self) -> str:\n    return f'{self.error} (status code: {self.status_code})'\n"
  },
  {
    "path": "ollama/_utils.py",
    "content": "from __future__ import annotations\n\nimport inspect\nimport re\nfrom collections import defaultdict\nfrom typing import Callable, Union\n\nimport pydantic\n\nfrom ollama._types import Tool\n\n\ndef _parse_docstring(doc_string: Union[str, None]) -> dict[str, str]:\n  parsed_docstring = defaultdict(str)\n  if not doc_string:\n    return parsed_docstring\n\n  key = str(hash(doc_string))\n  for line in doc_string.splitlines():\n    lowered_line = line.lower().strip()\n    if lowered_line.startswith('args:'):\n      key = 'args'\n    elif lowered_line.startswith(('returns:', 'yields:', 'raises:')):\n      key = '_'\n\n    else:\n      # maybe change to a list and join later\n      parsed_docstring[key] += f'{line.strip()}\\n'\n\n  last_key = None\n  for line in parsed_docstring['args'].splitlines():\n    line = line.strip()\n    if ':' in line:\n      # Split the line on either:\n      # 1. A parenthetical expression like (integer) - captured in group 1\n      # 2. A colon :\n      # Followed by optional whitespace. Only split on first occurrence.\n      parts = re.split(r'(?:\\(([^)]*)\\)|:)\\s*', line, maxsplit=1)\n\n      arg_name = parts[0].strip()\n      last_key = arg_name\n\n      # Get the description - will be in parts[1] if parenthetical or parts[-1] if after colon\n      arg_description = parts[-1].strip()\n      if len(parts) > 2 and parts[1]:  # Has parenthetical content\n        arg_description = parts[-1].split(':', 1)[-1].strip()\n\n      parsed_docstring[last_key] = arg_description\n\n    elif last_key and line:\n      parsed_docstring[last_key] += ' ' + line\n\n  return parsed_docstring\n\n\ndef convert_function_to_tool(func: Callable) -> Tool:\n  doc_string_hash = str(hash(inspect.getdoc(func)))\n  parsed_docstring = _parse_docstring(inspect.getdoc(func))\n  schema = type(\n    func.__name__,\n    (pydantic.BaseModel,),\n    {\n      '__annotations__': {k: v.annotation if v.annotation != inspect._empty else str for k, v in inspect.signature(func).parameters.items()},\n      '__signature__': inspect.signature(func),\n      '__doc__': parsed_docstring[doc_string_hash],\n    },\n  ).model_json_schema()\n\n  for k, v in schema.get('properties', {}).items():\n    # If type is missing, the default is string\n    types = {t.get('type', 'string') for t in v.get('anyOf')} if 'anyOf' in v else {v.get('type', 'string')}\n    if 'null' in types:\n      schema['required'].remove(k)\n      types.discard('null')\n\n    schema['properties'][k] = {\n      'description': parsed_docstring[k],\n      'type': ', '.join(types),\n    }\n\n  tool = Tool(\n    type='function',\n    function=Tool.Function(\n      name=func.__name__,\n      description=schema.get('description', ''),\n      parameters=Tool.Function.Parameters(**schema),\n    ),\n  )\n\n  return Tool.model_validate(tool)\n"
  },
  {
    "path": "ollama/py.typed",
    "content": ""
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = 'ollama'\ndescription = 'The official Python client for Ollama.'\nauthors = [\n    { email = 'hello@ollama.com' },\n]\nreadme = 'README.md'\nrequires-python = '>=3.8'\ndependencies = [\n    'httpx>=0.27',\n    'pydantic>=2.9',\n]\ndynamic = [ 'version' ]\nlicense = \"MIT\"\n\n[project.urls]\nhomepage = 'https://ollama.com'\nrepository = 'https://github.com/ollama/ollama-python'\nissues = 'https://github.com/ollama/ollama-python/issues'\n\n[build-system]\nrequires = [ 'hatchling', 'hatch-vcs' ]\nbuild-backend = 'hatchling.build'\n\n[tool.hatch.version]\nsource = 'vcs'\n\n[tool.hatch.envs.hatch-test]\ndefault-args = ['ollama', 'tests']\nextra-dependencies = [\n    'pytest-anyio',\n    'pytest-httpserver',\n]\n\n[tool.hatch.envs.hatch-static-analysis]\ndependencies = [ 'ruff>=0.9.1' ]\nconfig-path = 'none'\n\n[tool.ruff]\nline-length = 320 \nindent-width = 2\n\n[tool.ruff.format]\nquote-style = 'single'\nindent-style = 'space'\ndocstring-code-format = false\n\n[tool.ruff.lint]\nselect = [\n    'F', # pyflakes\n    'E', # pycodestyle errors\n    'W', # pycodestyle warnings\n    'I', # sort imports\n    'N', # pep8-naming\n    'ASYNC', # flake8-async\n    'FBT', # flake8-boolean-trap\n    'B', # flake8-bugbear\n    'C4', # flake8-comprehensions\n    'PIE', # flake8-pie\n    'SIM', # flake8-simplify\n    'FLY', # flynt\n    'RUF', # ruff-specific rules\n]\nignore = ['FBT001'] # Boolean-typed positional argument in function definition\n\n[tool.pytest.ini_options]\naddopts = ['--doctest-modules']\n"
  },
  {
    "path": "requirements.txt",
    "content": "# This file was autogenerated by uv via the following command:\n#    uv export\n-e .\nannotated-types==0.7.0 \\\n    --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \\\n    --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89\n    # via pydantic\nanyio==4.5.2 ; python_full_version < '3.9' \\\n    --hash=sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b \\\n    --hash=sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f\n    # via httpx\nanyio==4.8.0 ; python_full_version >= '3.9' \\\n    --hash=sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a \\\n    --hash=sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a\n    # via httpx\ncertifi==2025.1.31 \\\n    --hash=sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651 \\\n    --hash=sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe\n    # via\n    #   httpcore\n    #   httpx\nexceptiongroup==1.2.2 ; python_full_version < '3.11' \\\n    --hash=sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b \\\n    --hash=sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc\n    # via anyio\nh11==0.14.0 \\\n    --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \\\n    --hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761\n    # via httpcore\nhttpcore==1.0.7 \\\n    --hash=sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c \\\n    --hash=sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd\n    # via httpx\nhttpx==0.28.1 \\\n    --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \\\n    --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad\n    # via ollama\nidna==3.10 \\\n    --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \\\n    --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3\n    # via\n    #   anyio\n    #   httpx\npydantic==2.10.6 \\\n    --hash=sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584 \\\n    --hash=sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236\n    # via ollama\npydantic-core==2.27.2 \\\n    --hash=sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278 \\\n    --hash=sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50 \\\n    --hash=sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9 \\\n    --hash=sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f \\\n    --hash=sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6 \\\n    --hash=sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc \\\n    --hash=sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54 \\\n    --hash=sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630 \\\n    --hash=sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9 \\\n    --hash=sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236 \\\n    --hash=sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7 \\\n    --hash=sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee \\\n    --hash=sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b \\\n    --hash=sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048 \\\n    --hash=sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc \\\n    --hash=sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130 \\\n    --hash=sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4 \\\n    --hash=sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd \\\n    --hash=sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4 \\\n    --hash=sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7 \\\n    --hash=sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7 \\\n    --hash=sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4 \\\n    --hash=sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e \\\n    --hash=sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa \\\n    --hash=sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6 \\\n    --hash=sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962 \\\n    --hash=sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b \\\n    --hash=sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f \\\n    --hash=sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474 \\\n    --hash=sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5 \\\n    --hash=sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459 \\\n    --hash=sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf \\\n    --hash=sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a \\\n    --hash=sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c \\\n    --hash=sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76 \\\n    --hash=sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362 \\\n    --hash=sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4 \\\n    --hash=sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934 \\\n    --hash=sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320 \\\n    --hash=sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118 \\\n    --hash=sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96 \\\n    --hash=sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306 \\\n    --hash=sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046 \\\n    --hash=sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3 \\\n    --hash=sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2 \\\n    --hash=sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af \\\n    --hash=sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9 \\\n    --hash=sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67 \\\n    --hash=sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a \\\n    --hash=sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27 \\\n    --hash=sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35 \\\n    --hash=sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b \\\n    --hash=sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151 \\\n    --hash=sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b \\\n    --hash=sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154 \\\n    --hash=sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133 \\\n    --hash=sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef \\\n    --hash=sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145 \\\n    --hash=sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15 \\\n    --hash=sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4 \\\n    --hash=sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc \\\n    --hash=sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee \\\n    --hash=sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c \\\n    --hash=sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0 \\\n    --hash=sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5 \\\n    --hash=sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57 \\\n    --hash=sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b \\\n    --hash=sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8 \\\n    --hash=sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1 \\\n    --hash=sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da \\\n    --hash=sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e \\\n    --hash=sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc \\\n    --hash=sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993 \\\n    --hash=sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656 \\\n    --hash=sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4 \\\n    --hash=sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c \\\n    --hash=sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb \\\n    --hash=sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d \\\n    --hash=sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9 \\\n    --hash=sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e \\\n    --hash=sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1 \\\n    --hash=sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc \\\n    --hash=sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a \\\n    --hash=sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9 \\\n    --hash=sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506 \\\n    --hash=sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b \\\n    --hash=sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1 \\\n    --hash=sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d \\\n    --hash=sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99 \\\n    --hash=sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3 \\\n    --hash=sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31 \\\n    --hash=sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c \\\n    --hash=sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39 \\\n    --hash=sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a \\\n    --hash=sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308 \\\n    --hash=sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2 \\\n    --hash=sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228 \\\n    --hash=sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b \\\n    --hash=sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9 \\\n    --hash=sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad\n    # via pydantic\nsniffio==1.3.1 \\\n    --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \\\n    --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc\n    # via anyio\ntyping-extensions==4.12.2 \\\n    --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \\\n    --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8\n    # via\n    #   annotated-types\n    #   anyio\n    #   pydantic\n    #   pydantic-core\n"
  },
  {
    "path": "tests/test_client.py",
    "content": "import base64\nimport json\nimport os\nimport re\nimport tempfile\nfrom pathlib import Path\nfrom typing import Any\n\nimport pytest\nfrom httpx import Response as httpxResponse\nfrom pydantic import BaseModel\nfrom pytest_httpserver import HTTPServer, URIPattern\nfrom werkzeug.wrappers import Request, Response\n\nfrom ollama._client import CONNECTION_ERROR_MESSAGE, AsyncClient, Client, _copy_tools\nfrom ollama._types import Image, Message\n\nPNG_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGNgYGAAAAAEAAH2FzhVAAAAAElFTkSuQmCC'\nPNG_BYTES = base64.b64decode(PNG_BASE64)\n\npytestmark = pytest.mark.anyio\n\n\n@pytest.fixture\ndef anyio_backend():\n  return 'asyncio'\n\n\nclass PrefixPattern(URIPattern):\n  def __init__(self, prefix: str):\n    self.prefix = prefix\n\n  def match(self, uri):\n    return uri.startswith(self.prefix)\n\n\ndef test_client_chat(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/chat',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'messages': [{'role': 'user', 'content': 'Why is the sky blue?'}],\n      'tools': [],\n      'stream': False,\n    },\n  ).respond_with_json(\n    {\n      'model': 'dummy',\n      'message': {\n        'role': 'assistant',\n        'content': \"I don't know.\",\n      },\n    }\n  )\n\n  client = Client(httpserver.url_for('/'))\n  response = client.chat('dummy', messages=[{'role': 'user', 'content': 'Why is the sky blue?'}])\n  assert response['model'] == 'dummy'\n  assert response['message']['role'] == 'assistant'\n  assert response['message']['content'] == \"I don't know.\"\n\n\ndef test_client_chat_with_logprobs(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/chat',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'messages': [{'role': 'user', 'content': 'Hi'}],\n      'tools': [],\n      'stream': False,\n      'logprobs': True,\n      'top_logprobs': 3,\n    },\n  ).respond_with_json(\n    {\n      'model': 'dummy',\n      'message': {\n        'role': 'assistant',\n        'content': 'Hello',\n      },\n      'logprobs': [\n        {\n          'token': 'Hello',\n          'logprob': -0.1,\n          'top_logprobs': [\n            {'token': 'Hello', 'logprob': -0.1},\n            {'token': 'Hi', 'logprob': -1.0},\n          ],\n        }\n      ],\n    }\n  )\n\n  client = Client(httpserver.url_for('/'))\n  response = client.chat('dummy', messages=[{'role': 'user', 'content': 'Hi'}], logprobs=True, top_logprobs=3)\n  assert response['logprobs'][0]['token'] == 'Hello'\n  assert response['logprobs'][0]['top_logprobs'][1]['token'] == 'Hi'\n\n\ndef test_client_chat_stream(httpserver: HTTPServer):\n  def stream_handler(_: Request):\n    def generate():\n      for message in ['I ', \"don't \", 'know.']:\n        yield (\n          json.dumps(\n            {\n              'model': 'dummy',\n              'message': {\n                'role': 'assistant',\n                'content': message,\n              },\n            }\n          )\n          + '\\n'\n        )\n\n    return Response(generate())\n\n  httpserver.expect_ordered_request(\n    '/api/chat',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'messages': [{'role': 'user', 'content': 'Why is the sky blue?'}],\n      'tools': [],\n      'stream': True,\n    },\n  ).respond_with_handler(stream_handler)\n\n  client = Client(httpserver.url_for('/'))\n  response = client.chat('dummy', messages=[{'role': 'user', 'content': 'Why is the sky blue?'}], stream=True)\n\n  it = iter(['I ', \"don't \", 'know.'])\n  for part in response:\n    assert part['message']['role'] in 'assistant'\n    assert part['message']['content'] == next(it)\n\n\n@pytest.mark.parametrize('message_format', ('dict', 'pydantic_model'))\n@pytest.mark.parametrize('file_style', ('path', 'bytes'))\ndef test_client_chat_images(httpserver: HTTPServer, message_format: str, file_style: str, tmp_path):\n  from ollama._types import Image, Message\n\n  httpserver.expect_ordered_request(\n    '/api/chat',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'messages': [\n        {\n          'role': 'user',\n          'content': 'Why is the sky blue?',\n          'images': [PNG_BASE64],\n        },\n      ],\n      'tools': [],\n      'stream': False,\n    },\n  ).respond_with_json(\n    {\n      'model': 'dummy',\n      'message': {\n        'role': 'assistant',\n        'content': \"I don't know.\",\n      },\n    }\n  )\n\n  client = Client(httpserver.url_for('/'))\n\n  if file_style == 'bytes':\n    image_content = PNG_BYTES\n  elif file_style == 'path':\n    image_path = tmp_path / 'transparent.png'\n    image_path.write_bytes(PNG_BYTES)\n    image_content = str(image_path)\n\n  if message_format == 'pydantic_model':\n    messages = [Message(role='user', content='Why is the sky blue?', images=[Image(value=image_content)])]\n  elif message_format == 'dict':\n    messages = [{'role': 'user', 'content': 'Why is the sky blue?', 'images': [image_content]}]\n  else:\n    raise ValueError(f'Invalid message format: {message_format}')\n\n  response = client.chat('dummy', messages=messages)\n  assert response['model'] == 'dummy'\n  assert response['message']['role'] == 'assistant'\n  assert response['message']['content'] == \"I don't know.\"\n\n\ndef test_client_chat_format_json(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/chat',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'messages': [{'role': 'user', 'content': 'Why is the sky blue?'}],\n      'tools': [],\n      'format': 'json',\n      'stream': False,\n    },\n  ).respond_with_json(\n    {\n      'model': 'dummy',\n      'message': {\n        'role': 'assistant',\n        'content': '{\"answer\": \"Because of Rayleigh scattering\"}',\n      },\n    }\n  )\n\n  client = Client(httpserver.url_for('/'))\n  response = client.chat('dummy', messages=[{'role': 'user', 'content': 'Why is the sky blue?'}], format='json')\n  assert response['model'] == 'dummy'\n  assert response['message']['role'] == 'assistant'\n  assert response['message']['content'] == '{\"answer\": \"Because of Rayleigh scattering\"}'\n\n\ndef test_client_chat_format_pydantic(httpserver: HTTPServer):\n  class ResponseFormat(BaseModel):\n    answer: str\n    confidence: float\n\n  httpserver.expect_ordered_request(\n    '/api/chat',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'messages': [{'role': 'user', 'content': 'Why is the sky blue?'}],\n      'tools': [],\n      'format': {'title': 'ResponseFormat', 'type': 'object', 'properties': {'answer': {'title': 'Answer', 'type': 'string'}, 'confidence': {'title': 'Confidence', 'type': 'number'}}, 'required': ['answer', 'confidence']},\n      'stream': False,\n    },\n  ).respond_with_json(\n    {\n      'model': 'dummy',\n      'message': {\n        'role': 'assistant',\n        'content': '{\"answer\": \"Because of Rayleigh scattering\", \"confidence\": 0.95}',\n      },\n    }\n  )\n\n  client = Client(httpserver.url_for('/'))\n  response = client.chat('dummy', messages=[{'role': 'user', 'content': 'Why is the sky blue?'}], format=ResponseFormat.model_json_schema())\n  assert response['model'] == 'dummy'\n  assert response['message']['role'] == 'assistant'\n  assert response['message']['content'] == '{\"answer\": \"Because of Rayleigh scattering\", \"confidence\": 0.95}'\n\n\nasync def test_async_client_chat_format_json(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/chat',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'messages': [{'role': 'user', 'content': 'Why is the sky blue?'}],\n      'tools': [],\n      'format': 'json',\n      'stream': False,\n    },\n  ).respond_with_json(\n    {\n      'model': 'dummy',\n      'message': {\n        'role': 'assistant',\n        'content': '{\"answer\": \"Because of Rayleigh scattering\"}',\n      },\n    }\n  )\n\n  client = AsyncClient(httpserver.url_for('/'))\n  response = await client.chat('dummy', messages=[{'role': 'user', 'content': 'Why is the sky blue?'}], format='json')\n  assert response['model'] == 'dummy'\n  assert response['message']['role'] == 'assistant'\n  assert response['message']['content'] == '{\"answer\": \"Because of Rayleigh scattering\"}'\n\n\nasync def test_async_client_chat_format_pydantic(httpserver: HTTPServer):\n  class ResponseFormat(BaseModel):\n    answer: str\n    confidence: float\n\n  httpserver.expect_ordered_request(\n    '/api/chat',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'messages': [{'role': 'user', 'content': 'Why is the sky blue?'}],\n      'tools': [],\n      'format': {'title': 'ResponseFormat', 'type': 'object', 'properties': {'answer': {'title': 'Answer', 'type': 'string'}, 'confidence': {'title': 'Confidence', 'type': 'number'}}, 'required': ['answer', 'confidence']},\n      'stream': False,\n    },\n  ).respond_with_json(\n    {\n      'model': 'dummy',\n      'message': {\n        'role': 'assistant',\n        'content': '{\"answer\": \"Because of Rayleigh scattering\", \"confidence\": 0.95}',\n      },\n    }\n  )\n\n  client = AsyncClient(httpserver.url_for('/'))\n  response = await client.chat('dummy', messages=[{'role': 'user', 'content': 'Why is the sky blue?'}], format=ResponseFormat.model_json_schema())\n  assert response['model'] == 'dummy'\n  assert response['message']['role'] == 'assistant'\n  assert response['message']['content'] == '{\"answer\": \"Because of Rayleigh scattering\", \"confidence\": 0.95}'\n\n\ndef test_client_generate(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/generate',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'prompt': 'Why is the sky blue?',\n      'stream': False,\n    },\n  ).respond_with_json(\n    {\n      'model': 'dummy',\n      'response': 'Because it is.',\n    }\n  )\n\n  client = Client(httpserver.url_for('/'))\n  response = client.generate('dummy', 'Why is the sky blue?')\n  assert response['model'] == 'dummy'\n  assert response['response'] == 'Because it is.'\n\n\ndef test_client_generate_with_logprobs(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/generate',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'prompt': 'Why',\n      'stream': False,\n      'logprobs': True,\n      'top_logprobs': 2,\n    },\n  ).respond_with_json(\n    {\n      'model': 'dummy',\n      'response': 'Hello',\n      'logprobs': [\n        {\n          'token': 'Hello',\n          'logprob': -0.2,\n          'top_logprobs': [\n            {'token': 'Hello', 'logprob': -0.2},\n            {'token': 'Hi', 'logprob': -1.5},\n          ],\n        }\n      ],\n    }\n  )\n\n  client = Client(httpserver.url_for('/'))\n  response = client.generate('dummy', 'Why', logprobs=True, top_logprobs=2)\n  assert response['logprobs'][0]['token'] == 'Hello'\n  assert response['logprobs'][0]['top_logprobs'][1]['token'] == 'Hi'\n\n\ndef test_client_generate_with_image_type(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/generate',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'prompt': 'What is in this image?',\n      'stream': False,\n      'images': [PNG_BASE64],\n    },\n  ).respond_with_json(\n    {\n      'model': 'dummy',\n      'response': 'A blue sky.',\n    }\n  )\n\n  client = Client(httpserver.url_for('/'))\n  response = client.generate('dummy', 'What is in this image?', images=[Image(value=PNG_BASE64)])\n  assert response['model'] == 'dummy'\n  assert response['response'] == 'A blue sky.'\n\n\ndef test_client_generate_with_invalid_image(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/generate',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'prompt': 'What is in this image?',\n      'stream': False,\n      'images': ['invalid_base64'],\n    },\n  ).respond_with_json({'error': 'Invalid image data'}, status=400)\n\n  client = Client(httpserver.url_for('/'))\n  with pytest.raises(ValueError):\n    client.generate('dummy', 'What is in this image?', images=[Image(value='invalid_base64')])\n\n\ndef test_client_generate_stream(httpserver: HTTPServer):\n  def stream_handler(_: Request):\n    def generate():\n      for message in ['Because ', 'it ', 'is.']:\n        yield (\n          json.dumps(\n            {\n              'model': 'dummy',\n              'response': message,\n            }\n          )\n          + '\\n'\n        )\n\n    return Response(generate())\n\n  httpserver.expect_ordered_request(\n    '/api/generate',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'prompt': 'Why is the sky blue?',\n      'stream': True,\n    },\n  ).respond_with_handler(stream_handler)\n\n  client = Client(httpserver.url_for('/'))\n  response = client.generate('dummy', 'Why is the sky blue?', stream=True)\n\n  it = iter(['Because ', 'it ', 'is.'])\n  for part in response:\n    assert part['model'] == 'dummy'\n    assert part['response'] == next(it)\n\n\ndef test_client_generate_images(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/generate',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'prompt': 'Why is the sky blue?',\n      'stream': False,\n      'images': [PNG_BASE64],\n    },\n  ).respond_with_json(\n    {\n      'model': 'dummy',\n      'response': 'Because it is.',\n    }\n  )\n\n  client = Client(httpserver.url_for('/'))\n\n  with tempfile.NamedTemporaryFile() as temp:\n    temp.write(PNG_BYTES)\n    temp.flush()\n    response = client.generate('dummy', 'Why is the sky blue?', images=[temp.name])\n    assert response['model'] == 'dummy'\n    assert response['response'] == 'Because it is.'\n\n\ndef test_client_generate_format_json(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/generate',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'prompt': 'Why is the sky blue?',\n      'format': 'json',\n      'stream': False,\n    },\n  ).respond_with_json(\n    {\n      'model': 'dummy',\n      'response': '{\"answer\": \"Because of Rayleigh scattering\"}',\n    }\n  )\n\n  client = Client(httpserver.url_for('/'))\n  response = client.generate('dummy', 'Why is the sky blue?', format='json')\n  assert response['model'] == 'dummy'\n  assert response['response'] == '{\"answer\": \"Because of Rayleigh scattering\"}'\n\n\ndef test_client_generate_format_pydantic(httpserver: HTTPServer):\n  class ResponseFormat(BaseModel):\n    answer: str\n    confidence: float\n\n  httpserver.expect_ordered_request(\n    '/api/generate',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'prompt': 'Why is the sky blue?',\n      'format': {'title': 'ResponseFormat', 'type': 'object', 'properties': {'answer': {'title': 'Answer', 'type': 'string'}, 'confidence': {'title': 'Confidence', 'type': 'number'}}, 'required': ['answer', 'confidence']},\n      'stream': False,\n    },\n  ).respond_with_json(\n    {\n      'model': 'dummy',\n      'response': '{\"answer\": \"Because of Rayleigh scattering\", \"confidence\": 0.95}',\n    }\n  )\n\n  client = Client(httpserver.url_for('/'))\n  response = client.generate('dummy', 'Why is the sky blue?', format=ResponseFormat.model_json_schema())\n  assert response['model'] == 'dummy'\n  assert response['response'] == '{\"answer\": \"Because of Rayleigh scattering\", \"confidence\": 0.95}'\n\n\nasync def test_async_client_generate_format_json(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/generate',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'prompt': 'Why is the sky blue?',\n      'format': 'json',\n      'stream': False,\n    },\n  ).respond_with_json(\n    {\n      'model': 'dummy',\n      'response': '{\"answer\": \"Because of Rayleigh scattering\"}',\n    }\n  )\n\n  client = AsyncClient(httpserver.url_for('/'))\n  response = await client.generate('dummy', 'Why is the sky blue?', format='json')\n  assert response['model'] == 'dummy'\n  assert response['response'] == '{\"answer\": \"Because of Rayleigh scattering\"}'\n\n\nasync def test_async_client_generate_format_pydantic(httpserver: HTTPServer):\n  class ResponseFormat(BaseModel):\n    answer: str\n    confidence: float\n\n  httpserver.expect_ordered_request(\n    '/api/generate',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'prompt': 'Why is the sky blue?',\n      'format': {'title': 'ResponseFormat', 'type': 'object', 'properties': {'answer': {'title': 'Answer', 'type': 'string'}, 'confidence': {'title': 'Confidence', 'type': 'number'}}, 'required': ['answer', 'confidence']},\n      'stream': False,\n    },\n  ).respond_with_json(\n    {\n      'model': 'dummy',\n      'response': '{\"answer\": \"Because of Rayleigh scattering\", \"confidence\": 0.95}',\n    }\n  )\n\n  client = AsyncClient(httpserver.url_for('/'))\n  response = await client.generate('dummy', 'Why is the sky blue?', format=ResponseFormat.model_json_schema())\n  assert response['model'] == 'dummy'\n  assert response['response'] == '{\"answer\": \"Because of Rayleigh scattering\", \"confidence\": 0.95}'\n\n\ndef test_client_generate_image(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/generate',\n    method='POST',\n    json={\n      'model': 'dummy-image',\n      'prompt': 'a sunset over mountains',\n      'stream': False,\n      'width': 1024,\n      'height': 768,\n      'steps': 20,\n    },\n  ).respond_with_json(\n    {\n      'model': 'dummy-image',\n      'image': PNG_BASE64,\n      'done': True,\n      'done_reason': 'stop',\n    }\n  )\n\n  client = Client(httpserver.url_for('/'))\n  response = client.generate('dummy-image', 'a sunset over mountains', width=1024, height=768, steps=20)\n  assert response['model'] == 'dummy-image'\n  assert response['image'] == PNG_BASE64\n  assert response['done'] is True\n\n\ndef test_client_generate_image_stream(httpserver: HTTPServer):\n  def stream_handler(_: Request):\n    def generate():\n      # Progress updates\n      for i in range(1, 4):\n        yield (\n          json.dumps(\n            {\n              'model': 'dummy-image',\n              'completed': i,\n              'total': 3,\n              'done': False,\n            }\n          )\n          + '\\n'\n        )\n      # Final response with image\n      yield (\n        json.dumps(\n          {\n            'model': 'dummy-image',\n            'image': PNG_BASE64,\n            'done': True,\n            'done_reason': 'stop',\n          }\n        )\n        + '\\n'\n      )\n\n    return Response(generate())\n\n  httpserver.expect_ordered_request(\n    '/api/generate',\n    method='POST',\n    json={\n      'model': 'dummy-image',\n      'prompt': 'a sunset over mountains',\n      'stream': True,\n      'width': 512,\n      'height': 512,\n    },\n  ).respond_with_handler(stream_handler)\n\n  client = Client(httpserver.url_for('/'))\n  response = client.generate('dummy-image', 'a sunset over mountains', stream=True, width=512, height=512)\n\n  parts = list(response)\n  # Check progress updates\n  assert parts[0]['completed'] == 1\n  assert parts[0]['total'] == 3\n  assert parts[0]['done'] is False\n  # Check final response\n  assert parts[-1]['image'] == PNG_BASE64\n  assert parts[-1]['done'] is True\n\n\nasync def test_async_client_generate_image(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/generate',\n    method='POST',\n    json={\n      'model': 'dummy-image',\n      'prompt': 'a robot painting',\n      'stream': False,\n      'width': 1024,\n      'height': 1024,\n    },\n  ).respond_with_json(\n    {\n      'model': 'dummy-image',\n      'image': PNG_BASE64,\n      'done': True,\n    }\n  )\n\n  client = AsyncClient(httpserver.url_for('/'))\n  response = await client.generate('dummy-image', 'a robot painting', width=1024, height=1024)\n  assert response['model'] == 'dummy-image'\n  assert response['image'] == PNG_BASE64\n\n\ndef test_client_pull(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/pull',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'insecure': False,\n      'stream': False,\n    },\n  ).respond_with_json({'status': 'success'})\n\n  client = Client(httpserver.url_for('/'))\n  response = client.pull('dummy')\n  assert response['status'] == 'success'\n\n\ndef test_client_pull_stream(httpserver: HTTPServer):\n  def stream_handler(_: Request):\n    def generate():\n      yield json.dumps({'status': 'pulling manifest'}) + '\\n'\n      yield json.dumps({'status': 'verifying sha256 digest'}) + '\\n'\n      yield json.dumps({'status': 'writing manifest'}) + '\\n'\n      yield json.dumps({'status': 'removing any unused layers'}) + '\\n'\n      yield json.dumps({'status': 'success'}) + '\\n'\n\n    return Response(generate())\n\n  httpserver.expect_ordered_request(\n    '/api/pull',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'insecure': False,\n      'stream': True,\n    },\n  ).respond_with_handler(stream_handler)\n\n  client = Client(httpserver.url_for('/'))\n  response = client.pull('dummy', stream=True)\n\n  it = iter(['pulling manifest', 'verifying sha256 digest', 'writing manifest', 'removing any unused layers', 'success'])\n  for part in response:\n    assert part['status'] == next(it)\n\n\ndef test_client_push(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/push',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'insecure': False,\n      'stream': False,\n    },\n  ).respond_with_json({'status': 'success'})\n\n  client = Client(httpserver.url_for('/'))\n  response = client.push('dummy')\n  assert response['status'] == 'success'\n\n\ndef test_client_push_stream(httpserver: HTTPServer):\n  def stream_handler(_: Request):\n    def generate():\n      yield json.dumps({'status': 'retrieving manifest'}) + '\\n'\n      yield json.dumps({'status': 'pushing manifest'}) + '\\n'\n      yield json.dumps({'status': 'success'}) + '\\n'\n\n    return Response(generate())\n\n  httpserver.expect_ordered_request(\n    '/api/push',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'insecure': False,\n      'stream': True,\n    },\n  ).respond_with_handler(stream_handler)\n\n  client = Client(httpserver.url_for('/'))\n  response = client.push('dummy', stream=True)\n\n  it = iter(['retrieving manifest', 'pushing manifest', 'success'])\n  for part in response:\n    assert part['status'] == next(it)\n\n\n@pytest.fixture\ndef userhomedir():\n  with tempfile.TemporaryDirectory() as temp:\n    home = os.getenv('HOME', '')\n    os.environ['HOME'] = temp\n    yield Path(temp)\n    os.environ['HOME'] = home\n\n\ndef test_client_create_with_blob(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/create',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'files': {'test.gguf': 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'},\n      'stream': False,\n    },\n  ).respond_with_json({'status': 'success'})\n\n  client = Client(httpserver.url_for('/'))\n\n  with tempfile.NamedTemporaryFile():\n    response = client.create('dummy', files={'test.gguf': 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'})\n    assert response['status'] == 'success'\n\n\ndef test_client_create_with_parameters_roundtrip(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/create',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'quantize': 'q4_k_m',\n      'from': 'mymodel',\n      'adapters': {'someadapter.gguf': 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'},\n      'template': '[INST] <<SYS>>{{.System}}<</SYS>>\\n{{.Prompt}} [/INST]',\n      'license': 'this is my license',\n      'system': '\\nUse\\nmultiline\\nstrings.\\n',\n      'parameters': {'stop': ['[INST]', '[/INST]', '<<SYS>>', '<</SYS>>'], 'pi': 3.14159},\n      'messages': [{'role': 'user', 'content': 'Hello there!'}, {'role': 'assistant', 'content': 'Hello there yourself!'}],\n      'stream': False,\n    },\n  ).respond_with_json({'status': 'success'})\n\n  client = Client(httpserver.url_for('/'))\n\n  with tempfile.NamedTemporaryFile():\n    response = client.create(\n      'dummy',\n      quantize='q4_k_m',\n      from_='mymodel',\n      adapters={'someadapter.gguf': 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'},\n      template='[INST] <<SYS>>{{.System}}<</SYS>>\\n{{.Prompt}} [/INST]',\n      license='this is my license',\n      system='\\nUse\\nmultiline\\nstrings.\\n',\n      parameters={'stop': ['[INST]', '[/INST]', '<<SYS>>', '<</SYS>>'], 'pi': 3.14159},\n      messages=[{'role': 'user', 'content': 'Hello there!'}, {'role': 'assistant', 'content': 'Hello there yourself!'}],\n      stream=False,\n    )\n    assert response['status'] == 'success'\n\n\ndef test_client_create_from_library(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/create',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'from': 'llama2',\n      'stream': False,\n    },\n  ).respond_with_json({'status': 'success'})\n\n  client = Client(httpserver.url_for('/'))\n\n  response = client.create('dummy', from_='llama2')\n  assert response['status'] == 'success'\n\n\ndef test_client_create_blob(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(re.compile('^/api/blobs/sha256[:-][0-9a-fA-F]{64}$'), method='POST').respond_with_response(Response(status=201))\n\n  client = Client(httpserver.url_for('/'))\n\n  with tempfile.NamedTemporaryFile() as blob:\n    response = client.create_blob(blob.name)\n    assert response == 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'\n\n\ndef test_client_create_blob_exists(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(PrefixPattern('/api/blobs/'), method='POST').respond_with_response(Response(status=200))\n\n  client = Client(httpserver.url_for('/'))\n\n  with tempfile.NamedTemporaryFile() as blob:\n    response = client.create_blob(blob.name)\n    assert response == 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'\n\n\ndef test_client_delete(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(PrefixPattern('/api/delete'), method='DELETE').respond_with_response(Response(status=200))\n  client = Client(httpserver.url_for('/api/delete'))\n  response = client.delete('dummy')\n  assert response['status'] == 'success'\n\n\ndef test_client_copy(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(PrefixPattern('/api/copy'), method='POST').respond_with_response(Response(status=200))\n  client = Client(httpserver.url_for('/api/copy'))\n  response = client.copy('dum', 'dummer')\n  assert response['status'] == 'success'\n\n\nasync def test_async_client_chat(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/chat',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'messages': [{'role': 'user', 'content': 'Why is the sky blue?'}],\n      'tools': [],\n      'stream': False,\n    },\n  ).respond_with_json(\n    {\n      'model': 'dummy',\n      'message': {\n        'role': 'assistant',\n        'content': \"I don't know.\",\n      },\n    }\n  )\n\n  client = AsyncClient(httpserver.url_for('/'))\n  response = await client.chat('dummy', messages=[{'role': 'user', 'content': 'Why is the sky blue?'}])\n  assert response['model'] == 'dummy'\n  assert response['message']['role'] == 'assistant'\n  assert response['message']['content'] == \"I don't know.\"\n\n\nasync def test_async_client_chat_stream(httpserver: HTTPServer):\n  def stream_handler(_: Request):\n    def generate():\n      for message in ['I ', \"don't \", 'know.']:\n        yield (\n          json.dumps(\n            {\n              'model': 'dummy',\n              'message': {\n                'role': 'assistant',\n                'content': message,\n              },\n            }\n          )\n          + '\\n'\n        )\n\n    return Response(generate())\n\n  httpserver.expect_ordered_request(\n    '/api/chat',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'messages': [{'role': 'user', 'content': 'Why is the sky blue?'}],\n      'tools': [],\n      'stream': True,\n    },\n  ).respond_with_handler(stream_handler)\n\n  client = AsyncClient(httpserver.url_for('/'))\n  response = await client.chat('dummy', messages=[{'role': 'user', 'content': 'Why is the sky blue?'}], stream=True)\n\n  it = iter(['I ', \"don't \", 'know.'])\n  async for part in response:\n    assert part['message']['role'] == 'assistant'\n    assert part['message']['content'] == next(it)\n\n\nasync def test_async_client_chat_images(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/chat',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'messages': [\n        {\n          'role': 'user',\n          'content': 'Why is the sky blue?',\n          'images': [PNG_BASE64],\n        },\n      ],\n      'tools': [],\n      'stream': False,\n    },\n  ).respond_with_json(\n    {\n      'model': 'dummy',\n      'message': {\n        'role': 'assistant',\n        'content': \"I don't know.\",\n      },\n    }\n  )\n\n  client = AsyncClient(httpserver.url_for('/'))\n\n  response = await client.chat('dummy', messages=[{'role': 'user', 'content': 'Why is the sky blue?', 'images': [PNG_BYTES]}])\n  assert response['model'] == 'dummy'\n  assert response['message']['role'] == 'assistant'\n  assert response['message']['content'] == \"I don't know.\"\n\n\nasync def test_async_client_generate(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/generate',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'prompt': 'Why is the sky blue?',\n      'stream': False,\n    },\n  ).respond_with_json(\n    {\n      'model': 'dummy',\n      'response': 'Because it is.',\n    }\n  )\n\n  client = AsyncClient(httpserver.url_for('/'))\n  response = await client.generate('dummy', 'Why is the sky blue?')\n  assert response['model'] == 'dummy'\n  assert response['response'] == 'Because it is.'\n\n\nasync def test_async_client_generate_stream(httpserver: HTTPServer):\n  def stream_handler(_: Request):\n    def generate():\n      for message in ['Because ', 'it ', 'is.']:\n        yield (\n          json.dumps(\n            {\n              'model': 'dummy',\n              'response': message,\n            }\n          )\n          + '\\n'\n        )\n\n    return Response(generate())\n\n  httpserver.expect_ordered_request(\n    '/api/generate',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'prompt': 'Why is the sky blue?',\n      'stream': True,\n    },\n  ).respond_with_handler(stream_handler)\n\n  client = AsyncClient(httpserver.url_for('/'))\n  response = await client.generate('dummy', 'Why is the sky blue?', stream=True)\n\n  it = iter(['Because ', 'it ', 'is.'])\n  async for part in response:\n    assert part['model'] == 'dummy'\n    assert part['response'] == next(it)\n\n\nasync def test_async_client_generate_images(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/generate',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'prompt': 'Why is the sky blue?',\n      'stream': False,\n      'images': [PNG_BASE64],\n    },\n  ).respond_with_json(\n    {\n      'model': 'dummy',\n      'response': 'Because it is.',\n    }\n  )\n\n  client = AsyncClient(httpserver.url_for('/'))\n\n  with tempfile.NamedTemporaryFile() as temp:\n    temp.write(PNG_BYTES)\n    temp.flush()\n    response = await client.generate('dummy', 'Why is the sky blue?', images=[temp.name])\n    assert response['model'] == 'dummy'\n    assert response['response'] == 'Because it is.'\n\n\nasync def test_async_client_pull(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/pull',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'insecure': False,\n      'stream': False,\n    },\n  ).respond_with_json({'status': 'success'})\n\n  client = AsyncClient(httpserver.url_for('/'))\n  response = await client.pull('dummy')\n  assert response['status'] == 'success'\n\n\nasync def test_async_client_pull_stream(httpserver: HTTPServer):\n  def stream_handler(_: Request):\n    def generate():\n      yield json.dumps({'status': 'pulling manifest'}) + '\\n'\n      yield json.dumps({'status': 'verifying sha256 digest'}) + '\\n'\n      yield json.dumps({'status': 'writing manifest'}) + '\\n'\n      yield json.dumps({'status': 'removing any unused layers'}) + '\\n'\n      yield json.dumps({'status': 'success'}) + '\\n'\n\n    return Response(generate())\n\n  httpserver.expect_ordered_request(\n    '/api/pull',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'insecure': False,\n      'stream': True,\n    },\n  ).respond_with_handler(stream_handler)\n\n  client = AsyncClient(httpserver.url_for('/'))\n  response = await client.pull('dummy', stream=True)\n\n  it = iter(['pulling manifest', 'verifying sha256 digest', 'writing manifest', 'removing any unused layers', 'success'])\n  async for part in response:\n    assert part['status'] == next(it)\n\n\nasync def test_async_client_push(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/push',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'insecure': False,\n      'stream': False,\n    },\n  ).respond_with_json({'status': 'success'})\n\n  client = AsyncClient(httpserver.url_for('/'))\n  response = await client.push('dummy')\n  assert response['status'] == 'success'\n\n\nasync def test_async_client_push_stream(httpserver: HTTPServer):\n  def stream_handler(_: Request):\n    def generate():\n      yield json.dumps({'status': 'retrieving manifest'}) + '\\n'\n      yield json.dumps({'status': 'pushing manifest'}) + '\\n'\n      yield json.dumps({'status': 'success'}) + '\\n'\n\n    return Response(generate())\n\n  httpserver.expect_ordered_request(\n    '/api/push',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'insecure': False,\n      'stream': True,\n    },\n  ).respond_with_handler(stream_handler)\n\n  client = AsyncClient(httpserver.url_for('/'))\n  response = await client.push('dummy', stream=True)\n\n  it = iter(['retrieving manifest', 'pushing manifest', 'success'])\n  async for part in response:\n    assert part['status'] == next(it)\n\n\nasync def test_async_client_create_with_blob(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/create',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'files': {'test.gguf': 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'},\n      'stream': False,\n    },\n  ).respond_with_json({'status': 'success'})\n\n  client = AsyncClient(httpserver.url_for('/'))\n\n  with tempfile.NamedTemporaryFile():\n    response = await client.create('dummy', files={'test.gguf': 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'})\n    assert response['status'] == 'success'\n\n\nasync def test_async_client_create_with_parameters_roundtrip(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/create',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'quantize': 'q4_k_m',\n      'from': 'mymodel',\n      'adapters': {'someadapter.gguf': 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'},\n      'template': '[INST] <<SYS>>{{.System}}<</SYS>>\\n{{.Prompt}} [/INST]',\n      'license': 'this is my license',\n      'system': '\\nUse\\nmultiline\\nstrings.\\n',\n      'parameters': {'stop': ['[INST]', '[/INST]', '<<SYS>>', '<</SYS>>'], 'pi': 3.14159},\n      'messages': [{'role': 'user', 'content': 'Hello there!'}, {'role': 'assistant', 'content': 'Hello there yourself!'}],\n      'stream': False,\n    },\n  ).respond_with_json({'status': 'success'})\n\n  client = AsyncClient(httpserver.url_for('/'))\n\n  with tempfile.NamedTemporaryFile():\n    response = await client.create(\n      'dummy',\n      quantize='q4_k_m',\n      from_='mymodel',\n      adapters={'someadapter.gguf': 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'},\n      template='[INST] <<SYS>>{{.System}}<</SYS>>\\n{{.Prompt}} [/INST]',\n      license='this is my license',\n      system='\\nUse\\nmultiline\\nstrings.\\n',\n      parameters={'stop': ['[INST]', '[/INST]', '<<SYS>>', '<</SYS>>'], 'pi': 3.14159},\n      messages=[{'role': 'user', 'content': 'Hello there!'}, {'role': 'assistant', 'content': 'Hello there yourself!'}],\n      stream=False,\n    )\n    assert response['status'] == 'success'\n\n\nasync def test_async_client_create_from_library(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(\n    '/api/create',\n    method='POST',\n    json={\n      'model': 'dummy',\n      'from': 'llama2',\n      'stream': False,\n    },\n  ).respond_with_json({'status': 'success'})\n\n  client = AsyncClient(httpserver.url_for('/'))\n\n  response = await client.create('dummy', from_='llama2')\n  assert response['status'] == 'success'\n\n\nasync def test_async_client_create_blob(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(re.compile('^/api/blobs/sha256[:-][0-9a-fA-F]{64}$'), method='POST').respond_with_response(Response(status=201))\n\n  client = AsyncClient(httpserver.url_for('/'))\n\n  with tempfile.NamedTemporaryFile() as blob:\n    response = await client.create_blob(blob.name)\n    assert response == 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'\n\n\nasync def test_async_client_create_blob_exists(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(PrefixPattern('/api/blobs/'), method='POST').respond_with_response(Response(status=200))\n\n  client = AsyncClient(httpserver.url_for('/'))\n\n  with tempfile.NamedTemporaryFile() as blob:\n    response = await client.create_blob(blob.name)\n    assert response == 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'\n\n\nasync def test_async_client_delete(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(PrefixPattern('/api/delete'), method='DELETE').respond_with_response(Response(status=200))\n  client = AsyncClient(httpserver.url_for('/api/delete'))\n  response = await client.delete('dummy')\n  assert response['status'] == 'success'\n\n\nasync def test_async_client_copy(httpserver: HTTPServer):\n  httpserver.expect_ordered_request(PrefixPattern('/api/copy'), method='POST').respond_with_response(Response(status=200))\n  client = AsyncClient(httpserver.url_for('/api/copy'))\n  response = await client.copy('dum', 'dummer')\n  assert response['status'] == 'success'\n\n\ndef test_headers():\n  client = Client()\n  assert client._client.headers['content-type'] == 'application/json'\n  assert client._client.headers['accept'] == 'application/json'\n  assert client._client.headers['user-agent'].startswith('ollama-python/')\n\n  client = Client(\n    headers={\n      'X-Custom': 'value',\n      'Content-Type': 'text/plain',\n    }\n  )\n  assert client._client.headers['x-custom'] == 'value'\n  assert client._client.headers['content-type'] == 'application/json'\n\n\ndef test_copy_tools():\n  def func1(x: int) -> str:\n    \"\"\"Simple function 1.\n    Args:\n        x (integer): A number\n    \"\"\"\n\n  def func2(y: str) -> int:\n    \"\"\"Simple function 2.\n    Args:\n        y (string): A string\n    \"\"\"\n\n  # Test with list of functions\n  tools = list(_copy_tools([func1, func2]))\n  assert len(tools) == 2\n  assert tools[0].function.name == 'func1'\n  assert tools[1].function.name == 'func2'\n\n  # Test with empty input\n  assert list(_copy_tools()) == []\n  assert list(_copy_tools(None)) == []\n  assert list(_copy_tools([])) == []\n\n  # Test with mix of functions and tool dicts\n  tool_dict = {\n    'type': 'function',\n    'function': {\n      'name': 'test',\n      'description': 'Test function',\n      'parameters': {\n        'type': 'object',\n        'properties': {'x': {'type': 'string', 'description': 'A string', 'enum': ['a', 'b', 'c']}, 'y': {'type': ['integer', 'number'], 'description': 'An integer'}},\n        'required': ['x'],\n      },\n    },\n  }\n\n  tools = list(_copy_tools([func1, tool_dict]))\n  assert len(tools) == 2\n  assert tools[0].function.name == 'func1'\n  assert tools[1].function.name == 'test'\n\n\ndef test_tool_validation():\n  arbitrary_tool = {'type': 'custom_type', 'function': {'name': 'test'}}\n  tools = list(_copy_tools([arbitrary_tool]))\n  assert len(tools) == 1\n  assert tools[0].type == 'custom_type'\n  assert tools[0].function.name == 'test'\n\n\ndef test_client_connection_error():\n  client = Client('http://localhost:1234')\n\n  with pytest.raises(ConnectionError, match=CONNECTION_ERROR_MESSAGE):\n    client.chat('model', messages=[{'role': 'user', 'content': 'prompt'}])\n  with pytest.raises(ConnectionError, match=CONNECTION_ERROR_MESSAGE):\n    client.chat('model', messages=[{'role': 'user', 'content': 'prompt'}])\n  with pytest.raises(ConnectionError, match=CONNECTION_ERROR_MESSAGE):\n    client.generate('model', 'prompt')\n  with pytest.raises(ConnectionError, match=CONNECTION_ERROR_MESSAGE):\n    client.show('model')\n\n\nasync def test_async_client_connection_error():\n  client = AsyncClient('http://localhost:1234')\n  with pytest.raises(ConnectionError) as exc_info:\n    await client.chat('model', messages=[{'role': 'user', 'content': 'prompt'}])\n  assert str(exc_info.value) == 'Failed to connect to Ollama. Please check that Ollama is downloaded, running and accessible. https://ollama.com/download'\n  with pytest.raises(ConnectionError) as exc_info:\n    await client.generate('model', 'prompt')\n  assert str(exc_info.value) == 'Failed to connect to Ollama. Please check that Ollama is downloaded, running and accessible. https://ollama.com/download'\n  with pytest.raises(ConnectionError) as exc_info:\n    await client.show('model')\n  assert str(exc_info.value) == 'Failed to connect to Ollama. Please check that Ollama is downloaded, running and accessible. https://ollama.com/download'\n\n\ndef test_arbitrary_roles_accepted_in_message():\n  _ = Message(role='somerandomrole', content=\"I'm ok with you adding any role message now!\")\n\n\ndef _mock_request(*args: Any, **kwargs: Any) -> Response:\n  return httpxResponse(status_code=200, content=\"{'response': 'Hello world!'}\")\n\n\ndef test_arbitrary_roles_accepted_in_message_request(monkeypatch: pytest.MonkeyPatch):\n  monkeypatch.setattr(Client, '_request', _mock_request)\n\n  client = Client()\n\n  client.chat(model='llama3.1', messages=[{'role': 'somerandomrole', 'content': \"I'm ok with you adding any role message now!\"}, {'role': 'user', 'content': 'Hello world!'}])\n\n\nasync def _mock_request_async(*args: Any, **kwargs: Any) -> Response:\n  return httpxResponse(status_code=200, content=\"{'response': 'Hello world!'}\")\n\n\nasync def test_arbitrary_roles_accepted_in_message_request_async(monkeypatch: pytest.MonkeyPatch):\n  monkeypatch.setattr(AsyncClient, '_request', _mock_request_async)\n\n  client = AsyncClient()\n\n  await client.chat(model='llama3.1', messages=[{'role': 'somerandomrole', 'content': \"I'm ok with you adding any role message now!\"}, {'role': 'user', 'content': 'Hello world!'}])\n\n\ndef test_client_web_search_requires_bearer_auth_header(monkeypatch: pytest.MonkeyPatch):\n  monkeypatch.delenv('OLLAMA_API_KEY', raising=False)\n\n  client = Client()\n\n  with pytest.raises(ValueError, match='Authorization header with Bearer token is required for web search'):\n    client.web_search('test query')\n\n\ndef test_client_web_fetch_requires_bearer_auth_header(monkeypatch: pytest.MonkeyPatch):\n  monkeypatch.delenv('OLLAMA_API_KEY', raising=False)\n\n  client = Client()\n\n  with pytest.raises(ValueError, match='Authorization header with Bearer token is required for web fetch'):\n    client.web_fetch('https://example.com')\n\n\ndef _mock_request_web_search(self, cls, method, url, json=None, **kwargs):\n  assert method == 'POST'\n  assert url == 'https://ollama.com/api/web_search'\n  assert json is not None and 'query' in json and 'max_results' in json\n  return httpxResponse(status_code=200, content='{\"results\": {}, \"success\": true}')\n\n\ndef _mock_request_web_fetch(self, cls, method, url, json=None, **kwargs):\n  assert method == 'POST'\n  assert url == 'https://ollama.com/api/web_fetch'\n  assert json is not None and 'url' in json\n  return httpxResponse(status_code=200, content='{\"results\": {}, \"success\": true}')\n\n\ndef test_client_web_search_with_env_api_key(monkeypatch: pytest.MonkeyPatch):\n  monkeypatch.setenv('OLLAMA_API_KEY', 'test-key')\n  monkeypatch.setattr(Client, '_request', _mock_request_web_search)\n\n  client = Client()\n  client.web_search('what is ollama?', max_results=2)\n\n\ndef test_client_web_fetch_with_env_api_key(monkeypatch: pytest.MonkeyPatch):\n  monkeypatch.setenv('OLLAMA_API_KEY', 'test-key')\n  monkeypatch.setattr(Client, '_request', _mock_request_web_fetch)\n\n  client = Client()\n  client.web_fetch('https://example.com')\n\n\ndef test_client_web_search_with_explicit_bearer_header(monkeypatch: pytest.MonkeyPatch):\n  monkeypatch.delenv('OLLAMA_API_KEY', raising=False)\n  monkeypatch.setattr(Client, '_request', _mock_request_web_search)\n\n  client = Client(headers={'Authorization': 'Bearer custom-token'})\n  client.web_search('what is ollama?', max_results=1)\n\n\ndef test_client_web_fetch_with_explicit_bearer_header(monkeypatch: pytest.MonkeyPatch):\n  monkeypatch.delenv('OLLAMA_API_KEY', raising=False)\n  monkeypatch.setattr(Client, '_request', _mock_request_web_fetch)\n\n  client = Client(headers={'Authorization': 'Bearer custom-token'})\n  client.web_fetch('https://example.com')\n\n\ndef test_client_bearer_header_from_env(monkeypatch: pytest.MonkeyPatch):\n  monkeypatch.setenv('OLLAMA_API_KEY', 'env-token')\n\n  client = Client()\n  assert client._client.headers['authorization'] == 'Bearer env-token'\n\n\ndef test_client_explicit_bearer_header_overrides_env(monkeypatch: pytest.MonkeyPatch):\n  monkeypatch.setenv('OLLAMA_API_KEY', 'env-token')\n  monkeypatch.setattr(Client, '_request', _mock_request_web_search)\n\n  client = Client(headers={'Authorization': 'Bearer explicit-token'})\n  assert client._client.headers['authorization'] == 'Bearer explicit-token'\n  client.web_search('override check')\n\n\ndef test_client_close():\n  client = Client()\n  client.close()\n  assert client._client.is_closed\n\n\n@pytest.mark.anyio\nasync def test_async_client_close():\n  client = AsyncClient()\n  await client.close()\n  assert client._client.is_closed\n\n\ndef test_client_context_manager():\n  with Client() as client:\n    assert isinstance(client, Client)\n    assert not client._client.is_closed\n\n  assert client._client.is_closed\n\n\n@pytest.mark.anyio\nasync def test_async_client_context_manager():\n  async with AsyncClient() as client:\n    assert isinstance(client, AsyncClient)\n    assert not client._client.is_closed\n\n  assert client._client.is_closed\n"
  },
  {
    "path": "tests/test_type_serialization.py",
    "content": "import tempfile\nfrom base64 import b64encode\nfrom pathlib import Path\n\nimport pytest\n\nfrom ollama._types import CreateRequest, Image\n\n\ndef test_image_serialization_bytes():\n  image_bytes = b'test image bytes'\n  encoded_string = b64encode(image_bytes).decode()\n  img = Image(value=image_bytes)\n  assert img.model_dump() == encoded_string\n\n\ndef test_image_serialization_base64_string():\n  b64_str = 'dGVzdCBiYXNlNjQgc3RyaW5n'\n  img = Image(value=b64_str)\n  assert img.model_dump() == b64_str  # Should return as-is if valid base64\n\n\ndef test_image_serialization_long_base64_string():\n  b64_str = 'dGVzdCBiYXNlNjQgc3RyaW5n' * 1000\n  img = Image(value=b64_str)\n  assert img.model_dump() == b64_str  # Should return as-is if valid base64\n\n\ndef test_image_serialization_plain_string():\n  img = Image(value='not a path or base64')\n  assert img.model_dump() == 'not a path or base64'  # Should return as-is\n\n\ndef test_image_serialization_path():\n  with tempfile.NamedTemporaryFile() as temp_file:\n    temp_file.write(b'test file content')\n    temp_file.flush()\n    img = Image(value=Path(temp_file.name))\n    assert img.model_dump() == b64encode(b'test file content').decode()\n\n\ndef test_image_serialization_string_path():\n  with tempfile.NamedTemporaryFile() as temp_file:\n    temp_file.write(b'test file content')\n    temp_file.flush()\n    img = Image(value=temp_file.name)\n    assert img.model_dump() == b64encode(b'test file content').decode()\n\n  with pytest.raises(ValueError):\n    img = Image(value='some_path/that/does/not/exist.png')\n    img.model_dump()\n\n  with pytest.raises(ValueError):\n    img = Image(value='not an image')\n    img.model_dump()\n\n\ndef test_create_request_serialization():\n  request = CreateRequest(model='test-model', from_='base-model', quantize='q4_0', files={'file1': 'content1'}, adapters={'adapter1': 'content1'}, template='test template', license='MIT', system='test system', parameters={'param1': 'value1'})\n\n  serialized = request.model_dump()\n  assert serialized['from'] == 'base-model'\n  assert 'from_' not in serialized\n  assert serialized['quantize'] == 'q4_0'\n  assert serialized['files'] == {'file1': 'content1'}\n  assert serialized['adapters'] == {'adapter1': 'content1'}\n  assert serialized['template'] == 'test template'\n  assert serialized['license'] == 'MIT'\n  assert serialized['system'] == 'test system'\n  assert serialized['parameters'] == {'param1': 'value1'}\n\n\ndef test_create_request_serialization_exclude_none_true():\n  request = CreateRequest(model='test-model', from_=None, quantize=None)\n  serialized = request.model_dump(exclude_none=True)\n  assert serialized == {'model': 'test-model'}\n  assert 'from' not in serialized\n  assert 'from_' not in serialized\n  assert 'quantize' not in serialized\n\n\ndef test_create_request_serialization_exclude_none_false():\n  request = CreateRequest(model='test-model', from_=None, quantize=None)\n  serialized = request.model_dump(exclude_none=False)\n  assert 'from' in serialized\n  assert 'quantize' in serialized\n  assert 'adapters' in serialized\n  assert 'from_' not in serialized\n\n\ndef test_create_request_serialization_license_list():\n  request = CreateRequest(model='test-model', license=['MIT', 'Apache-2.0'])\n  serialized = request.model_dump()\n  assert serialized['license'] == ['MIT', 'Apache-2.0']\n"
  },
  {
    "path": "tests/test_utils.py",
    "content": "import json\nimport sys\nfrom typing import Dict, List, Mapping, Sequence, Set, Tuple, Union\n\nfrom ollama._utils import convert_function_to_tool\n\n\ndef test_function_to_tool_conversion():\n  def add_numbers(x: int, y: Union[int, None] = None) -> int:\n    \"\"\"Add two numbers together.\n    args:\n        x (integer): The first number\n        y (integer, optional): The second number\n\n    Returns:\n        integer: The sum of x and y\n    \"\"\"\n    return x + y\n\n  tool = convert_function_to_tool(add_numbers).model_dump()\n\n  assert tool['type'] == 'function'\n  assert tool['function']['name'] == 'add_numbers'\n  assert tool['function']['description'] == 'Add two numbers together.'\n  assert tool['function']['parameters']['type'] == 'object'\n  assert tool['function']['parameters']['properties']['x']['type'] == 'integer'\n  assert tool['function']['parameters']['properties']['x']['description'] == 'The first number'\n  assert tool['function']['parameters']['required'] == ['x']\n\n\ndef test_function_with_no_args():\n  def simple_func():\n    \"\"\"\n    A simple function with no arguments.\n    Args:\n        None\n    Returns:\n        None\n    \"\"\"\n\n  tool = convert_function_to_tool(simple_func).model_dump()\n  assert tool['function']['name'] == 'simple_func'\n  assert tool['function']['description'] == 'A simple function with no arguments.'\n  assert tool['function']['parameters']['properties'] == {}\n\n\ndef test_function_with_all_types():\n  if sys.version_info >= (3, 10):\n\n    def all_types(\n      x: int,\n      y: str,\n      z: list[int],\n      w: dict[str, int],\n      v: int | str | None,\n    ) -> int | dict[str, int] | str | list[int] | None:\n      \"\"\"\n      A function with all types.\n      Args:\n          x (integer): The first number\n          y (string): The second number\n          z (array): The third number\n          w (object): The fourth number\n          v (integer | string | None): The fifth number\n      \"\"\"\n  else:\n\n    def all_types(\n      x: int,\n      y: str,\n      z: Sequence,\n      w: Mapping[str, int],\n      d: Dict[str, int],\n      s: Set[int],\n      t: Tuple[int, str],\n      l: List[int],  # noqa: E741\n      o: Union[int, None],\n    ) -> Union[Mapping[str, int], str, None]:\n      \"\"\"\n      A function with all types.\n      Args:\n          x (integer): The first number\n          y (string): The second number\n          z (array): The third number\n          w (object): The fourth number\n          d (object): The fifth number\n          s (array): The sixth number\n          t (array): The seventh number\n          l (array): The eighth number\n          o (integer | None): The ninth number\n      \"\"\"\n\n  tool_json = convert_function_to_tool(all_types).model_dump_json()\n  tool = json.loads(tool_json)\n  assert tool['function']['parameters']['properties']['x']['type'] == 'integer'\n  assert tool['function']['parameters']['properties']['y']['type'] == 'string'\n\n  if sys.version_info >= (3, 10):\n    assert tool['function']['parameters']['properties']['z']['type'] == 'array'\n    assert tool['function']['parameters']['properties']['w']['type'] == 'object'\n    assert {x.strip().strip(\"'\") for x in tool['function']['parameters']['properties']['v']['type'].removeprefix('[').removesuffix(']').split(',')} == {'string', 'integer'}\n    assert tool['function']['parameters']['properties']['v']['type'] != 'null'\n    assert tool['function']['parameters']['required'] == ['x', 'y', 'z', 'w']\n  else:\n    assert tool['function']['parameters']['properties']['z']['type'] == 'array'\n    assert tool['function']['parameters']['properties']['w']['type'] == 'object'\n    assert tool['function']['parameters']['properties']['d']['type'] == 'object'\n    assert tool['function']['parameters']['properties']['s']['type'] == 'array'\n    assert tool['function']['parameters']['properties']['t']['type'] == 'array'\n    assert tool['function']['parameters']['properties']['l']['type'] == 'array'\n    assert tool['function']['parameters']['properties']['o']['type'] == 'integer'\n    assert tool['function']['parameters']['properties']['o']['type'] != 'null'\n    assert tool['function']['parameters']['required'] == ['x', 'y', 'z', 'w', 'd', 's', 't', 'l']\n\n\ndef test_function_docstring_parsing():\n  from typing import Any, Dict, List\n\n  def func_with_complex_docs(x: int, y: List[str]) -> Dict[str, Any]:\n    \"\"\"\n    Test function with complex docstring.\n\n    Args:\n        x (integer): A number\n            with multiple lines\n        y (array of string): A list\n            with multiple lines\n\n    Returns:\n        object: A dictionary\n            with multiple lines\n    \"\"\"\n\n  tool = convert_function_to_tool(func_with_complex_docs).model_dump()\n  assert tool['function']['description'] == 'Test function with complex docstring.'\n  assert tool['function']['parameters']['properties']['x']['description'] == 'A number with multiple lines'\n  assert tool['function']['parameters']['properties']['y']['description'] == 'A list with multiple lines'\n\n\ndef test_skewed_docstring_parsing():\n  def add_two_numbers(x: int, y: int) -> int:\n    \"\"\"\n    Add two numbers together.\n    Args:\n        x (integer): : The first number\n\n\n\n\n        y (integer ): The second number\n    Returns:\n        integer: The sum of x and y\n    \"\"\"\n\n  tool = convert_function_to_tool(add_two_numbers).model_dump()\n  assert tool['function']['parameters']['properties']['x']['description'] == ': The first number'\n  assert tool['function']['parameters']['properties']['y']['description'] == 'The second number'\n\n\ndef test_function_with_no_docstring():\n  def no_docstring(): ...\n\n  def no_docstring_with_args(x: int, y: int): ...\n\n  tool = convert_function_to_tool(no_docstring).model_dump()\n  assert tool['function']['description'] == ''\n\n  tool = convert_function_to_tool(no_docstring_with_args).model_dump()\n  assert tool['function']['description'] == ''\n  assert tool['function']['parameters']['properties']['x']['description'] == ''\n  assert tool['function']['parameters']['properties']['y']['description'] == ''\n\n\ndef test_function_with_only_description():\n  def only_description():\n    \"\"\"\n    A function with only a description.\n    \"\"\"\n\n  tool = convert_function_to_tool(only_description).model_dump()\n  assert tool['function']['description'] == 'A function with only a description.'\n  assert tool['function']['parameters'] == {'type': 'object', 'defs': None, 'items': None, 'required': None, 'properties': {}}\n\n  def only_description_with_args(x: int, y: int):\n    \"\"\"\n    A function with only a description.\n    \"\"\"\n\n  tool = convert_function_to_tool(only_description_with_args).model_dump()\n  assert tool['function']['description'] == 'A function with only a description.'\n  assert tool['function']['parameters'] == {\n    'type': 'object',\n    'defs': None,\n    'items': None,\n    'properties': {\n      'x': {'type': 'integer', 'description': '', 'enum': None, 'items': None},\n      'y': {'type': 'integer', 'description': '', 'enum': None, 'items': None},\n    },\n    'required': ['x', 'y'],\n  }\n\n\ndef test_function_with_yields():\n  def function_with_yields(x: int, y: int):\n    \"\"\"\n    A function with yields section.\n\n    Args:\n      x: the first number\n      y: the second number\n\n    Yields:\n      The sum of x and y\n    \"\"\"\n\n  tool = convert_function_to_tool(function_with_yields).model_dump()\n  assert tool['function']['description'] == 'A function with yields section.'\n  assert tool['function']['parameters']['properties']['x']['description'] == 'the first number'\n  assert tool['function']['parameters']['properties']['y']['description'] == 'the second number'\n\n\ndef test_function_with_no_types():\n  def no_types(a, b):\n    \"\"\"\n    A function with no types.\n    \"\"\"\n\n  tool = convert_function_to_tool(no_types).model_dump()\n  assert tool['function']['parameters']['properties']['a']['type'] == 'string'\n  assert tool['function']['parameters']['properties']['b']['type'] == 'string'\n\n\ndef test_function_with_parentheses():\n  def func_with_parentheses(a: int, b: int) -> int:\n    \"\"\"\n    A function with parentheses.\n    Args:\n        a: First (:thing) number to add\n        b: Second number to add\n    Returns:\n        int: The sum of a and b\n    \"\"\"\n\n  def func_with_parentheses_and_args(a: int, b: int):\n    \"\"\"\n    A function with parentheses and args.\n    Args:\n        a(integer) : First (:thing) number to add\n        b(integer) :Second number to add\n    \"\"\"\n\n  tool = convert_function_to_tool(func_with_parentheses).model_dump()\n  assert tool['function']['parameters']['properties']['a']['description'] == 'First (:thing) number to add'\n  assert tool['function']['parameters']['properties']['b']['description'] == 'Second number to add'\n\n  tool = convert_function_to_tool(func_with_parentheses_and_args).model_dump()\n  assert tool['function']['parameters']['properties']['a']['description'] == 'First (:thing) number to add'\n  assert tool['function']['parameters']['properties']['b']['description'] == 'Second number to add'\n"
  }
]