././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1764048066.3384526
llm_anthropic-0.23/ 0000755 0001751 0001751 00000000000 15111236302 013731 5 ustar 00runner runner ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1764048059.0
llm_anthropic-0.23/LICENSE 0000644 0001751 0001751 00000026135 15111236273 014754 0 ustar 00runner runner 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.
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1764048066.3384526
llm_anthropic-0.23/PKG-INFO 0000644 0001751 0001751 00000026143 15111236302 015034 0 ustar 00runner runner Metadata-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
[](https://pypi.org/project/llm-anthropic/)
[](https://github.com/simonw/llm-anthropic/releases)
[](https://github.com/simonw/llm-anthropic/actions/workflows/test.yml)
[](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
```
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1764048059.0
llm_anthropic-0.23/README.md 0000644 0001751 0001751 00000024421 15111236273 015222 0 ustar 00runner runner # llm-anthropic
[](https://pypi.org/project/llm-anthropic/)
[](https://github.com/simonw/llm-anthropic/releases)
[](https://github.com/simonw/llm-anthropic/actions/workflows/test.yml)
[](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
```
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1764048066.3374527
llm_anthropic-0.23/llm_anthropic.egg-info/ 0000755 0001751 0001751 00000000000 15111236302 020256 5 ustar 00runner runner ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1764048066.0
llm_anthropic-0.23/llm_anthropic.egg-info/PKG-INFO 0000644 0001751 0001751 00000026143 15111236302 021361 0 ustar 00runner runner Metadata-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
[](https://pypi.org/project/llm-anthropic/)
[](https://github.com/simonw/llm-anthropic/releases)
[](https://github.com/simonw/llm-anthropic/actions/workflows/test.yml)
[](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
```
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1764048066.0
llm_anthropic-0.23/llm_anthropic.egg-info/SOURCES.txt 0000644 0001751 0001751 00000000451 15111236302 022142 0 ustar 00runner runner LICENSE
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 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1764048066.0
llm_anthropic-0.23/llm_anthropic.egg-info/dependency_links.txt 0000644 0001751 0001751 00000000001 15111236302 024324 0 ustar 00runner runner
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1764048066.0
llm_anthropic-0.23/llm_anthropic.egg-info/entry_points.txt 0000644 0001751 0001751 00000000040 15111236302 023546 0 ustar 00runner runner [llm]
anthropic = llm_anthropic
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1764048066.0
llm_anthropic-0.23/llm_anthropic.egg-info/requires.txt 0000644 0001751 0001751 00000000150 15111236302 022652 0 ustar 00runner runner llm>=0.26
anthropic>=0.75
json-schema-to-pydantic
[test]
pytest
pytest-recording
pytest-asyncio
cogapp
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1764048066.0
llm_anthropic-0.23/llm_anthropic.egg-info/top_level.txt 0000644 0001751 0001751 00000000016 15111236302 023005 0 ustar 00runner runner llm_anthropic
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1764048059.0
llm_anthropic-0.23/llm_anthropic.py 0000644 0001751 0001751 00000076753 15111236273 017167 0 ustar 00runner runner from 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)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1764048059.0
llm_anthropic-0.23/pyproject.toml 0000644 0001751 0001751 00000001514 15111236273 016655 0 ustar 00runner runner [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"
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1764048066.3384526
llm_anthropic-0.23/setup.cfg 0000644 0001751 0001751 00000000046 15111236302 015552 0 ustar 00runner runner [egg_info]
tag_build =
tag_date = 0
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1764048066.3374527
llm_anthropic-0.23/tests/ 0000755 0001751 0001751 00000000000 15111236302 015073 5 ustar 00runner runner ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1764048059.0
llm_anthropic-0.23/tests/test_anthropic.py 0000644 0001751 0001751 00000026210 15111236273 020503 0 ustar 00runner runner import 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