././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1764048066.3384526 llm_anthropic-0.23/0000755000175100017510000000000015111236302013731 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1764048059.0 llm_anthropic-0.23/LICENSE0000644000175100017510000002613515111236273014754 0ustar00runnerrunner Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1764048066.3384526 llm_anthropic-0.23/PKG-INFO0000644000175100017510000002614315111236302015034 0ustar00runnerrunnerMetadata-Version: 2.4 Name: llm-anthropic Version: 0.23 Summary: LLM access to models by Anthropic, including the Claude series Author: Simon Willison License-Expression: Apache-2.0 Project-URL: Homepage, https://github.com/simonw/llm-anthropic Project-URL: Changelog, https://github.com/simonw/llm-anthropic/releases Project-URL: Issues, https://github.com/simonw/llm-anthropic/issues Project-URL: CI, https://github.com/simonw/llm-anthropic/actions Requires-Python: >=3.10 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: llm>=0.26 Requires-Dist: anthropic>=0.75 Requires-Dist: json-schema-to-pydantic Provides-Extra: test Requires-Dist: pytest; extra == "test" Requires-Dist: pytest-recording; extra == "test" Requires-Dist: pytest-asyncio; extra == "test" Requires-Dist: cogapp; extra == "test" Dynamic: license-file # llm-anthropic [![PyPI](https://img.shields.io/pypi/v/llm-anthropic.svg)](https://pypi.org/project/llm-anthropic/) [![Changelog](https://img.shields.io/github/v/release/simonw/llm-anthropic?include_prereleases&label=changelog)](https://github.com/simonw/llm-anthropic/releases) [![Tests](https://github.com/simonw/llm-anthropic/actions/workflows/test.yml/badge.svg)](https://github.com/simonw/llm-anthropic/actions/workflows/test.yml) [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/llm-anthropic/blob/main/LICENSE) LLM access to models by Anthropic, including the Claude series ## Installation Install this plugin in the same environment as [LLM](https://llm.datasette.io/). ```bash llm install llm-anthropic ```
Instructions for users who need to upgrade from llm-claude-3
If you previously used `llm-claude-3` you can upgrade like this: ```bash llm install -U llm-claude-3 llm keys set anthropic --value "$(llm keys get claude)" ``` The first line will remove the previous `llm-claude-3` version and install this one, because the latest `llm-claude-3` depends on `llm-anthropic`. The second line sets the `anthropic` key to whatever value you previously used for the `claude` key.
## Usage First, set [an API key](https://console.anthropic.com/settings/keys) for Anthropic: ```bash llm keys set anthropic # Paste key here ``` You can also set the key in the environment variable `ANTHROPIC_API_KEY` Run `llm models` to list the models, and `llm models --options` to include a list of their options. Run prompts like this: ```bash llm -m claude-opus-4.5 'Fun facts about walruses' llm -m claude-sonnet-4.5 'Fun facts about pelicans' llm -m claude-3.5-haiku 'Fun facts about armadillos' llm -m claude-haiku-4.5 'Fun facts about cormorants' ``` Image attachments are supported too: ```bash llm -m claude-sonnet-4.5 'describe this image' -a https://static.simonwillison.net/static/2024/pelicans.jpg llm -m claude-haiku-4.5 'extract text' -a page.png ``` The Claude 3.5 and 4 models can handle PDF files: ```bash llm -m claude-sonnet-4.5 'extract text' -a page.pdf ``` Anthropic's models support [schemas](https://llm.datasette.io/en/stable/schemas.html). Here's how to use Claude 4 Sonnet to invent a dog: ```bash llm -m claude-sonnet-4.5 --schema 'name,age int,bio: one sentence' 'invent a surprising dog' ``` Example output: ```json { "name": "Whiskers the Mathematical Mastiff", "age": 7, "bio": "Whiskers is a mastiff who can solve complex calculus problems by barking in binary code and has won three international mathematics competitions against human competitors." } ``` Newer models support web search for real-time information: ```bash llm -m claude-3.5-sonnet -o web_search 1 'What is the current weather in San Francisco?' ``` ## Usage from Python Python code can access the models like this: ```python import llm model = llm.get_model("claude-haiku-4.5") print(model.prompt("Fun facts about chipmunks")) ``` Consult [LLM's Python API documentation](https://llm.datasette.io/en/stable/python-api.html) for more details. You can also import the model classes directly, which is useful if you want to point the `base_url` at a different Anthropic-compatible endpoint: ```python from llm_anthropic import ClaudeMessages model = ClaudeMessages( "MiniMax-M2", base_url="https://api.minimax.io/anthropic" ) print(model.prompt("Fun facts about pangolins", key="eyJh...")) ``` ## Extended reasoning with Claude 3.7 Sonnet and higher Claude 3.7 introduced [extended thinking](https://www.anthropic.com/news/visible-extended-thinking) mode, where Claude can expend extra effort thinking through the prompt before producing a response. Use the `-o thinking 1` option to enable this feature: ```bash llm -m claude-3.7-sonnet -o thinking 1 'Write a convincing speech to congress about the need to protect the California Brown Pelican' ``` The chain of thought is not currently visible while using LLM, but it is logged to the database and can be viewed using this command: ```bash llm logs -c --json ``` Or in combination with `jq`: ```bash llm logs --json -c | jq '.[0].response_json.content[0].thinking' -r ``` By default up to 1024 tokens can be used for thinking. You can increase this budget with the `thinking_budget` option: ```bash llm -m claude-3.7-sonnet -o thinking_budget 32000 'Write a long speech about pelicans in French' ``` ## Model options The following options can be passed using `-o name value` on the CLI or as `keyword=value` arguments to the Python `model.prompt()` method: - **max_tokens**: `int` The maximum number of tokens to generate before stopping - **temperature**: `float` Amount of randomness injected into the response. Defaults to 1.0. Ranges from 0.0 to 1.0. Use temperature closer to 0.0 for analytical / multiple choice, and closer to 1.0 for creative and generative tasks. Note that even with temperature of 0.0, the results will not be fully deterministic. - **top_p**: `float` Use nucleus sampling. In nucleus sampling, we compute the cumulative distribution over all the options for each subsequent token in decreasing probability order and cut it off once it reaches a particular probability specified by top_p. You should either alter temperature or top_p, but not both. Recommended for advanced use cases only. You usually only need to use temperature. - **top_k**: `int` Only sample from the top K options for each subsequent token. Used to remove 'long tail' low probability responses. Recommended for advanced use cases only. You usually only need to use temperature. - **user_id**: `str` An external identifier for the user who is associated with the request - **prefill**: `str` A prefill to use for the response - **hide_prefill**: `boolean` Do not repeat the prefill value at the start of the response - **stop_sequences**: `array, str` Custom text sequences that will cause the model to stop generating - pass either a list of strings or a single string - **cache**: `boolean` Use Anthropic prompt cache for any attachments or fragments - **web_search**: `boolean` Enable web search capabilities - **web_search_max_uses**: `int` Maximum number of web searches to perform per request - **web_search_allowed_domains**: `array` List of domains to restrict web searches to - **web_search_blocked_domains**: `array` List of domains to exclude from web searches - **web_search_location**: `dict` User location for localizing search results (dict with city, region, country, timezone) - **thinking**: `boolean` Enable thinking mode - **thinking_budget**: `int` Number of tokens to budget for thinking The `prefill` option can be used to set the first part of the response. To increase the chance of returning JSON, set that to `{`: ```bash llm -m claude-sonnet-4.5 'Fun data about pelicans' \ -o prefill '{' ``` If you do not want the prefill token to be echoed in the response, set `hide_prefill` to `true`: ```bash llm -m claude-3.5-haiku 'Short python function describing a pelican' \ -o prefill '```python' \ -o hide_prefill true \ -o stop_sequences '```' ``` This example sets `` ``` `` as the stop sequence, so the response will be a Python function without the wrapping Markdown code block. To pass a single stop sequence, send a string: ```bash llm -m claude-sonnet-4.5 'Fun facts about pelicans' \ -o stop-sequences "beak" ``` For multiple stop sequences, pass a JSON array: ```bash llm -m claude-sonnet-4.5 'Fun facts about pelicans' \ -o stop-sequences '["beak", "feathers"]' ``` When using the Python API, pass a string or an array of strings: ```python response = llm.query( model="claude-sonnet-4.5", query="Fun facts about pelicans", stop_sequences=["beak", "feathers"], ) ``` ## Development To set up this plugin locally, first checkout the code. Then create a new virtual environment: ```bash cd llm-anthropic python3 -m venv venv source venv/bin/activate ``` Now install the dependencies and test dependencies: ```bash llm install -e '.[test]' ``` To run the tests: ```bash pytest ``` Alternatively, if you have [uv](https://github.com/astral-sh/uv) and [just](https://github.com/casey/just) installed you can run tests without creating a virtual environment like this: ```bash just # runs tests (default task) just test # runs tests just test -k test_name # pass arguments to pytest ``` You can also run the `llm` command in a `uv` managed environment like this: ```bash just llm 'your prompt here' ``` To enable debug logs while running ([like this](https://github.com/simonw/llm-anthropic/issues/54#issuecomment-3536842831)), set this environment variable: ```bash export ANTHROPIC_LOG=debug ``` This project uses [pytest-recording](https://github.com/kiwicom/pytest-recording) to record Anthropic API responses for the tests. If you add a new test that calls the API you can capture the API response like this: ```bash PYTEST_ANTHROPIC_API_KEY="$(llm keys get anthropic)" pytest --record-mode once ``` You will need to have stored a valid Anthropic API key using this command first: ```bash llm keys set anthropic # Paste key here ``` I use the following sequence: ```bash # First delete the relevant cassette if it exists already: rm tests/cassettes/test_anthropic/test_thinking_prompt.yaml # Run this failing test to recreate the cassette PYTEST_ANTHROPIC_API_KEY="$(llm keys get claude)" just test -k test_thinking_prompt --record-mode once # Now run the test again with --pdb to figure out how to update it just test -k test_thinking_prompt --pdb # Edit test ``` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1764048059.0 llm_anthropic-0.23/README.md0000644000175100017510000002442115111236273015222 0ustar00runnerrunner# llm-anthropic [![PyPI](https://img.shields.io/pypi/v/llm-anthropic.svg)](https://pypi.org/project/llm-anthropic/) [![Changelog](https://img.shields.io/github/v/release/simonw/llm-anthropic?include_prereleases&label=changelog)](https://github.com/simonw/llm-anthropic/releases) [![Tests](https://github.com/simonw/llm-anthropic/actions/workflows/test.yml/badge.svg)](https://github.com/simonw/llm-anthropic/actions/workflows/test.yml) [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/llm-anthropic/blob/main/LICENSE) LLM access to models by Anthropic, including the Claude series ## Installation Install this plugin in the same environment as [LLM](https://llm.datasette.io/). ```bash llm install llm-anthropic ```
Instructions for users who need to upgrade from llm-claude-3
If you previously used `llm-claude-3` you can upgrade like this: ```bash llm install -U llm-claude-3 llm keys set anthropic --value "$(llm keys get claude)" ``` The first line will remove the previous `llm-claude-3` version and install this one, because the latest `llm-claude-3` depends on `llm-anthropic`. The second line sets the `anthropic` key to whatever value you previously used for the `claude` key.
## Usage First, set [an API key](https://console.anthropic.com/settings/keys) for Anthropic: ```bash llm keys set anthropic # Paste key here ``` You can also set the key in the environment variable `ANTHROPIC_API_KEY` Run `llm models` to list the models, and `llm models --options` to include a list of their options. Run prompts like this: ```bash llm -m claude-opus-4.5 'Fun facts about walruses' llm -m claude-sonnet-4.5 'Fun facts about pelicans' llm -m claude-3.5-haiku 'Fun facts about armadillos' llm -m claude-haiku-4.5 'Fun facts about cormorants' ``` Image attachments are supported too: ```bash llm -m claude-sonnet-4.5 'describe this image' -a https://static.simonwillison.net/static/2024/pelicans.jpg llm -m claude-haiku-4.5 'extract text' -a page.png ``` The Claude 3.5 and 4 models can handle PDF files: ```bash llm -m claude-sonnet-4.5 'extract text' -a page.pdf ``` Anthropic's models support [schemas](https://llm.datasette.io/en/stable/schemas.html). Here's how to use Claude 4 Sonnet to invent a dog: ```bash llm -m claude-sonnet-4.5 --schema 'name,age int,bio: one sentence' 'invent a surprising dog' ``` Example output: ```json { "name": "Whiskers the Mathematical Mastiff", "age": 7, "bio": "Whiskers is a mastiff who can solve complex calculus problems by barking in binary code and has won three international mathematics competitions against human competitors." } ``` Newer models support web search for real-time information: ```bash llm -m claude-3.5-sonnet -o web_search 1 'What is the current weather in San Francisco?' ``` ## Usage from Python Python code can access the models like this: ```python import llm model = llm.get_model("claude-haiku-4.5") print(model.prompt("Fun facts about chipmunks")) ``` Consult [LLM's Python API documentation](https://llm.datasette.io/en/stable/python-api.html) for more details. You can also import the model classes directly, which is useful if you want to point the `base_url` at a different Anthropic-compatible endpoint: ```python from llm_anthropic import ClaudeMessages model = ClaudeMessages( "MiniMax-M2", base_url="https://api.minimax.io/anthropic" ) print(model.prompt("Fun facts about pangolins", key="eyJh...")) ``` ## Extended reasoning with Claude 3.7 Sonnet and higher Claude 3.7 introduced [extended thinking](https://www.anthropic.com/news/visible-extended-thinking) mode, where Claude can expend extra effort thinking through the prompt before producing a response. Use the `-o thinking 1` option to enable this feature: ```bash llm -m claude-3.7-sonnet -o thinking 1 'Write a convincing speech to congress about the need to protect the California Brown Pelican' ``` The chain of thought is not currently visible while using LLM, but it is logged to the database and can be viewed using this command: ```bash llm logs -c --json ``` Or in combination with `jq`: ```bash llm logs --json -c | jq '.[0].response_json.content[0].thinking' -r ``` By default up to 1024 tokens can be used for thinking. You can increase this budget with the `thinking_budget` option: ```bash llm -m claude-3.7-sonnet -o thinking_budget 32000 'Write a long speech about pelicans in French' ``` ## Model options The following options can be passed using `-o name value` on the CLI or as `keyword=value` arguments to the Python `model.prompt()` method: - **max_tokens**: `int` The maximum number of tokens to generate before stopping - **temperature**: `float` Amount of randomness injected into the response. Defaults to 1.0. Ranges from 0.0 to 1.0. Use temperature closer to 0.0 for analytical / multiple choice, and closer to 1.0 for creative and generative tasks. Note that even with temperature of 0.0, the results will not be fully deterministic. - **top_p**: `float` Use nucleus sampling. In nucleus sampling, we compute the cumulative distribution over all the options for each subsequent token in decreasing probability order and cut it off once it reaches a particular probability specified by top_p. You should either alter temperature or top_p, but not both. Recommended for advanced use cases only. You usually only need to use temperature. - **top_k**: `int` Only sample from the top K options for each subsequent token. Used to remove 'long tail' low probability responses. Recommended for advanced use cases only. You usually only need to use temperature. - **user_id**: `str` An external identifier for the user who is associated with the request - **prefill**: `str` A prefill to use for the response - **hide_prefill**: `boolean` Do not repeat the prefill value at the start of the response - **stop_sequences**: `array, str` Custom text sequences that will cause the model to stop generating - pass either a list of strings or a single string - **cache**: `boolean` Use Anthropic prompt cache for any attachments or fragments - **web_search**: `boolean` Enable web search capabilities - **web_search_max_uses**: `int` Maximum number of web searches to perform per request - **web_search_allowed_domains**: `array` List of domains to restrict web searches to - **web_search_blocked_domains**: `array` List of domains to exclude from web searches - **web_search_location**: `dict` User location for localizing search results (dict with city, region, country, timezone) - **thinking**: `boolean` Enable thinking mode - **thinking_budget**: `int` Number of tokens to budget for thinking The `prefill` option can be used to set the first part of the response. To increase the chance of returning JSON, set that to `{`: ```bash llm -m claude-sonnet-4.5 'Fun data about pelicans' \ -o prefill '{' ``` If you do not want the prefill token to be echoed in the response, set `hide_prefill` to `true`: ```bash llm -m claude-3.5-haiku 'Short python function describing a pelican' \ -o prefill '```python' \ -o hide_prefill true \ -o stop_sequences '```' ``` This example sets `` ``` `` as the stop sequence, so the response will be a Python function without the wrapping Markdown code block. To pass a single stop sequence, send a string: ```bash llm -m claude-sonnet-4.5 'Fun facts about pelicans' \ -o stop-sequences "beak" ``` For multiple stop sequences, pass a JSON array: ```bash llm -m claude-sonnet-4.5 'Fun facts about pelicans' \ -o stop-sequences '["beak", "feathers"]' ``` When using the Python API, pass a string or an array of strings: ```python response = llm.query( model="claude-sonnet-4.5", query="Fun facts about pelicans", stop_sequences=["beak", "feathers"], ) ``` ## Development To set up this plugin locally, first checkout the code. Then create a new virtual environment: ```bash cd llm-anthropic python3 -m venv venv source venv/bin/activate ``` Now install the dependencies and test dependencies: ```bash llm install -e '.[test]' ``` To run the tests: ```bash pytest ``` Alternatively, if you have [uv](https://github.com/astral-sh/uv) and [just](https://github.com/casey/just) installed you can run tests without creating a virtual environment like this: ```bash just # runs tests (default task) just test # runs tests just test -k test_name # pass arguments to pytest ``` You can also run the `llm` command in a `uv` managed environment like this: ```bash just llm 'your prompt here' ``` To enable debug logs while running ([like this](https://github.com/simonw/llm-anthropic/issues/54#issuecomment-3536842831)), set this environment variable: ```bash export ANTHROPIC_LOG=debug ``` This project uses [pytest-recording](https://github.com/kiwicom/pytest-recording) to record Anthropic API responses for the tests. If you add a new test that calls the API you can capture the API response like this: ```bash PYTEST_ANTHROPIC_API_KEY="$(llm keys get anthropic)" pytest --record-mode once ``` You will need to have stored a valid Anthropic API key using this command first: ```bash llm keys set anthropic # Paste key here ``` I use the following sequence: ```bash # First delete the relevant cassette if it exists already: rm tests/cassettes/test_anthropic/test_thinking_prompt.yaml # Run this failing test to recreate the cassette PYTEST_ANTHROPIC_API_KEY="$(llm keys get claude)" just test -k test_thinking_prompt --record-mode once # Now run the test again with --pdb to figure out how to update it just test -k test_thinking_prompt --pdb # Edit test ``` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1764048066.3374527 llm_anthropic-0.23/llm_anthropic.egg-info/0000755000175100017510000000000015111236302020256 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1764048066.0 llm_anthropic-0.23/llm_anthropic.egg-info/PKG-INFO0000644000175100017510000002614315111236302021361 0ustar00runnerrunnerMetadata-Version: 2.4 Name: llm-anthropic Version: 0.23 Summary: LLM access to models by Anthropic, including the Claude series Author: Simon Willison License-Expression: Apache-2.0 Project-URL: Homepage, https://github.com/simonw/llm-anthropic Project-URL: Changelog, https://github.com/simonw/llm-anthropic/releases Project-URL: Issues, https://github.com/simonw/llm-anthropic/issues Project-URL: CI, https://github.com/simonw/llm-anthropic/actions Requires-Python: >=3.10 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: llm>=0.26 Requires-Dist: anthropic>=0.75 Requires-Dist: json-schema-to-pydantic Provides-Extra: test Requires-Dist: pytest; extra == "test" Requires-Dist: pytest-recording; extra == "test" Requires-Dist: pytest-asyncio; extra == "test" Requires-Dist: cogapp; extra == "test" Dynamic: license-file # llm-anthropic [![PyPI](https://img.shields.io/pypi/v/llm-anthropic.svg)](https://pypi.org/project/llm-anthropic/) [![Changelog](https://img.shields.io/github/v/release/simonw/llm-anthropic?include_prereleases&label=changelog)](https://github.com/simonw/llm-anthropic/releases) [![Tests](https://github.com/simonw/llm-anthropic/actions/workflows/test.yml/badge.svg)](https://github.com/simonw/llm-anthropic/actions/workflows/test.yml) [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/llm-anthropic/blob/main/LICENSE) LLM access to models by Anthropic, including the Claude series ## Installation Install this plugin in the same environment as [LLM](https://llm.datasette.io/). ```bash llm install llm-anthropic ```
Instructions for users who need to upgrade from llm-claude-3
If you previously used `llm-claude-3` you can upgrade like this: ```bash llm install -U llm-claude-3 llm keys set anthropic --value "$(llm keys get claude)" ``` The first line will remove the previous `llm-claude-3` version and install this one, because the latest `llm-claude-3` depends on `llm-anthropic`. The second line sets the `anthropic` key to whatever value you previously used for the `claude` key.
## Usage First, set [an API key](https://console.anthropic.com/settings/keys) for Anthropic: ```bash llm keys set anthropic # Paste key here ``` You can also set the key in the environment variable `ANTHROPIC_API_KEY` Run `llm models` to list the models, and `llm models --options` to include a list of their options. Run prompts like this: ```bash llm -m claude-opus-4.5 'Fun facts about walruses' llm -m claude-sonnet-4.5 'Fun facts about pelicans' llm -m claude-3.5-haiku 'Fun facts about armadillos' llm -m claude-haiku-4.5 'Fun facts about cormorants' ``` Image attachments are supported too: ```bash llm -m claude-sonnet-4.5 'describe this image' -a https://static.simonwillison.net/static/2024/pelicans.jpg llm -m claude-haiku-4.5 'extract text' -a page.png ``` The Claude 3.5 and 4 models can handle PDF files: ```bash llm -m claude-sonnet-4.5 'extract text' -a page.pdf ``` Anthropic's models support [schemas](https://llm.datasette.io/en/stable/schemas.html). Here's how to use Claude 4 Sonnet to invent a dog: ```bash llm -m claude-sonnet-4.5 --schema 'name,age int,bio: one sentence' 'invent a surprising dog' ``` Example output: ```json { "name": "Whiskers the Mathematical Mastiff", "age": 7, "bio": "Whiskers is a mastiff who can solve complex calculus problems by barking in binary code and has won three international mathematics competitions against human competitors." } ``` Newer models support web search for real-time information: ```bash llm -m claude-3.5-sonnet -o web_search 1 'What is the current weather in San Francisco?' ``` ## Usage from Python Python code can access the models like this: ```python import llm model = llm.get_model("claude-haiku-4.5") print(model.prompt("Fun facts about chipmunks")) ``` Consult [LLM's Python API documentation](https://llm.datasette.io/en/stable/python-api.html) for more details. You can also import the model classes directly, which is useful if you want to point the `base_url` at a different Anthropic-compatible endpoint: ```python from llm_anthropic import ClaudeMessages model = ClaudeMessages( "MiniMax-M2", base_url="https://api.minimax.io/anthropic" ) print(model.prompt("Fun facts about pangolins", key="eyJh...")) ``` ## Extended reasoning with Claude 3.7 Sonnet and higher Claude 3.7 introduced [extended thinking](https://www.anthropic.com/news/visible-extended-thinking) mode, where Claude can expend extra effort thinking through the prompt before producing a response. Use the `-o thinking 1` option to enable this feature: ```bash llm -m claude-3.7-sonnet -o thinking 1 'Write a convincing speech to congress about the need to protect the California Brown Pelican' ``` The chain of thought is not currently visible while using LLM, but it is logged to the database and can be viewed using this command: ```bash llm logs -c --json ``` Or in combination with `jq`: ```bash llm logs --json -c | jq '.[0].response_json.content[0].thinking' -r ``` By default up to 1024 tokens can be used for thinking. You can increase this budget with the `thinking_budget` option: ```bash llm -m claude-3.7-sonnet -o thinking_budget 32000 'Write a long speech about pelicans in French' ``` ## Model options The following options can be passed using `-o name value` on the CLI or as `keyword=value` arguments to the Python `model.prompt()` method: - **max_tokens**: `int` The maximum number of tokens to generate before stopping - **temperature**: `float` Amount of randomness injected into the response. Defaults to 1.0. Ranges from 0.0 to 1.0. Use temperature closer to 0.0 for analytical / multiple choice, and closer to 1.0 for creative and generative tasks. Note that even with temperature of 0.0, the results will not be fully deterministic. - **top_p**: `float` Use nucleus sampling. In nucleus sampling, we compute the cumulative distribution over all the options for each subsequent token in decreasing probability order and cut it off once it reaches a particular probability specified by top_p. You should either alter temperature or top_p, but not both. Recommended for advanced use cases only. You usually only need to use temperature. - **top_k**: `int` Only sample from the top K options for each subsequent token. Used to remove 'long tail' low probability responses. Recommended for advanced use cases only. You usually only need to use temperature. - **user_id**: `str` An external identifier for the user who is associated with the request - **prefill**: `str` A prefill to use for the response - **hide_prefill**: `boolean` Do not repeat the prefill value at the start of the response - **stop_sequences**: `array, str` Custom text sequences that will cause the model to stop generating - pass either a list of strings or a single string - **cache**: `boolean` Use Anthropic prompt cache for any attachments or fragments - **web_search**: `boolean` Enable web search capabilities - **web_search_max_uses**: `int` Maximum number of web searches to perform per request - **web_search_allowed_domains**: `array` List of domains to restrict web searches to - **web_search_blocked_domains**: `array` List of domains to exclude from web searches - **web_search_location**: `dict` User location for localizing search results (dict with city, region, country, timezone) - **thinking**: `boolean` Enable thinking mode - **thinking_budget**: `int` Number of tokens to budget for thinking The `prefill` option can be used to set the first part of the response. To increase the chance of returning JSON, set that to `{`: ```bash llm -m claude-sonnet-4.5 'Fun data about pelicans' \ -o prefill '{' ``` If you do not want the prefill token to be echoed in the response, set `hide_prefill` to `true`: ```bash llm -m claude-3.5-haiku 'Short python function describing a pelican' \ -o prefill '```python' \ -o hide_prefill true \ -o stop_sequences '```' ``` This example sets `` ``` `` as the stop sequence, so the response will be a Python function without the wrapping Markdown code block. To pass a single stop sequence, send a string: ```bash llm -m claude-sonnet-4.5 'Fun facts about pelicans' \ -o stop-sequences "beak" ``` For multiple stop sequences, pass a JSON array: ```bash llm -m claude-sonnet-4.5 'Fun facts about pelicans' \ -o stop-sequences '["beak", "feathers"]' ``` When using the Python API, pass a string or an array of strings: ```python response = llm.query( model="claude-sonnet-4.5", query="Fun facts about pelicans", stop_sequences=["beak", "feathers"], ) ``` ## Development To set up this plugin locally, first checkout the code. Then create a new virtual environment: ```bash cd llm-anthropic python3 -m venv venv source venv/bin/activate ``` Now install the dependencies and test dependencies: ```bash llm install -e '.[test]' ``` To run the tests: ```bash pytest ``` Alternatively, if you have [uv](https://github.com/astral-sh/uv) and [just](https://github.com/casey/just) installed you can run tests without creating a virtual environment like this: ```bash just # runs tests (default task) just test # runs tests just test -k test_name # pass arguments to pytest ``` You can also run the `llm` command in a `uv` managed environment like this: ```bash just llm 'your prompt here' ``` To enable debug logs while running ([like this](https://github.com/simonw/llm-anthropic/issues/54#issuecomment-3536842831)), set this environment variable: ```bash export ANTHROPIC_LOG=debug ``` This project uses [pytest-recording](https://github.com/kiwicom/pytest-recording) to record Anthropic API responses for the tests. If you add a new test that calls the API you can capture the API response like this: ```bash PYTEST_ANTHROPIC_API_KEY="$(llm keys get anthropic)" pytest --record-mode once ``` You will need to have stored a valid Anthropic API key using this command first: ```bash llm keys set anthropic # Paste key here ``` I use the following sequence: ```bash # First delete the relevant cassette if it exists already: rm tests/cassettes/test_anthropic/test_thinking_prompt.yaml # Run this failing test to recreate the cassette PYTEST_ANTHROPIC_API_KEY="$(llm keys get claude)" just test -k test_thinking_prompt --record-mode once # Now run the test again with --pdb to figure out how to update it just test -k test_thinking_prompt --pdb # Edit test ``` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1764048066.0 llm_anthropic-0.23/llm_anthropic.egg-info/SOURCES.txt0000644000175100017510000000045115111236302022142 0ustar00runnerrunnerLICENSE README.md llm_anthropic.py pyproject.toml llm_anthropic.egg-info/PKG-INFO llm_anthropic.egg-info/SOURCES.txt llm_anthropic.egg-info/dependency_links.txt llm_anthropic.egg-info/entry_points.txt llm_anthropic.egg-info/requires.txt llm_anthropic.egg-info/top_level.txt tests/test_anthropic.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1764048066.0 llm_anthropic-0.23/llm_anthropic.egg-info/dependency_links.txt0000644000175100017510000000000115111236302024324 0ustar00runnerrunner ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1764048066.0 llm_anthropic-0.23/llm_anthropic.egg-info/entry_points.txt0000644000175100017510000000004015111236302023546 0ustar00runnerrunner[llm] anthropic = llm_anthropic ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1764048066.0 llm_anthropic-0.23/llm_anthropic.egg-info/requires.txt0000644000175100017510000000015015111236302022652 0ustar00runnerrunnerllm>=0.26 anthropic>=0.75 json-schema-to-pydantic [test] pytest pytest-recording pytest-asyncio cogapp ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1764048066.0 llm_anthropic-0.23/llm_anthropic.egg-info/top_level.txt0000644000175100017510000000001615111236302023005 0ustar00runnerrunnerllm_anthropic ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1764048059.0 llm_anthropic-0.23/llm_anthropic.py0000644000175100017510000007675315111236273017167 0ustar00runnerrunnerfrom anthropic import Anthropic, AsyncAnthropic, transform_schema import enum import llm import json from typing import Optional, List from pydantic import Field, field_validator, model_validator from json_schema_to_pydantic import create_model as json_schema_to_model DEFAULT_THINKING_TOKENS = 1024 DEFAULT_TEMPERATURE = 1.0 # Thinking effort enum, string, low, medium or high class ThinkingEffort(str, enum.Enum): LOW = "low" MEDIUM = "medium" HIGH = "high" @llm.hookimpl def register_models(register): # https://docs.anthropic.com/claude/docs/models-overview register( ClaudeMessages("claude-3-opus-20240229"), AsyncClaudeMessages("claude-3-opus-20240229"), ) register( ClaudeMessages("claude-3-opus-latest"), AsyncClaudeMessages("claude-3-opus-latest"), aliases=("claude-3-opus",), ) register( ClaudeMessages("claude-3-sonnet-20240229"), AsyncClaudeMessages("claude-3-sonnet-20240229"), aliases=("claude-3-sonnet",), ) register( ClaudeMessages("claude-3-haiku-20240307"), AsyncClaudeMessages("claude-3-haiku-20240307"), aliases=("claude-3-haiku",), ) # 3.5 models register( ClaudeMessages( "claude-3-5-sonnet-20240620", supports_pdf=True, default_max_tokens=8192 ), AsyncClaudeMessages( "claude-3-5-sonnet-20240620", supports_pdf=True, default_max_tokens=8192 ), ) register( ClaudeMessages( "claude-3-5-sonnet-20241022", supports_pdf=True, supports_web_search=True, default_max_tokens=8192, ), AsyncClaudeMessages( "claude-3-5-sonnet-20241022", supports_pdf=True, supports_web_search=True, default_max_tokens=8192, ), ) register( ClaudeMessages( "claude-3-5-sonnet-latest", supports_pdf=True, supports_web_search=True, default_max_tokens=8192, ), AsyncClaudeMessages( "claude-3-5-sonnet-latest", supports_pdf=True, supports_web_search=True, default_max_tokens=8192, ), aliases=("claude-3.5-sonnet", "claude-3.5-sonnet-latest"), ) register( ClaudeMessages( "claude-3-5-haiku-latest", supports_web_search=True, default_max_tokens=8192 ), AsyncClaudeMessages( "claude-3-5-haiku-latest", supports_web_search=True, default_max_tokens=8192 ), aliases=("claude-3.5-haiku",), ) # 3.7 register( ClaudeMessages( "claude-3-7-sonnet-20250219", supports_pdf=True, supports_thinking=True, supports_web_search=True, default_max_tokens=8192, ), AsyncClaudeMessages( "claude-3-7-sonnet-20250219", supports_pdf=True, supports_thinking=True, supports_web_search=True, default_max_tokens=8192, ), ) register( ClaudeMessages( "claude-3-7-sonnet-latest", supports_pdf=True, supports_thinking=True, supports_web_search=True, default_max_tokens=8192, ), AsyncClaudeMessages( "claude-3-7-sonnet-latest", supports_pdf=True, supports_thinking=True, supports_web_search=True, default_max_tokens=8192, ), aliases=("claude-3.7-sonnet", "claude-3.7-sonnet-latest"), ) register( ClaudeMessages( "claude-opus-4-0", supports_pdf=True, supports_thinking=True, supports_web_search=True, default_max_tokens=8192, ), AsyncClaudeMessages( "claude-opus-4-0", supports_pdf=True, supports_thinking=True, supports_web_search=True, default_max_tokens=8192, ), aliases=("claude-4-opus",), ) register( ClaudeMessages( "claude-sonnet-4-0", supports_pdf=True, supports_thinking=True, supports_web_search=True, default_max_tokens=8192, ), AsyncClaudeMessages( "claude-sonnet-4-0", supports_pdf=True, supports_thinking=True, supports_web_search=True, default_max_tokens=8192, ), aliases=("claude-4-sonnet",), ) register( ClaudeMessages( "claude-opus-4-1-20250805", supports_pdf=True, supports_thinking=True, supports_web_search=True, use_structured_outputs=True, default_max_tokens=8192, ), AsyncClaudeMessages( "claude-opus-4-1-20250805", supports_pdf=True, supports_thinking=True, supports_web_search=True, use_structured_outputs=True, default_max_tokens=8192, ), aliases=("claude-opus-4.1",), ) # claude-sonnet-4-5 register( ClaudeMessages( "claude-sonnet-4-5", supports_pdf=True, supports_thinking=True, use_structured_outputs=True, default_max_tokens=8192, ), AsyncClaudeMessages( "claude-sonnet-4-5", supports_pdf=True, supports_thinking=True, use_structured_outputs=True, default_max_tokens=8192, ), aliases=("claude-sonnet-4.5",), ) # claude-haiku-4-5 register( ClaudeMessages( "claude-haiku-4-5-20251001", supports_pdf=True, supports_thinking=True, default_max_tokens=8192, ), AsyncClaudeMessages( "claude-haiku-4-5-20251001", supports_pdf=True, supports_thinking=True, default_max_tokens=8192, ), aliases=("claude-haiku-4.5",), ) # claude-opus-4-5 register( ClaudeMessages( "claude-opus-4-5-20251101", supports_pdf=True, supports_thinking=True, supports_thinking_effort=True, supports_web_search=True, default_max_tokens=8192, ), AsyncClaudeMessages( "claude-opus-4-5-20251101", supports_pdf=True, supports_thinking=True, supports_thinking_effort=True, supports_web_search=True, default_max_tokens=8192, ), aliases=("claude-opus-4.5",), ) class ClaudeOptions(llm.Options): max_tokens: int | None = Field( description="The maximum number of tokens to generate before stopping", default=None, ) temperature: float | None = Field( description="Amount of randomness injected into the response. Defaults to 1.0. Ranges from 0.0 to 1.0. Use temperature closer to 0.0 for analytical / multiple choice, and closer to 1.0 for creative and generative tasks. Note that even with temperature of 0.0, the results will not be fully deterministic.", default=None, ) top_p: float | None = Field( description="Use nucleus sampling. In nucleus sampling, we compute the cumulative distribution over all the options for each subsequent token in decreasing probability order and cut it off once it reaches a particular probability specified by top_p. You should either alter temperature or top_p, but not both. Recommended for advanced use cases only. You usually only need to use temperature.", default=None, ) top_k: int | None = Field( description="Only sample from the top K options for each subsequent token. Used to remove 'long tail' low probability responses. Recommended for advanced use cases only. You usually only need to use temperature.", default=None, ) user_id: str | None = Field( description="An external identifier for the user who is associated with the request", default=None, ) prefill: str | None = Field( description="A prefill to use for the response", default=None, ) hide_prefill: bool | None = Field( description="Do not repeat the prefill value at the start of the response", default=None, ) stop_sequences: list[str] | str | None = Field( description=( "Custom text sequences that will cause the model to stop generating - " "pass either a list of strings or a single string" ), default=None, ) cache: bool | None = Field( description="Use Anthropic prompt cache for any attachments or fragments", default=None, ) web_search: Optional[bool] = Field( description="Enable web search capabilities", default=None, ) web_search_max_uses: Optional[int] = Field( description="Maximum number of web searches to perform per request", default=None, ) web_search_allowed_domains: Optional[List[str]] = Field( description="List of domains to restrict web searches to", default=None, ) web_search_blocked_domains: Optional[List[str]] = Field( description="List of domains to exclude from web searches", default=None, ) web_search_location: Optional[dict] = Field( description="User location for localizing search results (dict with city, region, country, timezone)", default=None, ) @field_validator("stop_sequences") def validate_stop_sequences(cls, stop_sequences): error_msg = "stop_sequences must be a list of strings or a single string" if isinstance(stop_sequences, str): try: stop_sequences = json.loads(stop_sequences) if not isinstance(stop_sequences, list) or not all( isinstance(seq, str) for seq in stop_sequences ): raise ValueError(error_msg) return stop_sequences except json.JSONDecodeError: return [stop_sequences] elif isinstance(stop_sequences, list): if not all(isinstance(seq, str) for seq in stop_sequences): raise ValueError(error_msg) return stop_sequences else: raise ValueError(error_msg) @field_validator("temperature") @classmethod def validate_temperature(cls, temperature): if not (0.0 <= temperature <= 1.0): raise ValueError("temperature must be in range 0.0-1.0") return temperature @field_validator("top_p") @classmethod def validate_top_p(cls, top_p): if top_p is not None and not (0.0 <= top_p <= 1.0): raise ValueError("top_p must be in range 0.0-1.0") return top_p @field_validator("top_k") @classmethod def validate_top_k(cls, top_k): if top_k is not None and top_k <= 0: raise ValueError("top_k must be a positive integer") return top_k @field_validator("web_search_max_uses") @classmethod def validate_web_search_max_uses(cls, max_uses): if max_uses is not None and max_uses <= 0: raise ValueError("web_search_max_uses must be a positive integer") return max_uses @field_validator("web_search_allowed_domains", "web_search_blocked_domains") @classmethod def validate_web_search_domains(cls, domains): if domains is not None: if not isinstance(domains, list): raise ValueError("web_search domains must be a list of strings") if not all(isinstance(domain, str) for domain in domains): raise ValueError("web_search domains must be a list of strings") return domains @field_validator("web_search_location") @classmethod def validate_web_search_location(cls, location): if location is not None: if not isinstance(location, dict): raise ValueError("web_search_location must be a dictionary") required_fields = {"city", "region", "country", "timezone"} if not all(field in location for field in required_fields): raise ValueError(f"web_search_location must contain: {required_fields}") return location @model_validator(mode="after") def validate_temperature_top_p(self): if self.temperature != 1.0 and self.top_p is not None: raise ValueError("Only one of temperature and top_p can be set") return self @model_validator(mode="after") def validate_web_search_domains_conflict(self): if ( self.web_search_allowed_domains is not None and self.web_search_blocked_domains is not None ): raise ValueError( "Cannot use both web_search_allowed_domains and web_search_blocked_domains" ) return self class ClaudeOptionsWithThinking(ClaudeOptions): thinking: bool | None = Field( description="Enable thinking mode", default=None, ) thinking_budget: int | None = Field( description="Number of tokens to budget for thinking", default=None ) class ClaudeOptionsWithThinkingEffort(ClaudeOptionsWithThinking): thinking_effort: ThinkingEffort | None = Field( description="Level of thinking effort to apply: low, medium, or high", default=None, ) def source_for_attachment(attachment): if attachment.url: return { "type": "url", "url": attachment.url, } else: return { "data": attachment.base64_content(), "media_type": attachment.resolve_type(), "type": "base64", } class _Shared: needs_key = "anthropic" key_env_var = "ANTHROPIC_API_KEY" can_stream = True base_url = None supports_thinking = False supports_thinking_effort = False supports_schema = True supports_tools = True supports_web_search = False default_max_tokens = 4096 class Options(ClaudeOptions): ... def __init__( self, model_id, claude_model_id=None, supports_images=True, supports_pdf=False, supports_thinking=False, supports_thinking_effort=False, supports_web_search=False, use_structured_outputs=False, default_max_tokens=None, base_url=None, ): self.model_id = "anthropic/" + model_id self.claude_model_id = claude_model_id or model_id self.base_url = base_url self.use_structured_outputs = use_structured_outputs self.attachment_types = set() if supports_images: self.attachment_types.update( { "image/png", "image/jpeg", "image/webp", "image/gif", } ) if supports_pdf: self.attachment_types.add("application/pdf") if supports_thinking: self.supports_thinking = True self.Options = ClaudeOptionsWithThinking if supports_thinking_effort: self.supports_thinking_effort = True self.Options = ClaudeOptionsWithThinkingEffort if default_max_tokens is not None: self.default_max_tokens = default_max_tokens self.supports_web_search = supports_web_search def prefill_text(self, prompt): if prompt.options.prefill and not prompt.options.hide_prefill: return prompt.options.prefill return "" def _model_dump_suppress_warnings(self, message): """ Call model_dump() on a message while suppressing Pydantic serialization warnings. When using dynamically created Pydantic models with the SDK's stream() helper, the returned ParsedBetaMessage has strict type annotations that don't match our dynamic models, causing harmless serialization warnings. This suppresses those warnings while still producing correct output. """ import warnings with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=UserWarning, module="pydantic") return message.model_dump() def build_messages(self, prompt, conversation) -> list[dict]: messages = [] # Process existing conversation history if conversation: for response in conversation.responses: # Build user message user_content = [] # Handle attachments in user message if response.attachments: for attachment in response.attachments: attachment_type = ( "document" if attachment.resolve_type() == "application/pdf" else "image" ) user_content.append( { "type": attachment_type, "source": source_for_attachment(attachment), } ) # Add text content if it exists if response.prompt.prompt: user_content.append( {"type": "text", "text": response.prompt.prompt} ) # Handle tool results if present (add to the same user message) if response.prompt.tool_results: for tool_result in response.prompt.tool_results: user_content.append( { "type": "tool_result", "tool_use_id": tool_result.tool_call_id, "content": tool_result.output, } ) # Only add the message if we have content if user_content: messages.append({"role": "user", "content": user_content}) # Build assistant message assistant_content = [] text_content = response.text_or_raise() # Add text content if it exists if text_content: assistant_content.append({"type": "text", "text": text_content}) # Add tool calls if they exist tool_calls = response.tool_calls_or_raise() for tool_call in tool_calls: assistant_content.append( { "type": "tool_use", "id": tool_call.tool_call_id, "name": tool_call.name, "input": tool_call.arguments, } ) # Add assistant message if we have content if assistant_content: messages.append({"role": "assistant", "content": assistant_content}) # Handle current prompt's tool results and user content together user_content = [] # Add attachments if prompt.attachments: for attachment in prompt.attachments: attachment_type = ( "document" if attachment.resolve_type() == "application/pdf" else "image" ) user_content.append( { "type": attachment_type, "source": source_for_attachment(attachment), } ) # Add cache control if needed if prompt.options.cache and user_content: user_content[-1]["cache_control"] = {"type": "ephemeral"} # Add current prompt's tool results if prompt.tool_results: for tool_result in prompt.tool_results: user_content.append( { "type": "tool_result", "tool_use_id": tool_result.tool_call_id, "content": tool_result.output, } ) # Add text content if it exists if prompt.prompt: user_content.append({"type": "text", "text": prompt.prompt}) # Add the user message if we have content if user_content: messages.append({"role": "user", "content": user_content}) # Handle cache control for messages without attachments elif prompt.options.cache and messages: last_message = messages[-1] if isinstance(last_message.get("content"), list): last_message["content"][-1]["cache_control"] = {"type": "ephemeral"} else: # This should not happen with our new structure, but keeping as a fallback last_message["cache_control"] = {"type": "ephemeral"} # Add prefill if specified if prompt.options.prefill: prefill_content = [{"type": "text", "text": prompt.options.prefill}] messages.append({"role": "assistant", "content": prefill_content}) return messages def build_kwargs(self, prompt, conversation): if prompt.schema and prompt.tools: raise ValueError( "llm-anthropic does not yet support using both schema and tools in the same prompt" ) # Validate web search support if prompt.options.web_search and not self.supports_web_search: raise ValueError( f"Web search is not supported by model {self.model_id}. " f"Supported models include: claude-3.5-sonnet-latest, claude-3.5-haiku-latest, " f"claude-3.7-sonnet-latest, claude-4-opus, claude-4-sonnet, claude-opus-4.1" ) kwargs = { "model": self.claude_model_id, "messages": self.build_messages(prompt, conversation), } if prompt.options.user_id: kwargs["metadata"] = {"user_id": prompt.options.user_id} if prompt.options.top_p: kwargs["top_p"] = prompt.options.top_p else: kwargs["temperature"] = ( prompt.options.temperature if prompt.options.temperature is not None else DEFAULT_TEMPERATURE ) if prompt.options.top_k: kwargs["top_k"] = prompt.options.top_k if prompt.system: kwargs["system"] = prompt.system if prompt.options.stop_sequences: kwargs["stop_sequences"] = prompt.options.stop_sequences thinking_effort_enabled = ( self.supports_thinking_effort and prompt.options.thinking_effort ) if self.supports_thinking and ( prompt.options.thinking or prompt.options.thinking_budget or thinking_effort_enabled ): prompt.options.thinking = True if thinking_effort_enabled: kwargs["output_config"] = { "effort": prompt.options.thinking_effort.value } else: budget_tokens = ( prompt.options.thinking_budget or DEFAULT_THINKING_TOKENS ) kwargs["thinking"] = {"type": "enabled", "budget_tokens": budget_tokens} max_tokens = self.default_max_tokens if prompt.options.max_tokens is not None: max_tokens = prompt.options.max_tokens if ( self.supports_thinking and prompt.options.thinking_budget is not None and prompt.options.thinking_budget > max_tokens ): max_tokens = prompt.options.thinking_budget + 1 kwargs["max_tokens"] = max_tokens # Determine which beta headers to use betas = [] if "output_config" in kwargs: betas.append("effort-2025-11-24") if max_tokens > 64000: betas.append("output-128k-2025-02-19") if "thinking" in kwargs: kwargs["extra_body"] = {"thinking": kwargs.pop("thinking")} # Check if we should use new structured outputs use_structured_outputs = prompt.schema and self.use_structured_outputs if use_structured_outputs: # Use new structured outputs mechanism betas.append("structured-outputs-2025-11-13") # Transform schema to ensure it meets structured output requirements kwargs["output_format"] = { "type": "json_schema", "schema": transform_schema(prompt.schema), } if betas: kwargs["betas"] = betas tools = [] # Add web search tool if enabled if prompt.options.web_search: web_search_tool = { "type": "web_search_20250305", "name": "web_search", } # Add optional web search parameters if prompt.options.web_search_max_uses: web_search_tool["max_uses"] = prompt.options.web_search_max_uses if prompt.options.web_search_allowed_domains: web_search_tool["allowed_domains"] = ( prompt.options.web_search_allowed_domains ) if prompt.options.web_search_blocked_domains: web_search_tool["blocked_domains"] = ( prompt.options.web_search_blocked_domains ) if prompt.options.web_search_location: location = prompt.options.web_search_location.copy() location["type"] = "approximate" # Required by API web_search_tool["user_location"] = location tools.append(web_search_tool) if prompt.schema and not use_structured_outputs: # Fall back to tools workaround for models that don't support structured outputs tools.append( { "name": "output_structured_data", "input_schema": prompt.schema, } ) kwargs["tool_choice"] = {"type": "tool", "name": "output_structured_data"} if prompt.tools: tools.extend( [ { "name": tool.name, "description": tool.description or "", "input_schema": tool.input_schema, } for tool in prompt.tools ] ) if tools: kwargs["tools"] = tools return kwargs def set_usage(self, response): usage = response.response_json.pop("usage") input_tokens = usage.pop("input_tokens") output_tokens = usage.pop("output_tokens") # Only include usage details if prompt caching was on or web search was used details = None if response.prompt.options.cache or usage.get("server_tool_use"): details = usage response.set_usage(input=input_tokens, output=output_tokens, details=details) def add_tool_usage(self, response, last_message) -> bool: tool_uses = [ item for item in last_message["content"] if item["type"] == "tool_use" ] for tool_use in tool_uses: response.add_tool_call( llm.ToolCall( tool_call_id=tool_use["id"], name=tool_use["name"], arguments=tool_use["input"], ) ) return bool(tool_uses) def __str__(self): return "Anthropic Messages: {}".format(self.model_id) class ClaudeMessages(_Shared, llm.KeyModel): def execute(self, prompt, stream, response, conversation, key): client = Anthropic(api_key=self.get_key(key), base_url=self.base_url) kwargs = self.build_kwargs(prompt, conversation) prefill_text = self.prefill_text(prompt) if "betas" in kwargs: messages_client = client.beta.messages else: messages_client = client.messages # For structured outputs, convert JSON schema to Pydantic model for stream() if "output_format" in kwargs and stream: # Extract the schema and convert to Pydantic model schema = kwargs["output_format"]["schema"] pydantic_model = json_schema_to_model(schema) kwargs["output_format"] = pydantic_model if stream: with messages_client.stream(**kwargs) as stream: if prefill_text: yield prefill_text for chunk in stream: if hasattr(chunk, "delta"): delta = chunk.delta if hasattr(delta, "text"): yield delta.text elif hasattr(delta, "partial_json") and prompt.schema: yield delta.partial_json # This records usage and other data: last_message = self._model_dump_suppress_warnings( stream.get_final_message() ) response.response_json = last_message if self.add_tool_usage(response, last_message): # Avoid "can have dragons.Now that I " bug yield " " else: completion = messages_client.create(**kwargs) text = "".join( [item.text for item in completion.content if hasattr(item, "text")] ) yield prefill_text + text response.response_json = completion.model_dump() if self.add_tool_usage(response, response.response_json): yield " " self.set_usage(response) class AsyncClaudeMessages(_Shared, llm.AsyncKeyModel): async def execute(self, prompt, stream, response, conversation, key): client = AsyncAnthropic(api_key=self.get_key(key), base_url=self.base_url) kwargs = self.build_kwargs(prompt, conversation) if "betas" in kwargs: messages_client = client.beta.messages else: messages_client = client.messages prefill_text = self.prefill_text(prompt) # For structured outputs, convert JSON schema to Pydantic model for stream() if "output_format" in kwargs and stream: # Extract the schema and convert to Pydantic model schema = kwargs["output_format"]["schema"] pydantic_model = json_schema_to_model(schema) kwargs["output_format"] = pydantic_model if stream: async with messages_client.stream(**kwargs) as stream_obj: if prefill_text: yield prefill_text async for chunk in stream_obj: if hasattr(chunk, "delta"): delta = chunk.delta if hasattr(delta, "text"): yield delta.text elif hasattr(delta, "partial_json") and prompt.schema: yield delta.partial_json response.response_json = self._model_dump_suppress_warnings( await stream_obj.get_final_message() ) # TODO: Test this: self.add_tool_usage(response, response.response_json) else: completion = await messages_client.create(**kwargs) text = "".join( [item.text for item in completion.content if hasattr(item, "text")] ) yield prefill_text + text response.response_json = completion.model_dump() # TODO: Test this: self.add_tool_usage(response, response.response_json) self.set_usage(response) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1764048059.0 llm_anthropic-0.23/pyproject.toml0000644000175100017510000000151415111236273016655 0ustar00runnerrunner[project] name = "llm-anthropic" version = "0.23" description = "LLM access to models by Anthropic, including the Claude series" readme = "README.md" authors = [{name = "Simon Willison"}] license = "Apache-2.0" requires-python = ">=3.10" classifiers = [] dependencies = [ "llm>=0.26", "anthropic>=0.75", "json-schema-to-pydantic", ] [project.urls] Homepage = "https://github.com/simonw/llm-anthropic" Changelog = "https://github.com/simonw/llm-anthropic/releases" Issues = "https://github.com/simonw/llm-anthropic/issues" CI = "https://github.com/simonw/llm-anthropic/actions" [project.entry-points.llm] anthropic = "llm_anthropic" [project.optional-dependencies] test = ["pytest", "pytest-recording", "pytest-asyncio", "cogapp"] [tool.pytest.ini_options] asyncio_mode = "strict" asyncio_default_fixture_loop_scope = "function" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1764048066.3384526 llm_anthropic-0.23/setup.cfg0000644000175100017510000000004615111236302015552 0ustar00runnerrunner[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1764048066.3374527 llm_anthropic-0.23/tests/0000755000175100017510000000000015111236302015073 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1764048059.0 llm_anthropic-0.23/tests/test_anthropic.py0000644000175100017510000002621015111236273020503 0ustar00runnerrunnerimport json import llm import os import pytest from pydantic import BaseModel TINY_PNG = ( b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\xa6\x00\x00\x01\x1a" b"\x02\x03\x00\x00\x00\xe6\x99\xc4^\x00\x00\x00\tPLTE\xff\xff\xff" b"\x00\xff\x00\xfe\x01\x00\x12t\x01J\x00\x00\x00GIDATx\xda\xed\xd81\x11" b"\x000\x08\xc0\xc0.]\xea\xaf&Q\x89\x04V\xe0>\xf3+\xc8\x91Z\xf4\xa2\x08EQ\x14E" b"Q\x14EQ\x14EQ\xd4B\x91$I3\xbb\xbf\x08EQ\x14EQ\x14EQ\x14E\xd1\xa5" b"\xd4\x17\x91\xc6\x95\x05\x15\x0f\x9f\xc5\t\x9f\xa4\x00\x00\x00\x00IEND\xaeB`" b"\x82" ) ANTHROPIC_API_KEY = os.environ.get("PYTEST_ANTHROPIC_API_KEY", None) or "sk-..." @pytest.mark.vcr def test_prompt(): model = llm.get_model("claude-sonnet-4.5") model.key = model.key or ANTHROPIC_API_KEY response = model.prompt("Two names for a pet pelican, be brief") assert str(response) == "- Captain\n- Scoop" response_dict = dict(response.response_json) response_dict.pop("id") # differs between requests assert response_dict == { "content": [{"citations": None, "text": "- Captain\n- Scoop", "type": "text"}], "model": "claude-sonnet-4-5-20250929", "role": "assistant", "stop_reason": "end_turn", "stop_sequence": None, "type": "message", } assert response.input_tokens == 17 assert response.output_tokens == 10 assert response.token_details is None @pytest.mark.vcr @pytest.mark.asyncio async def test_async_prompt(): model = llm.get_async_model("claude-sonnet-4.5") model.key = model.key or ANTHROPIC_API_KEY # don't override existing key conversation = model.conversation() response = await conversation.prompt("Two names for a pet pelican, be brief") assert await response.text() == "- Captain\n- Scoop" response_dict = dict(response.response_json) response_dict.pop("id") # differs between requests assert response_dict == { "content": [{"citations": None, "text": "- Captain\n- Scoop", "type": "text"}], "model": "claude-sonnet-4-5-20250929", "role": "assistant", "stop_reason": "end_turn", "stop_sequence": None, "type": "message", } assert response.input_tokens == 17 assert response.output_tokens == 10 assert response.token_details is None response2 = await conversation.prompt("in french") assert await response2.text() == '- Capitaine\n- Bec (meaning "beak")' EXPECTED_IMAGE_TEXT = "Red square, green square." @pytest.mark.vcr def test_image_prompt(): model = llm.get_model("claude-sonnet-4.5") model.key = model.key or ANTHROPIC_API_KEY response = model.prompt( "Describe image in three words", attachments=[llm.Attachment(content=TINY_PNG)], ) assert str(response) == EXPECTED_IMAGE_TEXT response_dict = response.response_json response_dict.pop("id") # differs between requests assert response_dict == { "content": [{"citations": None, "text": EXPECTED_IMAGE_TEXT, "type": "text"}], "model": "claude-sonnet-4-5-20250929", "role": "assistant", "stop_reason": "end_turn", "stop_sequence": None, "type": "message", } assert response.input_tokens == 83 assert response.output_tokens == 9 assert response.token_details is None @pytest.mark.vcr def test_image_with_no_prompt(): model = llm.get_model("claude-sonnet-4.5") model.key = model.key or ANTHROPIC_API_KEY response = model.prompt( prompt=None, attachments=[llm.Attachment(content=TINY_PNG)], ) assert str(response) == ( "I need to describe what I see in this image.\n\n" "The image shows two solid colored rectangles arranged vertically on a white background:\n\n" "1. **Top rectangle**: A bright red rectangle positioned in the upper portion of the image\n" "2. **Bottom rectangle**: A bright green (lime green) rectangle positioned in the lower portion of the image\n\n" "Both rectangles appear to be roughly the same size and shape (horizontal rectangles/landscape orientation), " "and they are separated by white space between them." ) @pytest.mark.vcr def test_url_prompt(): model = llm.get_model("claude-sonnet-4.5") model.key = model.key or ANTHROPIC_API_KEY response = model.prompt( prompt="describe image", attachments=[ llm.Attachment( url="https://static.simonwillison.net/static/2024/pelican.jpg" ) ], ) assert str(response) == ( "This image shows a **brown pelican** perched on rocky terrain at what appears " "to be a marina or harbor. The pelican is captured in profile, displaying its " "distinctive features:\n\n" "- **Long, prominent bill** with the characteristic pelican pouch\n" "- **White head and neck** with darker gray-brown plumage on its body and wings\n" "- **Sturdy build** with detailed feather texture visible in the wings\n\n" "The background shows several **boats docked in a marina**, slightly out of " "focus, creating a typical coastal or waterfront setting. The lighting suggests " "this photo was taken during daytime, with bright natural light that creates " "a slight halo effect around the bird's head.\n\n" "The pelican appears calm and at rest, which is common behavior for these " "seabirds in harbor areas where they often wait for fishing opportunities or " "scraps from nearby boats. The rocky perch and marina setting are typical " "habitats where pelicans congregate along coastlines." ) class Dog(BaseModel): name: str age: int bio: str @pytest.mark.vcr def test_schema_prompt(): model = llm.get_model("claude-sonnet-4.5") response = model.prompt("Invent a good dog", schema=Dog, key=ANTHROPIC_API_KEY) dog = json.loads(response.text()) assert dog == { "name": "Biscuit", "age": 4, "bio": ( "Biscuit is a loyal golden retriever with a gentle temperament and " "boundless enthusiasm for life. He loves swimming in lakes, playing fetch " "until sunset, and has an uncanny ability to sense when someone needs " "comfort. Known in his neighborhood for his friendly demeanor, Biscuit " "volunteers at the local children's hospital bringing joy to young " "patients." ), } @pytest.mark.vcr @pytest.mark.asyncio async def test_schema_prompt_async(): model = llm.get_async_model("claude-sonnet-4.5") response = await model.prompt( "Invent a terrific dog", schema=Dog, key=ANTHROPIC_API_KEY ) dog_json = await response.text() dog = json.loads(dog_json) assert dog == { "age": 3, "bio": ( "Biscuit is a golden retriever with a heart of pure sunshine. This " "enthusiastic pup has mastered over 50 tricks, volunteers at the local " "children's hospital bringing smiles to young patients, and has an uncanny " "ability to sense when someone needs comfort. With her flowing golden " "coat, perpetually wagging tail, and gentle brown eyes, Biscuit embodies " "loyalty and joy. She loves swimming in lakes, playing fetch until sunset, " "and curling up for story time with her family." ), "name": "Biscuit", } @pytest.mark.vcr def test_prompt_with_prefill_and_stop_sequences(): model = llm.get_model("claude-3.5-haiku") response = model.prompt( "Very short function describing a pelican", prefill="```python", stop_sequences=["```"], hide_prefill=True, key=ANTHROPIC_API_KEY, ) text = response.text() assert text == ( "\ndef describe_pelican():\n" ' """Returns a brief description of a pelican."""\n' ' return "Large seabird with a massive bill, capable of scooping fish from water."\n' ) @pytest.mark.vcr def test_thinking_prompt(): model = llm.get_model("claude-sonnet-4.5") conversation = model.conversation() response = conversation.prompt( "Two names for a pet pelican, be brief", thinking=True, key=ANTHROPIC_API_KEY ) assert response.text() == "Bill\nScoop" response_dict = dict(response.response_json) response_dict.pop("id") # differs between requests assert response_dict == { "content": [ { "signature": "ErUBCkYICRgCIkCUCsMUICiFm+sgvS255wUaTjAJEX2tc5h+Mir6Kq6OozIF+9a3ygFFnCLjPYf2Jl18eMVqkYVs0Vq9rRJpl6N/EgySPIWO4SVcxV0VqecaDM6REdwo/8lOJenaQCIwzQfkXeoR1nwYqsvrQsf4/NwhTuKfWDtM8a0XHfoH7EFwizaRuTrwV21Ny1nWKbu2Kh2qjLQOYn34/0pMKErgEexGTvXvn5PMMSAqOVqz8BgC", "thinking": "I need to provide two brief names for a pet pelican. Pelicans are large water birds with distinctive pouched bills, so I could create names that relate to:\n- Their appearance (bill, pouch)\n- Water/ocean themes\n- Their fishing abilities\n- Classic pet names with a twist\n\nI'll provide two short, creative names without additional commentary, as the request asks me to be brief.", "type": "thinking", }, {"citations": None, "text": "Bill\nScoop", "type": "text"}, ], "model": "claude-3-7-sonnet-20250219", "role": "assistant", "stop_reason": "end_turn", "stop_sequence": None, "type": "message", } assert response.input_tokens == 46 assert response.output_tokens == 103 assert response.token_details is None @pytest.mark.vcr def test_tools(): model = llm.get_model("claude-3.5-haiku") names = ["Charles", "Sammy"] chain_response = model.chain( "Two names for a pet pelican", tools=[ llm.Tool.function(lambda: names.pop(0), name="pelican_name_generator"), ], key=ANTHROPIC_API_KEY, ) text = chain_response.text() assert text == ( "I'll help you generate two potential names for a pet pelican by using the pelican " "name generator tool. Great! The tool has generated two fun names for a pet pelican:\n" "1. Charles - A dignified name that could suit a sophisticated pelican\n" "2. Sammy - A friendly and playful name that gives your pelican a cute personality\n\n" "Would you like me to generate more names or do you like these options?" ) tool_calls = chain_response._responses[0].tool_calls() assert len(tool_calls) == 2 assert all(call.name == "pelican_name_generator" for call in tool_calls) assert [ result.output for result in chain_response._responses[1].prompt.tool_results ] == ["Charles", "Sammy"] @pytest.mark.vcr def test_web_search(): model = llm.get_model("claude-opus-4.1") model.key = model.key or ANTHROPIC_API_KEY response = model.prompt( "What is the current weather in San Francisco?", web_search=True ) response_text = str(response) assert len(response_text) > 0 assert any( word in response_text.lower() for word in ["weather", "temperature", "san francisco", "degree", "forecast"] ) response_dict = dict(response.response_json) assert "content" in response_dict assert len(response_dict["content"]) > 0