python-fastllm 0.0.50__tar.gz → 0.0.51__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 (22) hide show
  1. {python_fastllm-0.0.50 → python_fastllm-0.0.51}/PKG-INFO +16 -72
  2. {python_fastllm-0.0.50 → python_fastllm-0.0.51}/README.md +14 -70
  3. {python_fastllm-0.0.50 → python_fastllm-0.0.51}/fastllm/__init__.py +1 -1
  4. {python_fastllm-0.0.50 → python_fastllm-0.0.51}/fastllm/_modidx.py +6 -1
  5. {python_fastllm-0.0.50 → python_fastllm-0.0.51}/fastllm/anthropic.py +17 -7
  6. {python_fastllm-0.0.50 → python_fastllm-0.0.51}/fastllm/responses.py +19 -0
  7. {python_fastllm-0.0.50 → python_fastllm-0.0.51}/fastllm/types.py +9 -8
  8. {python_fastllm-0.0.50 → python_fastllm-0.0.51}/pyproject.toml +1 -1
  9. {python_fastllm-0.0.50 → python_fastllm-0.0.51}/python_fastllm.egg-info/PKG-INFO +16 -72
  10. {python_fastllm-0.0.50 → python_fastllm-0.0.51}/python_fastllm.egg-info/requires.txt +1 -1
  11. {python_fastllm-0.0.50 → python_fastllm-0.0.51}/fastllm/acomplete.py +0 -0
  12. {python_fastllm-0.0.50 → python_fastllm-0.0.51}/fastllm/chat.py +0 -0
  13. {python_fastllm-0.0.50 → python_fastllm-0.0.51}/fastllm/codex.py +0 -0
  14. {python_fastllm-0.0.50 → python_fastllm-0.0.51}/fastllm/gemini.py +0 -0
  15. {python_fastllm-0.0.50 → python_fastllm-0.0.51}/fastllm/openai_chat.py +0 -0
  16. {python_fastllm-0.0.50 → python_fastllm-0.0.51}/fastllm/openai_responses.py +0 -0
  17. {python_fastllm-0.0.50 → python_fastllm-0.0.51}/fastllm/streaming.py +0 -0
  18. {python_fastllm-0.0.50 → python_fastllm-0.0.51}/python_fastllm.egg-info/SOURCES.txt +0 -0
  19. {python_fastllm-0.0.50 → python_fastllm-0.0.51}/python_fastllm.egg-info/dependency_links.txt +0 -0
  20. {python_fastllm-0.0.50 → python_fastllm-0.0.51}/python_fastllm.egg-info/entry_points.txt +0 -0
  21. {python_fastllm-0.0.50 → python_fastllm-0.0.51}/python_fastllm.egg-info/top_level.txt +0 -0
  22. {python_fastllm-0.0.50 → python_fastllm-0.0.51}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fastllm
3
- Version: 0.0.50
3
+ Version: 0.0.51
4
4
  Author-email: Kerem Turgutlu <keremturgutlu@gmail.com>
5
5
  License: Apache-2.0
6
6
  Project-URL: Repository, https://github.com/AnswerDotAI/fastllm
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
10
10
  Requires-Python: >=3.10
11
11
  Description-Content-Type: text/markdown
12
12
  Requires-Dist: fastcore>=2.1.18
13
- Requires-Dist: aidialog>=0.0.24
13
+ Requires-Dist: aidialog>=0.0.25
14
14
  Requires-Dist: fasttransport>=0.0.2
15
15
  Requires-Dist: pillow
16
16
 
@@ -28,7 +28,7 @@ Run `pip install python-fastllm` or clone from github and install locally.
28
28
  ``` python
29
29
  from aidialog.msg_parts import Msg, Part, Text, Thinking, ToolUse, InputImage, mk_tool_res_msg, Completion
30
30
  from fastllm.acomplete import acomplete
31
- from fastllm.types import sample_img_url
31
+ from fastllm.types import sample_img_url, sample_doms
32
32
  import asyncio, json
33
33
 
34
34
  # Helpers
@@ -57,7 +57,7 @@ The same `acomplete` call works with Claude, GPT, Gemini, and Kimi. Just change
57
57
 
58
58
  ``` python
