# Claudette’s source


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

This is the ‘literate’ source code for Claudette. You can view the fully
rendered version of the notebook
[here](https://claudette.answer.ai/core.html), or you can clone the git
repo and run the [interactive
notebook](https://github.com/AnswerDotAI/claudette/blob/main/00_core.ipynb)
in Jupyter. The notebook is converted the [Python module
claudette/core.py](https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py)
using [nbdev](https://nbdev.fast.ai/). The goal of this source code is
to both create the Python module, and also to teach the reader *how* it
is created, without assuming much existing knowledge about Claude’s API.

Most of the time you’ll see that we write some source code *first*, and
then a description or discussion of it *afterwards*.

## Setup

``` python
import os
# os.environ['ANTHROPIC_LOG'] = 'debug'
```

To print every HTTP request and response in full, uncomment the above
line. This functionality is provided by Anthropic’s SDK.

``` python
from anthropic.types import Model
from claudette.text_editor import *
from typing import get_args
from datetime import datetime
from pprint import pprint
from IPython.display import Image
from cachy import enable_cachy

import warnings
```

``` python
enable_cachy()
```

``` python
warnings.filterwarnings("ignore", message="Pydantic serializer warnings")
```

<div>

> **Tip**
>
> If you’re reading the rendered version of this notebook, you’ll see an
> “Exported source” collapsible widget below. If you’re reading the
> source notebook directly, you’ll see `#| exports` at the top of the
> cell. These show that this piece of code will be exported into the
> python module that this notebook creates. No other code will be
> included – any other code in this notebook is just for demonstration,
> documentation, and testing.
>
> You can toggle expanding/collapsing the source code of all exported
> sections by using the `</> Code` menu in the top right of the rendered
> notebook page.

</div>

<details open class="code-fold">
<summary>Exported source</summary>

``` python
model_types = {
    # Anthropic
    'claude-opus-4-6': 'opus',
    'claude-sonnet-4-6': 'sonnet',
    'claude-haiku-4-5': 'haiku',
    'claude-opus-4-5': 'opus-4-5',
    'claude-sonnet-4-5': 'sonnet-4-5',
    'claude-opus-4-1-20250805': 'opus-4-1',
    'claude-opus-4-20250514': 'opus-4',
    'claude-3-opus-20240229': 'opus-3',
    'claude-sonnet-4-20250514': 'sonnet-4',
    'claude-3-7-sonnet-20250219': 'sonnet-3-7',
    'claude-3-5-sonnet-20241022': 'sonnet-3-5',
    'claude-3-haiku-20240307': 'haiku-3',
    'claude-3-5-haiku-20241022': 'haiku-3-5',
    # AWS
    'anthropic.claude-opus-4-1-20250805-v1:0': 'opus',
    'anthropic.claude-3-5-sonnet-20241022-v2:0': 'sonnet',
    'anthropic.claude-3-opus-20240229-v1:0': 'opus-3',
    'anthropic.claude-3-sonnet-20240229-v1:0': 'sonnet',
    'anthropic.claude-3-haiku-20240307-v1:0': 'haiku',
    # Google
    'claude-opus-4-1@20250805': 'opus',
    'claude-3-5-sonnet-v2@20241022': 'sonnet',
    'claude-3-opus@20240229': 'opus-3',
    'claude-3-sonnet@20240229': 'sonnet',
    'claude-3-haiku@20240307': 'haiku',
}

all_models = list(model_types)
```

</details>

``` python
models
```

    ['claude-opus-4-6',
     'claude-sonnet-4-6',
     'claude-haiku-4-5',
     'claude-opus-4-5',
     'claude-sonnet-4-5',
     'claude-opus-4-1-20250805',
     'claude-opus-4-20250514',
     'claude-3-opus-20240229',
     'claude-sonnet-4-20250514',
     'claude-3-7-sonnet-20250219']

<details open class="code-fold">
<summary>Exported source</summary>

``` python
text_only_models = ('claude-3-5-haiku-20241022',)
```

</details>

<details open class="code-fold">
<summary>Exported source</summary>

``` python
has_streaming_models = set(all_models)
has_system_prompt_models = set(all_models)
has_temperature_models = set(all_models)
has_extended_thinking_models = {
    'claude-opus-4-6', 'claude-sonnet-4-6',
    'claude-opus-4-5', 'claude-sonnet-4-5', 'claude-haiku-4-5',
    'claude-opus-4-1-20250805', 'claude-opus-4-20250514', 'claude-sonnet-4-20250514', 'claude-3-7-sonnet-20250219'
}
```

</details>

``` python
has_extended_thinking_models
```

    {'claude-3-7-sonnet-20250219',
     'claude-haiku-4-5',
     'claude-opus-4-1-20250805',
     'claude-opus-4-20250514',
     'claude-opus-4-5',
     'claude-opus-4-6',
     'claude-sonnet-4-20250514',
     'claude-sonnet-4-5',
     'claude-sonnet-4-6'}

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L114"
target="_blank" style="float:right; font-size:smaller">source</a>

### can_use_extended_thinking

``` python

def can_use_extended_thinking(
    m
):

```

*Call self as a function.*

<details open class="code-fold">
<summary>Exported source</summary>

``` python
def can_stream(m): return m in has_streaming_models
def can_set_system_prompt(m): return m in has_system_prompt_models
def can_set_temperature(m): return m in has_temperature_models
def can_use_extended_thinking(m): return m in has_extended_thinking_models
```

</details>

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L113"
target="_blank" style="float:right; font-size:smaller">source</a>

### can_set_temperature

``` python

def can_set_temperature(
    m
):

```

*Call self as a function.*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L112"
target="_blank" style="float:right; font-size:smaller">source</a>

### can_set_system_prompt

``` python

def can_set_system_prompt(
    m
):

```

*Call self as a function.*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L111"
target="_blank" style="float:right; font-size:smaller">source</a>

### can_stream

``` python

def can_stream(
    m
):

```

*Call self as a function.*

We include these functions to provide a uniform library interface with
cosette since openai models such as o1 do not have many of these
capabilities.

``` python
assert can_stream('claude-3-5-sonnet-20241022') and can_set_system_prompt('claude-3-5-sonnet-20241022') and can_set_temperature('claude-3-5-sonnet-20241022')
```

These are the current versions and
[prices](https://www.anthropic.com/pricing#anthropic-api) of Anthropic’s
models at the time of writing.

``` python
model = models[1]
model
```

    'claude-sonnet-4-6'

## Antropic SDK

``` python
cli = Anthropic()
```

This is what Anthropic’s SDK provides for interacting with Python. To
use it, pass it a list of *messages*, with *content* and a *role*. The
roles should alternate between *user* and *assistant*.

<div>

> **Tip**
>
> After the code below you’ll see an indented section with an orange
> vertical line on the left. This is used to show the *result* of
> running the code above. Because the code is running in a Jupyter
> Notebook, we don’t have to use `print` to display results, we can just
> type the expression directly, as we do with `r` here.

</div>

``` python
m = {'role': 'user', 'content': "I'm Jeremy"}
r = cli.messages.create(messages=[m], model=model, max_tokens=100)
r
```

Hi Jeremy! Nice to meet you. How can I help you today?

<details>

- id: `msg_01Q3123HGtAxWnUraLL65H24`
- container: `None`
- content:
  `[{'citations': None, 'text': 'Hi Jeremy! Nice to meet you. How can I help you today?', 'type': 'text'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `end_turn`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 10, 'output_tokens': 18, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

### Formatting output

That output is pretty long and hard to read, so let’s clean it up. We’ll
start by pulling out the `Content` part of the message. To do that,
we’re going to write our first function which will be included to the
`claudette/core.py` module.

<div>

> **Tip**
>
> This is the first exported public function or class we’re creating
> (the previous export was of a variable). In the rendered version of
> the notebook for these you’ll see 4 things, in this order (unless the
> symbol starts with a single `_`, which indicates it’s *private*):
>
> - The signature (with the symbol name as a heading, with a horizontal
>   rule above)
> - A table of paramater docs (if provided)
> - The doc string (in italics).
> - The source code (in a collapsible “Exported source” block)
>
> After that, we generally provide a bit more detail on what we’ve
> created, and why, along with a sample usage.

</div>

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L121"
target="_blank" style="float:right; font-size:smaller">source</a>

### find_block

``` python

def find_block(
    r:Mapping, # The message to look in
    blk_type:type | str=TextBlock, # The type of block to find
):

```

*Find the first block of type `blk_type` in `r.content`.*

<details open class="code-fold">
<summary>Exported source</summary>

``` python
def _type(x):
    try: return x.type
    except AttributeError: return x.get('type')

def find_block(r:abc.Mapping, # The message to look in
               blk_type:type|str=TextBlock  # The type of block to find
              ):
    "Find the first block of type `blk_type` in `r.content`."
    f = (lambda x:_type(x)==blk_type) if isinstance(blk_type,str) else (lambda x:isinstance(x,blk_type))
    return first(o for o in r.content if f(o))
```

</details>

This makes it easier to grab the needed parts of Claude’s responses,
which can include multiple pieces of content. By default, we look for
the first text block. That will generally have the content we want to
display.

``` python
find_block(r)
```

    TextBlock(citations=None, text='Hi Jeremy! Nice to meet you. How can I help you today?', type='text')

``` python
def contents(r):
    "Helper to get the contents from Claude response `r`."
    blk = find_block(r)
    if not blk and r.content: blk = r.content[0]
    if hasattr(blk,'text'): return blk.text.strip()
    elif hasattr(blk,'content'): return blk.content.strip()
    elif hasattr(blk,'source'): return f'*Media Type - {blk.type}*'
    return str(blk)
```

For display purposes, we often just want to show the text itself.

``` python
contents(r)
```

    'Hi Jeremy! Nice to meet you. How can I help you today?'

<details open class="code-fold">
<summary>Exported source</summary>

``` python
@patch
def _repr_markdown_(self:(Message)):
    det = '\n- '.join(f'{k}: `{v}`' for k,v in self.model_dump().items())
    cts = re.sub(r'\$', '&#36;', contents(self))  # escape `$` for jupyter latex
    return f"""{cts}

<details>

- {det}

</details>"""
```

</details>

Jupyter looks for a `_repr_markdown_` method in displayed objects; we
add this in order to display just the content text, and collapse full
details into a hideable section. Note that `patch` is from
[fastcore](https://fastcore.fast.ai/), and is used to add (or replace)
functionality in an existing class. We pass the class(es) that we want
to patch as type annotations to `self`. In this case, `_repr_markdown_`
is being added to Anthropic’s `Message` class, so when we display the
message now we just see the contents, and the details are hidden away in
a collapsible details block.

``` python
r
```

Hi Jeremy! Nice to meet you. How can I help you today?

<details>

- id: `msg_01Q3123HGtAxWnUraLL65H24`
- container: `None`
- content:
  `[{'citations': None, 'text': 'Hi Jeremy! Nice to meet you. How can I help you today?', 'type': 'text'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `end_turn`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 10, 'output_tokens': 18, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

One key part of the response is the
[`usage`](https://claudette.answer.ai/core.html#usage) key, which tells
us how many tokens we used by returning a `Usage` object.

We’ll add some helpers to make things a bit cleaner for creating and
formatting these objects.

``` python
r.usage
```

    In: 10; Out: 18; Cache create: 0; Cache read: 0; Total Tokens: 28; Search: 0

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L142"
target="_blank" style="float:right; font-size:smaller">source</a>

### server_tool_usage

``` python

def server_tool_usage(
    web_search_requests:int=0, web_fetch_requests:int=0
):

```

*Little helper to create a server tool usage object*

<details open class="code-fold">
<summary>Exported source</summary>

``` python
def server_tool_usage(web_search_requests=0, web_fetch_requests=0):
    'Little helper to create a server tool usage object'
    return ServerToolUsage(web_search_requests=web_search_requests, web_fetch_requests=web_fetch_requests)
```

</details>

``` python
server_tool_usage(3,2)
```

    ServerToolUsage(web_fetch_requests=2, web_search_requests=3)

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L147"
target="_blank" style="float:right; font-size:smaller">source</a>

### usage

``` python

def usage(
    inp:int=0, # input tokens
    out:int=0, # Output tokens
    cache_create:int=0, # Cache creation tokens
    cache_read:int=0, # Cache read tokens
    server_tool_use:ServerToolUsage=ServerToolUsage(web_fetch_requests=0, web_search_requests=0), # server tool use
):

```

*Slightly more concise version of `Usage`.*

<details open class="code-fold">
<summary>Exported source</summary>

``` python
def usage(inp=0, # input tokens
          out=0,  # Output tokens
          cache_create=0, # Cache creation tokens
          cache_read=0, # Cache read tokens
          server_tool_use=server_tool_usage() # server tool use
         ):
    'Slightly more concise version of `Usage`.'
    return Usage(input_tokens=inp, output_tokens=out, cache_creation_input_tokens=cache_create,
                 cache_read_input_tokens=cache_read, server_tool_use=server_tool_use)
```

</details>

The constructor provided by Anthropic is rather verbose, so we clean it
up a bit, using a lowercase version of the name.

``` python
usage(5)
```

    In: 5; Out: 0; Cache create: 0; Cache read: 0; Total Tokens: 5; Search: 0

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L163"
target="_blank" style="float:right; font-size:smaller">source</a>

### Usage.total

``` python

def total(
    
):

```

*Call self as a function.*

<details open class="code-fold">
<summary>Exported source</summary>

``` python
def _dgetattr(o,s,d): 
    "Like getattr, but returns the default if the result is None"
    return getattr(o,s,d) or d

@patch(as_prop=True)
def total(self:Usage): return self.input_tokens+self.output_tokens+_dgetattr(self, "cache_creation_input_tokens",0)+_dgetattr(self, "cache_read_input_tokens",0)
```

</details>

Adding a `total` property to `Usage` makes it easier to see how many
tokens we’ve used up altogether.

``` python
usage(5,1).total
```

    6

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L167"
target="_blank" style="float:right; font-size:smaller">source</a>

### Usage.\_\_repr\_\_

``` python

def __repr__(
    
):

```

*Return repr(self).*

<details open class="code-fold">
<summary>Exported source</summary>

``` python
@patch
def __repr__(self:Usage):
    io_toks = f'In: {self.input_tokens}; Out: {self.output_tokens}'
    cache_toks = f'Cache create: {_dgetattr(self, "cache_creation_input_tokens",0)}; Cache read: {_dgetattr(self, "cache_read_input_tokens",0)}'
    server_tool_use = _dgetattr(self, "server_tool_use",server_tool_usage())
    server_tool_use_str = f'Search: {server_tool_use.web_search_requests}; Fetch: {server_tool_use.web_fetch_requests}'
    total_tok = f'Total Tokens: {self.total}'
    return f'{io_toks}; {cache_toks}; {total_tok}; {server_tool_use_str}'
```

</details>

In python, patching `__repr__` lets us change how an object is
displayed. (More generally, methods starting and ending in `__` in
Python are called `dunder` methods, and have some `magic` behavior –
such as, in this case, changing how an object is displayed.) We won’t be
directly displaying ServerToolUsage’s, so we can handle its display
behavior in the same Usage `__repr__`

``` python
usage(5)
```

    In: 5; Out: 0; Cache create: 0; Cache read: 0; Total Tokens: 5; Search: 0; Fetch: 0

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L177"
target="_blank" style="float:right; font-size:smaller">source</a>

### ServerToolUsage.\_\_add\_\_

``` python

def __add__(
    b
):

```

*Add together each of the server tool use counts*

<details open class="code-fold">
<summary>Exported source</summary>

``` python
@patch
def __add__(self:ServerToolUsage, b):
    "Add together each of the server tool use counts"
    return ServerToolUsage(web_search_requests=self.web_search_requests+b.web_search_requests,
        web_fetch_requests=self.web_fetch_requests+b.web_fetch_requests)
```

</details>

And, patching `__add__` lets `+` work on a `ServerToolUsage` as well as
a `Usage` object.

``` python
server_tool_usage(1) + server_tool_usage(2,3)
```

    ServerToolUsage(web_fetch_requests=3, web_search_requests=3)

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L184"
target="_blank" style="float:right; font-size:smaller">source</a>

### Usage.\_\_add\_\_

``` python

def __add__(
    b
):

```

*Add together each of `input_tokens` and `output_tokens`*

<details open class="code-fold">
<summary>Exported source</summary>

``` python
@patch
def __add__(self:Usage, b):
    "Add together each of `input_tokens` and `output_tokens`"
    return usage(self.input_tokens+b.input_tokens, self.output_tokens+b.output_tokens,
                 _dgetattr(self,'cache_creation_input_tokens',0)+_dgetattr(b,'cache_creation_input_tokens',0),
                 _dgetattr(self,'cache_read_input_tokens',0)+_dgetattr(b,'cache_read_input_tokens',0),
                 _dgetattr(self,'server_tool_use',server_tool_usage())+_dgetattr(b,'server_tool_use',server_tool_usage()))
```

</details>

``` python
r.usage+r.usage + usage(server_tool_use=server_tool_usage(1))
```

    In: 20; Out: 36; Cache create: 0; Cache read: 0; Total Tokens: 56; Search: 1; Fetch: 0

### Creating messages

Creating correctly formatted `dict`s from scratch every time isn’t very
handy, so we’ll import a couple of helper functions from the `msglm`
library.

Let’s use `mk_msg` to recreate our msg
`{'role': 'user', 'content': "I'm Jeremy"}` from earlier.

``` python
prompt = "I'm Jeremy"
m = mk_msg(prompt)
r = cli.messages.create(messages=[m], model=model, max_tokens=100)
r
```

Hi Jeremy! Nice to meet you. How can I help you today?

<details>

- id: `msg_01Q3123HGtAxWnUraLL65H24`
- container: `None`
- content:
  `[{'citations': None, 'text': 'Hi Jeremy! Nice to meet you. How can I help you today?', 'type': 'text'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `end_turn`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 10, 'output_tokens': 18, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

We can pass more than just text messages to Claude. As we’ll see later
we can also pass images, SDK objects, etc. To handle these different
data types we need to pass the type along with our content to Claude.

Here”s an example of a multimodal message containing text and images.

``` json
{
    "role": "user", 
    "content": [
        {"type":"text", "text":"What is in the image?"},
        {
            "type":"image", 
            "source": {
                "type":"base64", "media_type":"media_type", "data": "data"
            }
        }
    ]
}
```

`mk_msg` infers the type automatically and creates the appropriate data
structure.

LLMs, don’t actually have state, but instead dialogs are created by
passing back all previous prompts and responses every time. With Claude,
they always alternate *user* and *assistant*. We’ll use `mk_msgs` from
`msglm` to make it easier to build up these dialog lists.

``` python
msgs = mk_msgs([prompt, r, "I forgot my name. Can you remind me please?"]) 
msgs
```

    [{'role': 'user', 'content': "I'm Jeremy"},
     {'role': 'assistant',
      'content': [TextBlock(citations=None, text='Hi Jeremy! Nice to meet you. How can I help you today?', type='text')]},
     {'role': 'user', 'content': 'I forgot my name. Can you remind me please?'}]

``` python
cli.messages.create(messages=msgs, model=model, max_tokens=200)
```

Your name is Jeremy! You told me that at the start of our conversation.
😊

<details>

- id: `msg_01QTvdUwQAyKRQT3H73STdU3`
- container: `None`
- content:
  `[{'citations': None, 'text': 'Your name is Jeremy! You told me that at the start of our conversation. 😊', 'type': 'text'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `end_turn`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 42, 'output_tokens': 22, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

## Client

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L192"
target="_blank" style="float:right; font-size:smaller">source</a>

### Client

``` python

def Client(
    model, cli:NoneType=None, log:bool=False, cache:bool=False
):

```

*Basic Anthropic messages client.*

<details open class="code-fold">
<summary>Exported source</summary>

``` python
class Client:
    def __init__(self, model, cli=None, log=False, cache=False):
        "Basic Anthropic messages client."
        self.model,self.use = model,usage()
        self.text_only = model in text_only_models
        self.log = [] if log else None
        self.c = (cli or Anthropic(default_headers={'anthropic-beta': 'prompt-caching-2024-07-31'}))
        self.cache = cache
```

</details>

We’ll create a simple
[`Client`](https://claudette.answer.ai/core.html#client) for `Anthropic`
which tracks usage stores the model to use. We don’t add any methods
right away – instead we’ll use `patch` for that so we can add and
document them incrementally.

``` python
c = Client(model)
c.use
```

    In: 0; Out: 0; Cache create: 0; Cache read: 0; Total Tokens: 0; Search: 0; Fetch: 0

<details open class="code-fold">
<summary>Exported source</summary>

``` python
@patch
def _r(self:Client, r:Message, prefill=''):
    "Store the result of the message and accrue total usage."
    if prefill:
        blk = find_block(r)
        if blk: blk.text = prefill + (blk.text or '')
    self.result = r
    self.use += r.usage
    self.stop_reason = r.stop_reason
    self.stop_sequence = r.stop_sequence
    return r
```

</details>

We use a `_` prefix on private methods, but we document them here in the
interests of literate source code.

`_r` will be used each time we get a new result, to track usage and also
to keep the result available for later.

``` python
c._r(r)
c.use
```

    In: 10; Out: 18; Cache create: 0; Cache read: 0; Total Tokens: 28; Search: 0; Fetch: 0

Whereas OpenAI’s models use a `stream` parameter for streaming,
Anthropic’s use a separate method. We implement Anthropic’s approach in
a private method, and then use a `stream` parameter in `__call__` for
consistency:

<details open class="code-fold">
<summary>Exported source</summary>

``` python
@patch
def _log(self:Client, final, prefill, msgs, **kwargs):
    self._r(final, prefill)
    if self.log is not None: self.log.append({
        "msgs": msgs, **kwargs,
        "result": self.result, "use": self.use, "stop_reason": self.stop_reason, "stop_sequence": self.stop_sequence
    })
    return self.result
```

</details>

Once streaming is complete, we need to store the final message and call
any completion callback that’s needed.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L234"
target="_blank" style="float:right; font-size:smaller">source</a>

### get_types

``` python

def get_types(
    msgs
):

```

*Call self as a function.*

<details open class="code-fold">
<summary>Exported source</summary>

``` python
@save_iter
def _stream(o, cm, prefill, cb):
    with cm as s:
        yield prefill
        yield from s.text_stream
        o.value = s.get_final_message()
        cb(o.value)
```

</details>

``` python
get_types(msgs)
```

    ['text', 'text', 'text']

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L243"
target="_blank" style="float:right; font-size:smaller">source</a>

### mk_tool_choice

``` python

def mk_tool_choice(
    choose:Union
)->dict:

```

*Create a `tool_choice` dict that’s ‘auto’ if `choose` is `None`, ‘any’
if it is True, or ‘tool’ otherwise*

``` python
print(mk_tool_choice('sums'))
print(mk_tool_choice(True))
print(mk_tool_choice(None))
```

    {'type': 'tool', 'name': 'sums'}
    {'type': 'any'}
    {'type': 'auto'}

Claude can be forced to use a particular tool, or select from a specific
list of tools, or decide for itself when to use a tool. If you want to
force a tool (or force choosing from a list), include a `tool_choice`
param with a dict from
[`mk_tool_choice`](https://claudette.answer.ai/core.html#mk_tool_choice).

Claude supports adding an extra `assistant` message at the end, which
contains the *prefill* – i.e. the text we want Claude to assume the
response starts with. However Claude doesn’t actually repeat that in the
response, so for convenience we add it.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L269"
target="_blank" style="float:right; font-size:smaller">source</a>

### Client.\_\_call\_\_

``` python

def __call__(
    msgs:list, # List of messages in the dialog
    sp:str='', # The system prompt
    temp:int=0, # Temperature
    maxtok:int=4096, # Maximum tokens
    maxthinktok:int=0, # Maximum thinking tokens
    prefill:str='', # Optional prefill to pass to Claude as start of its response
    stream:bool=False, # Stream response?
    stop:NoneType=None, # Stop sequence
    tools:Optional=None, # List of tools to make available to Claude
    tool_choice:Optional=None, # Optionally force use of some tool
    cb:NoneType=None, # Callback to pass result to when complete
    cache_control:Optional[CacheControlEphemeralParam] | Omit=<anthropic.Omit object at 0x7f45180ce210>,
    container:Optional[str] | Omit=<anthropic.Omit object at 0x7f45180ce210>,
    inference_geo:Optional[str] | Omit=<anthropic.Omit object at 0x7f45180ce210>,
    metadata:MetadataParam | Omit=<anthropic.Omit object at 0x7f45180ce210>,
    output_config:OutputConfigParam | Omit=<anthropic.Omit object at 0x7f45180ce210>,
    service_tier:Literal['auto', 'standard_only'] | Omit=<anthropic.Omit object at 0x7f45180ce210>,
    stop_sequences:SequenceNotStr[str] | Omit=<anthropic.Omit object at 0x7f45180ce210>,
    system:Union[str, Iterable[TextBlockParam]] | Omit=<anthropic.Omit object at 0x7f45180ce210>,
    temperature:float | Omit=<anthropic.Omit object at 0x7f45180ce210>,
    thinking:ThinkingConfigParam | Omit=<anthropic.Omit object at 0x7f45180ce210>,
    top_k:int | Omit=<anthropic.Omit object at 0x7f45180ce210>,
    top_p:float | Omit=<anthropic.Omit object at 0x7f45180ce210>,
    extra_headers:Headers | None=None, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
The extra values given here take precedence over values defined on the client or passed to this method.
    extra_query:Query | None=None, extra_body:Body | None=None,
    timeout:float | httpx.Timeout | None | NotGiven=NOT_GIVEN
):

```

*Make a call to Claude.*

<details open class="code-fold">
<summary>Exported source</summary>

``` python
@patch
def _precall(self:Client, msgs, prefill, sp, temp, maxtok, maxthinktok, stream,
             stop, tools, tool_choice, kwargs):
    if tools: kwargs['tools'] = [get_schema(o) if callable(o) else o for o in listify(tools)]
    if tool_choice: kwargs['tool_choice'] = mk_tool_choice(tool_choice)
    if maxthinktok: 
        kwargs['thinking'] = {'type':'enabled', 'budget_tokens':maxthinktok} 
        temp,prefill = 1,''
    pref = [prefill.strip()] if prefill else []
    if not isinstance(msgs,list): msgs = [msgs]
    if stop is not None:
        if not isinstance(stop, (list)): stop = [stop]
        kwargs["stop_sequences"] = stop
    msgs = mk_msgs(msgs+pref, cache=self.cache, cache_last_ckpt_only=self.cache)
    assert not ('image' in get_types(msgs) and self.text_only), f"Images not supported by: {self.model}"
    kwargs |= dict(max_tokens=maxtok, system=sp, temperature=temp)
    return msgs, kwargs
```

</details>

<details open class="code-fold">
<summary>Exported source</summary>

``` python
@patch
@delegates(messages.Messages.create)
def __call__(self:Client,
             msgs:list, # List of messages in the dialog
             sp='', # The system prompt
             temp=0, # Temperature
             maxtok=4096, # Maximum tokens
             maxthinktok=0, # Maximum thinking tokens
             prefill='', # Optional prefill to pass to Claude as start of its response
             stream:bool=False, # Stream response?
             stop=None, # Stop sequence
             tools:Optional[list]=None, # List of tools to make available to Claude
             tool_choice:Optional[dict]=None, # Optionally force use of some tool
             cb=None, # Callback to pass result to when complete
             **kwargs):
    "Make a call to Claude."
    msgs,kwargs = self._precall(msgs, prefill, sp, temp, maxtok, maxthinktok, stream,
                                stop, tools, tool_choice, kwargs)
    m = self.c.messages
    f = m.stream if stream else m.create
    res = f(model=self.model, messages=msgs, **kwargs)
    def _cb(v):
        self._log(v, prefill=prefill, msgs=msgs, **kwargs)
        if cb: cb(v)
    if stream: return _stream(res, prefill, _cb)
    try: return res
    finally: _cb(res)
```

</details>

Defining `__call__` let’s us use an object like a function (i.e it’s
*callable*). We use it as a small wrapper over `messages.create`.

``` python
c = Client(model, log=True)
c.use
```

    In: 0; Out: 0; Cache create: 0; Cache read: 0; Total Tokens: 0; Search: 0; Fetch: 0

``` python
c('Hi')
```

Hi there! How are you doing? Is there something I can help you with
today? 😊

<details>

- id: `msg_01NSbD7ZmQJKeY16gJ6apjAH`
- container: `None`
- content:
  `[{'citations': None, 'text': 'Hi there! How are you doing? Is there something I can help you with today? 😊', 'type': 'text'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `end_turn`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 8, 'output_tokens': 24, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

Usage details are automatically updated after each call:

``` python
c.use
```

    In: 8; Out: 24; Cache create: 0; Cache read: 0; Total Tokens: 32; Search: 0; Fetch: 0

A log of all messages is kept if `log=True` is passed:

``` python
pprint(c.log)
```

    [{'max_tokens': 4096,
      'msgs': [{'content': 'Hi', 'role': 'user'}],
      'result': Message(id='msg_01NSbD7ZmQJKeY16gJ6apjAH', container=None, content=[TextBlock(citations=None, text='Hi there! How are you doing? Is there something I can help you with today? 😊', type='text')], model='claude-sonnet-4-6', role='assistant', stop_reason='end_turn', stop_sequence=None, type='message', usage=In: 8; Out: 24; Cache create: 0; Cache read: 0; Total Tokens: 32; Search: 0; Fetch: 0),
      'stop_reason': 'end_turn',
      'stop_sequence': None,
      'system': '',
      'temperature': 0,
      'use': In: 8; Out: 24; Cache create: 0; Cache read: 0; Total Tokens: 32; Search: 0; Fetch: 0}]

``` python
c.use
```

    In: 8; Out: 24; Cache create: 0; Cache read: 0; Total Tokens: 32; Search: 0; Fetch: 0

We can pass `stream=True` to stream the response back incrementally:

``` python
r = c('Hi', stream=True)
for o in r: print(o, end='')
```

    Hi there! How are you doing? Is there something I can help you with today? 😊

``` python
c.use
```

    In: 16; Out: 48; Cache create: 0; Cache read: 0; Total Tokens: 64; Search: 0; Fetch: 0

The full final message after completion of streaming is in the `value`
attr of the response:

``` python
r.value
```

Hi there! How are you doing? Is there something I can help you with
today? 😊

<details>

- id: `msg_01Nu7hy5Yi1KdQXg48jpFVCS`
- container: `None`
- content:
  `[{'citations': None, 'text': 'Hi there! How are you doing? Is there something I can help you with today? 😊', 'type': 'text', 'parsed_output': None}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `end_turn`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 8, 'output_tokens': 24, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

Pass a stop sequence if you want claude to stop generating text when it
encounters it.

``` python
c("Count from 1 to 10", stop="5")
```

Here are the numbers from 1 to 10:

1, 2, 3, 4,

<details>

- id: `msg_01UNUPpjNHteRG8DTrSUw4Hm`
- container: `None`
- content:
  `[{'citations': None, 'text': 'Here are the numbers from 1 to 10:\n\n1, 2, 3, 4, ', 'type': 'text'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `stop_sequence`
- stop_sequence: `5`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 15, 'output_tokens': 26, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

This also works with streaming, and you can pass more than one stop
sequence:

``` python
for o in c("Count from 1 to 10", stop=["3", "yellow"], stream=True): print(o, end='')
print()
print(c.stop_reason, c.stop_sequence)
```

    Here is counting from 1 to 10:

    1, 2, 
    stop_sequence 3

We’ve shown the token usage but we really care about is pricing. Let’s
extract the latest
[pricing](https://www.anthropic.com/pricing#anthropic-api) from
Anthropic into a `pricing` dict.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L305"
target="_blank" style="float:right; font-size:smaller">source</a>

### get_pricing

``` python

def get_pricing(
    m, u
):

```

*Call self as a function.*

<details open class="code-fold">
<summary>Exported source</summary>

``` python
def get_pricing(m, u):
    return pricing[m][:3] if u.prompt_token_count < 128_000 else pricing[m][3:]
```

</details>

Similarly, let’s get the pricing for the latest [server tools]():

We’ll patch `Usage` to enable it compute the cost given pricing.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L315"
target="_blank" style="float:right; font-size:smaller">source</a>

### Usage.cost

``` python

def cost(
    costs:tuple
)->float:

```

*Call self as a function.*

<details open class="code-fold">
<summary>Exported source</summary>

``` python
@patch
def cost(self:Usage, costs:tuple) -> float:
    cache_w, cache_r = _dgetattr(self, "cache_creation_input_tokens",0), _dgetattr(self, "cache_read_input_tokens",0)
    tok_cost = sum([self.input_tokens * costs[0] +  self.output_tokens * costs[1] +  cache_w * costs[2] + cache_r * costs[3]]) / 1e6
    server_tool_use = _dgetattr(self, "server_tool_use",server_tool_usage())
    server_tool_cost = server_tool_use.web_search_requests * server_tool_pricing['web_search_requests'] / 1e3
    return tok_cost + server_tool_cost
```

</details>

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L324"
target="_blank" style="float:right; font-size:smaller">source</a>

### Client.cost

``` python

def cost(
    
)->float:

```

*Call self as a function.*

<details open class="code-fold">
<summary>Exported source</summary>

``` python
@patch(as_prop=True)
def cost(self: Client) -> float: return self.use.cost(pricing[model_types[self.model]])
```

</details>

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L327"
target="_blank" style="float:right; font-size:smaller">source</a>

### get_costs

``` python

def get_costs(
    c
):

```

*Call self as a function.*

<details open class="code-fold">
<summary>Exported source</summary>

``` python
def get_costs(c):
    costs = pricing[model_types[c.model]]
    
    inp_cost = c.use.input_tokens * costs[0] / 1e6
    out_cost = c.use.output_tokens * costs[1] / 1e6

    cache_w = c.use.cache_creation_input_tokens   
    cache_r = c.use.cache_read_input_tokens
    cache_cost = (cache_w * costs[2] + cache_r * costs[3]) / 1e6

    server_tool_use = c.use.server_tool_use
    server_tool_cost = server_tool_use.web_search_requests * server_tool_pricing['web_search_requests'] / 1e3
    return inp_cost, out_cost, cache_cost, cache_w + cache_r, server_tool_cost
```

</details>

The markdown repr of the client itself will show the latest result,
along with the usage so far.

<details open class="code-fold">
<summary>Exported source</summary>

``` python
@patch
def _repr_markdown_(self:Client):
    if not hasattr(self,'result'): return 'No results yet'
    msg = contents(self.result)
    inp_cost, out_cost, cache_cost, cached_toks, server_tool_cost = get_costs(self)
    return f"""{msg}

| Metric | Count | Cost (USD) |
|--------|------:|-----:|
| Input tokens | {self.use.input_tokens:,} | {inp_cost:.6f} |
| Output tokens | {self.use.output_tokens:,} | {out_cost:.6f} |
| Cache tokens | {cached_toks:,} | {cache_cost:.6f} |
| Server tool use | {self.use.server_tool_use.web_search_requests:,} | {server_tool_cost:.6f} |
| **Total** | **{self.use.total:,}** | **${self.cost:.6f}** |"""
```

</details>

``` python
c
```

Here is counting from 1 to 10:

1, 2,

<table>
<thead>
<tr>
<th>Metric</th>
<th style="text-align: right;">Count</th>
<th style="text-align: right;">Cost (USD)</th>
</tr>
</thead>
<tbody>
<tr>
<td>Input tokens</td>
<td style="text-align: right;">46</td>
<td style="text-align: right;">0.000138</td>
</tr>
<tr>
<td>Output tokens</td>
<td style="text-align: right;">93</td>
<td style="text-align: right;">0.001395</td>
</tr>
<tr>
<td>Cache tokens</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0.000000</td>
</tr>
<tr>
<td>Server tool use</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0.000000</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td style="text-align: right;"><strong>139</strong></td>
<td style="text-align: right;"><strong>$0.001533</strong></td>
</tr>
</tbody>
</table>

Pass a list of alternating user/assistant messages to give Claude a
“dialog”.

``` python
c(["My name is Jeremy", "Hi Jeremy!", "Can you remind me what my name is?"])
```

Your name is Jeremy! You told me that at the start of our conversation.

<details>

- id: `msg_01UF9i1HRnHDXaMHvNdWJJwf`
- container: `None`
- content:
  `[{'citations': None, 'text': 'Your name is Jeremy! You told me that at the start of our conversation.', 'type': 'text'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `end_turn`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 29, 'output_tokens': 19, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

## Tool use

Let’s now look more at tool use (aka *function calling*).

For testing, we need a function that Claude can call; we’ll write a
simple function that adds numbers together, and will tell us when it’s
being called:

``` python
@dataclass
class MySum: val:int

def sums(
    a:int,  # First thing to sum
    b:int=1 # Second thing to sum
) -> int: # The sum of the inputs
    "Adds a + b."
    print(f"Finding the sum of {a} and {b}")
    return MySum(a + b)
```

``` python
a,b = 604542,6458932
pr = f"What is {a}+{b}?"
sp = "Always use tools when calculations are required."
```

Claudette can autogenerate a schema thanks to the `toolslm` library.
We’ll force the use of the tool using the function we created earlier.

``` python
tools=[get_schema(sums)]
choice = mk_tool_choice('sums')
```

We’ll start a dialog with Claude now. We’ll store the messages of our
dialog in `msgs`. The first message will be our prompt `pr`, and we’ll
pass our `tools` schema.

``` python
msgs = mk_msgs(pr)
r = c(msgs, sp=sp, tools=tools, tool_choice=choice)
r
```

ToolUseBlock(id=‘toolu_01Rk1eL2Zkx92c4c3FJJAjFu’,
caller=DirectCaller(type=‘direct’), input={‘a’: 604542, ‘b’: 6458932},
name=‘sums’, type=‘tool_use’)

<details>

- id: `msg_01KK6wvRhJM93T4ZMo5XtYqZ`
- container: `None`
- content:
  `[{'id': 'toolu_01Rk1eL2Zkx92c4c3FJJAjFu', 'caller': {'type': 'direct'}, 'input': {'a': 604542, 'b': 6458932}, 'name': 'sums', 'type': 'tool_use'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `tool_use`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 720, 'output_tokens': 57, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

When Claude decides that it should use a tool, it passes back a
`ToolUseBlock` with the name of the tool to call, and the params to use.

We don’t want to allow it to call just any possible function (that would
be a security disaster!) so we create a *namespace* – that is, a
dictionary of allowable function names to call.

``` python
ns = mk_ns(sums)
ns
```

    {'sums': <function __main__.sums(a: int, b: int = 1) -> int>}

[`ToolResult`](https://claudette.answer.ai/core.html#toolresult) is used
for two special cases:

1)  When tool calls are RPCs with claudette running on an application
    server and code execution happening elsewhere, wrapping with a
    `result_type` field is used as a type descriptor for the claudette
    client.

2)  Different types are handled in message history with specific format,
    so [`mk_funcres`](https://claudette.answer.ai/core.html#mk_funcres)
    branches the Anthropic representation (see depending on the
    `result_type`.

Currently images are the only supported tool result type - see
https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/implement-tool-use#example-of-tool-result-with-images
for the format implemented in
[`mk_funcres`](https://claudette.answer.ai/core.html#mk_funcres).

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L358"
target="_blank" style="float:right; font-size:smaller">source</a>

### ToolResult

``` python

def ToolResult(
    result_type:str, data
):

```

*Base class for objects needing a basic `__repr__`*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L368"
target="_blank" style="float:right; font-size:smaller">source</a>

### mk_funcres

``` python

def mk_funcres(
    fc, ns
):

```

*Given tool use block ‘fc’, get tool result, and create a tool_result
response.*

We can now use the function requested by Claude. We look it up in `ns`,
and pass in the provided parameters.

``` python
fcs = [o for o in r.content if isinstance(o,ToolUseBlock)]
fcs
```

    [ToolUseBlock(id='toolu_01Rk1eL2Zkx92c4c3FJJAjFu', caller=DirectCaller(type='direct'), input={'a': 604542, 'b': 6458932}, name='sums', type='tool_use')]

``` python
res = [mk_funcres(fc, ns=ns) for fc in fcs]
res
```

    Finding the sum of 604542 and 6458932

    [{'type': 'tool_result',
      'tool_use_id': 'toolu_01Rk1eL2Zkx92c4c3FJJAjFu',
      'content': 'MySum(val=7063474)'}]

``` python
res = [mk_funcres(fc, ns={}) for fc in fcs]
res
```

    [{'type': 'tool_result',
      'tool_use_id': 'toolu_01Rk1eL2Zkx92c4c3FJJAjFu',
      'content': 'Error - tool not defined in the tool_schemas: sums'}]

### Limit tool access

``` python
@dataclass
class MyProd: val:int

def mults(a:int,b:int):
    "Multiplies two integers"
    return MyProd(a*b)
```

``` python
tools = [get_schema(sums), get_schema(mults)]
choice = mk_tool_choice('sums')
```

``` python
tools
```

    [{'name': 'sums',
      'description': 'Adds a + b.\n\nReturns:\n- The sum of the inputs (type: integer)',
      'input_schema': {'type': 'object',
       'properties': {'a': {'type': 'integer',
         'description': 'First thing to sum'},
        'b': {'type': 'integer',
         'description': 'Second thing to sum',
         'default': 1}},
       'required': ['a']}},
     {'name': 'mults',
      'description': 'Multiplies two integers',
      'input_schema': {'type': 'object',
       'properties': {'a': {'type': 'integer', 'description': ''},
        'b': {'type': 'integer', 'description': ''}},
       'required': ['a', 'b']}}]

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L377"
target="_blank" style="float:right; font-size:smaller">source</a>

### allowed_tools

``` python

def allowed_tools(
    specs:Optional, choice:Union=None
):

```

*Call self as a function.*

<details open class="code-fold">
<summary>Exported source</summary>

``` python
def allowed_tools(specs: Optional[list[Union[str,abc.Callable]]], choice: Optional[Union[dict,str]]=None):
    if not isinstance(choice, dict): choice=mk_tool_choice(choice)
    if choice['type'] == 'tool': return {choice['name']}
    if choice['type'] == 'none': return set()
    return {v['name'] if isinstance(v, dict) else v.__name__ for v in specs or []}
```

</details>

``` python
allowed_tools(tools, choice)
```

    {'sums'}

``` python
allowed_tools([sums, mults], 'mults')
```

    {'mults'}

``` python
allowed_tools(tools, None)
```

    {'mults', 'sums'}

``` python
allowed_tools(tools, 'sums')
```

    {'sums'}

``` python
allowed_tools(tools, True)
```

    {'mults', 'sums'}

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L384"
target="_blank" style="float:right; font-size:smaller">source</a>

### limit_ns

``` python

def limit_ns(
    ns:Optional=None, # Namespace to search for tools
    specs:Optional=None, # List of the tools that are allowed for llm to call
    choice:Union=None, # Tool choice as defined by Anthropic API
):

```

*Filter namespace `ns` to only include tools allowed by `specs` and
`choice`*

<details open class="code-fold">
<summary>Exported source</summary>

``` python
def limit_ns(
    ns:Optional[abc.Mapping]=None, # Namespace to search for tools
    specs:Optional[list[Union[str,abc.Callable]]]=None, # List of the tools that are allowed for llm to call
    choice:Optional[Union[dict,str]]=None # Tool choice as defined by Anthropic API
    ):
    "Filter namespace `ns` to only include tools allowed by `specs` and `choice`"
    if ns is None: ns=globals()
    if not isinstance(ns, abc.Mapping): ns = mk_ns(ns)
    ns = {k:ns[k] for k in allowed_tools(specs, choice) if k in ns}
    return ns
```

</details>

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L396"
target="_blank" style="float:right; font-size:smaller">source</a>

### mk_toolres

``` python

def mk_toolres(
    r:Mapping, # Tool use request response from Claude
    ns:Optional=None, # Namespace to search for tools
):

```

*Create a `tool_result` message from response `r`.*

<details open class="code-fold">
<summary>Exported source</summary>

``` python
def mk_toolres(
    r:abc.Mapping, # Tool use request response from Claude
    ns:Optional[abc.Mapping]=None, # Namespace to search for tools
    ):
    "Create a `tool_result` message from response `r`."
    cts = getattr(r, 'content', [])
    res = [mk_msg(r.model_dump(exclude_none=True), role='assistant')]
    if ns is None: ns=globals()
    tcs = [mk_funcres(o, ns) for o in cts if isinstance(o,ToolUseBlock)]
    if tcs: res.append(mk_msg(tcs))
    return res
```

</details>

``` python
foo = []
foo.append({})
foo.append({})
foo
```

    [{}, {}]

In order to tell Claude the result of the tool call, we pass back the
tool use assistant request and the `tool_result` response.

``` python
tr = mk_toolres(r, ns=ns)
tr
```

    Finding the sum of 604542 and 6458932

    [{'role': 'assistant',
      'content': [{'id': 'toolu_01Rk1eL2Zkx92c4c3FJJAjFu',
        'caller': {'type': 'direct'},
        'input': {'a': 604542, 'b': 6458932},
        'name': 'sums',
        'type': 'tool_use'}]},
     {'role': 'user',
      'content': [{'type': 'tool_result',
        'tool_use_id': 'toolu_01Rk1eL2Zkx92c4c3FJJAjFu',
        'content': 'MySum(val=7063474)'}]}]

``` python
msgs
```

    [{'role': 'user', 'content': 'What is 604542+6458932?'}]

We add this to our dialog, and now Claude has all the information it
needs to answer our question.

``` python
msgs += tr
contents(c(msgs, sp=sp, tools=tools))
```

    'The sum of 604,542 + 6,458,932 = **7,063,474**.'

``` python
contents(msgs[-1])
```

    'MySum(val=7063474)'

``` python
msgs
```

    [{'role': 'user', 'content': 'What is 604542+6458932?'},
     {'role': 'assistant',
      'content': [{'id': 'toolu_01Rk1eL2Zkx92c4c3FJJAjFu',
        'caller': {'type': 'direct'},
        'input': {'a': 604542, 'b': 6458932},
        'name': 'sums',
        'type': 'tool_use'}]},
     {'role': 'user',
      'content': [{'type': 'tool_result',
        'tool_use_id': 'toolu_01Rk1eL2Zkx92c4c3FJJAjFu',
        'content': 'MySum(val=7063474)'}]}]

The tools calls are limited to what is present in tool_spec.

``` python
tr = mk_toolres(r, ns=limit_ns(None, tools, 'mults'))
tr
```

    [{'role': 'assistant',
      'content': [{'id': 'toolu_01Rk1eL2Zkx92c4c3FJJAjFu',
        'caller': {'type': 'direct'},
        'input': {'a': 604542, 'b': 6458932},
        'name': 'sums',
        'type': 'tool_use'}]},
     {'role': 'user',
      'content': [{'type': 'tool_result',
        'tool_use_id': 'toolu_01Rk1eL2Zkx92c4c3FJJAjFu',
        'content': 'Error - tool not defined in the tool_schemas: sums'}]}]

### Text editing

Anthropic also has a special tool type specific to text editing.

``` python
tools = [text_editor_conf['sonnet']]
tools
```

    [{'type': 'text_editor_20250728', 'name': 'str_replace_based_edit_tool'}]

``` python
pr = 'Could you please very briefly explain my _quarto.yml file?'
msgs = [mk_msg(pr)]
r = c(msgs, sp=sp, tools=tools)
find_block(r, ToolUseBlock)
```

    ToolUseBlock(id='toolu_01VuDPR85jDHdSrnCmK52WgL', caller=DirectCaller(type='direct'), input={'command': 'view', 'path': '/_quarto.yml'}, name='str_replace_based_edit_tool', type='tool_use')

We’ve gone ahead and create a reference implementation that you can
directly use from our `text_editor` module. Or use as reference for
creating your own.

``` python
ns = mk_ns(str_replace_based_edit_tool)
tr = mk_toolres(r, ns=ns)
msgs += tr
print(contents(c(msgs, sp=sp, tools=tools))[:128])
```

    It seems the file wasn't found at the root path. Could you provide the exact location of your `_quarto.yml` file so I can take a

## Structured data

``` python
a,b = 604542,6458932
pr = f"What is {a}+{b}?"
sp = "Always use your tools for calculations."
```

``` python
for tools in [sums, [get_schema(sums)]]:
    r = c(pr, tools=tools, tool_choice='sums')
    print(r)
```

    Message(id='msg_01LD3r7hwAkYtWg4XW6A1UaB', container=None, content=[ToolUseBlock(id='toolu_014XCGyV6twGuQxkv8dWGbSc', caller=DirectCaller(type='direct'), input={'a': 604542, 'b': 6458932}, name='sums', type='tool_use')], model='claude-sonnet-4-6', role='assistant', stop_reason='tool_use', stop_sequence=None, type='message', usage=In: 715; Out: 53; Cache create: 0; Cache read: 0; Total Tokens: 768; Search: 0; Fetch: 0)
    Message(id='msg_01LD3r7hwAkYtWg4XW6A1UaB', container=None, content=[ToolUseBlock(id='toolu_014XCGyV6twGuQxkv8dWGbSc', caller=DirectCaller(type='direct'), input={'a': 604542, 'b': 6458932}, name='sums', type='tool_use')], model='claude-sonnet-4-6', role='assistant', stop_reason='tool_use', stop_sequence=None, type='message', usage=In: 715; Out: 53; Cache create: 0; Cache read: 0; Total Tokens: 768; Search: 0; Fetch: 0)

``` python
ns = mk_ns(sums)
tr = mk_toolres(r, ns=ns)
```

    Finding the sum of 604542 and 6458932

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L411"
target="_blank" style="float:right; font-size:smaller">source</a>

### Client.structured

``` python

def structured(
    msgs:list, # List of messages in the dialog
    tools:list=None, # List of tools to make available to Claude
    ns:Optional=None, # Namespace to search for tools
    sp:str='', # The system prompt
    temp:int=0, # Temperature
    maxtok:int=4096, # Maximum tokens
    maxthinktok:int=0, # Maximum thinking tokens
    prefill:str='', # Optional prefill to pass to Claude as start of its response
    stream:bool=False, # Stream response?
    stop:NoneType=None, # Stop sequence
    tool_choice:Optional=None, # Optionally force use of some tool
    cb:NoneType=None, # Callback to pass result to when complete
    cache_control:Optional[CacheControlEphemeralParam] | Omit=<anthropic.Omit object at 0x7f45180ce210>,
    container:Optional[str] | Omit=<anthropic.Omit object at 0x7f45180ce210>,
    inference_geo:Optional[str] | Omit=<anthropic.Omit object at 0x7f45180ce210>,
    metadata:MetadataParam | Omit=<anthropic.Omit object at 0x7f45180ce210>,
    output_config:OutputConfigParam | Omit=<anthropic.Omit object at 0x7f45180ce210>,
    service_tier:Literal['auto', 'standard_only'] | Omit=<anthropic.Omit object at 0x7f45180ce210>,
    stop_sequences:SequenceNotStr[str] | Omit=<anthropic.Omit object at 0x7f45180ce210>,
    system:Union[str, Iterable[TextBlockParam]] | Omit=<anthropic.Omit object at 0x7f45180ce210>,
    temperature:float | Omit=<anthropic.Omit object at 0x7f45180ce210>,
    thinking:ThinkingConfigParam | Omit=<anthropic.Omit object at 0x7f45180ce210>,
    top_k:int | Omit=<anthropic.Omit object at 0x7f45180ce210>,
    top_p:float | Omit=<anthropic.Omit object at 0x7f45180ce210>,
    extra_headers:Headers | None=None, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
The extra values given here take precedence over values defined on the client or passed to this method.
    extra_query:Query | None=None, extra_body:Body | None=None,
    timeout:float | httpx.Timeout | None | NotGiven=NOT_GIVEN
):

```

*Return the value of all tool calls (generally used for structured
outputs)*

<details open class="code-fold">
<summary>Exported source</summary>

``` python
@patch
@delegates(Client.__call__)
def structured(self:Client,
               msgs:list, # List of messages in the dialog
               tools:list[abc.Callable]=None, # List of tools to make available to Claude
               ns:Optional[abc.Mapping]=None, # Namespace to search for tools
               **kwargs):
    "Return the value of all tool calls (generally used for structured outputs)"
    tools = listify(tools)
    res = self(msgs, tools=tools, tool_choice=tools, **kwargs)
    if ns is None: ns=mk_ns(*tools)
    cts = getattr(res, 'content', [])
    tcs = [call_func(o.name, o.input, ns=ns) for o in cts if isinstance(o,ToolUseBlock)]
    return tcs
```

</details>

Anthropic’s API does not support response formats directly, so instead
we provide a `structured` method to use tool calling to achieve the same
result. The result of the tool is not passed back to Claude in this
case, but instead is returned directly to the user.

``` python
c.structured(pr, tools=[sums])
```

    Finding the sum of 604542 and 6458932

    [MySum(val=7063474)]

``` python
c
```

ToolUseBlock(id=‘toolu_01ENhz5EnfqZFT8PeizJ3gKr’,
caller=DirectCaller(type=‘direct’), input={‘a’: 604542, ‘b’: 6458932},
name=‘sums’, type=‘tool_use’)

<table>
<thead>
<tr>
<th>Metric</th>
<th style="text-align: right;">Count</th>
<th style="text-align: right;">Cost (USD)</th>
</tr>
</thead>
<tbody>
<tr>
<td>Input tokens</td>
<td style="text-align: right;">6,400</td>
<td style="text-align: right;">0.019200</td>
</tr>
<tr>
<td>Output tokens</td>
<td style="text-align: right;">499</td>
<td style="text-align: right;">0.007485</td>
</tr>
<tr>
<td>Cache tokens</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0.000000</td>
</tr>
<tr>
<td>Server tool use</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0.000000</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td style="text-align: right;"><strong>6,899</strong></td>
<td style="text-align: right;"><strong>$0.026685</strong></td>
</tr>
</tbody>
</table>

## Custom Types with Tools Use

We need to add tool support for custom types too. Let’s test out custom
types using a minimal example.

``` python
class Book(BasicRepr):
    def __init__(self, title: str, pages: int): store_attr()
    def __repr__(self):
        return f"Book Title : {self.title}\nNumber of Pages : {self.pages}"
```

``` python
Book("War and Peace", 950)
```

    Book Title : War and Peace
    Number of Pages : 950

``` python
def find_page(book: Book, # The book to find the halfway point of
              percent: int, # Percent of a book to read to, e.g. halfway == 50, 
) -> int:
    "The page number corresponding to `percent` completion of a book"
    return round(book.pages * (percent / 100.0))
```

``` python
get_schema(find_page)
```

    {'name': 'find_page',
     'description': 'The page number corresponding to `percent` completion of a book\n\nReturns:\n- type: integer',
     'input_schema': {'type': 'object',
      'properties': {'book': {'type': 'object',
        'description': 'The book to find the halfway point of',
        '$ref': '#/$defs/Book'},
       'percent': {'type': 'integer',
        'description': 'Percent of a book to read to, e.g. halfway == 50,'}},
      'required': ['book', 'percent'],
      '$defs': {'Book': {'type': 'object',
        'properties': {'title': {'type': 'string', 'description': ''},
         'pages': {'type': 'integer', 'description': ''}},
        'title': 'Book',
        'required': ['title', 'pages']}}}}

``` python
choice = mk_tool_choice('find_page')
choice
```

    {'type': 'tool', 'name': 'find_page'}

Claudette will pack objects as dict, so we’ll transform tool functions
with user-defined types into tool functions that accept a dict in lieu
of the user-defined type.

First let’s convert a single argument:

[`_is_builtin`](https://claudette.answer.ai/core.html#_is_builtin)
decides whether to pass an argument through as-is. Let’s check the
argument conversion:

``` python
(_is_builtin(int), _is_builtin(Book), _is_builtin(List))
```

    (True, False, True)

``` python
(_convert(555, int),
 _convert({"title": "War and Peace", "pages": 923}, Book),
 _convert([1, 2, 3, 4], List))
```

    (555,
     Book Title : War and Peace
     Number of Pages : 923,
     [1, 2, 3, 4])

To apply [`tool()`](https://claudette.answer.ai/core.html#tool) to a
function is to return a new function where the user-defined types are
replaced with dictionary inputs.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L437"
target="_blank" style="float:right; font-size:smaller">source</a>

### tool

``` python

def tool(
    func
):

```

*Call self as a function.*

A function is transformed into a function with dict arguments
substituted for user-defined types. Built-in types such as `percent`
here are left untouched.

``` python
find_page(book=Book("War and Peace", 950), percent=50)
```

    475

``` python
tool(find_page)({"title": "War and Peace", "pages": 950}, percent=50)
```

    475

By passing tools wrapped by
[`tool()`](https://claudette.answer.ai/core.html#tool), user-defined
types now work completes without failing in tool calls.

``` python
pr = "How many pages do I have to read to get halfway through my 950 page copy of War and Peace"
tools = tool(find_page)
tools
```

    <function __main__.find_page(book: __main__.Book, percent: int) -> int>

``` python
r = c(pr, tools=[tools])
find_block(r, ToolUseBlock)
```

    ToolUseBlock(id='toolu_01DpqxJGneGatm3JyYr3aNie', caller=DirectCaller(type='direct'), input={'book': {'title': 'War and Peace', 'pages': 950}, 'percent': 50}, name='find_page', type='tool_use')

``` python
tr = mk_toolres(r, ns=[tools])
tr
```

    [{'role': 'assistant',
      'content': [{'text': 'Let me find the halfway point of your book right away!',
        'type': 'text'},
       {'id': 'toolu_01DpqxJGneGatm3JyYr3aNie',
        'caller': {'type': 'direct'},
        'input': {'book': {'title': 'War and Peace', 'pages': 950}, 'percent': 50},
        'name': 'find_page',
        'type': 'tool_use'}]},
     {'role': 'user',
      'content': [{'type': 'tool_result',
        'tool_use_id': 'toolu_01DpqxJGneGatm3JyYr3aNie',
        'content': '475'}]}]

``` python
msgs = [pr]+tr
contents(c(msgs, sp=sp, tools=[tools]))
```

    "You'll need to read **475 pages** to get halfway through your 950-page copy of *War and Peace*. You've got quite the reading adventure ahead of you — happy reading! 📖"

## Chat

Rather than manually adding the responses to a dialog, we’ll create a
simple [`Chat`](https://claudette.answer.ai/core.html#chat) class to do
that for us, each time we make a request. We’ll also store the system
prompt and tools here, to avoid passing them every time.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L448"
target="_blank" style="float:right; font-size:smaller">source</a>

### Chat

``` python

def Chat(
    model:Optional=None, # Model to use (leave empty if passing `cli`)
    cli:Optional=None, # Client to use (leave empty if passing `model`)
    sp:str='', # Optional system prompt
    tools:Optional=None, # List of tools to make available to Claude
    temp:int=0, # Temperature
    cont_pr:Optional=None, # User prompt to continue an assistant response
    cache:bool=False, # Use Claude cache?
    hist:list=None, # Initialize history
    ns:Optional=None, # Namespace to search for tools
):

```

*Anthropic chat client.*

The class stores the
[`Client`](https://claudette.answer.ai/core.html#client) that will
provide the responses in `c`, and a history of messages in `h`.

``` python
sp = "Never mention what tools you use."
chat = Chat(model, sp=sp)
chat.c.use, chat.h
```

    (In: 0; Out: 0; Cache create: 0; Cache read: 0; Total Tokens: 0; Search: 0; Fetch: 0,
     [])

``` python
chat.c.use.cost(pricing[model_types[chat.c.model]])
```

    0.0

This is clunky. Let’s add `cost` as a property for the
[`Chat`](https://claudette.answer.ai/core.html#chat) class. It will pass
in the appropriate prices for the current model to the usage cost
calculator.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L474"
target="_blank" style="float:right; font-size:smaller">source</a>

### Chat.cost

``` python

def cost(
    
)->float:

```

*Call self as a function.*

<details open class="code-fold">
<summary>Exported source</summary>

``` python
@patch(as_prop=True)
def cost(self: Chat) -> float: return self.c.cost
```

</details>

``` python
chat.cost
```

    0.0

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L494"
target="_blank" style="float:right; font-size:smaller">source</a>

### Chat.\_\_call\_\_

``` python

def __call__(
    pr:NoneType=None, # Prompt / message
    temp:NoneType=None, # Temperature
    maxtok:int=4096, # Maximum tokens
    maxthinktok:int=0, # Maximum thinking tokens
    stream:bool=False, # Stream response?
    prefill:str='', # Optional prefill to pass to Claude as start of its response
    tool_choice:Optional=None, # Optionally force use of some tool
    kw:VAR_KEYWORD
):

```

*Call self as a function.*

<details open class="code-fold">
<summary>Exported source</summary>

``` python
@patch
def _post_pr(self:Chat, pr, prev_role):
    if pr is None and prev_role == 'assistant':
        if self.cont_pr is None:
            raise ValueError("Prompt must be given after completion, or use `self.cont_pr`.")
        pr = self.cont_pr # No user prompt, keep the chain
    if pr: self.h.append(mk_msg(pr, cache=self.cache))
```

</details>

<details open class="code-fold">
<summary>Exported source</summary>

``` python
@patch
def _append_pr(self:Chat, pr=None):
    prev_role = nested_idx(self.h, -1, 'role') if self.h else 'assistant' # First message should be 'user'
    if pr and prev_role == 'user': self() # already user request pending
    self._post_pr(pr, prev_role)
```

</details>

<details open class="code-fold">
<summary>Exported source</summary>

``` python
@patch
def __call__(self:Chat,
             pr=None,  # Prompt / message
             temp=None, # Temperature
             maxtok=4096, # Maximum tokens
             maxthinktok=0, # Maximum thinking tokens
             stream=False, # Stream response?
             prefill='', # Optional prefill to pass to Claude as start of its response
             tool_choice:Optional[dict]=None, # Optionally force use of some tool
             **kw):
    if temp is None: temp=self.temp
    self._append_pr(pr)
    def _cb(v):
        self.last = mk_toolres(v, ns=limit_ns(self.ns, self.tools, tool_choice))
        self.h += self.last
    return self.c(self.h, stream=stream, prefill=prefill, sp=self.sp, temp=temp, maxtok=maxtok, maxthinktok=maxthinktok,
                 tools=self.tools, tool_choice=tool_choice, cb=_cb, **kw)
```

</details>

The `__call__` method just passes the request along to the
[`Client`](https://claudette.answer.ai/core.html#client), but rather
than just passing in this one prompt, it appends it to the history and
passes it all along. As a result, we now have state!

``` python
chat = Chat(model, sp=sp)
```

``` python
chat("I'm Jeremy")
chat("What's my name?")
```

Your name is Jeremy! You told me that at the start of our conversation.

<details>

- id: `msg_01P1dvBtCrRx33Z6FSzcoaYT`
- container: `None`
- content:
  `[{'citations': None, 'text': 'Your name is Jeremy! You told me that at the start of our conversation.', 'type': 'text'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `end_turn`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 44, 'output_tokens': 19, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

``` python
chat.use, chat.cost
```

    (In: 62; Out: 37; Cache create: 0; Cache read: 0; Total Tokens: 99; Search: 0; Fetch: 0,
     0.000741)

By default messages must be in user, assistant, user format. If this
isn’t followed (aka calling `chat()` without a user message) it will
error out:

``` python
try: chat()
except ValueError as e: print("Error:", e)
```

    Error: Prompt must be given after completion, or use `self.cont_pr`.

Setting `cont_pr` allows a “default prompt” to be specified when a
prompt isn’t specified. Usually used to prompt the model to continue.

``` python
chat.cont_pr = "Tell me a little more..."
chat()
```

That’s actually all I know about you! You just told me your name is
Jeremy, but beyond that, I don’t have any other information about you. I
only know what you share with me in our conversation.

Is there something you’d like to tell me about yourself, or is there
something I can help you with? 😊

<details>

- id: `msg_014GNbvuJySM2FNydyTvHnZ6`
- container: `None`
- content:
  `[{'citations': None, 'text': "That's actually all I know about you! You just told me your name is Jeremy, but beyond that, I don't have any other information about you. I only know what you share with me in our conversation.\n\nIs there something you'd like to tell me about yourself, or is there something I can help you with? 😊", 'type': 'text'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `end_turn`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 72, 'output_tokens': 73, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

We can also use streaming:

``` python
chat = Chat(model, sp=sp)
for o in chat("I'm Jeremy", stream=True): print(o, end='')
```

    Hi Jeremy! Nice to meet you. How can I help you today?

You can provide a history of messages to initialise
[`Chat`](https://claudette.answer.ai/core.html#chat) with:

``` python
chat = Chat(model, sp=sp, hist=["Can you guess my name?", "Hmmm I really don't know. Is it 'Merlin G. Penfolds'?"])
chat('Wow how did you know?')
```

I have to be honest with you - I didn’t actually know! That was just a
random guess, and it was quite a coincidence that it matched your name!
I have no way of knowing personal information about you unless you share
it with me. I should have been upfront about that from the start rather
than making a random guess. I’m sorry for any confusion!

<details>

- id: `msg_01RmtMSvQpWsDNdTgZdRpKXb`
- container: `None`
- content:
  `[{'citations': None, 'text': "I have to be honest with you - I didn't actually know! That was just a random guess, and it was quite a coincidence that it matched your name! I have no way of knowing personal information about you unless you share it with me. I should have been upfront about that from the start rather than making a random guess. I'm sorry for any confusion!", 'type': 'text'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `end_turn`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 59, 'output_tokens': 79, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

### Chat tool use

We automagically get streamlined tool use as well:

``` python
pr = f"What is {a}+{b}?"
pr
```

    'What is 604542+6458932?'

``` python
chat = Chat(model, sp=sp, tools=[sums])
r = chat(pr)
r
```

    Finding the sum of 604542 and 6458932

Let me calculate that for you!

<details>

- id: `msg_01J64NLNAmMicFA6nhmuT6pM`
- container: `None`
- content:
  `[{'citations': None, 'text': 'Let me calculate that for you!', 'type': 'text'}, {'id': 'toolu_017rP1c4tyoBaMyDT2pjRMcH', 'caller': {'type': 'direct'}, 'input': {'a': 604542, 'b': 6458932}, 'name': 'sums', 'type': 'tool_use'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `tool_use`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 627, 'output_tokens': 80, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

Now we need to send this result to Claude—calling the object with no
parameters tells it to return the tool result to Claude:

``` python
chat()
```

The result of **604,542 + 6,458,932 = 7,063,474**!

<details>

- id: `msg_013HLnrJxLLM165PKGnnHBmJ`
- container: `None`
- content:
  `[{'citations': None, 'text': 'The result of **604,542 + 6,458,932 = 7,063,474**!', 'type': 'text'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `end_turn`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 727, 'output_tokens': 28, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

It should be correct, because it actually used our Python function to do
the addition. Let’s check:

``` python
a+b
```

    7063474

Let’s try the same thing with streaming:

``` python
chat = Chat(model, sp=sp, tools=[sums])
r = chat(pr, stream=True)
for o in r: print(o, end='')
```

    Let me calculate that for you!Finding the sum of 604542 and 6458932

The full message, including tool call details, are in `value`:

``` python
r.value
```

Let me calculate that for you!

<details>

- id: `msg_01CthQaWS7F2MTeQjHPtKwVS`
- container: `None`
- content:
  `[{'citations': None, 'text': 'Let me calculate that for you!', 'type': 'text', 'parsed_output': None}, {'id': 'toolu_01E9JqVMwnUSrEGwfYsegP63', 'caller': {'type': 'direct'}, 'input': {'a': 604542, 'b': 6458932}, 'name': 'sums', 'type': 'tool_use'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `tool_use`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 627, 'output_tokens': 80, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

``` python
r = chat(stream=True)
for o in r: print(o, end='')
```

    The result of **604,542 + 6,458,932 = 7,063,474**!

``` python
r.value
```

The result of **604,542 + 6,458,932 = 7,063,474**!

<details>

- id: `msg_01KDCwZXa6fbqy9WLXwYq9gY`
- container: `None`
- content:
  `[{'citations': None, 'text': 'The result of **604,542 + 6,458,932 = 7,063,474**!', 'type': 'text', 'parsed_output': None}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `end_turn`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 727, 'output_tokens': 28, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

The history shows both the tool_use and tool_result messages:

``` python
chat.h
```

    [{'role': 'user', 'content': 'What is 604542+6458932?'},
     {'role': 'assistant',
      'content': [{'text': 'Let me calculate that for you!', 'type': 'text'},
       {'id': 'toolu_01E9JqVMwnUSrEGwfYsegP63',
        'caller': {'type': 'direct'},
        'input': {'a': 604542, 'b': 6458932},
        'name': 'sums',
        'type': 'tool_use'}]},
     {'role': 'user',
      'content': [{'type': 'tool_result',
        'tool_use_id': 'toolu_01E9JqVMwnUSrEGwfYsegP63',
        'content': 'MySum(val=7063474)'}]},
     {'role': 'assistant',
      'content': [{'text': 'The result of **604,542 + 6,458,932 = 7,063,474**!',
        'type': 'text'}]}]

The [`Chat`](https://claudette.answer.ai/core.html#chat) class
automatically validates tool calls against the provided `tools` list. If
the model attempts to call a tool that isn’t in the allowed set (whether
due to hallucination or a mismatch between `tools` and `ns`), the tool
call will fail with an error message rather than executing arbitrary
code.

This provides an important safety mechanism - even if the model invents
a function name or tries to call a tool that shouldn’t be available,
[`Chat`](https://claudette.answer.ai/core.html#chat) ensures only
explicitly allowed tools can be executed.

``` python
def _force_sums(*args, cb=None, **kwargs): 
    def cb_(r): 
        for block in r.content:
            if isinstance(block, ToolUseBlock): block.name = f'sums' # Forces tool name to 'sums'
        return cb(r) if cb else r
    return Client(model)(*args, cb=cb_, **kwargs)
```

``` python
chat = Chat(cli=_force_sums, sp=sp, tools=[mults]) # Only 'mults' is allowed
chat.ns = [mults, sums] # Both function exist in namespace
r = chat("Calc 604542 * 33392837120239382")
r
```

I need to multiply these two large numbers. Let me break this down into
smaller multiplications and combine the results.

I’ll express **33392837120239382** in parts: - 33392837120239382 =
3339283712 \* 10⁷ + 239382 \* 10⁰ (wait, let me be more precise) -
33392837120239382 = **33392837120** \* 10⁶ + **239382**

And I’ll compute: - **604542 \* 33392837120** and **604542 \* 239382**
separately.

Further breaking down: - 33392837120 = **3339283** \* 10⁴ + **7120** -
So: 604542 \* 33392837120 = (604542 \* 3339283) \* 10⁴ + (604542 \*
7120)

And 3339283 = **3339** \* 10³ + **283** - So: 604542 \* 3339283 =
(604542 \* 3339) \* 10³ + (604542 \* 283)

Let me fire all the independent multiplications simultaneously!

<details>

- id: `msg_01MGBGG5gTd6y1NbvS3fu5SU`
- container: `None`
- content:
  `[{'citations': None, 'text': "I need to multiply these two large numbers. Let me break this down into smaller multiplications and combine the results.\n\nI'll express **33392837120239382** in parts:\n- 33392837120239382 = 3339283712 * 10⁷ + 239382 * 10⁰ (wait, let me be more precise)\n- 33392837120239382 = **33392837120** * 10⁶ + **239382**\n\nAnd I'll compute:\n- **604542 * 33392837120** and **604542 * 239382** separately.\n\nFurther breaking down:\n- 33392837120 = **3339283** * 10⁴ + **7120**\n- So: 604542 * 33392837120 = (604542 * 3339283) * 10⁴ + (604542 * 7120)\n\nAnd 3339283 = **3339** * 10³ + **283**\n- So: 604542 * 3339283 = (604542 * 3339) * 10³ + (604542 * 283)\n\nLet me fire all the independent multiplications simultaneously!", 'type': 'text'}, {'id': 'toolu_01PTTnLJSNpmoYTC9EADnJC9', 'caller': {'type': 'direct'}, 'input': {'a': 604542, 'b': 3339}, 'name': 'sums', 'type': 'tool_use'}, {'id': 'toolu_011gQv4XAQcpCnQahMccsR15', 'caller': {'type': 'direct'}, 'input': {'a': 604542, 'b': 283}, 'name': 'sums', 'type': 'tool_use'}, {'id': 'toolu_01Fx8aFaN99yDMxhFJ8RuCFW', 'caller': {'type': 'direct'}, 'input': {'a': 604542, 'b': 7120}, 'name': 'sums', 'type': 'tool_use'}, {'id': 'toolu_01HRnSCYyrz8Kdwz4YJ1aAuN', 'caller': {'type': 'direct'}, 'input': {'a': 604542, 'b': 239382}, 'name': 'sums', 'type': 'tool_use'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `tool_use`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 599, 'output_tokens': 520, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

``` python
chat.h
```

    [{'role': 'user', 'content': 'Calc 604542 * 33392837120239382'},
     {'role': 'assistant',
      'content': [{'text': "I need to multiply these two large numbers. Let me break this down into smaller multiplications and combine the results.\n\nI'll express **33392837120239382** in parts:\n- 33392837120239382 = 3339283712 * 10⁷ + 239382 * 10⁰ (wait, let me be more precise)\n- 33392837120239382 = **33392837120** * 10⁶ + **239382**\n\nAnd I'll compute:\n- **604542 * 33392837120** and **604542 * 239382** separately.\n\nFurther breaking down:\n- 33392837120 = **3339283** * 10⁴ + **7120**\n- So: 604542 * 33392837120 = (604542 * 3339283) * 10⁴ + (604542 * 7120)\n\nAnd 3339283 = **3339** * 10³ + **283**\n- So: 604542 * 3339283 = (604542 * 3339) * 10³ + (604542 * 283)\n\nLet me fire all the independent multiplications simultaneously!",
        'type': 'text'},
       {'id': 'toolu_01PTTnLJSNpmoYTC9EADnJC9',
        'caller': {'type': 'direct'},
        'input': {'a': 604542, 'b': 3339},
        'name': 'sums',
        'type': 'tool_use'},
       {'id': 'toolu_011gQv4XAQcpCnQahMccsR15',
        'caller': {'type': 'direct'},
        'input': {'a': 604542, 'b': 283},
        'name': 'sums',
        'type': 'tool_use'},
       {'id': 'toolu_01Fx8aFaN99yDMxhFJ8RuCFW',
        'caller': {'type': 'direct'},
        'input': {'a': 604542, 'b': 7120},
        'name': 'sums',
        'type': 'tool_use'},
       {'id': 'toolu_01HRnSCYyrz8Kdwz4YJ1aAuN',
        'caller': {'type': 'direct'},
        'input': {'a': 604542, 'b': 239382},
        'name': 'sums',
        'type': 'tool_use'}]},
     {'role': 'user',
      'content': [{'type': 'tool_result',
        'tool_use_id': 'toolu_01PTTnLJSNpmoYTC9EADnJC9',
        'content': 'Error - tool not defined in the tool_schemas: sums'},
       {'type': 'tool_result',
        'tool_use_id': 'toolu_011gQv4XAQcpCnQahMccsR15',
        'content': 'Error - tool not defined in the tool_schemas: sums'},
       {'type': 'tool_result',
        'tool_use_id': 'toolu_01Fx8aFaN99yDMxhFJ8RuCFW',
        'content': 'Error - tool not defined in the tool_schemas: sums'},
       {'type': 'tool_result',
        'tool_use_id': 'toolu_01HRnSCYyrz8Kdwz4YJ1aAuN',
        'content': 'Error - tool not defined in the tool_schemas: sums'}]}]

Let’s test a function with user defined types.

``` python
chat = Chat(model, sp=sp, tools=[find_page])
r = chat("How many pages is three quarters of the way through my 80 page edition of Tao Te Ching?")
r
```

ToolUseBlock(id=‘toolu_01G77V2RMBcxVDKqsCYmZ9Au’,
caller=DirectCaller(type=‘direct’), input={‘book’: {‘title’: ‘Tao Te
Ching’, ‘pages’: 80}, ‘percent’: 75}, name=‘find_page’, type=‘tool_use’)

<details>

- id: `msg_01Uzs8htuGbvrC1Nq1skRD8t`
- container: `None`
- content:
  `[{'id': 'toolu_01G77V2RMBcxVDKqsCYmZ9Au', 'caller': {'type': 'direct'}, 'input': {'book': {'title': 'Tao Te Ching', 'pages': 80}, 'percent': 75}, 'name': 'find_page', 'type': 'tool_use'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `tool_use`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 731, 'output_tokens': 87, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

``` python
chat()
```

Three quarters of the way through your 80-page edition of the Tao Te
Ching is **page 60**!

<details>

- id: `msg_017zd57Uf73R262oY6F1T9ia`
- container: `None`
- content:
  `[{'citations': None, 'text': 'Three quarters of the way through your 80-page edition of the Tao Te Ching is **page 60**!', 'type': 'text'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `end_turn`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 831, 'output_tokens': 31, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

<details open class="code-fold">
<summary>Exported source</summary>

``` python
@patch
def _repr_markdown_(self:Chat):
    if not hasattr(self.c, 'result'): return 'No results yet'
    last_msg = contents(self.c.result)
    
    def fmt_msg(m):
        t = contents(m)
        if isinstance(t, dict): return t['content']
        return t
        
    history = '\n\n'.join(f"**{m['role']}**: {fmt_msg(m)}" 
                         for m in self.h)
    det = self.c._repr_markdown_().split('\n\n')[-1]
    if history: history = f"""
<details>
<summary>► History</summary>

{history}

</details>
"""

    return f"""{last_msg}
{history}
{det}"""
```

</details>

``` python
chat
```

Three quarters of the way through your 80-page edition of the Tao Te
Ching is **page 60**!

<details>

<summary>

► History
</summary>

**user**: H

**assistant**: {‘id’: ‘toolu_01G77V2RMBcxVDKqsCYmZ9Au’, ‘caller’:
{‘type’: ‘direct’}, ‘input’: {‘book’: {‘title’: ‘Tao Te Ching’, ‘pages’:
80}, ‘percent’: 75}, ‘name’: ‘find_page’, ‘type’: ‘tool_use’}

**user**: 60

**assistant**: Three quarters of the way through your 80-page edition of
the Tao Te Ching is **page 60**!

</details>

<table>
<thead>
<tr>
<th>Metric</th>
<th style="text-align: right;">Count</th>
<th style="text-align: right;">Cost (USD)</th>
</tr>
</thead>
<tbody>
<tr>
<td>Input tokens</td>
<td style="text-align: right;">1,562</td>
<td style="text-align: right;">0.004686</td>
</tr>
<tr>
<td>Output tokens</td>
<td style="text-align: right;">118</td>
<td style="text-align: right;">0.001770</td>
</tr>
<tr>
<td>Cache tokens</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0.000000</td>
</tr>
<tr>
<td>Server tool use</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0.000000</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td style="text-align: right;"><strong>1,680</strong></td>
<td style="text-align: right;"><strong>$0.006456</strong></td>
</tr>
</tbody>
</table>

``` python
chat = Chat(model, tools=[text_editor_conf['sonnet']], ns=mk_ns(str_replace_based_edit_tool))
```

When not providing tools directly as Python functions (like `sum`), you
**must** create and pass a namespace dictionary (mapping the tool name
string to the function object) using the `ns` parameter to methods like
[`mk_toolres`](https://claudette.answer.ai/core.html#mk_toolres) or
`toolloop`. `toolslm` cannot automatically generate the namespace in
this case. For schema-based tools (i.e., Python functions), `claudette`
handles namespace creation automatically.

``` python
r = chat('Please explain very concisely what my _quarto.yml does. It is in the current path. Use your tools')
find_block(r, ToolUseBlock)
```

    ToolUseBlock(id='toolu_01SGFXBDAd9ykwQ8fnpVApWu', caller=DirectCaller(type='direct'), input={'command': 'view', 'path': '/_quarto.yml'}, name='str_replace_based_edit_tool', type='tool_use')

``` python
chat()
```

ToolUseBlock(id=‘toolu_01VJcCojtvzkaSjENnFACwn5’,
caller=DirectCaller(type=‘direct’), input={‘command’: ‘view’, ‘path’:
‘.’}, name=‘str_replace_based_edit_tool’, type=‘tool_use’)

<details>

- id: `msg_01LED9Xieob4miFXjXouNTue`
- container: `None`
- content:
  `[{'id': 'toolu_01VJcCojtvzkaSjENnFACwn5', 'caller': {'type': 'direct'}, 'input': {'command': 'view', 'path': '.'}, 'name': 'str_replace_based_edit_tool', 'type': 'tool_use'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `tool_use`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 1379, 'output_tokens': 75, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

## Images

Claude can handle image data as well. As everyone knows, when testing
image APIs you have to use a cute puppy.

``` python
# Image is Cute_dog.jpg from Wikimedia
fn = Path('samples/puppy.jpg')
Image(filename=fn, width=200)
```

![](00_core_files/figure-commonmark/cell-175-output-1.jpeg)

``` python
img = fn.read_bytes()
```

Claude expects an image message to have the following structure

``` js
{
    'role': 'user', 
    'content': [
        {'type':'text', 'text':'What is in the image?'},
        {
            'type':'image', 
            'source': {
                'type':'base64', 'media_type':'media_type', 'data': 'data'
            }
        }
    ]
}
```

`msglm` automatically detects if a message is an image, encodes it, and
generates the data structure above. All we need to do is a create a list
containing our image and a query and then pass it to `mk_msg`.

Let’s try it out…

``` python
q = "In brief, what color flowers are in this image?"
msg = mk_msg([img, q])
```

``` python
c([msg])
```

The flowers in the image are **purple/lavender** (blue-purple). They
appear to be **asters**.

<details>

- id: `msg_01DUds87tAj47fozgFfbEPrQ`
- container: `None`
- content:
  `[{'citations': None, 'text': 'The flowers in the image are **purple/lavender** (blue-purple). They appear to be **asters**.', 'type': 'text'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `end_turn`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 110, 'output_tokens': 28, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

You don’t need to call `mk_msg` on each individual message before
passing them to the [`Chat`](https://claudette.answer.ai/core.html#chat)
class. Instead you can pass your messages in a list and the
[`Chat`](https://claudette.answer.ai/core.html#chat) class will
automatically call `mk_msgs` in the background.

``` python
c(["How are you?", r])
```

For messages that contain multiple content types (like an image with a
question), you’ll need to enclose the message contents in a list as
shown below:

``` python
c(["How are you?", r, [img, q]])
```

``` python
c = Chat(model)
c([img, q])
```

The flowers in the image are **purple/lavender** (blue-purple). They
appear to be **asters**.

<details>

- id: `msg_01DUds87tAj47fozgFfbEPrQ`
- container: `None`
- content:
  `[{'citations': None, 'text': 'The flowers in the image are **purple/lavender** (blue-purple). They appear to be **asters**.', 'type': 'text'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `end_turn`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 110, 'output_tokens': 28, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

``` python
contents(c.h[0])
```

    '*Media Type - image*'

``` python
c
```

The flowers in the image are **purple/lavender** (blue-purple). They
appear to be **asters**.

<details>

<summary>

► History
</summary>

**user**: *Media Type - image*

**assistant**: The flowers in the image are **purple/lavender**
(blue-purple). They appear to be **asters**.

</details>

<table>
<thead>
<tr>
<th>Metric</th>
<th style="text-align: right;">Count</th>
<th style="text-align: right;">Cost (USD)</th>
</tr>
</thead>
<tbody>
<tr>
<td>Input tokens</td>
<td style="text-align: right;">110</td>
<td style="text-align: right;">0.000330</td>
</tr>
<tr>
<td>Output tokens</td>
<td style="text-align: right;">28</td>
<td style="text-align: right;">0.000420</td>
</tr>
<tr>
<td>Cache tokens</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0.000000</td>
</tr>
<tr>
<td>Server tool use</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0.000000</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td style="text-align: right;"><strong>138</strong></td>
<td style="text-align: right;"><strong>$0.000750</strong></td>
</tr>
</tbody>
</table>

<div>

> **Note**
>
> Unfortunately, not all Claude models support images 😞. This
> [table](https://docs.anthropic.com/en/docs/about-claude/models#model-comparison-table)
> summarizes the capabilities of each Claude model and the different
> modalities they support.

</div>

## Caching

Claude supports context caching by adding a `cache_control` header to
the message content.

``` js
{
    "role": "user",
    "content": [
        {
            "type": "text", 
            "text": "Please cache my message", 
            "cache_control": {"type": "ephemeral"}
        }
    ]
}
```

To cache a message, we simply set `cache=True` when calling `mk_msg`.

``` python
mk_msg(['hi', 'there'], cache=True)
```

``` python
{ 'content': [ {'text': 'hi', 'type': 'text'},
               { 'cache_control': {'type': 'ephemeral'},
                 'text': 'there',
                 'type': 'text'}],
  'role': 'user'}
```

Claude also now supports smart cache look-ups, so it’s very simple to
keep an entire conversation in cache by constantly telling it to update
the cache with the latest message. To do this, we just need to set
`cache=True` when creating a
[`Chat`](https://claudette.answer.ai/core.html#chat).

``` python
chat = Chat(model, sp=sp, cache=True)
```

Caching has a minimum token limit of 1024 tokens for Sonnet and Opus,
and 2048 for Haiku. If your conversation is below this limit, it will
not be cached.

``` python
chat("Hi, I'm Jeremy.")
```

Hi Jeremy! Nice to meet you. How are you doing today? Is there something
I can help you with?

<details>

- id: `msg_01WQUgAZDsjnFgUd4tzxLJHN`
- container: `None`
- content:
  `[{'citations': None, 'text': 'Hi Jeremy! Nice to meet you. How are you doing today? Is there something I can help you with?', 'type': 'text'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `end_turn`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 21, 'output_tokens': 26, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

``` python
chat.use
```

    In: 21; Out: 26; Cache create: 0; Cache read: 0; Total Tokens: 47; Search: 0; Fetch: 0

Note the usage: no cache is created, nor used. Now, let’s send a long
enough message to trigger caching.

``` python
chat("""Lorem ipsum dolor sit amet""" * 150)
```

It looks like you’ve sent a large block of repeated “Lorem ipsum dolor
sit amet” placeholder text. This is commonly used in design and
publishing as dummy text.

Was this sent by accident, or is there something specific you’d like
help with, Jeremy? 😊

<details>

- id: `msg_0164VgWyvwHC2HAGrzFJUbDB`
- container: `None`
- content:
  `[{'citations': None, 'text': 'It looks like you\'ve sent a large block of repeated "Lorem ipsum dolor sit amet" placeholder text. This is commonly used in design and publishing as dummy text.\n\nWas this sent by accident, or is there something specific you\'d like help with, Jeremy? 😊', 'type': 'text'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `end_turn`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 1098, 'output_tokens': 59, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

``` python
chat.use
```

    In: 1119; Out: 85; Cache create: 0; Cache read: 0; Total Tokens: 1204; Search: 0; Fetch: 0

The context is now long enough for cache to be used. All the
conversation history has now been written to the temporary cache. Any
subsequent message will read from it rather than re-processing the
entire conversation history.

``` python
chat("Oh thank you! Sorry, my lorem ipsum generator got out of control!")
```

Ha, no worries at all, Jeremy! Those lorem ipsum generators can
definitely get a little wild sometimes! 😄 It happens to the best of us.
Is there something I can actually help you with today?

<details>

- id: `msg_01FeLGZ2T49tUHaWKaQAJAhn`
- container: `None`
- content:
  `[{'citations': None, 'text': 'Ha, no worries at all, Jeremy! Those lorem ipsum generators can definitely get a little wild sometimes! 😄 It happens to the best of us. Is there something I can actually help you with today?', 'type': 'text'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `end_turn`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 1175, 'output_tokens': 48, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

``` python
chat.use
```

    In: 2294; Out: 133; Cache create: 0; Cache read: 0; Total Tokens: 2427; Search: 0; Fetch: 0

## Extended Thinking

Claude \>=3.7 have enhanced reasoning capabilities for complex tasks.
See
[docs](https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking)
for more info.

We can enable extended thinking by passing a `thinking` param with the
following structure.

``` js
thinking={ "type": "enabled", "budget_tokens": 16000 }
```

When extended thinking is enabled a thinking block is included in the
response as shown below.

``` js
{
  "content": [
    {
      "type": "thinking",
      "thinking": "To approach this, let's think about...",
      "signature": "Imtakcjsu38219c0.eyJoYXNoIjoiYWJjM0NTY3fQ...."
    },
    {
      "type": "text",
      "text": "Yes, there are infinitely many prime numbers such that..."
    }
  ]
}
```

*Note: When thinking is
[enabled](https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#important-considerations-when-using-extended-thinking)
`prefill` must be empty and the `temp` must be 1.*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L539"
target="_blank" style="float:right; font-size:smaller">source</a>

### think_md

``` python

def think_md(
    txt, thk
):

```

*Call self as a function.*

``` python
def contents(r, show_thk=True):
    "Helper to get the contents from Claude response `r`."
    blk = find_block(r)
    if show_thk:
        tk_blk = find_block(r, blk_type=ThinkingBlock)
        if tk_blk: return think_md(blk.text.strip(), tk_blk.thinking.strip())
    if not blk and r.content: blk = r.content[0]
    if hasattr(blk,'text'): return blk.text.strip()
    elif hasattr(blk,'content'): return blk.content.strip()
    elif hasattr(blk,'source'): return f'*Media Type - {blk.type}*'
    return str(blk)
```

Let’s call the model without extended thinking enabled.

``` python
chat = Chat(model)
```

``` python
chat("Write a sentence about Python!")
```

Here’s a sentence about Python:

**Python is a versatile, beginner-friendly programming language known
for its clean, readable syntax and wide range of applications, from web
development and data science to artificial intelligence and
automation.**

Would you like to know more about Python? 🐍

<details>

- id: `msg_0133ueaeXR2KvELPiPDnpFAC`
- container: `None`
- content:
  `[{'citations': None, 'text': "Here's a sentence about Python:\n\n**Python is a versatile, beginner-friendly programming language known for its clean, readable syntax and wide range of applications, from web development and data science to artificial intelligence and automation.**\n\nWould you like to know more about Python? 🐍", 'type': 'text'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `end_turn`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 13, 'output_tokens': 63, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

Now, let’s call the model with extended thinking enabled.

``` python
chat("Write a sentence about Python!", maxthinktok=1024)
```

Here’s a sentence about Python:

**Python is one of the most popular programming languages in the world,
loved by developers for its simplicity, powerful libraries, and ability
to handle complex tasks with just a few lines of code.**

Fun fact: Python was named after the British comedy group **Monty
Python**, not the snake! 🐍😄

Would you like to learn more about Python?

<details>

<summary>

Thinking
</summary>

The user wants another sentence about Python. Let me write a different
one this time.
</details>

<details>

- id: `msg_01Vuqd8xUwuB2tTnmcexcfFp`
- container: `None`
- content:
  `[{'signature': 'Ev0BCkYICxgCKkAmUcWkc6z1j9s2pIRVL/EIaNbJYCaXR1stNeEAhQoCylzrssuUy2ZTq4vGWUl7WbH2AbV30H8aRazSNGwgnQp+Egzq7GmzjPUY/hkdH1oaDKbN8/A53z7nu/TenyIwIAhQomWRMzdCY7ueYlSChv7bcBIpnPT6Pf1gboh/KOlgAQIV0DRGX3r28/rVxj0KKmXdM/z1lMTxHXSMLE5PQkpDsNEavF0XqqGZlFXgd1Sg4mZSiaV3/9h9FTb2VWNtbpsvtztVbdvt4BbTnrO6YtNv5ehK0/7kaArbOwi61aQtZson8/+gmZgnox2vDieBHhpCc6t3ThgB', 'thinking': 'The user wants another sentence about Python. Let me write a different one this time.', 'type': 'thinking'}, {'citations': None, 'text': "Here's a sentence about Python:\n\n**Python is one of the most popular programming languages in the world, loved by developers for its simplicity, powerful libraries, and ability to handle complex tasks with just a few lines of code.**\n\nFun fact: Python was named after the British comedy group **Monty Python**, not the snake! 🐍😄\n\nWould you like to learn more about Python?", 'type': 'text'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `end_turn`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 114, 'output_tokens': 113, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

## Server Tools and Web Search

The
[`str_replace`](https://claudette.answer.ai/text_editor.html#str_replace)
special tool type is a client side tool, i.e., one where we provide the
implementation. However, Anthropic also supports server side tools. The
current one available is their search tool, which you can find the
documentation for
[here](https://docs.anthropic.com/en/docs/build-with-claude/tool-use/web-search-tool).
When provided as a tool to claude, claude can decide to search the web
in order to answer or solve the task at hand.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L550"
target="_blank" style="float:right; font-size:smaller">source</a>

### search_conf

``` python

def search_conf(
    max_uses:int=None, allowed_domains:list=None, blocked_domains:list=None, user_location:dict=None
):

```

*Little helper to create a search tool config*

Similar to client side tools, you provide to the `tools` argument in the
anthropic api a non-schema dictionary with the tool’s name, type, and
any additional metadata specific to that tool. Here’s a function to make
that process easier for the web search tool.

``` python
search_conf()
```

    {'type': 'web_search_20250305', 'name': 'web_search'}

The web search tool returns a list of `TextBlock`s comprised of response
text from the model, `ServerToolUseBlock` and server tool results block
such as `WebSearchToolResultBlock`. Some of these `TextBlock`s will
contain citations with references to the results of the web search tool.
Here is what all this looks like:

``` js
{
  "content": [
    {
      "type": "text",
      "text": "I'll check the current weather in...",
    },
    {
      "type": "server_tool_use",
      "name": "web_search",
      "input": {"query": "San Diego weather forecast today May 12 2025"},
      "id":"srvtoolu_014t7fS449voTHRCVzi5jQGC"
    },
    {
      "type": "web_search_tool_result",
      "tool_use_id": "srvtoolu_014t7fS449voTHRCVzi5jQGC",
      "content": [
        "type": "web_search_result",
        "title": "Heat Advisory issued May 9...",
        "url": "https://kesq.com/weather/...",
        ...
      ]
    }
    {
      "type": "text",
      "citations": [
        {
            "cited_text": 'The average temperature during this month...',
            "title": "Weather San Diego in May 2025:...",
            "url": "https://en.climate-data.org/...",
            "encrypted_index": "EpMBCioIAxgCIiQ4ODk4YTF..."
        }
      ],
      "text": "The average temperature in San Diego during May is..."
    },
    ...
  ]
}
```

Let’s update our
[`contents`](https://claudette.answer.ai/core.html#contents) function to
handle these cases. For handling citations, we will use the excellent
reference syntax in markdown to make clickable citation links.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L560"
target="_blank" style="float:right; font-size:smaller">source</a>

### find_blocks

``` python

def find_blocks(
    r, blk_type:ModelMetaclass=TextBlock, type:str='text'
):

```

*Helper to find all blocks of type `blk_type` in response `r`.*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L566"
target="_blank" style="float:right; font-size:smaller">source</a>

### blks2cited_txt

``` python

def blks2cited_txt(
    txt_blks
):

```

*Helper to get the contents from a list of `TextBlock`s, with
citations.*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/claudette/blob/main/claudette/core.py#L590"
target="_blank" style="float:right; font-size:smaller">source</a>

### contents

``` python

def contents(
    r, show_thk:bool=True
):

```

*Helper to get the contents from Claude response `r`.*

``` python
chat = Chat(model, sp='Be concise in your responses.', tools=[search_conf()], cache=True)
pr = 'What is the weather in San Diego?'
r = chat(pr)
r
```

Here’s the current weather for **San Diego, CA** today, Sunday, February
22:

- **Condition:** Mainly sunny to start, then a few afternoon clouds. [1]
- **High:** 69°F [2]
- **Low:** 48°F [3]
- **Wind:** NNW at 10 to 15 mph [4]
- **Humidity:** 73% [5]

**Tonight:** Mostly clear with a low of 48°F and light, variable winds.
[6]

**Looking ahead:** High temperatures are expected to increase throughout
the week, reaching 10 to 20 degrees above average by Friday, with highs
in the mid-70s at the coast and mid-80s inland. [7]

<details>

- id: `msg_01CiY6bqkVHro5CXX1jVNjMG`
- container: `None`
- content:
  `[{'id': 'srvtoolu_01EmFt11mrox1xAAjpz8f5QF', 'caller': {'type': 'direct'}, 'input': {'query': 'weather in San Diego today'}, 'name': 'web_search', 'type': 'server_tool_use'}, {'caller': {'type': 'direct'}, 'content': [{'encrypted_content': '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', 'page_age': '11 hours ago', 'title': 'San Diego, CA Weather Forecast | AccuWeather', 'type': 'web_search_result', 'url': 'https://www.accuweather.com/en/us/san-diego/92101/weather-forecast/347628'}, {'encrypted_content': 'EpoCCioIDRgCIiQ4ODk4YTFkYy0yMTNkLTRhNmYtOTljYi03ZTBlNTUzZDc0NWISDPmW+VGbGo5aPjqG4xoMfkBfdsdLoG2LM8wqIjAI94rS2DM8ak8JsDELXHQTXh429Z7xwTwZoTIOlQfh98VydUJkywIBIWgCXpOJVxkqnQFplIe+px4BQIADOma4Ifs+KlzYIWvXuUSBRMU1i/Rt7YPg1Z9ibw6ClQ1TA41tzl9ozicI1DcXWFyvftsnf6ei6wosLF/08ef3uKeIBl0JAjRW2VEVFz+1dYW3oUprmVq5BKF4K7jUSolq3YOxlOYjBnbqS7QJ4k4nGg19TFQTeFo2umdB3spC2Qkdq+8cIpBJ3CC/k1Pf3Ql7LJOdGAM=', 'page_age': '6 days ago', 'title': 'San Diego weather forecast – NBC 7 San Diego', 'type': 'web_search_result', 'url': 'https://www.nbcsandiego.com/weather/'}, {'encrypted_content': 'ErkDCioIDRgCIiQ4ODk4YTFkYy0yMTNkLTRhNmYtOTljYi03ZTBlNTUzZDc0NWISDLOH6/BHtC+UtVZ10RoMdYZ1dAG25M4Z0uf4IjABr8pN+SRe0h6KZJq8DqBjuZDvInCWkWuX05Rjt2KlYKzXnZ+JJwxnA/Nj61P6qVcqvAKHdVVJF1FGOyhxqm9GE9k0adScSC/RFNqwl9y4IM+xlhfwReqnou4RCCxQQglmE+7OA6vNAAnptB9TU1yqEWuUvk+fgIrXZ5jsIMtZWjy7KuneSeaHDDToX9pTPTPc5cF+rV2cTF6AbeC5H8Qi/WW0f2wv8CmAcyGSnJdkXTw6EEA7R8//lYwqPMEIkq6iyeL5R9by8ktrFKnmoQg6wEfH1WV3V80CRloeQWfF/XpIZc+rxKc+mkP55WcsvKiE9mc2uGQmFRpbNJFM7GUmHUyD6G+26ZU9a8B1kWq0Y7fZi86wgdExlrvCzZaPEyVnpArsz1r+4BDE9YULIVQbMBcjBdyr0OA4yTjA2xr0I2DarWp2O2ETG/2mtZ0FaLS5QSD9mELy+YxvaTYVHiRPUwoBtfFYd8hSN/j3LekjGAM=', 'page_age': '19 hours ago', 'title': 'San Diego, CA', 'type': 'web_search_result', 'url': 'https://www.weather.gov/sgx'}, {'encrypted_content': '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', 'page_age': None, 'title': 'San Diego, CA Hourly Weather Forecast | Weather Underground', 'type': 'web_search_result', 'url': 'https://www.wunderground.com/hourly/us/ca/san-diego'}, {'encrypted_content': 'EuYJCioIDRgCIiQ4ODk4YTFkYy0yMTNkLTRhNmYtOTljYi03ZTBlNTUzZDc0NWISDD8E5j6RXUvlgDH2phoMgK2iuOIWQPU4+KrxIjDb0BhChdLD8DyV2iwVtQ8MIMxqkj7ZVCdCpvf/O6VFVsVd+7K3OJC4mlxfmBEebnoq6QjR/iMPSPN34WurujQfR9lBUsYLbxHlFBKxfmtfVQL4d9+F013XGXz4e6LwcRgfDvAyALfhoR3wzSMldIAaRFMiqo0fm6MKortjmcC9k9x8dClnpzpOpyTALDN+CyH+s6iRvs0myEr0VDpD6cUeNTdJMwYdPFUSjUfSX1DYG5SizA9dNKNgAcIZ2UsGlUD//ACls2PcStmjQSajg3t+0wmS7f99SZ4KAAtnTsrrfwVJeTRWfjZAl3J+OzqOWuVDNYHYwUZoyPaSDQ3iMArVdvEt6FJKAORAaeXjyQDySeMkyglz5fjBG7f7dkL9od3MItobvdgUvols7j49wLCB6aJBMdyKngFMSKJxKy7NK0rQocNPg28jypoTFKREZqQEzqtQVo460KloaFukoI7tODNAPbZ0iJbjWEbfy/s9z54dLTaOISYmAmWwEmHBptJ8o/4/BDYDMGDVFUZecIwBcS0LqsbkE1Qlo7PCDOYf6uzjOJkmTLHhOq8frS7M3lhwZqmJ3DJ/bmmifEb+gF+jKaIuh1hMKLxK3oGeiHbEVGvNX0/VQbUZmiOT1v28LLrkeFHHy3doyhfjgZEQirkVBntwpEZz7Kj2chq2+K+R8K5YgSN+v+fqhde+cabHGkOtaPQ4QKQUggWYB/1tx4RztmZFU6nO++/niZxCpuBndcoyVe5U01+23ArS36OirydAZ5qRc2b1Z0jb9McqEhv1/Tk9mjCwYl/EcX4QDy8cHpolufMjhC591Xv9y6WQNz1bJk2oDXkf82sA3lbS/osrcT5p/b81/nSvuR8lbiHyILjFYLK4E3D9EF8FHi68SPpxEbM+NlShwJiSnlVmPfuJ7od3HPxtxcLTdFCbYBLpi8DAHHyLQ6UOE825URoq/dhw+u+S3cflvfLdNdH1eI8qPetrHhiRyju3BjMbqraotzQuv3IleyybgqTl2guUtie77SSP7YY/cLya27iYzY5g9kRNSRE/GxNM4dsNKTjaDEkcv9AxNSykhHq+f4xqE0+nD0n1IRMV9wqxwiqh+SUvMsBj/t1WzDnE7nqch/Zd0NaY0B40fMuFOR+PyHJECEsIoxMvprFrADTemM0xZVzFwHlbY3cCBWN+JsJfBcn4nKbunLd2H/9UsUGhHjO4fM0huv5QlzMOPOd8bJz0scOfM/zmPxINr5FN2E655pYgxZ7j3V/zk8CA9v976Jy8pQc1s64yqbhAQo2V6ZMmWTDaA2tIkwHKMi3S6QuEdq/1IiuqibCzw0mvhvcx7VXVIQeMcIDva2u4yr4PGv2vBhHgexUkmX7VZMEIgw6WFK0warFnpSx3LjigyH45MdA1I6jnHm8YFevAj5E+THssSVu+YpRe2YVw02UGFrClaJkd8VGz/eSgS4OO85fymEPKH3n/ny3soTDLy4RJ5UDPRLDfLrkAmkx5oF/t+zL7AG4z12K41y4iKJWr4CtiAnuwDgFOPl1gbPqR9zydJ6RuMm64lPgTbzw98e5/m1jUGAM=', 'page_age': None, 'title': 'San Diego, CA Weather Forecast | KGTV | kgtv.com', 'type': 'web_search_result', 'url': 'https://www.10news.com/weather'}, {'encrypted_content': 'EpsJCioIDRgCIiQ4ODk4YTFkYy0yMTNkLTRhNmYtOTljYi03ZTBlNTUzZDc0NWISDMCCjZJJjOuTE7338hoMkvEw85j6yFkTAJTbIjDeiWnqVdocqrSsrXLXDsgrhtOjAroFBneXL6kkSjTAZaC2a/WTFfYNPGXCG+SUP7QqnghJN4dtmMtq3IEDhmxy/9T+jWoYnWQtmQQvogQIdmRPxRWFIa6oUmXHnFgh1bK6aSRdeAQlVweJ3O08zkUxpL5bZLLBPT+260iIEb3O5z46hflTmVkZ8TnPK1WXXF9jq7vwdJKfPSRDD1l5q4SGlnK3NPhK2Xwb7alpIfjjS3H5S5j/29nWeZZX3K9DxQQysky1w/aKTgSIDEgXhuD8UlLcmix9NIwiHw5ypZEDIFqLZLKPEWf1/cgiqo+OBh5XJ5xD7VyfyyZFQuWD4nbk5O9YqOBN3Decm8DupB54rvG1jA2MYa68dvYXTqNNiVd2oP4nYi2fUDleVlx2Mcb5VIz3LSsaEJMHCW6ud389UErbgvu4VRzJc3zUTDkm0y2LWMmmVbJ9/HRt6DOqMjTjo5mdpOJ77E7Hxr7UiLQUTDv1vKToj0bRMBjLqJqEguds4RBF6fJ+kd8IeExi33Cv4icldiQtcYmh0Sj7AiFSQ+94y6tNx11E+GHZWBLyv0kwPrH2RjucBnS5gvY5M8B/GZSzn5S/ztEJnbUM9Hqxl8npAcrV+ZhD2A+/v0KUXkX+yWOL6DTOanx99NGbtq1K9xYXNbdbsQbiDOyaOxYAXYl1hB0HqIiy1p4sqciNDlpWTZ9igGZHegbtpG4M0ZToCUKv4htPNeB8UHssXm08fbNciCGSjO7MDcypce+TtEp7KqByuYAPg+2nWyryH47CoRQrG3GMf7GBT1EWxMKE3UbP17EuK5/3w3iD00ZsrvZhqIYgb0ytmA+HgmdB7T3vNCOmznqVGl49MT4lAzJcn6t8fP+nELT8ZRwwkpOho0WLT6wpxfHJ/zRdBmgGZvoozVl22w8Sd94ZZ8t9KCS1i9mwNhyJShHHoLbpWEUmn51UIcywJ7jXiJMcMN2z4xl2RSPsFY7ghheItIX8ItcVQNe3XIPQIaxw8t4P5VXTk2zwoUzNdLnus7Y1vKZ+7WhjQGTT2pVTs4/x95cF4f9RbDkZMvJ76Tc6520HtC7PnL8vtWu4UOPPBZBzg0ci5OsZotIYxRe79ELBOwg1nIyiNVU++xNWjC+AAJg/F7dttoT1t2WxIN5K/8ODgN9095CKpyFWckPiHrcDdyvHuxDzcN5o22mQH3NR5Gg2C1vDxPG9zImp22Z8lVz7yeW4Fb00dioulSInYcfio+Vavb4m9ncOrogtyNGEA1/EruyOWEHKcWnkGGnzceMA4QMABkWkBZ5rQmDwYdkOBJPK56lZPLQLy5P/gqHBA2toAEMMpzaf4qTMkM8WgCt8byGynjNg2FRJjdc3YZMt+IDd3e/ZiykKTkxj+BbNomht903Gtlkh5PjUIRT3CFI/u17JqPoHTg3tno3KMo8hfcBJ/CJzenSignK3yoCLg4VAXFg4B4BvGAM=', 'page_age': 'October 29, 2025', 'title': 'San Diego Weather & 7-Day Forecast | FOX 5 San Diego', 'type': 'web_search_result', 'url': 'https://fox5sandiego.com/weather/'}, {'encrypted_content': 'Et0BCioIDRgCIiQ4ODk4YTFkYy0yMTNkLTRhNmYtOTljYi03ZTBlNTUzZDc0NWISDNzCV/HulREWIE7UGxoMavEi+rhChl6gS/pDIjAuGc3UixskBcE3fArDmyrQCMC7cgicHqhm0nwbxIHzTFYmZfdZzAfB5agKDVPlbXoqYUVYf0qPZKu+9kFo+Bg/kgIRIHzvImfqlhcYjw+v5pRPCJPWmiyE5ld0a3gA7WKqXYpcuR/epEKlSqo4ht793d4XCTlY3aueqd5dm1rZKhZHlGtgV3gzPe6EFIl89XfjyW4YAw==', 'page_age': None, 'title': 'Weather for San Diego, California, USA', 'type': 'web_search_result', 'url': 'https://www.timeanddate.com/weather/usa/san-diego'}, {'encrypted_content': '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', 'page_age': 'February 14, 2012', 'title': 'NWS San Diego (@NWSSanDiego) / X', 'type': 'web_search_result', 'url': 'https://x.com/nwssandiego'}, {'encrypted_content': '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', 'page_age': None, 'title': 'San Diego, CA Current Weather | AccuWeather', 'type': 'web_search_result', 'url': 'https://www.accuweather.com/en/us/san-diego/92101/current-weather/347628'}, {'encrypted_content': 'Eu0ECioIDRgCIiQ4ODk4YTFkYy0yMTNkLTRhNmYtOTljYi03ZTBlNTUzZDc0NWISDC+eBMbh0beNMTdcjBoMPVDl1mNgtMDF/i7dIjCL3csRo3DyvtCpYVZ0TTHHv5QpXvXtEtZucEyNAYBUuUGkH6Uwkvw845Cjes6Ss9sq8AMHwmRl1bzgKcjTkQJaGL6mOYBEAaOOTbSzoKRsC/og9sbJYZahLQsytqSRB5SgrXRxQgahjOc/aL6YjNShu8lw0KjOXePKR61KbTogOrKNrcsfYESVJpR+pRORnMXmPe8IdnCsxZ1d8l3Eqbqu+wzi67Y7JcXvycg3Ee++F+2NdSv/c345BbcbLtMk1zS3uaatEPSPErphiMzJjMaz983gd6U17C+nl02DKRkT/nU6hAsVe25wEvrXsYu1+I6i3SpXgefcLxxV3LIqqclZmd3nQPj+UzSrP3q3wXj4uB5/isnCl4bK8rfnPSartxrhVjntIDvZGmkaIBgMB/nl2CINdcBka+xJkw982idZ0ifmFEzDaJk5gFud6E8vB16/rLJYy5J5Qf655wJ2V2Ry7Fow3cDOlNd8ZKl+SqxmKtC1iHVhmyXqxMHpZmttGyHSQJOlIDDK2y5azjTj25+lddmNbtaLuH9zp3NLZsxXB8a5gZFKIvPgegQnsW1sMUKkygXBb0HeAw2FmJC5jKFzXNHvri7bggzfu6L+V0cE0dvnv4aVjPiyKph8E/dJ1jd/xsLBkONkBr1i6DkYX3xgAEFvnEluKrFucEIEcoIzzB15TlZYnLBYrQDRVARg9KDnDESe6kc/McEzK55PH9WInOGkGAM=', 'page_age': '1 month ago', 'title': 'San Diego, CA Weather Conditions | Weather Underground', 'type': 'web_search_result', 'url': 'https://www.wunderground.com/weather/us/ca/san-diego'}], 'tool_use_id': 'srvtoolu_01EmFt11mrox1xAAjpz8f5QF', 'type': 'web_search_tool_result'}, {'citations': None, 'text': "Here's the current weather for **San Diego, CA** today, Sunday, February 22:\n\n- **Condition:** ", 'type': 'text'}, {'citations': [{'cited_text': 'zoom out · Showing Stations · Hourly Forecast for Today, Sunday 02/22Hourly for Today, Sun 02/22 · Today 02/22 · 2% / 0 in · Mainly sunny to start, th...', 'encrypted_index': 'Eo8BCioIDRgCIiQ4ODk4YTFkYy0yMTNkLTRhNmYtOTljYi03ZTBlNTUzZDc0NWISDKPnjrs9hI81UdheSBoMjOEBwau6ksWBtHPSIjBSGb5wiRTDaDToTYzb06X7vpO/NAk1SOD79RwkX8u1xbcqVUpcQJOWtNXPXdKGc6kqE+T7ttYmZQIDTaXTSG7TR/6PGtsYBA==', 'title': 'San Diego, CA Hourly Weather Forecast | Weather Underground', 'type': 'web_search_result_location', 'url': 'https://www.wunderground.com/hourly/us/ca/san-diego'}], 'text': 'Mainly sunny to start, then a few afternoon clouds.', 'type': 'text'}, {'citations': None, 'text': '\n- **High:** ', 'type': 'text'}, {'citations': [{'cited_text': 'High 69F. ', 'encrypted_index': 'Eo8BCioIDRgCIiQ4ODk4YTFkYy0yMTNkLTRhNmYtOTljYi03ZTBlNTUzZDc0NWISDBxCY+HYFWzXa/eQ5BoMPJFKGEz/4bLKgNYSIjD3IvzFy8S+Ur6X07t4RyeIR0/ZLGLTr1/650UZ93tw81lHOko8MJ7w83lZGKLMHo4qE4hnkd/a/SnrpjD2xx/D8bof2C0YBA==', 'title': 'San Diego, CA Hourly Weather Forecast | Weather Underground', 'type': 'web_search_result_location', 'url': 'https://www.wunderground.com/hourly/us/ca/san-diego'}], 'text': '69°F', 'type': 'text'}, {'citations': None, 'text': '\n- **Low:** ', 'type': 'text'}, {'citations': [{'cited_text': 'Low 48F. ', 'encrypted_index': 'Eo8BCioIDRgCIiQ4ODk4YTFkYy0yMTNkLTRhNmYtOTljYi03ZTBlNTUzZDc0NWISDCfRLDkRQlN9Ru2j0RoMNrMvsZ2P6dpavhJmIjClJV+JLmfYoC6q1mVtUpfADT+usthNfmaOqv+7Fm2OHUDezfL7XNCvm+k2eb81kqoqEwQO0t5Pkl9chxw4zxRTA0+RxXEYBA==', 'title': 'San Diego, CA Hourly Weather Forecast | Weather Underground', 'type': 'web_search_result_location', 'url': 'https://www.wunderground.com/hourly/us/ca/san-diego'}], 'text': '48°F', 'type': 'text'}, {'citations': None, 'text': '\n- **Wind:** ', 'type': 'text'}, {'citations': [{'cited_text': 'Winds NNW at 10 to 15 mph. ', 'encrypted_index': 'Eo8BCioIDRgCIiQ4ODk4YTFkYy0yMTNkLTRhNmYtOTljYi03ZTBlNTUzZDc0NWISDCUBdvhnZQj2+zDAshoMaWIbNAYyQ0Isx7qhIjBLGGiVXWEd9aIvyDbbRrqeg/Vv2C59yDUSOHIXhhCgCOcOWg0BXYDQuZHox/I7x3UqE3X8jaHlFlx5i8fCxZYsvLefEJQYBA==', 'title': 'San Diego, CA Hourly Weather Forecast | Weather Underground', 'type': 'web_search_result_location', 'url': 'https://www.wunderground.com/hourly/us/ca/san-diego'}], 'text': 'NNW at 10 to 15 mph', 'type': 'text'}, {'citations': None, 'text': '\n- **Humidity:** ', 'type': 'text'}, {'citations': [{'cited_text': 'Weather Alerts · , Enter zip code to change location · AK · AL · AR · AZ · CA · CO · CT · DC · DE · FL · GA · HI · IA · ID · IL · IN · KS · KY · LA · ...', 'encrypted_index': 'Eo8BCioIDRgCIiQ4ODk4YTFkYy0yMTNkLTRhNmYtOTljYi03ZTBlNTUzZDc0NWISDCTjv+m/hjq8As8IwxoMGQlayvWmSb9+A8hKIjDY+8aotwhJ05D3rn4qP4u3MgUvTL2dnvmdIpYybmCaGQrwOCjpuKVwVq40ozuEbbwqE3kYV+eQLCNQKXRE2S/nyZMqM6oYBA==', 'title': 'San Diego, CA Weather Forecast | KGTV | kgtv.com', 'type': 'web_search_result_location', 'url': 'https://www.10news.com/weather'}], 'text': '73%', 'type': 'text'}, {'citations': None, 'text': '\n\n**Tonight:** ', 'type': 'text'}, {'citations': [{'cited_text': 'Tonight 02/22 · 14 % / 0 in · Mostly clear. Low 48F. Winds light and variable. ', 'encrypted_index': 'EpIBCioIDRgCIiQ4ODk4YTFkYy0yMTNkLTRhNmYtOTljYi03ZTBlNTUzZDc0NWISDHOFAnCo7mjGSRui9hoMS8ribfl6H67/LfWMIjDJ21oTZ/NUzd4mIHe0bf+CvA8WJY4O57xQoKsuj2Y3RvA5V49z1WJ4kcohYt9/RMIqFuZDjilpgTI9AhMVUfC8H5YGzvMY1dIYBA==', 'title': 'San Diego, CA Hourly Weather Forecast | Weather Underground', 'type': 'web_search_result_location', 'url': 'https://www.wunderground.com/hourly/us/ca/san-diego'}], 'text': 'Mostly clear with a low of 48°F and light, variable winds.', 'type': 'text'}, {'citations': None, 'text': '\n\n**Looking ahead:** ', 'type': 'text'}, {'citations': [{'cited_text': 'High temperatures continue to increase throughout the week, reaching up to 10 to 20 degrees above average by Friday. Highs reach into the mid-70s at t...', 'encrypted_index': 'EpEBCioIDRgCIiQ4ODk4YTFkYy0yMTNkLTRhNmYtOTljYi03ZTBlNTUzZDc0NWISDP2q0p/VI0gCB2a1KhoM0pPJHMZHqghhpS0yIjCf0Mv7lKMuxsBEUBUPcXBlN9Lz7hXdXgePFC6xTr7G747EanbklIjVjTCcbGLWDNUqFQwGIjUA0WzAMV6otmRq7amSKNqOGBgE', 'title': 'San Diego, CA', 'type': 'web_search_result_location', 'url': 'https://www.weather.gov/sgx'}], 'text': 'High temperatures are expected to increase throughout the week, reaching 10 to 20 degrees above average by Friday, with highs in the mid-70s at the coast and mid-80s inland.', 'type': 'text'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `end_turn`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 2227}, 'cache_creation_input_tokens': 8771, 'cache_read_input_tokens': 2227, 'inference_geo': 'global', 'input_tokens': 4, 'output_tokens': 337, 'server_tool_use': {'web_fetch_requests': 0, 'web_search_requests': 1}, 'service_tier': 'standard'}`

</details>

We can cite custom documents too.

``` python
from msglm import mk_ant_doc
```

``` python
doc = mk_ant_doc("The capital of France is Paris. It has a population of about 2.2 million.", title="France Facts")
chat = Chat(model=models[1])
r = chat([doc, "What is the capital of France and what is its population?"])
r
```

The capital of France is Paris. [8] It has a population of about 2.2
million. [9]

<details>

- id: `msg_01JrDTZVtNzSi1MyjPvPF2xS`
- container: `None`
- content:
  `[{'citations': [{'cited_text': 'The capital of France is Paris. ', 'document_index': 0, 'document_title': 'France Facts', 'end_char_index': 32, 'file_id': None, 'start_char_index': 0, 'type': 'char_location'}], 'text': 'The capital of France is Paris.', 'type': 'text'}, {'citations': None, 'text': ' ', 'type': 'text'}, {'citations': [{'cited_text': 'It has a population of about 2.2 million.', 'document_index': 0, 'document_title': 'France Facts', 'end_char_index': 73, 'file_id': None, 'start_char_index': 32, 'type': 'char_location'}], 'text': 'It has a population of about 2.2 million.', 'type': 'text'}]`
- model: `claude-sonnet-4-6`
- role: `assistant`
- stop_reason: `end_turn`
- stop_sequence: `None`
- type: `message`
- usage:
  `{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 604, 'output_tokens': 55, 'server_tool_use': None, 'service_tier': 'standard'}`

</details>

## Third party providers

NB: The 3rd party model list is currently out of date–PRs to fix that
would be welcome!

### Amazon Bedrock

These are Amazon’s current Claude models:

``` python
models_aws
```

    ['anthropic.claude-opus-4-1-20250805-v1:0',
     'anthropic.claude-sonnet-4-20250514-v1:0',
     'claude-3-5-haiku-20241022',
     'claude-3-7-sonnet-20250219',
     'anthropic.claude-3-opus-20240229-v1:0',
     'anthropic.claude-3-5-sonnet-20241022-v2:0']

Provided `boto3` is installed, we otherwise don’t need any extra code to
support Amazon Bedrock – we just have to set up the approach client:

``` python
ab = AnthropicBedrock(
    aws_access_key=os.environ['AWS_ACCESS_KEY'],
    aws_secret_key=os.environ['AWS_SECRET_KEY'],
)
client = Client(models_aws[0], ab)

chat = Chat(cli=client)
chat("I'm Jeremy")
```

### Google Vertex

``` python
models_goog
```

    ['claude-opus-4-1@20250805',
     'anthropic.claude-3-sonnet-20240229-v1:0',
     'anthropic.claude-3-haiku-20240307-v1:0',
     'claude-3-opus@20240229',
     'claude-3-5-sonnet-v2@20241022',
     'claude-3-sonnet@20240229',
     'claude-3-haiku@20240307']

``` python
from anthropic import AnthropicVertex
import google.auth
```

``` python
project_id = google.auth.default()[1]
region = "us-east5"
gv = AnthropicVertex(project_id=project_id, region=region)
client = Client(models_goog[-1], gv)

chat = Chat(cli=client)
chat("I'm Jeremy")
```

[1] France Facts “The capital of France is Paris.”

[2] France Facts “It has a population of about 2.2 million.”

[3] https://www.wunderground.com/hourly/us/ca/san-diego “Low 48F.”

[4] https://www.wunderground.com/hourly/us/ca/san-diego “Winds NNW at 10
to 15 mph.”

[5] https://www.10news.com/weather “Weather Alerts · , Enter zip code to
change location · AK · AL · AR · AZ · CA · CO · CT · DC · DE · FL · GA ·
HI · IA · ID · IL · IN · KS · KY · LA · …”

[6] https://www.wunderground.com/hourly/us/ca/san-diego “Tonight 02/22 ·
14 % / 0 in · Mostly clear. Low 48F. Winds light and variable.”

[7] https://www.weather.gov/sgx “High temperatures continue to increase
throughout the week, reaching up to 10 to 20 degrees above average by
Friday. Highs reach into the mid-70s at t…”

[8] France Facts “The capital of France is Paris.”

[9] France Facts “It has a population of about 2.2 million.”
