python-fastllm 0.0.58__tar.gz → 0.0.60__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_fastllm-0.0.60/PKG-INFO +231 -0
- python_fastllm-0.0.60/README.md +215 -0
- {python_fastllm-0.0.58 → python_fastllm-0.0.60}/fastllm/__init__.py +1 -1
- {python_fastllm-0.0.58 → python_fastllm-0.0.60}/fastllm/_modidx.py +6 -1
- {python_fastllm-0.0.58 → python_fastllm-0.0.60}/fastllm/acomplete.py +1 -1
- {python_fastllm-0.0.58 → python_fastllm-0.0.60}/fastllm/anthropic.py +25 -20
- {python_fastllm-0.0.58 → python_fastllm-0.0.60}/fastllm/chat.py +13 -9
- {python_fastllm-0.0.58 → python_fastllm-0.0.60}/fastllm/gemini.py +31 -34
- {python_fastllm-0.0.58 → python_fastllm-0.0.60}/fastllm/openai_chat.py +2 -0
- {python_fastllm-0.0.58 → python_fastllm-0.0.60}/fastllm/openai_responses.py +7 -7
- {python_fastllm-0.0.58 → python_fastllm-0.0.60}/fastllm/responses.py +19 -7
- {python_fastllm-0.0.58 → python_fastllm-0.0.60}/fastllm/streaming.py +3 -4
- {python_fastllm-0.0.58 → python_fastllm-0.0.60}/fastllm/types.py +9 -4
- python_fastllm-0.0.60/python_fastllm.egg-info/PKG-INFO +231 -0
- python_fastllm-0.0.58/PKG-INFO +0 -373
- python_fastllm-0.0.58/README.md +0 -357
- python_fastllm-0.0.58/python_fastllm.egg-info/PKG-INFO +0 -373
- {python_fastllm-0.0.58 → python_fastllm-0.0.60}/fastllm/codex.py +0 -0
- {python_fastllm-0.0.58 → python_fastllm-0.0.60}/fastllm/model_prices.json +0 -0
- {python_fastllm-0.0.58 → python_fastllm-0.0.60}/pyproject.toml +0 -0
- {python_fastllm-0.0.58 → python_fastllm-0.0.60}/python_fastllm.egg-info/SOURCES.txt +0 -0
- {python_fastllm-0.0.58 → python_fastllm-0.0.60}/python_fastllm.egg-info/dependency_links.txt +0 -0
- {python_fastllm-0.0.58 → python_fastllm-0.0.60}/python_fastllm.egg-info/entry_points.txt +0 -0
- {python_fastllm-0.0.58 → python_fastllm-0.0.60}/python_fastllm.egg-info/requires.txt +0 -0
- {python_fastllm-0.0.58 → python_fastllm-0.0.60}/python_fastllm.egg-info/top_level.txt +0 -0
- {python_fastllm-0.0.58 → python_fastllm-0.0.60}/setup.cfg +0 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-fastllm
|
|
3
|
+
Version: 0.0.60
|
|
4
|
+
Author-email: Kerem Turgutlu <keremturgutlu@gmail.com>
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Project-URL: Repository, https://github.com/AnswerDotAI/fastllm
|
|
7
|
+
Keywords: nbdev
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: fastcore>=2.1.18
|
|
13
|
+
Requires-Dist: aidialog>=0.0.27
|
|
14
|
+
Requires-Dist: fasttransport>=0.0.2
|
|
15
|
+
Requires-Dist: pillow
|
|
16
|
+
|
|
17
|
+
# fastllm
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->
|
|
21
|
+
|
|
22
|
+
`fastllm` provides a common async interface for Anthropic, OpenAI Responses and Chat, Gemini, and OpenAI-compatible providers such as Kimi. Define messages and tools in a shared format and call `acomplete`. Select the model and, where needed, its provider without rewriting the conversation or tool loop. Supported features vary by provider.
|
|
23
|
+
|
|
24
|
+
`AsyncResponses` provides a streaming Responses API interface over the same providers. It translates input items and events and keeps a canonical `ResponseState` between turns. For transports without provider continuation ids, it replays that state automatically.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
Run `pip install python-fastllm` or clone from github and install locally.
|
|
29
|
+
|
|
30
|
+
## Setup
|
|
31
|
+
|
|
32
|
+
``` python
|
|
33
|
+
from aidialog.msg_parts import Msg, Part, Text, Thinking, ToolUse, InputImage, mk_tool_res_msg, Completion
|
|
34
|
+
from fastllm.acomplete import acomplete
|
|
35
|
+
from fastllm.types import sample_img_url, sample_doms
|
|
36
|
+
import asyncio, json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
``` python
|
|
40
|
+
def user(text): return Msg(role='user', content=[Text(text)])
|
|
41
|
+
|
|
42
|
+
async def stream(msgs, model, max_think=0, **kw):
|
|
43
|
+
"Print streamed parts and return the final Completion; limit printed thinking deltas with max_think."
|
|
44
|
+
cnt = 0
|
|
45
|
+
async for o in await acomplete(msgs, model, stream=True, **kw):
|
|
46
|
+
if not isinstance(o, Part): continue
|
|
47
|
+
if isinstance(o, Thinking):
|
|
48
|
+
cnt += 1
|
|
49
|
+
if cnt > max_think: continue
|
|
50
|
+
print(o.formatted, end='', flush=True)
|
|
51
|
+
print()
|
|
52
|
+
return o
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
``` python
|
|
56
|
+
mtok = 1024
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Chat across providers
|
|
60
|
+
|
|
61
|
+
Use the same `acomplete` interface with Claude, GPT, Gemini, or Kimi. Set `model` and any provider-specific routing arguments:
|
|
62
|
+
|
|
63
|
+
``` python
|
|
64
|
+
models = [
|
|
65
|
+
('claude-sonnet-5', {}),
|
|
66
|
+
('gpt-4o-mini', {}),
|
|
67
|
+
('models/gemini-3-flash-preview', {}),
|
|
68
|
+
('accounts/fireworks/models/kimi-k3', dict(vendor_name='fireworks_ai'))
|
|
69
|
+
]
|
|
70
|
+
for name, kw in models:
|
|
71
|
+
r = await acomplete([user("Translate 'hello' into French. Return only the translation.")],
|
|
72
|
+
model=name, max_tokens=mtok, **kw)
|
|
73
|
+
text = ''.join(p.text for p in r.message.content if isinstance(p, Text))
|
|
74
|
+
print(f"{name}: {text.strip()}")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
claude-sonnet-5: Bonjour
|
|
78
|
+
gpt-4o-mini: Bonjour
|
|
79
|
+
models/gemini-3-flash-preview: Bonjour
|
|
80
|
+
accounts/fireworks/models/kimi-k3: Bonjour
|
|
81
|
+
|
|
82
|
+
## System prompts
|
|
83
|
+
|
|
84
|
+
Pass `system` to supply a system prompt. `fastllm` translates it to the provider’s field: Anthropic `system`, OpenAI Responses `instructions`, or Gemini `system_instruction`.
|
|
85
|
+
|
|
86
|
+
``` python
|
|
87
|
+
sys = "You are a pirate chef. Always respond in pirate speak and mention food. Use one short sentence."
|
|
88
|
+
|
|
89
|
+
print("Claude: ", end='')
|
|
90
|
+
r = await stream([user("What should I do today?")], model='claude-sonnet-5', system=sys, max_tokens=mtok)
|
|
91
|
+
|
|
92
|
+
print("Gemini: ", end='')
|
|
93
|
+
r = await stream([user("What should I do today?")], model='models/gemini-3-flash-preview', system=sys, max_tokens=mtok)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Claude: Arrr, chart a course fer the galley and cook up a hearty stew, matey!
|
|
97
|
+
Gemini: Sharpen yer cutlass and feast on spicy shark stew, ye scurvy dog!
|
|
98
|
+
|
|
99
|
+
## Tool calling
|
|
100
|
+
|
|
101
|
+
Define tools in the shared schema. `fastllm` translates it to the provider’s tool format. This example handles a tool request, adds the result to the history, and continues the conversation:
|
|
102
|
+
|
|
103
|
+
``` python
|
|
104
|
+
tools = [{"type": "function", "function": {
|
|
105
|
+
"name": "get_weather",
|
|
106
|
+
"description": "Get current weather for a city",
|
|
107
|
+
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
|
|
108
|
+
}}]
|
|
109
|
+
|
|
110
|
+
msgs = [user("What's the weather in Paris?")]
|
|
111
|
+
r1 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
|
|
112
|
+
print("Tool calls:", r1.tool_calls)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- ⏳ `get_weather(city="Paris")` ⏳
|
|
116
|
+
|
|
117
|
+
Tool calls: [ToolUse(raw={'caller': {'type': 'direct'}}, cache_control=None, id='toolu_01XacBpJHnsjtPWJWr55nA2m', name='get_weather', arguments={'city': 'Paris'}, server=False, text=None)]
|
|
118
|
+
|
|
119
|
+
``` python
|
|
120
|
+
msgs += [r1.message, mk_tool_res_msg(r1.tool_calls, ['22°C, sunny with light clouds']),
|
|
121
|
+
user("Should I bring a jacket? Answer in one short sentence.")]
|
|
122
|
+
r2 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
No, a jacket isn't necessary — it's a mild, sunny 22°C day.
|
|
126
|
+
|
|
127
|
+
## Tool Choice
|
|
128
|
+
|
|
129
|
+
Control whether the model must use tools, can’t use tools, or decides on its own:
|
|
130
|
+
|
|
131
|
+
``` python
|
|
132
|
+
r = await acomplete([user("Hello there!")], model='claude-sonnet-5',
|
|
133
|
+
tools=tools, tool_choice='required', max_tokens=mtok)
|
|
134
|
+
print("Required:", [tc.name for tc in r.tool_calls])
|
|
135
|
+
|
|
136
|
+
r = await acomplete([user("What's the weather?")], model='claude-sonnet-5',
|
|
137
|
+
tools=tools, tool_choice='none', max_tokens=mtok)
|
|
138
|
+
print("None:", r.tool_calls)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Required: ['get_weather']
|
|
142
|
+
None: []
|
|
143
|
+
|
|
144
|
+
## Thinking and extended reasoning
|
|
145
|
+
|
|
146
|
+
Set `reasoning_effort` to `low`, `medium`, or `high` for supported models. The adapters translate these values to provider-specific settings. Reasoning output uses `Thinking` parts; this example prints a short excerpt alongside the answer.
|
|
147
|
+
|
|
148
|
+
``` python
|
|
149
|
+
print("Claude: ", end='')
|
|
150
|
+
r = await stream([user("What is 127 × 849? Return just the number.")], model='claude-sonnet-4-6',
|
|
151
|
+
reasoning_effort='low', max_tokens=8192)
|
|
152
|
+
for p in r.message.content:
|
|
153
|
+
if isinstance(p, Thinking): print(f"Thinking excerpt: {p.text[:150]}...")
|
|
154
|
+
|
|
155
|
+
print("Kimi: ", end='')
|
|
156
|
+
r = await stream([user("What is 127 × 849? Return just the number.")],
|
|
157
|
+
model='accounts/fireworks/models/kimi-k3', vendor_name='fireworks_ai',
|
|
158
|
+
reasoning_effort='low', max_tokens=8192)
|
|
159
|
+
for p in r.message.content:
|
|
160
|
+
if isinstance(p, Thinking): print(f"Thinking excerpt: {p.text[:150]}...")
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Claude: 107823
|
|
164
|
+
Thinking excerpt: 127 × 849 = 127 × 800 + 127 × 49 = 101600 + 6223 = 107823...
|
|
165
|
+
Kimi: 107823
|
|
166
|
+
Thinking excerpt: 127 × 849 = 107823. Let me verify: 127*800=101600; 127*49=6223; total 107823. Return just number....
|
|
167
|
+
|
|
168
|
+
## Web Search (Server Tools)
|
|
169
|
+
|
|
170
|
+
OpenAI’s Responses API supports server-side web search. Server tool calls are normalized alongside regular tool calls:
|
|
171
|
+
|
|
172
|
+
``` python
|
|
173
|
+
ws_tools = [{"type": "web_search_preview"}]
|
|
174
|
+
r = await acomplete([user("What is the latest Python release? Reply with the version number only.")],
|
|
175
|
+
model='gpt-4o-mini', tools=ws_tools, max_tokens=512)
|
|
176
|
+
print("Server tools used:", [tc.name for tc in r.tool_calls if tc.server])
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Server tools used: ['web_search']
|
|
180
|
+
|
|
181
|
+
## Caching with Anthropic
|
|
182
|
+
|
|
183
|
+
Set a part’s `cache_control` to request prompt caching. Later requests can reuse eligible cached content. The example prints usage for two calls with the same system prompt:
|
|
184
|
+
|
|
185
|
+
``` python
|
|
186
|
+
long_ctx = "You are an expert on the solar system. " * 200
|
|
187
|
+
system = Text(long_ctx, cache_control={'type': 'ephemeral'})
|
|
188
|
+
|
|
189
|
+
r1 = await acomplete([user("What is Jupiter's mass? Answer in one sentence.")],
|
|
190
|
+
model='claude-sonnet-5', system=system, max_tokens=mtok)
|
|
191
|
+
print("First call: cache creation tokens", r1.usage.cache_creation_tokens)
|
|
192
|
+
|
|
193
|
+
r2 = await acomplete([user("What is Saturn's mass? Answer in one sentence.")],
|
|
194
|
+
model='claude-sonnet-5', system=system, max_tokens=mtok)
|
|
195
|
+
print("Second call: cache read tokens", r2.usage.cached_tokens)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
First call: cache creation tokens 2204
|
|
199
|
+
Second call: cache read tokens 2204
|
|
200
|
+
|
|
201
|
+
## Media inputs
|
|
202
|
+
|
|
203
|
+
Use `InputImage` with a model that supports images. The example sends the same image message to three providers:
|
|
204
|
+
|
|
205
|
+
``` python
|
|
206
|
+
img_msg = Msg(role='user', content=[
|
|
207
|
+
InputImage(sample_img_url),
|
|
208
|
+
Text("List three visible objects, using nouns only.")
|
|
209
|
+
])
|
|
210
|
+
|
|
211
|
+
for name, kw in [('claude-sonnet-5', {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {})]:
|
|
212
|
+
print(f"{name}: ", end='')
|
|
213
|
+
r = await stream([img_msg], model=name, max_tokens=mtok, **kw)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
claude-sonnet-5: Mountains, lake, trees
|
|
217
|
+
gpt-4o-mini: 1. Mountains
|
|
218
|
+
2. Trees
|
|
219
|
+
3. Lake
|
|
220
|
+
models/gemini-3-flash-preview: planks, lake, mountains
|
|
221
|
+
|
|
222
|
+
The media adapters support the following part types:
|
|
223
|
+
|
|
224
|
+
| Media part | Anthropic | OpenAI Responses | OpenAI Chat | Gemini |
|
|
225
|
+
|--------------|-----------|------------------|-------------|--------|
|
|
226
|
+
| `InputImage` | Yes | Yes | Yes | Yes |
|
|
227
|
+
| `InputAudio` | No | No | Yes | Yes |
|
|
228
|
+
| `InputVideo` | No | No | No | Yes |
|
|
229
|
+
| `InputFile` | Yes | Yes | Yes | Yes |
|
|
230
|
+
|
|
231
|
+
Media parts accept a URL or base64 data URL as their `text`. An unsupported combination raises `ValueError`.
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# fastllm
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->
|
|
5
|
+
|
|
6
|
+
`fastllm` provides a common async interface for Anthropic, OpenAI Responses and Chat, Gemini, and OpenAI-compatible providers such as Kimi. Define messages and tools in a shared format and call `acomplete`. Select the model and, where needed, its provider without rewriting the conversation or tool loop. Supported features vary by provider.
|
|
7
|
+
|
|
8
|
+
`AsyncResponses` provides a streaming Responses API interface over the same providers. It translates input items and events and keeps a canonical `ResponseState` between turns. For transports without provider continuation ids, it replays that state automatically.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
Run `pip install python-fastllm` or clone from github and install locally.
|
|
13
|
+
|
|
14
|
+
## Setup
|
|
15
|
+
|
|
16
|
+
``` python
|
|
17
|
+
from aidialog.msg_parts import Msg, Part, Text, Thinking, ToolUse, InputImage, mk_tool_res_msg, Completion
|
|
18
|
+
from fastllm.acomplete import acomplete
|
|
19
|
+
from fastllm.types import sample_img_url, sample_doms
|
|
20
|
+
import asyncio, json
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
``` python
|
|
24
|
+
def user(text): return Msg(role='user', content=[Text(text)])
|
|
25
|
+
|
|
26
|
+
async def stream(msgs, model, max_think=0, **kw):
|
|
27
|
+
"Print streamed parts and return the final Completion; limit printed thinking deltas with max_think."
|
|
28
|
+
cnt = 0
|
|
29
|
+
async for o in await acomplete(msgs, model, stream=True, **kw):
|
|
30
|
+
if not isinstance(o, Part): continue
|
|
31
|
+
if isinstance(o, Thinking):
|
|
32
|
+
cnt += 1
|
|
33
|
+
if cnt > max_think: continue
|
|
34
|
+
print(o.formatted, end='', flush=True)
|
|
35
|
+
print()
|
|
36
|
+
return o
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
``` python
|
|
40
|
+
mtok = 1024
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Chat across providers
|
|
44
|
+
|
|
45
|
+
Use the same `acomplete` interface with Claude, GPT, Gemini, or Kimi. Set `model` and any provider-specific routing arguments:
|
|
46
|
+
|
|
47
|
+
``` python
|
|
48
|
+
models = [
|
|
49
|
+
('claude-sonnet-5', {}),
|
|
50
|
+
('gpt-4o-mini', {}),
|
|
51
|
+
('models/gemini-3-flash-preview', {}),
|
|
52
|
+
('accounts/fireworks/models/kimi-k3', dict(vendor_name='fireworks_ai'))
|
|
53
|
+
]
|
|
54
|
+
for name, kw in models:
|
|
55
|
+
r = await acomplete([user("Translate 'hello' into French. Return only the translation.")],
|
|
56
|
+
model=name, max_tokens=mtok, **kw)
|
|
57
|
+
text = ''.join(p.text for p in r.message.content if isinstance(p, Text))
|
|
58
|
+
print(f"{name}: {text.strip()}")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
claude-sonnet-5: Bonjour
|
|
62
|
+
gpt-4o-mini: Bonjour
|
|
63
|
+
models/gemini-3-flash-preview: Bonjour
|
|
64
|
+
accounts/fireworks/models/kimi-k3: Bonjour
|
|
65
|
+
|
|
66
|
+
## System prompts
|
|
67
|
+
|
|
68
|
+
Pass `system` to supply a system prompt. `fastllm` translates it to the provider’s field: Anthropic `system`, OpenAI Responses `instructions`, or Gemini `system_instruction`.
|
|
69
|
+
|
|
70
|
+
``` python
|
|
71
|
+
sys = "You are a pirate chef. Always respond in pirate speak and mention food. Use one short sentence."
|
|
72
|
+
|
|
73
|
+
print("Claude: ", end='')
|
|
74
|
+
r = await stream([user("What should I do today?")], model='claude-sonnet-5', system=sys, max_tokens=mtok)
|
|
75
|
+
|
|
76
|
+
print("Gemini: ", end='')
|
|
77
|
+
r = await stream([user("What should I do today?")], model='models/gemini-3-flash-preview', system=sys, max_tokens=mtok)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Claude: Arrr, chart a course fer the galley and cook up a hearty stew, matey!
|
|
81
|
+
Gemini: Sharpen yer cutlass and feast on spicy shark stew, ye scurvy dog!
|
|
82
|
+
|
|
83
|
+
## Tool calling
|
|
84
|
+
|
|
85
|
+
Define tools in the shared schema. `fastllm` translates it to the provider’s tool format. This example handles a tool request, adds the result to the history, and continues the conversation:
|
|
86
|
+
|
|
87
|
+
``` python
|
|
88
|
+
tools = [{"type": "function", "function": {
|
|
89
|
+
"name": "get_weather",
|
|
90
|
+
"description": "Get current weather for a city",
|
|
91
|
+
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
|
|
92
|
+
}}]
|
|
93
|
+
|
|
94
|
+
msgs = [user("What's the weather in Paris?")]
|
|
95
|
+
r1 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
|
|
96
|
+
print("Tool calls:", r1.tool_calls)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- ⏳ `get_weather(city="Paris")` ⏳
|
|
100
|
+
|
|
101
|
+
Tool calls: [ToolUse(raw={'caller': {'type': 'direct'}}, cache_control=None, id='toolu_01XacBpJHnsjtPWJWr55nA2m', name='get_weather', arguments={'city': 'Paris'}, server=False, text=None)]
|
|
102
|
+
|
|
103
|
+
``` python
|
|
104
|
+
msgs += [r1.message, mk_tool_res_msg(r1.tool_calls, ['22°C, sunny with light clouds']),
|
|
105
|
+
user("Should I bring a jacket? Answer in one short sentence.")]
|
|
106
|
+
r2 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
No, a jacket isn't necessary — it's a mild, sunny 22°C day.
|
|
110
|
+
|
|
111
|
+
## Tool Choice
|
|
112
|
+
|
|
113
|
+
Control whether the model must use tools, can’t use tools, or decides on its own:
|
|
114
|
+
|
|
115
|
+
``` python
|
|
116
|
+
r = await acomplete([user("Hello there!")], model='claude-sonnet-5',
|
|
117
|
+
tools=tools, tool_choice='required', max_tokens=mtok)
|
|
118
|
+
print("Required:", [tc.name for tc in r.tool_calls])
|
|
119
|
+
|
|
120
|
+
r = await acomplete([user("What's the weather?")], model='claude-sonnet-5',
|
|
121
|
+
tools=tools, tool_choice='none', max_tokens=mtok)
|
|
122
|
+
print("None:", r.tool_calls)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Required: ['get_weather']
|
|
126
|
+
None: []
|
|
127
|
+
|
|
128
|
+
## Thinking and extended reasoning
|
|
129
|
+
|
|
130
|
+
Set `reasoning_effort` to `low`, `medium`, or `high` for supported models. The adapters translate these values to provider-specific settings. Reasoning output uses `Thinking` parts; this example prints a short excerpt alongside the answer.
|
|
131
|
+
|
|
132
|
+
``` python
|
|
133
|
+
print("Claude: ", end='')
|
|
134
|
+
r = await stream([user("What is 127 × 849? Return just the number.")], model='claude-sonnet-4-6',
|
|
135
|
+
reasoning_effort='low', max_tokens=8192)
|
|
136
|
+
for p in r.message.content:
|
|
137
|
+
if isinstance(p, Thinking): print(f"Thinking excerpt: {p.text[:150]}...")
|
|
138
|
+
|
|
139
|
+
print("Kimi: ", end='')
|
|
140
|
+
r = await stream([user("What is 127 × 849? Return just the number.")],
|
|
141
|
+
model='accounts/fireworks/models/kimi-k3', vendor_name='fireworks_ai',
|
|
142
|
+
reasoning_effort='low', max_tokens=8192)
|
|
143
|
+
for p in r.message.content:
|
|
144
|
+
if isinstance(p, Thinking): print(f"Thinking excerpt: {p.text[:150]}...")
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Claude: 107823
|
|
148
|
+
Thinking excerpt: 127 × 849 = 127 × 800 + 127 × 49 = 101600 + 6223 = 107823...
|
|
149
|
+
Kimi: 107823
|
|
150
|
+
Thinking excerpt: 127 × 849 = 107823. Let me verify: 127*800=101600; 127*49=6223; total 107823. Return just number....
|
|
151
|
+
|
|
152
|
+
## Web Search (Server Tools)
|
|
153
|
+
|
|
154
|
+
OpenAI’s Responses API supports server-side web search. Server tool calls are normalized alongside regular tool calls:
|
|
155
|
+
|
|
156
|
+
``` python
|
|
157
|
+
ws_tools = [{"type": "web_search_preview"}]
|
|
158
|
+
r = await acomplete([user("What is the latest Python release? Reply with the version number only.")],
|
|
159
|
+
model='gpt-4o-mini', tools=ws_tools, max_tokens=512)
|
|
160
|
+
print("Server tools used:", [tc.name for tc in r.tool_calls if tc.server])
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Server tools used: ['web_search']
|
|
164
|
+
|
|
165
|
+
## Caching with Anthropic
|
|
166
|
+
|
|
167
|
+
Set a part’s `cache_control` to request prompt caching. Later requests can reuse eligible cached content. The example prints usage for two calls with the same system prompt:
|
|
168
|
+
|
|
169
|
+
``` python
|
|
170
|
+
long_ctx = "You are an expert on the solar system. " * 200
|
|
171
|
+
system = Text(long_ctx, cache_control={'type': 'ephemeral'})
|
|
172
|
+
|
|
173
|
+
r1 = await acomplete([user("What is Jupiter's mass? Answer in one sentence.")],
|
|
174
|
+
model='claude-sonnet-5', system=system, max_tokens=mtok)
|
|
175
|
+
print("First call: cache creation tokens", r1.usage.cache_creation_tokens)
|
|
176
|
+
|
|
177
|
+
r2 = await acomplete([user("What is Saturn's mass? Answer in one sentence.")],
|
|
178
|
+
model='claude-sonnet-5', system=system, max_tokens=mtok)
|
|
179
|
+
print("Second call: cache read tokens", r2.usage.cached_tokens)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
First call: cache creation tokens 2204
|
|
183
|
+
Second call: cache read tokens 2204
|
|
184
|
+
|
|
185
|
+
## Media inputs
|
|
186
|
+
|
|
187
|
+
Use `InputImage` with a model that supports images. The example sends the same image message to three providers:
|
|
188
|
+
|
|
189
|
+
``` python
|
|
190
|
+
img_msg = Msg(role='user', content=[
|
|
191
|
+
InputImage(sample_img_url),
|
|
192
|
+
Text("List three visible objects, using nouns only.")
|
|
193
|
+
])
|
|
194
|
+
|
|
195
|
+
for name, kw in [('claude-sonnet-5', {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {})]:
|
|
196
|
+
print(f"{name}: ", end='')
|
|
197
|
+
r = await stream([img_msg], model=name, max_tokens=mtok, **kw)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
claude-sonnet-5: Mountains, lake, trees
|
|
201
|
+
gpt-4o-mini: 1. Mountains
|
|
202
|
+
2. Trees
|
|
203
|
+
3. Lake
|
|
204
|
+
models/gemini-3-flash-preview: planks, lake, mountains
|
|
205
|
+
|
|
206
|
+
The media adapters support the following part types:
|
|
207
|
+
|
|
208
|
+
| Media part | Anthropic | OpenAI Responses | OpenAI Chat | Gemini |
|
|
209
|
+
|--------------|-----------|------------------|-------------|--------|
|
|
210
|
+
| `InputImage` | Yes | Yes | Yes | Yes |
|
|
211
|
+
| `InputAudio` | No | No | Yes | Yes |
|
|
212
|
+
| `InputVideo` | No | No | No | Yes |
|
|
213
|
+
| `InputFile` | Yes | Yes | Yes | Yes |
|
|
214
|
+
|
|
215
|
+
Media parts accept a URL or base64 data URL as their `text`. An unsupported combination raises `ValueError`.
|
|
@@ -22,6 +22,8 @@ d = { 'settings': { 'branch': 'main',
|
|
|
22
22
|
'fastllm.anthropic': { 'fastllm.anthropic._add_cache_control': ('anthropic.html#_add_cache_control', 'fastllm/anthropic.py'),
|
|
23
23
|
'fastllm.anthropic._ant_cc': ('anthropic.html#_ant_cc', 'fastllm/anthropic.py'),
|
|
24
24
|
'fastllm.anthropic._ant_part_type': ('anthropic.html#_ant_part_type', 'fastllm/anthropic.py'),
|
|
25
|
+
'fastllm.anthropic._clear_cache_control': ( 'anthropic.html#_clear_cache_control',
|
|
26
|
+
'fastllm/anthropic.py'),
|
|
25
27
|
'fastllm.anthropic._sanid': ('anthropic.html#_sanid', 'fastllm/anthropic.py'),
|
|
26
28
|
'fastllm.anthropic._url_cite': ('anthropic.html#_url_cite', 'fastllm/anthropic.py'),
|
|
27
29
|
'fastllm.anthropic.acollect_stream': ('anthropic.html#acollect_stream', 'fastllm/anthropic.py'),
|
|
@@ -140,7 +142,8 @@ d = { 'settings': { 'branch': 'main',
|
|
|
140
142
|
'fastllm.codex.main': ('codex.html#main', 'fastllm/codex.py'),
|
|
141
143
|
'fastllm.codex.mk_jwt': ('codex.html#mk_jwt', 'fastllm/codex.py'),
|
|
142
144
|
'fastllm.codex.token_expired': ('codex.html#token_expired', 'fastllm/codex.py')},
|
|
143
|
-
'fastllm.gemini': { 'fastllm.gemini.
|
|
145
|
+
'fastllm.gemini': { 'fastllm.gemini._fn_call': ('gemini.html#_fn_call', 'fastllm/gemini.py'),
|
|
146
|
+
'fastllm.gemini._fn_calls': ('gemini.html#_fn_calls', 'fastllm/gemini.py'),
|
|
144
147
|
'fastllm.gemini._gem_filter_sch': ('gemini.html#_gem_filter_sch', 'fastllm/gemini.py'),
|
|
145
148
|
'fastllm.gemini._gem_part_type': ('gemini.html#_gem_part_type', 'fastllm/gemini.py'),
|
|
146
149
|
'fastllm.gemini._url_cite': ('gemini.html#_url_cite', 'fastllm/gemini.py'),
|
|
@@ -315,7 +318,9 @@ d = { 'settings': { 'branch': 'main',
|
|
|
315
318
|
'fastllm.responses._msg_parts': ('responses.html#_msg_parts', 'fastllm/responses.py'),
|
|
316
319
|
'fastllm.responses._new_id': ('responses.html#_new_id', 'fastllm/responses.py'),
|
|
317
320
|
'fastllm.responses._response_base': ('responses.html#_response_base', 'fastllm/responses.py'),
|
|
321
|
+
'fastllm.responses._server_call': ('responses.html#_server_call', 'fastllm/responses.py'),
|
|
318
322
|
'fastllm.responses._text_content': ('responses.html#_text_content', 'fastllm/responses.py'),
|
|
323
|
+
'fastllm.responses._visible_parts': ('responses.html#_visible_parts', 'fastllm/responses.py'),
|
|
319
324
|
'fastllm.responses.normalize_call_ids': ('responses.html#normalize_call_ids', 'fastllm/responses.py'),
|
|
320
325
|
'fastllm.responses.pending_response': ('responses.html#pending_response', 'fastllm/responses.py'),
|
|
321
326
|
'fastllm.responses.response_event': ('responses.html#response_event', 'fastllm/responses.py'),
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Translate Anthropic Messages API requests, responses, and streams to and from fastllm's shared message types, with tool replay, media, and prompt caching"""
|
|
2
|
+
|
|
1
3
|
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/04_anthropic.ipynb.
|
|
2
4
|
|
|
3
5
|
# %% auto #0
|
|
@@ -16,7 +18,7 @@ from fasttransport.errors import api_error_from_event
|
|
|
16
18
|
|
|
17
19
|
from .types import *
|
|
18
20
|
from aidialog.msg_parts import (MdStr, Part, PartType, mk_part, Msg, Completion, Text, Thinking, ToolUse, ToolResult, tool_text,
|
|
19
|
-
|
|
21
|
+
InputImage, InputAudio, InputVideo, InputFile, data_url)
|
|
20
22
|
from .streaming import *
|
|
21
23
|
from .streaming import mk_acollect_stream
|
|
22
24
|
|
|
@@ -44,8 +46,8 @@ def norm_usage(resp):
|
|
|
44
46
|
pt = int(usg.get("input_tokens", 0) or 0) + cached + cache_creation
|
|
45
47
|
ct = int(usg.get("output_tokens", 0) or 0)
|
|
46
48
|
return Usage(prompt_tokens=pt, completion_tokens=ct, total_tokens=pt + ct,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
cached_tokens=cached, cache_creation_tokens=cache_creation, reasoning_tokens=0, raw=usg,
|
|
50
|
+
search_queries=int(nested_idx(usg, 'server_tool_use', 'web_search_requests') or 0))
|
|
49
51
|
|
|
50
52
|
def finalize_usage(usg, parts):
|
|
51
53
|
"Split reasoning tokens out of `output_tokens`: the reported `thinking_tokens`, else an estimate from the thinking text."
|
|
@@ -110,7 +112,9 @@ def norm_tr_parts(content):
|
|
|
110
112
|
if typ=='text': parts.append(Text(b.get('text',''), raw=b))
|
|
111
113
|
elif typ=='image':
|
|
112
114
|
s = b['source']
|
|
113
|
-
|
|
115
|
+
if s['type']=='url': parts.append(InputImage(s['url'], raw=b))
|
|
116
|
+
elif s['type']=='base64': parts.append(InputImage(f"data:{s['media_type']};base64,{s['data']}", mime=s['media_type'], raw=b))
|
|
117
|
+
else: raise ValueError(f"Unsupported Anthropic image source type: {s['type']!r}")
|
|
114
118
|
else: parts.append(mk_part(**b))
|
|
115
119
|
return parts
|
|
116
120
|
|
|
@@ -238,13 +242,11 @@ def denorm_reasoning(v):
|
|
|
238
242
|
# %% ../nbs/04_anthropic.ipynb #8fa9fbb8
|
|
239
243
|
def denorm_web_search(v):
|
|
240
244
|
"Map canonical web_search_options to Anthropic hosted web_search tool."
|
|
241
|
-
_max_uses =
|
|
245
|
+
_max_uses = dict(low=1, medium=5, high=10)
|
|
242
246
|
t = {"type": "web_search_20250305", "name": "web_search"}
|
|
243
247
|
if (typ := (v or {}).get("type")): t["type"] = typ
|
|
244
|
-
if (s := (v or {}).get("search_context_size")):
|
|
245
|
-
|
|
246
|
-
if (u := (v or {}).get("user_location", {}).get("approximate")):
|
|
247
|
-
t["user_location"] = {"type": "approximate", **u}
|
|
248
|
+
if (s := (v or {}).get("search_context_size")): t["max_uses"] = _max_uses.get(s, 5)
|
|
249
|
+
if (u := (v or {}).get("user_location", {}).get("approximate")): t["user_location"] = {"type": "approximate", **u}
|
|
248
250
|
return t
|
|
249
251
|
|
|
250
252
|
# %% ../nbs/04_anthropic.ipynb #6b485275
|
|
@@ -269,12 +271,12 @@ def denorm_user(m:Msg):
|
|
|
269
271
|
def denorm_image(p, max_sz=None):
|
|
270
272
|
if (b64:=data_url(p.text)):
|
|
271
273
|
data = resize_b64(b64[1], max_sz) if max_sz else b64[1]
|
|
272
|
-
return {"type": "image", "source":
|
|
274
|
+
return {"type": "image", "source": dict(type="base64", media_type=b64[0], data=data)}
|
|
273
275
|
return {"type": "image", "source": {"type": "url", "url": p.text}}
|
|
274
276
|
|
|
275
277
|
# %% ../nbs/04_anthropic.ipynb #fc6bbdfc
|
|
276
278
|
def denorm_file(p):
|
|
277
|
-
if (b64:=data_url(p.text)): return {"type": "document", "source":
|
|
279
|
+
if (b64:=data_url(p.text)): return {"type": "document", "source": dict(type="base64", media_type=b64[0], data=b64[1])}
|
|
278
280
|
return {"type": "document", "source": {"type": "url", "url": p.text}}
|
|
279
281
|
|
|
280
282
|
# %% ../nbs/04_anthropic.ipynb #37b63cc2
|
|
@@ -303,15 +305,21 @@ def _add_cache_control(
|
|
|
303
305
|
return msg
|
|
304
306
|
|
|
305
307
|
# %% ../nbs/04_anthropic.ipynb #6bc52ef5
|
|
308
|
+
def _clear_cache_control(obj):
|
|
309
|
+
"Clear canonical cache marks from a message or part."
|
|
310
|
+
if isinstance(obj, Msg):
|
|
311
|
+
for p in obj.content: _clear_cache_control(p)
|
|
312
|
+
elif isinstance(obj, Part): obj.cache_control = None
|
|
313
|
+
|
|
306
314
|
def apply_cache_idxs(
|
|
307
315
|
msgs, # Canonical Msgs to mark
|
|
308
316
|
cache_idxs=[-1], # Breakpoint idxs into `msgs` (tool-role msgs excluded); 0 is `sp` when given
|
|
309
317
|
sp=None, # System prompt: str, Msg, or Part
|
|
310
318
|
ttl=None # Cache TTL: '5m' (default) or '1h'
|
|
311
319
|
):
|
|
312
|
-
"
|
|
313
|
-
for m in msgs:
|
|
314
|
-
|
|
320
|
+
"Replace cache breakpoints in `msgs` and `sp`, then return the system prompt."
|
|
321
|
+
for m in msgs: _clear_cache_control(m)
|
|
322
|
+
_clear_cache_control(sp)
|
|
315
323
|
if sp:
|
|
316
324
|
if 0 in cache_idxs:
|
|
317
325
|
if isinstance(sp, str): sp = Text(sp)
|
|
@@ -335,10 +343,8 @@ def mk_payload(msgs, model, **kwargs):
|
|
|
335
343
|
if tools:=kwargs.get('tools'): payload['tools'] = denorm_tool_schs(tools)
|
|
336
344
|
if tchc:=kwargs.get('tool_choice'): payload['tool_choice'] = denorm_tool_choice(tchc)
|
|
337
345
|
if thk:=kwargs.get('reasoning_effort'): payload.update(denorm_reasoning(thk))
|
|
338
|
-
if (wopts:=kwargs.get('web_search_options')) is not None:
|
|
339
|
-
|
|
340
|
-
if (temp:=kwargs.get('temperature')) is not None:
|
|
341
|
-
payload['temperature'] = temp
|
|
346
|
+
if (wopts:=kwargs.get('web_search_options')) is not None: payload.setdefault('tools', []).append(denorm_web_search(wopts))
|
|
347
|
+
if (temp:=kwargs.get('temperature')) is not None: payload['temperature'] = temp
|
|
342
348
|
return payload
|
|
343
349
|
|
|
344
350
|
# %% ../nbs/04_anthropic.ipynb #60d52c1f
|
|
@@ -367,5 +373,4 @@ def cost(usage, m):
|
|
|
367
373
|
# %% ../nbs/04_anthropic.ipynb #f7c0b989
|
|
368
374
|
api_registry.register('anthropic', norm_tool_calls=norm_tool_calls, norm_parts=norm_parts, norm_finish=norm_finish, norm_usage=norm_usage,
|
|
369
375
|
finalize_usage=finalize_usage, raw_msg=raw_msg, collate_raw=collate_raw, acollect_stream=acollect_stream, mk_payload=mk_payload,
|
|
370
|
-
cost=cost, get_hdrs=get_hdrs,
|
|
371
|
-
endpoint='/v1/messages')
|
|
376
|
+
cost=cost, get_hdrs=get_hdrs, endpoint='/v1/messages')
|