59
59
  models = [
60
- ('claude-sonnet-4-20250514', {}),
60
+ ('claude-sonnet-5', {}),
61
61
  ('gpt-4o-mini', {}),
62
62
  ('models/gemini-3-flash-preview', {}),
63
63
  ('accounts/fireworks/models/kimi-k2p5', dict(vendor_name='fireworks_ai'))
@@ -81,46 +81,6 @@ for name, kw in models:
81
81
 
82
82
  The response should be simple and direct.
83
83
 
84
- ## Multi-Turn — Swap Providers Mid-Conversation
85
-
86
- Build a conversation with one provider, then seamlessly continue it with another. `fastllm` translates between every provider’s native format automatically:
87
-
88
- ``` python
89
- # Turn 1: Claude starts the conversation
90
- msgs = [user("Name the 3 largest planets in our solar system. One sentence.")]
91
- print("Claude: ", end='')
92
- r1 = await stream(msgs, model='claude-sonnet-4-20250514', max_tokens=mtok)
93
- ```
94
-
95
- Claude: The three largest planets in our solar system are Jupiter, Saturn, and Neptune.
96
-
97
- ``` python
98
- # Turn 2: Switch to GPT — just change the model string
99
- msgs += [r1.message, user("Which one has the most moons?")]
100
- print("GPT: ", end='')
101
- r2 = await stream(msgs, model='gpt-4o-mini', max_tokens=mtok)
102
- ```
103
-
104
- GPT: As of now, Saturn has the most moons, with over 80 confirmed moons.
105
-
106
- ``` python
107
- # Turn 3: Switch to Gemini — same msgs, different provider
108
- msgs += [r2.message, user("Summarize our conversation in one sentence.")]
109
- print("Gemini: ", end='')
110
- r3 = await stream(msgs, model='models/gemini-3-flash-preview', max_tokens=mtok)
111
- ```
112
-
113
- Gemini: The conversation identified the three largest planets in the solar system and noted that Saturn currently has the most moons.
114
-
115
- ``` python
116
- # Turn 4: Switch to Kimi — works the same way
117
- msgs += [r3.message, user("Thanks! What's one surprising fact about Saturn?")]
118
- print("Kimi: ", end='')
119
- r4 = await stream(msgs, model='accounts/fireworks/models/kimi-k2p5', vendor_name='fireworks_ai', max_tokens=mtok)
120
- ```
121
-
122
- Kimi: 🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠Saturn is less dense than water, meaning it would theoretically float if you could find a bathtub large enough to hold it.
123
-
124
84
  ## System Prompts
125
85
 
126
86
  Pass a system prompt to any provider — `fastllm` maps it to each API’s native mechanism (Anthropic `system`, OpenAI `instructions`, Gemini `system_instruction`):
@@ -129,7 +89,7 @@ Pass a system prompt to any provider — `fastllm` maps it to each API’s nativ
129
89
  sys = "You are a pirate chef. Always respond in pirate speak and mention food."
130
90
 
131
91
  print("Claude: ", end='')
132
- r = await stream([user("What should I do today?")], model='claude-sonnet-4-20250514', system=sys, max_tokens=mtok)
92
+ r = await stream([user("What should I do today?")], model='claude-sonnet-5', system=sys, max_tokens=mtok)
133
93
 
134
94
  print("Gemini: ", end='')
135
95
  r = await stream([user("What should I do today?")], model='models/gemini-3-flash-preview', system=sys, max_tokens=mtok)
@@ -162,7 +122,7 @@ r = await stream([user("What should I do today?")], model='models/gemini-3-flash
162
122
 
163
123
  ## Tool Calling — Define Once, Use Anywhere
164
124
 
165
- Define tools in a single canonical format. `fastllm` translates to each provider’s native tool schema automatically. Here we start a tool-use conversation with Claude, provide the result, then switch to GPT and Gemini to continue:
125
+ Define tools in a single canonical format. `fastllm` translates to each provider’s native tool schema automatically. Here Claude requests the tool, receives the result, and answers:
166
126
 
167
127
  ``` python
168
128
  tools = [{"type": "function", "function": {
@@ -173,7 +133,7 @@ tools = [{"type": "function", "function": {
173
133
 
174
134
  # Turn 1: Claude requests the tool
175
135
  msgs = [user("What's the weather in Paris?")]
176
- r1 = await stream(msgs, model='claude-sonnet-4-20250514', tools=tools, max_tokens=mtok)
136
+ r1 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
177
137
  print("Tool calls:", r1.tool_calls)
178
138
  ```
179
139
 
@@ -183,44 +143,28 @@ print("Tool calls:", r1.tool_calls)
183
143
  Tool calls: [ToolUse(raw={'caller': {'type': 'direct'}}, cache_control=None, id='toolu_01RMN1WM7vPBT3ovv5Ex6VzC', name='get_weather', arguments={'city': 'Paris'}, server=False, text=None)]
184
144
 
185
145
  ``` python
186
- # Provide the tool result
187
- msgs += [r1.message, mk_tool_res_msg(r1.tool_calls, ['22°C, sunny with light clouds'])]
188
-
189
- # Turn 2: Switch to GPT to interpret the result
190
- msgs.append(user("Should I bring a jacket?"))
191
- print("GPT: ", end='')
192
- r2 = await stream(msgs, model='gpt-4o-mini', tools=tools, max_tokens=mtok)
146
+ # Provide the tool result, then continue the same conversation
147
+ msgs += [r1.message, mk_tool_res_msg(r1.tool_calls, ['22°C, sunny with light clouds']), user("Should I bring a jacket?")]
148
+ print("Claude: ", end='')
149
+ r2 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
193
150
  ```
194
151
 
195
152
  GPT: With the temperature at 22°C and sunny with light clouds, a jacket isn't necessary for most people. However, if you tend to feel cold easily or if you plan to be out in the evening when it might cool down a bit, it could be a good idea to bring a light jacket.
196
153
 
197
- ``` python
198
- # Turn 3: Gemini sees the full cross-provider tool history
199
- msgs += [r2.message, user("How about tomorrow — will it rain?")]
200
- r3 = await stream(msgs, model='models/gemini-3-flash-preview', tools=tools, max_tokens=mtok, web_search_options={})
201
- ```
202
-
203
- Tomorrow in Paris (Saturday, June 13), the weather is expected to be mostly pleasant and sunny.
204
-
205
- While there is a small chance of light, passing showers (around a 20-25% chance), most forecasts indicate a dry day with mostly clear or scattered clouds. Temperatures will likely reach a comfortable high of around 22°C to 27°C (72°F to 80°F), making it a great day for being outdoors.
206
-
207
- So, while you might see a stray shower, you likely won't need to worry about heavy rain!
208
-
209
154
  ## Tool Choice
210
155
 
211
156
  Control whether the model must use tools, can’t use tools, or decides on its own:
212
157
 
213
158
  ``` python
214
159
  # Force the model to call a tool (even for a greeting)
215
- r = await stream([user("Hello there!")], model='claude-sonnet-4-20250514', tools=tools, tool_choice='required', max_tokens=mtok)
160
+ r = await stream([user("Hello there!")], model='claude-sonnet-5', tools=tools, tool_choice='required', max_tokens=mtok)
216
161
  print("Forced:", r.tool_calls)
217
162
 
218
163
  # Prevent tool use (model must answer directly)
219
164
  print("\nNo tools: ", end='')
220
- r = await stream([user("What's the weather?")], model='claude-sonnet-4-20250514', tools=tools, tool_choice='none', max_tokens=mtok)
165
+ r = await stream([user("What's the weather?")], model='claude-sonnet-5', tools=tools, tool_choice='none', max_tokens=mtok)
221
166
  ```
222
167
 
223
-
224
168
  - ⏳ `get_weather(city="<UNKNOWN>")` ⏳
225
169
 
226
170
  Forced: [ToolUse(raw={'caller': {'type': 'direct'}}, cache_control=None, id='toolu_01U7tAXjXAtwjp6xNSMbsyPU', name='get_weather', arguments={'city': '<UNKNOWN>'}, server=False, text=None)]
@@ -329,11 +273,11 @@ long_ctx = "You are an expert on the solar system. " * 200
329
273
  system = Text(long_ctx, cache_control={'type': 'ephemeral'})
330
274
 
331
275
  print("Call 1: ", end='')
332
- r1 = await stream([user("What is Jupiter's mass?")], model='claude-sonnet-4-20250514', system=system, max_tokens=mtok)
276
+ r1 = await stream([user("What is Jupiter's mass?")], model='claude-sonnet-5', system=system, max_tokens=mtok)
333
277
  print(f"Usage: {r1.usage}")
334
278
 
335
279
  print("Call 2: ", end='')
336
- r2 = await stream([user("How about Saturn?")], model='claude-sonnet-4-20250514', system=system, max_tokens=mtok)
280
+ r2 = await stream([user("How about Saturn?")], model='claude-sonnet-5', system=system, max_tokens=mtok)
337
281
  print(f"Usage: {r2.usage}")
338
282
  ```
339
283
 
@@ -388,7 +332,7 @@ img_msg = Msg(role='user', content=[
388
332
  Text("What do you see in this image?")
389
333
  ])
390
334
 
391
- for name, kw in [('claude-sonnet-4-20250514', {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {})]:
335
+ for name, kw in [('claude-sonnet-5', {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {})]:
392
336
  print(f"{name:>30s}: ", end='')
393
337
  r = await stream([img_msg], model=name, max_tokens=mtok, **kw)
394
338
  ```
@@ -12,7 +12,7 @@ Run `pip install python-fastllm` or clone from github and install locally.
12
12
  ``` python
13
13
  from aidialog.msg_parts import Msg, Part, Text, Thinking, ToolUse, InputImage, mk_tool_res_msg, Completion
14
14
  from fastllm.acomplete import acomplete
15
- from fastllm.types import sample_img_url
15
+ from fastllm.types import sample_img_url, sample_doms
16
16
  import asyncio, json
17
17
 
18
18
  # Helpers
@@ -41,7 +41,7 @@ The same `acomplete` call works with Claude, GPT, Gemini, and Kimi. Just change
41
41
 
42
42
  ``` python
43
43
  models = [
44
- ('claude-sonnet-4-20250514', {}),
44
+ ('claude-sonnet-5', {}),
45
45
  ('gpt-4o-mini', {}),
46
46
  ('models/gemini-3-flash-preview', {}),
47
47
  ('accounts/fireworks/models/kimi-k2p5', dict(vendor_name='fireworks_ai'))
@@ -65,46 +65,6 @@ for name, kw in models:
65
65
 
66
66
  The response should be simple and direct.
67
67
 
68
- ## Multi-Turn — Swap Providers Mid-Conversation
69
-
70
- Build a conversation with one provider, then seamlessly continue it with another. `fastllm` translates between every provider’s native format automatically:
71
-
72
- ``` python
73
- # Turn 1: Claude starts the conversation
74
- msgs = [user("Name the 3 largest planets in our solar system. One sentence.")]
75
- print("Claude: ", end='')
76
- r1 = await stream(msgs, model='claude-sonnet-4-20250514', max_tokens=mtok)
77
- ```
78
-
79
- Claude: The three largest planets in our solar system are Jupiter, Saturn, and Neptune.
80
-
81
- ``` python
82
- # Turn 2: Switch to GPT — just change the model string
83
- msgs += [r1.message, user("Which one has the most moons?")]
84
- print("GPT: ", end='')
85
- r2 = await stream(msgs, model='gpt-4o-mini', max_tokens=mtok)
86
- ```
87
-
88
- GPT: As of now, Saturn has the most moons, with over 80 confirmed moons.
89
-
90
- ``` python
91
- # Turn 3: Switch to Gemini — same msgs, different provider
92
- msgs += [r2.message, user("Summarize our conversation in one sentence.")]
93
- print("Gemini: ", end='')
94
- r3 = await stream(msgs, model='models/gemini-3-flash-preview', max_tokens=mtok)
95
- ```
96
-
97
- Gemini: The conversation identified the three largest planets in the solar system and noted that Saturn currently has the most moons.
98
-
99
- ``` python
100
- # Turn 4: Switch to Kimi — works the same way
101
- msgs += [r3.message, user("Thanks! What's one surprising fact about Saturn?")]
102
- print("Kimi: ", end='')
103
- r4 = await stream(msgs, model='accounts/fireworks/models/kimi-k2p5', vendor_name='fireworks_ai', max_tokens=mtok)
104
- ```
105
-
106
- Kimi: 🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠Saturn is less dense than water, meaning it would theoretically float if you could find a bathtub large enough to hold it.
107
-
108
68
  ## System Prompts
109
69
 
110
70
  Pass a system prompt to any provider — `fastllm` maps it to each API’s native mechanism (Anthropic `system`, OpenAI `instructions`, Gemini `system_instruction`):
@@ -113,7 +73,7 @@ Pass a system prompt to any provider — `fastllm` maps it to each API’s nativ
113
73
  sys = "You are a pirate chef. Always respond in pirate speak and mention food."
114
74
 
115
75
  print("Claude: ", end='')
116
- r = await stream([user("What should I do today?")], model='claude-sonnet-4-20250514', system=sys, max_tokens=mtok)
76
+ r = await stream([user("What should I do today?")], model='claude-sonnet-5', system=sys, max_tokens=mtok)
117
77
 
118
78
  print("Gemini: ", end='')
119
79
  r = await stream([user("What should I do today?")], model='models/gemini-3-flash-preview', system=sys, max_tokens=mtok)
@@ -146,7 +106,7 @@ r = await stream([user("What should I do today?")], model='models/gemini-3-flash
146
106
 
147
107
  ## Tool Calling — Define Once, Use Anywhere
148
108
 
149
- Define tools in a single canonical format. `fastllm` translates to each provider’s native tool schema automatically. Here we start a tool-use conversation with Claude, provide the result, then switch to GPT and Gemini to continue:
109
+ Define tools in a single canonical format. `fastllm` translates to each provider’s native tool schema automatically. Here Claude requests the tool, receives the result, and answers:
150
110
 
151
111
  ``` python
152
112
  tools = [{"type": "function", "function": {
@@ -157,7 +117,7 @@ tools = [{"type": "function", "function": {
157
117
 
158
118
  # Turn 1: Claude requests the tool
159
119
  msgs = [user("What's the weather in Paris?")]
160
- r1 = await stream(msgs, model='claude-sonnet-4-20250514', tools=tools, max_tokens=mtok)
120
+ r1 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
161
121
  print("Tool calls:", r1.tool_calls)
162
122
  ```
163
123
 
@@ -167,44 +127,28 @@ print("Tool calls:", r1.tool_calls)
167
127
  Tool calls: [ToolUse(raw={'caller': {'type': 'direct'}}, cache_control=None, id='toolu_01RMN1WM7vPBT3ovv5Ex6VzC', name='get_weather', arguments={'city': 'Paris'}, server=False, text=None)]
168
128
 
169
129
  ``` python
170
- # Provide the tool result
171
- msgs += [r1.message, mk_tool_res_msg(r1.tool_calls, ['22°C, sunny with light clouds'])]
172
-
173
- # Turn 2: Switch to GPT to interpret the result
174
- msgs.append(user("Should I bring a jacket?"))
175
- print("GPT: ", end='')
176
- r2 = await stream(msgs, model='gpt-4o-mini', tools=tools, max_tokens=mtok)
130
+ # Provide the tool result, then continue the same conversation
131
+ msgs += [r1.message, mk_tool_res_msg(r1.tool_calls, ['22°C, sunny with light clouds']), user("Should I bring a jacket?")]
132
+ print("Claude: ", end='')
133
+ r2 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
177
134
  ```
178
135
 
179
136
  GPT: With the temperature at 22°C and sunny with light clouds, a jacket isn't necessary for most people. However, if you tend to feel cold easily or if you plan to be out in the evening when it might cool down a bit, it could be a good idea to bring a light jacket.
180
137
 
181
- ``` python
182
- # Turn 3: Gemini sees the full cross-provider tool history
183
- msgs += [r2.message, user("How about tomorrow — will it rain?")]
184
- r3 = await stream(msgs, model='models/gemini-3-flash-preview', tools=tools, max_tokens=mtok, web_search_options={})
185
- ```
186
-
187
- Tomorrow in Paris (Saturday, June 13), the weather is expected to be mostly pleasant and sunny.
188
-
189
- While there is a small chance of light, passing showers (around a 20-25% chance), most forecasts indicate a dry day with mostly clear or scattered clouds. Temperatures will likely reach a comfortable high of around 22°C to 27°C (72°F to 80°F), making it a great day for being outdoors.
190
-
191
- So, while you might see a stray shower, you likely won't need to worry about heavy rain!
192
-
193
138
  ## Tool Choice
194
139
 
195
140
  Control whether the model must use tools, can’t use tools, or decides on its own:
196
141
 
197
142
  ``` python
198
143
  # Force the model to call a tool (even for a greeting)
199
- r = await stream([user("Hello there!")], model='claude-sonnet-4-20250514', tools=tools, tool_choice='required', max_tokens=mtok)
144
+ r = await stream([user("Hello there!")], model='claude-sonnet-5', tools=tools, tool_choice='required', max_tokens=mtok)
200
145
  print("Forced:", r.tool_calls)
201
146
 
202
147
  # Prevent tool use (model must answer directly)
203
148
  print("\nNo tools: ", end='')
204
- r = await stream([user("What's the weather?")], model='claude-sonnet-4-20250514', tools=tools, tool_choice='none', max_tokens=mtok)
149
+ r = await stream([user("What's the weather?")], model='claude-sonnet-5', tools=tools, tool_choice='none', max_tokens=mtok)
205
150
  ```
206
151
 
207
-
208
152
  - ⏳ `get_weather(city="<UNKNOWN>")` ⏳
209
153
 
210
154
  Forced: [ToolUse(raw={'caller': {'type': 'direct'}}, cache_control=None, id='toolu_01U7tAXjXAtwjp6xNSMbsyPU', name='get_weather', arguments={'city': '<UNKNOWN>'}, server=False, text=None)]
@@ -313,11 +257,11 @@ long_ctx = "You are an expert on the solar system. " * 200
313
257
  system = Text(long_ctx, cache_control={'type': 'ephemeral'})
314
258
 
315
259
  print("Call 1: ", end='')
316
- r1 = await stream([user("What is Jupiter's mass?")], model='claude-sonnet-4-20250514', system=system, max_tokens=mtok)
260
+ r1 = await stream([user("What is Jupiter's mass?")], model='claude-sonnet-5', system=system, max_tokens=mtok)
317
261
  print(f"Usage: {r1.usage}")
318
262
 
319
263
  print("Call 2: ", end='')
320
- r2 = await stream([user("How about Saturn?")], model='claude-sonnet-4-20250514', system=system, max_tokens=mtok)
264
+ r2 = await stream([user("How about Saturn?")], model='claude-sonnet-5', system=system, max_tokens=mtok)
321
265
  print(f"Usage: {r2.usage}")
322
266
  ```
323
267
 
@@ -372,7 +316,7 @@ img_msg = Msg(role='user', content=[
372
316
  Text("What do you see in this image?")
373
317
  ])
374
318
 
375
- for name, kw in [('claude-sonnet-4-20250514', {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {})]:
319
+ for name, kw in [('claude-sonnet-5', {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {})]:
376
320
  print(f"{name:>30s}: ", end='')
377
321
  r = await stream([img_msg], model=name, max_tokens=mtok, **kw)
378
322
  ```
@@ -2,4 +2,4 @@
2
2
 
3
3
  - `fastllm.responses`: Provider-independent Responses resources and streaming over FastLLM"""
4
4
 
5
- __version__ = "0.0.50"
5
+ __version__ = "0.0.51"
@@ -51,7 +51,8 @@ d = { 'settings': { 'branch': 'main',
51
51
  'fastllm.anthropic.norm_tool_calls': ('anthropic.html#norm_tool_calls', 'fastllm/anthropic.py'),
52
52
  'fastllm.anthropic.norm_tr_parts': ('anthropic.html#norm_tr_parts', 'fastllm/anthropic.py'),
53
53
  'fastllm.anthropic.norm_usage': ('anthropic.html#norm_usage', 'fastllm/anthropic.py'),
54
- 'fastllm.anthropic.raw_msg': ('anthropic.html#raw_msg', 'fastllm/anthropic.py')},
54
+ 'fastllm.anthropic.raw_msg': ('anthropic.html#raw_msg', 'fastllm/anthropic.py'),
55
+ 'fastllm.anthropic.search_hits': ('anthropic.html#search_hits', 'fastllm/anthropic.py')},
55
56
  'fastllm.chat': { 'fastllm.chat.AsyncChat': ('chat.html#asyncchat', 'fastllm/chat.py'),
56
57
  'fastllm.chat.AsyncChat.__call__': ('chat.html#asyncchat.__call__', 'fastllm/chat.py'),
57
58
  'fastllm.chat.AsyncChat.__init__': ('chat.html#asyncchat.__init__', 'fastllm/chat.py'),
@@ -255,11 +256,15 @@ d = { 'settings': { 'branch': 'main',
255
256
  'fastllm.openai_responses.raw_msg': ( 'oai_responses.html#raw_msg',
256
257
  'fastllm/openai_responses.py')},
257
258
  'fastllm.responses': { 'fastllm.responses.AsyncResponses': ('responses.html#asyncresponses', 'fastllm/responses.py'),
259
+ 'fastllm.responses.AsyncResponses.__init__': ( 'responses.html#asyncresponses.__init__',
260
+ 'fastllm/responses.py'),
258
261
  'fastllm.responses.AsyncResponses.call': ('responses.html#asyncresponses.call', 'fastllm/responses.py'),
259
262
  'fastllm.responses.AsyncResponses.events': ( 'responses.html#asyncresponses.events',
260
263
  'fastllm/responses.py'),
261
264
  'fastllm.responses.AsyncResponses.prepare': ( 'responses.html#asyncresponses.prepare',
262
265
  'fastllm/responses.py'),
266
+ 'fastllm.responses.AsyncResponses.serve': ( 'responses.html#asyncresponses.serve',
267
+ 'fastllm/responses.py'),
263
268
  'fastllm.responses.AsyncResponses.server_results': ( 'responses.html#asyncresponses.server_results',
264
269
  'fastllm/responses.py'),
265
270
  'fastllm.responses.AsyncResponses.server_turn': ( 'responses.html#asyncresponses.server_turn',
@@ -1,9 +1,9 @@
1
1
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/04_anthropic.ipynb.
2
2
 
3
3
  # %% auto #0
4
- __all__ = ['ant_tc_types', 'norm_tool_call', 'norm_tool_calls', 'norm_usage', 'finalize_usage', 'norm_finish', 'norm_parts',
5
- 'raw_msg', 'norm_tr_parts', 'norm_sse_event', 'delta_index_fn', 'collate_raw', 'acollect_stream',
6
- 'denorm_tool_use', 'denorm_assistant', 'denorm_tool', 'denorm_msgs', 'denorm_tool_schs',
4
+ __all__ = ['ant_tc_types', 'norm_tool_call', 'norm_tool_calls', 'norm_usage', 'finalize_usage', 'norm_finish', 'search_hits',
5
+ 'norm_parts', 'raw_msg', 'norm_tr_parts', 'norm_sse_event', 'delta_index_fn', 'collate_raw',
6
+ 'acollect_stream', 'denorm_tool_use', 'denorm_assistant', 'denorm_tool', 'denorm_msgs', 'denorm_tool_schs',
7
7
  'denorm_tool_choice', 'denorm_reasoning', 'denorm_web_search', 'denorm_system', 'denorm_user',
8
8
  'denorm_image', 'denorm_file', 'denorm_tool_result', 'apply_cache_idxs', 'mk_payload', 'get_hdrs', 'cost']
9
9
 
@@ -15,7 +15,7 @@ from fastcore.meta import *
15
15
  from fasttransport.errors import api_error_from_event
16
16
 
17
17
  from .types import *
18
- from aidialog.msg_parts import (Part, PartType, mk_part, Msg, Text, Thinking, ToolUse, ToolResult, tool_text,
18
+ from aidialog.msg_parts import (Part, PartType, mk_part, Msg, Completion, Text, Thinking, ToolUse, ToolResult, tool_text,
19
19
  InputImage, InputAudio, InputVideo, InputFile, data_url)
20
20
  from .streaming import *
21
21
  from .streaming import mk_acollect_stream
@@ -69,8 +69,16 @@ def _ant_part_type(typ):
69
69
  ant_parts_mapping = dict(text=PartType.text, thinking=PartType.thinking, redacted_thinking=PartType.thinking, tool_use=PartType.tool_use, server_tool_use=PartType.tool_use, mcp_tool_use=PartType.tool_use)
70
70
  return ant_parts_mapping.get(typ, typ)
71
71
 
72
+ def search_hits(parts, blocks):
73
+ "Set each server call's `text` to the `title: url` lines of its result block, when the response has one"
74
+ calls = {p.id: p for p in parts if isinstance(p, ToolUse)}
75
+ for b in blocks:
76
+ if b.get('type', '').endswith('_tool_result') and (tc := calls.get(b.get('tool_use_id'))):
77
+ tc.text = '\n'.join(f"{r.get('title', '')}: {r.get('url', '')}" for r in b.get('content') or [] if isinstance(r, dict) and r.get('url'))
78
+ return parts
79
+
72
80
  def norm_parts(resp):
73
- "Canonical parts of a response; a server tool's result block lives only in the raw message"
81
+ "Canonical parts of a response; a server tool's result block lives in the raw message, and as the call's `text`"
74
82
  parts = []
75
83
  for b in resp.get("content", []):
76
84
  typ = _ant_part_type(b.get("type", "text"))
@@ -78,7 +86,7 @@ def norm_parts(resp):
78
86
  elif typ == PartType.tool_use:
79
87
  if tc := norm_tool_call(b): parts.append(tc)
80
88
  elif typ == PartType.text: parts.append(Text(b.get("text", ""), raw=b))
81
- return parts
89
+ return search_hits(parts, resp.get("content", []))
82
90
 
83
91
  # %% ../nbs/04_anthropic.ipynb #f24e52db
84
92
  def raw_msg(resp):
@@ -150,7 +158,9 @@ def collate_raw(deltas):
150
158
  @delegates(mk_acollect_stream, but=['index_fn', 'api_name'])
151
159
  async def acollect_stream(resp, **kwargs):
152
160
  res = mk_acollect_stream(norm_and_yield(resp, norm_sse_event), index_fn=delta_index_fn, api_name='anthropic', **kwargs)
153
- async for o in res: yield o
161
+ async for o in res:
162
+ if isinstance(o, Completion): search_hits(o.message.content, o.message.raw['content'])
163
+ yield o
154
164
 
155
165
  # %% ../nbs/04_anthropic.ipynb #bb11ab62
156
166
  def _ant_cc(block, p):
@@ -160,6 +160,7 @@ class ResponseTurn(BasicRepr):
160
160
  # %% ../nbs/06a_responses.ipynb #a648297f
161
161
  class AsyncResponses:
162
162
  "Run individual Responses turns over FastLLM."
163
+ def __init__(self): self.tasks = set() # served turns still running
163
164
  def prepare(self, body, previous=None, server_tools=None, **kwargs):
164
165
  if not isinstance(body, dict): raise ResponsesError('Request body must be an object')
165
166
  model = body.get('model')
@@ -343,3 +344,21 @@ async def events(self:AsyncResponses, turn, finalize=None, stream=None):
343
344
  except ResponsesError as exc: yield state.event('error', code=exc.code, message=str(exc), param=exc.param)
344
345
  except APIError as exc: yield state.event('error', code=exc.code or 'provider_error', message=str(exc), param=None)
345
346
 
347
+
348
+ # %% ../nbs/06a_responses.ipynb #cc32ce40
349
+ @patch
350
+ def serve(self:AsyncResponses, turn, finalize=None, stream=None, timeout=None):
351
+ "The frames of `events`, from a task that outlives its reader, so a disconnected client still gets its turn finalized"
352
+ q = asyncio.Queue()
353
+ async def _run():
354
+ try:
355
+ async with asyncio.timeout(timeout):
356
+ async for frame in self.events(turn, finalize=finalize, stream=stream): q.put_nowait(frame)
357
+ except TimeoutError: q.put_nowait(response_event('error', 0, code='timeout', message=f'turn exceeded {timeout}s', param=None))
358
+ finally: q.put_nowait(None)
359
+ t = asyncio.create_task(_run())
360
+ self.tasks.add(t)
361
+ t.add_done_callback(self.tasks.discard)
362
+ async def _frames():
363
+ while (frame := await q.get()) is not None: yield frame
364
+ return _frames()
@@ -3,14 +3,14 @@
3
3
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_types.ipynb.
4
4
 
5
5
  # %% auto #0
6
- __all__ = ['FinishReason', 'api_registry', 'model_prices_url', 'sample_img_url', 'haik45', 'sonn45', 'sonn46', 'sonn', 'sonn5',
7
- 'opus46', 'opus48', 'opus', 'opus5', 'fable', 'fable5', 'fable51', 'gpt54', 'gpt54m', 'gpt55', 'codex54',
8
- 'codex54m', 'codex55', 'codex53spark', 'model_info_registry', 'modern_llm', 'effort_codes',
9
- 'deepseek_v4_common', 'deepseek_v4_flash_prices', 'mimo_v25_common', 'codex_pricing', 'sol', 'terra', 'luna',
10
- 'gpt56s', 'Usage', 'APIRegistry', 'mk_completion', 'fn_schema', 'payload_kwargs', 'provider_req',
11
- 'get_api_key', 'wrap_typed', 'unwrap_typed', 'resize_b64', 'model_prices_meta', 'infer_api_name',
12
- 'get_model_meta', 'register_model_info', 'get_model_info', 'effort_code', 'effort_levels', 'resolve_effort',
13
- 'get_model_pricing', 'approx_pricing', 'is_deepseek_peak_hour', 'price_tier', 'tier_rate']
6
+ __all__ = ['FinishReason', 'api_registry', 'model_prices_url', 'sample_img_url', 'sample_doms', 'haik45', 'sonn45', 'sonn46',
7
+ 'sonn', 'sonn5', 'opus46', 'opus48', 'opus', 'opus5', 'fable', 'fable5', 'fable51', 'gpt54', 'gpt54m',
8
+ 'gpt55', 'codex54', 'codex54m', 'codex55', 'codex53spark', 'model_info_registry', 'modern_llm',
9
+ 'effort_codes', 'deepseek_v4_common', 'deepseek_v4_flash_prices', 'mimo_v25_common', 'codex_pricing', 'sol',
10
+ 'terra', 'luna', 'gpt56s', 'Usage', 'APIRegistry', 'mk_completion', 'fn_schema', 'payload_kwargs',
11
+ 'provider_req', 'get_api_key', 'wrap_typed', 'unwrap_typed', 'resize_b64', 'model_prices_meta',
12
+ 'infer_api_name', 'get_model_meta', 'register_model_info', 'get_model_info', 'effort_code', 'effort_levels',
13
+ 'resolve_effort', 'get_model_pricing', 'approx_pricing', 'is_deepseek_peak_hour', 'price_tier', 'tier_rate']
14
14
 
15
15
  # %% ../nbs/00_types.ipynb #b4d047fd
16
16
  import httpx2, base64, io
@@ -170,6 +170,7 @@ def model_prices_meta():
170
170
 
171
171
  # %% ../nbs/00_types.ipynb #019882a3
172
172
  sample_img_url = "https://img.magnific.com/free-photo/mountain-range-body-water_53876-139760.jpg?semt=ais_hybrid&w=740&q=80"
173
+ sample_doms = ('img.magnific.com', 'arxiv.org', 'openaiassets.blob.core.windows.net') # hosts of the sample media the lessons fetch, for cachy
173
174
 
174
175
  # %% ../nbs/00_types.ipynb #68e488d8
175
176
  def infer_api_name(model):
@@ -15,7 +15,7 @@ classifiers = [
15
15
  "Programming Language :: Python :: 3",
16
16
  "Programming Language :: Python :: 3 :: Only",
17
17
  ]
18
- dependencies = ['fastcore>=2.1.18', 'aidialog>=0.0.24', 'fasttransport>=0.0.2', 'pillow']
18
+ dependencies = ['fastcore>=2.1.18', 'aidialog>=0.0.25', 'fasttransport>=0.0.2', 'pillow']
19
19
 
20
20
  [project.scripts]
21
21
  fastllm-auth = "fastllm.codex:main"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fastllm
3
- Version: 0.0.50
3
+ Version: 0.0.51
4
4
  Author-email: Kerem Turgutlu <keremturgutlu@gmail.com>
5
5
  License: Apache-2.0
6
6
  Project-URL: Repository, https://github.com/AnswerDotAI/fastllm
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
10
10
  Requires-Python: >=3.10
11
11
  Description-Content-Type: text/markdown
12
12
  Requires-Dist: fastcore>=2.1.18
13
- Requires-Dist: aidialog>=0.0.24
13
+ Requires-Dist: aidialog>=0.0.25
14
14
  Requires-Dist: fasttransport>=0.0.2
15
15
  Requires-Dist: pillow
16
16
 
@@ -28,7 +28,7 @@ Run `pip install python-fastllm` or clone from github and install locally.
28
28
  ``` python
29
29
  from aidialog.msg_parts import Msg, Part, Text, Thinking, ToolUse, InputImage, mk_tool_res_msg, Completion
30
30
  from fastllm.acomplete import acomplete
31
- from fastllm.types import sample_img_url
31
+ from fastllm.types import sample_img_url, sample_doms
32
32
  import asyncio, json
33
33
 
34
34
  # Helpers
@@ -57,7 +57,7 @@ The same `acomplete` call works with Claude, GPT, Gemini, and Kimi. Just change
57
57
 
58
58
  ``` python
59
59
  models = [
60
- ('claude-sonnet-4-20250514', {}),
60
+ ('claude-sonnet-5', {}),
61
61
  ('gpt-4o-mini', {}),
62
62
  ('models/gemini-3-flash-preview', {}),
63
63
  ('accounts/fireworks/models/kimi-k2p5', dict(vendor_name='fireworks_ai'))
@@ -81,46 +81,6 @@ for name, kw in models:
81
81
 
82
82
  The response should be simple and direct.
83
83
 
84
- ## Multi-Turn — Swap Providers Mid-Conversation
85
-
86
- Build a conversation with one provider, then seamlessly continue it with another. `fastllm` translates between every provider’s native format automatically:
87
-
88
- ``` python
89
- # Turn 1: Claude starts the conversation
90
- msgs = [user("Name the 3 largest planets in our solar system. One sentence.")]
91
- print("Claude: ", end='')
92
- r1 = await stream(msgs, model='claude-sonnet-4-20250514', max_tokens=mtok)
93
- ```
94
-
95
- Claude: The three largest planets in our solar system are Jupiter, Saturn, and Neptune.
96
-
97
- ``` python
98
- # Turn 2: Switch to GPT — just change the model string
99
- msgs += [r1.message, user("Which one has the most moons?")]
100
- print("GPT: ", end='')
101
- r2 = await stream(msgs, model='gpt-4o-mini', max_tokens=mtok)
102
- ```
103
-
104
- GPT: As of now, Saturn has the most moons, with over 80 confirmed moons.
105
-
106
- ``` python
107
- # Turn 3: Switch to Gemini — same msgs, different provider
108
- msgs += [r2.message, user("Summarize our conversation in one sentence.")]
109
- print("Gemini: ", end='')
110
- r3 = await stream(msgs, model='models/gemini-3-flash-preview', max_tokens=mtok)
111
- ```
112
-
113
- Gemini: The conversation identified the three largest planets in the solar system and noted that Saturn currently has the most moons.
114
-
115
- ``` python
116
- # Turn 4: Switch to Kimi — works the same way
117
- msgs += [r3.message, user("Thanks! What's one surprising fact about Saturn?")]
118
- print("Kimi: ", end='')
119
- r4 = await stream(msgs, model='accounts/fireworks/models/kimi-k2p5', vendor_name='fireworks_ai', max_tokens=mtok)
120
- ```
121
-
122
- Kimi: 🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠Saturn is less dense than water, meaning it would theoretically float if you could find a bathtub large enough to hold it.
123
-
124
84
  ## System Prompts
125
85
 
126
86
  Pass a system prompt to any provider — `fastllm` maps it to each API’s native mechanism (Anthropic `system`, OpenAI `instructions`, Gemini `system_instruction`):
@@ -129,7 +89,7 @@ Pass a system prompt to any provider — `fastllm` maps it to each API’s nativ
129
89
  sys = "You are a pirate chef. Always respond in pirate speak and mention food."
130
90
 
131
91
  print("Claude: ", end='')
132
- r = await stream([user("What should I do today?")], model='claude-sonnet-4-20250514', system=sys, max_tokens=mtok)
92
+ r = await stream([user("What should I do today?")], model='claude-sonnet-5', system=sys, max_tokens=mtok)
133
93
 
134
94
  print("Gemini: ", end='')
135
95
  r = await stream([user("What should I do today?")], model='models/gemini-3-flash-preview', system=sys, max_tokens=mtok)
@@ -162,7 +122,7 @@ r = await stream([user("What should I do today?")], model='models/gemini-3-flash
162
122
 
163
123
  ## Tool Calling — Define Once, Use Anywhere
164
124
 
165
- Define tools in a single canonical format. `fastllm` translates to each provider’s native tool schema automatically. Here we start a tool-use conversation with Claude, provide the result, then switch to GPT and Gemini to continue:
125
+ Define tools in a single canonical format. `fastllm` translates to each provider’s native tool schema automatically. Here Claude requests the tool, receives the result, and answers:
166
126
 
167
127
  ``` python
168
128
  tools = [{"type": "function", "function": {
@@ -173,7 +133,7 @@ tools = [{"type": "function", "function": {
173
133
 
174
134
  # Turn 1: Claude requests the tool
175
135
  msgs = [user("What's the weather in Paris?")]
176
- r1 = await stream(msgs, model='claude-sonnet-4-20250514', tools=tools, max_tokens=mtok)
136
+ r1 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
177
137
  print("Tool calls:", r1.tool_calls)
178
138
  ```
179
139
 
@@ -183,44 +143,28 @@ print("Tool calls:", r1.tool_calls)
183
143
  Tool calls: [ToolUse(raw={'caller': {'type': 'direct'}}, cache_control=None, id='toolu_01RMN1WM7vPBT3ovv5Ex6VzC', name='get_weather', arguments={'city': 'Paris'}, server=False, text=None)]
184
144
 
185
145
  ``` python
186
- # Provide the tool result
187
- msgs += [r1.message, mk_tool_res_msg(r1.tool_calls, ['22°C, sunny with light clouds'])]
188
-
189
- # Turn 2: Switch to GPT to interpret the result
190
- msgs.append(user("Should I bring a jacket?"))
191
- print("GPT: ", end='')
192
- r2 = await stream(msgs, model='gpt-4o-mini', tools=tools, max_tokens=mtok)
146
+ # Provide the tool result, then continue the same conversation
147
+ msgs += [r1.message, mk_tool_res_msg(r1.tool_calls, ['22°C, sunny with light clouds']), user("Should I bring a jacket?")]
148
+ print("Claude: ", end='')
149
+ r2 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
193
150
  ```
194
151
 
195
152
  GPT: With the temperature at 22°C and sunny with light clouds, a jacket isn't necessary for most people. However, if you tend to feel cold easily or if you plan to be out in the evening when it might cool down a bit, it could be a good idea to bring a light jacket.
196
153
 
197
- ``` python
198
- # Turn 3: Gemini sees the full cross-provider tool history
199
- msgs += [r2.message, user("How about tomorrow — will it rain?")]
200
- r3 = await stream(msgs, model='models/gemini-3-flash-preview', tools=tools, max_tokens=mtok, web_search_options={})
201
- ```
202
-
203
- Tomorrow in Paris (Saturday, June 13), the weather is expected to be mostly pleasant and sunny.
204
-
205
- While there is a small chance of light, passing showers (around a 20-25% chance), most forecasts indicate a dry day with mostly clear or scattered clouds. Temperatures will likely reach a comfortable high of around 22°C to 27°C (72°F to 80°F), making it a great day for being outdoors.
206
-
207
- So, while you might see a stray shower, you likely won't need to worry about heavy rain!
208
-
209
154
  ## Tool Choice
210
155
 
211
156
  Control whether the model must use tools, can’t use tools, or decides on its own:
212
157
 
213
158
  ``` python
214
159
  # Force the model to call a tool (even for a greeting)
215
- r = await stream([user("Hello there!")], model='claude-sonnet-4-20250514', tools=tools, tool_choice='required', max_tokens=mtok)
160
+ r = await stream([user("Hello there!")], model='claude-sonnet-5', tools=tools, tool_choice='required', max_tokens=mtok)
216
161
  print("Forced:", r.tool_calls)
217
162
 
218
163
  # Prevent tool use (model must answer directly)
219
164
  print("\nNo tools: ", end='')
220
- r = await stream([user("What's the weather?")], model='claude-sonnet-4-20250514', tools=tools, tool_choice='none', max_tokens=mtok)
165
+ r = await stream([user("What's the weather?")], model='claude-sonnet-5', tools=tools, tool_choice='none', max_tokens=mtok)
221
166
  ```
222
167
 
223
-
224
168
  - ⏳ `get_weather(city="<UNKNOWN>")` ⏳
225
169
 
226
170
  Forced: [ToolUse(raw={'caller': {'type': 'direct'}}, cache_control=None, id='toolu_01U7tAXjXAtwjp6xNSMbsyPU', name='get_weather', arguments={'city': '<UNKNOWN>'}, server=False, text=None)]
@@ -329,11 +273,11 @@ long_ctx = "You are an expert on the solar system. " * 200
329
273
  system = Text(long_ctx, cache_control={'type': 'ephemeral'})
330
274
 
331
275
  print("Call 1: ", end='')
332
- r1 = await stream([user("What is Jupiter's mass?")], model='claude-sonnet-4-20250514', system=system, max_tokens=mtok)
276
+ r1 = await stream([user("What is Jupiter's mass?")], model='claude-sonnet-5', system=system, max_tokens=mtok)
333
277
  print(f"Usage: {r1.usage}")
334
278
 
335
279
  print("Call 2: ", end='')
336
- r2 = await stream([user("How about Saturn?")], model='claude-sonnet-4-20250514', system=system, max_tokens=mtok)
280
+ r2 = await stream([user("How about Saturn?")], model='claude-sonnet-5', system=system, max_tokens=mtok)
337
281
  print(f"Usage: {r2.usage}")
338
282
  ```
339
283
 
@@ -388,7 +332,7 @@ img_msg = Msg(role='user', content=[
388
332
  Text("What do you see in this image?")
389
333
  ])
390
334
 
391
- for name, kw in [('claude-sonnet-4-20250514', {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {})]:
335
+ for name, kw in [('claude-sonnet-5', {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {})]:
392
336
  print(f"{name:>30s}: ", end='')
393
337
  r = await stream([img_msg], model=name, max_tokens=mtok, **kw)
394
338
  ```
@@ -1,4 +1,4 @@
1
1
  fastcore>=2.1.18
2
- aidialog>=0.0.24
2
+ aidialog>=0.0.25
3
3
  fasttransport>=0.0.2
4
4
  pillow