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.
Files changed (26) hide show
  1. python_fastllm-0.0.60/PKG-INFO +231 -0
  2. python_fastllm-0.0.60/README.md +215 -0
  3. {python_fastllm-0.0.58 → python_fastllm-0.0.60}/fastllm/__init__.py +1 -1
  4. {python_fastllm-0.0.58 → python_fastllm-0.0.60}/fastllm/_modidx.py +6 -1
  5. {python_fastllm-0.0.58 → python_fastllm-0.0.60}/fastllm/acomplete.py +1 -1
  6. {python_fastllm-0.0.58 → python_fastllm-0.0.60}/fastllm/anthropic.py +25 -20
  7. {python_fastllm-0.0.58 → python_fastllm-0.0.60}/fastllm/chat.py +13 -9
  8. {python_fastllm-0.0.58 → python_fastllm-0.0.60}/fastllm/gemini.py +31 -34
  9. {python_fastllm-0.0.58 → python_fastllm-0.0.60}/fastllm/openai_chat.py +2 -0
  10. {python_fastllm-0.0.58 → python_fastllm-0.0.60}/fastllm/openai_responses.py +7 -7
  11. {python_fastllm-0.0.58 → python_fastllm-0.0.60}/fastllm/responses.py +19 -7
  12. {python_fastllm-0.0.58 → python_fastllm-0.0.60}/fastllm/streaming.py +3 -4
  13. {python_fastllm-0.0.58 → python_fastllm-0.0.60}/fastllm/types.py +9 -4
  14. python_fastllm-0.0.60/python_fastllm.egg-info/PKG-INFO +231 -0
  15. python_fastllm-0.0.58/PKG-INFO +0 -373
  16. python_fastllm-0.0.58/README.md +0 -357
  17. python_fastllm-0.0.58/python_fastllm.egg-info/PKG-INFO +0 -373
  18. {python_fastllm-0.0.58 → python_fastllm-0.0.60}/fastllm/codex.py +0 -0
  19. {python_fastllm-0.0.58 → python_fastllm-0.0.60}/fastllm/model_prices.json +0 -0
  20. {python_fastllm-0.0.58 → python_fastllm-0.0.60}/pyproject.toml +0 -0
  21. {python_fastllm-0.0.58 → python_fastllm-0.0.60}/python_fastllm.egg-info/SOURCES.txt +0 -0
  22. {python_fastllm-0.0.58 → python_fastllm-0.0.60}/python_fastllm.egg-info/dependency_links.txt +0 -0
  23. {python_fastllm-0.0.58 → python_fastllm-0.0.60}/python_fastllm.egg-info/entry_points.txt +0 -0
  24. {python_fastllm-0.0.58 → python_fastllm-0.0.60}/python_fastllm.egg-info/requires.txt +0 -0
  25. {python_fastllm-0.0.58 → python_fastllm-0.0.60}/python_fastllm.egg-info/top_level.txt +0 -0
  26. {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`.
@@ -2,4 +2,4 @@
2
2
 
3
3
  - `fastllm.responses`: Provider-independent Responses resources and streaming over FastLLM"""
4
4
 
5
- __version__ = "0.0.58"
5
+ __version__ = "0.0.60"
@@ -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._fn_calls': ('gemini.html#_fn_calls', 'fastllm/gemini.py'),
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,4 +1,4 @@
1
- """A high level unified function make api calls"""
1
+ """A unified async API for model completions and streaming"""
2
2
 
3
3
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/06_acomplete.ipynb.
4
4
 
@@ -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
- InputImage, InputAudio, InputVideo, InputFile, data_url)
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
- cached_tokens=cached, cache_creation_tokens=cache_creation, reasoning_tokens=0, raw=usg,
48
- search_queries=int(nested_idx(usg, 'server_tool_use', 'web_search_requests') or 0))
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
- parts.append(InputImage(f"data:{s['media_type']};base64,{s['data']}", mime=s['media_type'], raw=b))
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 = {"low": 1, "medium": 5, "high": 10}
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
- t["max_uses"] = _max_uses.get(s, 5)
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": {"type": "base64", "media_type": b64[0], "data": data}}
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": {"type": "base64", "media_type": b64[0], "data": b64[1]}}
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
- "Strip all cache breakpoints from `msgs`, then mark `cache_idxs`; returns `sp`, marked if 0 is included"
313
- for m in msgs:
314
- for p in m.content: p.cache_control = None
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
- payload.setdefault('tools', []).append(denorm_web_search(wopts))
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')