python-fastllm 0.0.42__tar.gz → 0.0.44__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 (29) hide show
  1. {python_fastllm-0.0.42/python_fastllm.egg-info → python_fastllm-0.0.44}/PKG-INFO +27 -6
  2. python_fastllm-0.0.42/PKG-INFO → python_fastllm-0.0.44/README.md +25 -21
  3. python_fastllm-0.0.44/fastllm/__init__.py +5 -0
  4. {python_fastllm-0.0.42 → python_fastllm-0.0.44}/fastllm/_modidx.py +54 -1
  5. {python_fastllm-0.0.42 → python_fastllm-0.0.44}/fastllm/acomplete.py +36 -54
  6. {python_fastllm-0.0.42 → python_fastllm-0.0.44}/fastllm/anthropic.py +1 -1
  7. {python_fastllm-0.0.42 → python_fastllm-0.0.44}/fastllm/chat.py +51 -32
  8. {python_fastllm-0.0.42 → python_fastllm-0.0.44}/fastllm/gemini.py +12 -8
  9. {python_fastllm-0.0.42 → python_fastllm-0.0.44}/fastllm/openai_chat.py +28 -18
  10. {python_fastllm-0.0.42 → python_fastllm-0.0.44}/fastllm/openai_responses.py +45 -30
  11. python_fastllm-0.0.44/fastllm/responses.py +299 -0
  12. {python_fastllm-0.0.42 → python_fastllm-0.0.44}/fastllm/types.py +32 -4
  13. {python_fastllm-0.0.42 → python_fastllm-0.0.44}/pyproject.toml +1 -4
  14. python_fastllm-0.0.42/README.md → python_fastllm-0.0.44/python_fastllm.egg-info/PKG-INFO +42 -5
  15. {python_fastllm-0.0.42 → python_fastllm-0.0.44}/python_fastllm.egg-info/SOURCES.txt +1 -6
  16. {python_fastllm-0.0.42 → python_fastllm-0.0.44}/python_fastllm.egg-info/requires.txt +1 -0
  17. python_fastllm-0.0.42/fastllm/__init__.py +0 -1
  18. python_fastllm-0.0.42/fastllm/specs/anthropic.json +0 -1
  19. python_fastllm-0.0.42/fastllm/specs/anthropic.yml +0 -15684
  20. python_fastllm-0.0.42/fastllm/specs/gemini.json +0 -6951
  21. python_fastllm-0.0.42/fastllm/specs/openai.with-code-samples.json +0 -1
  22. python_fastllm-0.0.42/fastllm/specs/openai.with-code-samples.yml +0 -73650
  23. python_fastllm-0.0.42/fastllm/specs/spec_manifest.json +0 -17
  24. {python_fastllm-0.0.42 → python_fastllm-0.0.44}/fastllm/codex.py +0 -0
  25. {python_fastllm-0.0.42 → python_fastllm-0.0.44}/fastllm/streaming.py +0 -0
  26. {python_fastllm-0.0.42 → python_fastllm-0.0.44}/python_fastllm.egg-info/dependency_links.txt +0 -0
  27. {python_fastllm-0.0.42 → python_fastllm-0.0.44}/python_fastllm.egg-info/entry_points.txt +0 -0
  28. {python_fastllm-0.0.42 → python_fastllm-0.0.44}/python_fastllm.egg-info/top_level.txt +0 -0
  29. {python_fastllm-0.0.42 → python_fastllm-0.0.44}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fastllm
3
- Version: 0.0.42
3
+ Version: 0.0.44
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
@@ -12,6 +12,7 @@ Description-Content-Type: text/markdown
12
12
  Requires-Dist: fastcore>=2.1.18
13
13
  Requires-Dist: aidialog>=0.0.10
14
14
  Requires-Dist: fastspec>=0.2.1
15
+ Requires-Dist: fasttransport>=0.0.2
15
16
  Requires-Dist: pillow
16
17
 
17
18
  # fastllm
@@ -26,8 +27,7 @@ Clone and install locally into your `aai-ws` env
26
27
  ## Setup
27
28
 
28
29
  ``` python
29
- from fastllm.types import Completion
30
- from aidialog.msg_parts import Msg, Part, Text, Thinking, ToolUse, InputImage, mk_tool_res_msg
30
+ from aidialog.msg_parts import Msg, Part, Text, Thinking, ToolUse, InputImage, mk_tool_res_msg, Completion
31
31
  from fastllm.acomplete import acomplete
32
32
  import asyncio, json
33
33
 
@@ -178,7 +178,9 @@ print("Tool calls:", r1.tool_calls)
178
178
  ```
179
179
 
180
180
  I'll check the current weather in Paris for you.
181
- Tool calls: [ToolCall(id='toolu_01RMN1WM7vPBT3ovv5Ex6VzC', name='get_weather', arguments={'city': 'Paris'}, server=False, extra={'caller': {'type': 'direct'}})]
181
+ - `get_weather(city="Paris")`
182
+
183
+ Tool calls: [ToolUse(raw={'caller': {'type': 'direct'}}, cache_control=None, id='toolu_01RMN1WM7vPBT3ovv5Ex6VzC', name='get_weather', arguments={'city': 'Paris'}, server=False, text=None)]
182
184
 
183
185
  ``` python
184
186
  # Provide the tool result
@@ -218,8 +220,9 @@ print("\nNo tools: ", end='')
218
220
  r = await stream([user("What's the weather?")], model='claude-sonnet-4-20250514', tools=tools, tool_choice='none', max_tokens=mtok)
219
221
  ```
220
222
 
223
+ - ⏳ `get_weather(city="<UNKNOWN>")` ⏳
221
224
 
222
- Forced: [ToolCall(id='toolu_01U7tAXjXAtwjp6xNSMbsyPU', name='get_weather', arguments={'city': '<UNKNOWN>'}, server=False, extra={'caller': {'type': 'direct'}})]
225
+ Forced: [ToolUse(raw={'caller': {'type': 'direct'}}, cache_control=None, id='toolu_01U7tAXjXAtwjp6xNSMbsyPU', name='get_weather', arguments={'city': '<UNKNOWN>'}, server=False, text=None)]
223
226
 
224
227
  No tools: I'd be happy to help you get the weather information! However, I need to know which city you'd like me to check the weather for. Could you please tell me the city name?
225
228
 
@@ -281,7 +284,25 @@ r = await stream([user("What is the latest Python release?")], model='gpt-4o-min
281
284
  print(f"\nServer tools used: {[tc.name for tc in r.tool_calls if tc.server]}")
282
285
  ```
283
286
 
284
- GPT + web search: As of June 12, 2026, the latest stable release of Python is version 3.14.5, which was released on May 10, 2026. ([test.python.org](https://test.python.org/downloads/latest?utm_source=openai))
287
+ GPT + web search:
288
+
289
+ ```json {.tool}
290
+ {
291
+ "id": "ws_0f48f4b49d3a4937006a2c04cb944481a3882056257ae43956",
292
+ "name": "web_search",
293
+ "args": {
294
+ "type": "search",
295
+ "queries": [
296
+ "latest Python release October 2023"
297
+ ],
298
+ "query": "latest Python release October 2023"
299
+ },
300
+ "result": "Server tool call executed.",
301
+ "server": true
302
+ }
303
+ ```
304
+
305
+ As of June 12, 2026, the latest stable release of Python is version 3.14.5, which was released on May 10, 2026. ([test.python.org](https://test.python.org/downloads/latest?utm_source=openai))
285
306
 
286
307
  Python 3.14 introduced several significant features, including:
287
308
 
@@ -1,19 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: python-fastllm
3
- Version: 0.0.42
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.10
14
- Requires-Dist: fastspec>=0.2.1
15
- Requires-Dist: pillow
16
-
17
1
  # fastllm
18
2
 
19
3
 
@@ -26,8 +10,7 @@ Clone and install locally into your `aai-ws` env
26
10
  ## Setup
27
11
 
28
12
  ``` python
29
- from fastllm.types import Completion
30
- from aidialog.msg_parts import Msg, Part, Text, Thinking, ToolUse, InputImage, mk_tool_res_msg
13
+ from aidialog.msg_parts import Msg, Part, Text, Thinking, ToolUse, InputImage, mk_tool_res_msg, Completion
31
14
  from fastllm.acomplete import acomplete
32
15
  import asyncio, json
33
16
 
@@ -178,7 +161,9 @@ print("Tool calls:", r1.tool_calls)
178
161
  ```
179
162
 
180
163
  I'll check the current weather in Paris for you.
181
- Tool calls: [ToolCall(id='toolu_01RMN1WM7vPBT3ovv5Ex6VzC', name='get_weather', arguments={'city': 'Paris'}, server=False, extra={'caller': {'type': 'direct'}})]
164
+ - `get_weather(city="Paris")`
165
+
166
+ Tool calls: [ToolUse(raw={'caller': {'type': 'direct'}}, cache_control=None, id='toolu_01RMN1WM7vPBT3ovv5Ex6VzC', name='get_weather', arguments={'city': 'Paris'}, server=False, text=None)]
182
167
 
183
168
  ``` python
184
169
  # Provide the tool result
@@ -218,8 +203,9 @@ print("\nNo tools: ", end='')
218
203
  r = await stream([user("What's the weather?")], model='claude-sonnet-4-20250514', tools=tools, tool_choice='none', max_tokens=mtok)
219
204
  ```
220
205
 
206
+ - ⏳ `get_weather(city="<UNKNOWN>")` ⏳
221
207
 
222
- Forced: [ToolCall(id='toolu_01U7tAXjXAtwjp6xNSMbsyPU', name='get_weather', arguments={'city': '<UNKNOWN>'}, server=False, extra={'caller': {'type': 'direct'}})]
208
+ Forced: [ToolUse(raw={'caller': {'type': 'direct'}}, cache_control=None, id='toolu_01U7tAXjXAtwjp6xNSMbsyPU', name='get_weather', arguments={'city': '<UNKNOWN>'}, server=False, text=None)]
223
209
 
224
210
  No tools: I'd be happy to help you get the weather information! However, I need to know which city you'd like me to check the weather for. Could you please tell me the city name?
225
211
 
@@ -281,7 +267,25 @@ r = await stream([user("What is the latest Python release?")], model='gpt-4o-min
281
267
  print(f"\nServer tools used: {[tc.name for tc in r.tool_calls if tc.server]}")
282
268
  ```
283
269
 
284
- GPT + web search: As of June 12, 2026, the latest stable release of Python is version 3.14.5, which was released on May 10, 2026. ([test.python.org](https://test.python.org/downloads/latest?utm_source=openai))
270
+ GPT + web search:
271
+
272
+ ```json {.tool}
273
+ {
274
+ "id": "ws_0f48f4b49d3a4937006a2c04cb944481a3882056257ae43956",
275
+ "name": "web_search",
276
+ "args": {
277
+ "type": "search",
278
+ "queries": [
279
+ "latest Python release October 2023"
280
+ ],
281
+ "query": "latest Python release October 2023"
282
+ },
283
+ "result": "Server tool call executed.",
284
+ "server": true
285
+ }
286
+ ```
287
+
288
+ As of June 12, 2026, the latest stable release of Python is version 3.14.5, which was released on May 10, 2026. ([test.python.org](https://test.python.org/downloads/latest?utm_source=openai))
285
289
 
286
290
  Python 3.14 introduced several significant features, including:
287
291
 
@@ -0,0 +1,5 @@
1
+ """Modules:
2
+
3
+ - `fastllm.responses`: Provider-independent Responses resources and streaming over FastLLM"""
4
+
5
+ __version__ = "0.0.44"
@@ -141,6 +141,7 @@ d = { 'settings': { 'branch': 'main',
141
141
  'fastllm.gemini.denorm_user': ('gemini.html#denorm_user', 'fastllm/gemini.py'),
142
142
  'fastllm.gemini.denorm_video': ('gemini.html#denorm_video', 'fastllm/gemini.py'),
143
143
  'fastllm.gemini.denorm_web_search': ('gemini.html#denorm_web_search', 'fastllm/gemini.py'),
144
+ 'fastllm.gemini.endpoint': ('gemini.html#endpoint', 'fastllm/gemini.py'),
144
145
  'fastllm.gemini.get_hdrs': ('gemini.html#get_hdrs', 'fastllm/gemini.py'),
145
146
  'fastllm.gemini.mk_payload': ('gemini.html#mk_payload', 'fastllm/gemini.py'),
146
147
  'fastllm.gemini.norm_finish': ('gemini.html#norm_finish', 'fastllm/gemini.py'),
@@ -167,13 +168,18 @@ d = { 'settings': { 'branch': 'main',
167
168
  'fastllm.openai_chat.denorm_tool_use': ('oai_chat.html#denorm_tool_use', 'fastllm/openai_chat.py'),
168
169
  'fastllm.openai_chat.denorm_user': ('oai_chat.html#denorm_user', 'fastllm/openai_chat.py'),
169
170
  'fastllm.openai_chat.denorm_web_search': ('oai_chat.html#denorm_web_search', 'fastllm/openai_chat.py'),
171
+ 'fastllm.openai_chat.fix_payload': ('oai_chat.html#fix_payload', 'fastllm/openai_chat.py'),
170
172
  'fastllm.openai_chat.get_hdrs': ('oai_chat.html#get_hdrs', 'fastllm/openai_chat.py'),
171
173
  'fastllm.openai_chat.mk_payload': ('oai_chat.html#mk_payload', 'fastllm/openai_chat.py'),
172
174
  'fastllm.openai_chat.norm_finish': ('oai_chat.html#norm_finish', 'fastllm/openai_chat.py'),
173
175
  'fastllm.openai_chat.norm_parts': ('oai_chat.html#norm_parts', 'fastllm/openai_chat.py'),
174
176
  'fastllm.openai_chat.norm_sse_event': ('oai_chat.html#norm_sse_event', 'fastllm/openai_chat.py'),
175
177
  'fastllm.openai_chat.norm_tool_calls': ('oai_chat.html#norm_tool_calls', 'fastllm/openai_chat.py')},
176
- 'fastllm.openai_responses': { 'fastllm.openai_responses._sanid': ('oai_responses.html#_sanid', 'fastllm/openai_responses.py'),
178
+ 'fastllm.openai_responses': { 'fastllm.openai_responses.Completion.response_id': ( 'oai_responses.html#completion.response_id',
179
+ 'fastllm/openai_responses.py'),
180
+ 'fastllm.openai_responses.Completion.response_id_reusable': ( 'oai_responses.html#completion.response_id_reusable',
181
+ 'fastllm/openai_responses.py'),
182
+ 'fastllm.openai_responses._sanid': ('oai_responses.html#_sanid', 'fastllm/openai_responses.py'),
177
183
  'fastllm.openai_responses.acollect_stream': ( 'oai_responses.html#acollect_stream',
178
184
  'fastllm/openai_responses.py'),
179
185
  'fastllm.openai_responses.cost': ('oai_responses.html#cost', 'fastllm/openai_responses.py'),
@@ -207,6 +213,8 @@ d = { 'settings': { 'branch': 'main',
207
213
  'fastllm/openai_responses.py'),
208
214
  'fastllm.openai_responses.denorm_web_search': ( 'oai_responses.html#denorm_web_search',
209
215
  'fastllm/openai_responses.py'),
216
+ 'fastllm.openai_responses.fix_payload': ( 'oai_responses.html#fix_payload',
217
+ 'fastllm/openai_responses.py'),
210
218
  'fastllm.openai_responses.get_hdrs': ( 'oai_responses.html#get_hdrs',
211
219
  'fastllm/openai_responses.py'),
212
220
  'fastllm.openai_responses.mk_payload': ( 'oai_responses.html#mk_payload',
@@ -223,6 +231,50 @@ d = { 'settings': { 'branch': 'main',
223
231
  'fastllm/openai_responses.py'),
224
232
  'fastllm.openai_responses.norm_usage': ( 'oai_responses.html#norm_usage',
225
233
  'fastllm/openai_responses.py')},
234
+ 'fastllm.responses': { 'fastllm.responses.AsyncResponses': ('responses.html#asyncresponses', 'fastllm/responses.py'),
235
+ 'fastllm.responses.AsyncResponses.call': ('responses.html#asyncresponses.call', 'fastllm/responses.py'),
236
+ 'fastllm.responses.AsyncResponses.events': ( 'responses.html#asyncresponses.events',
237
+ 'fastllm/responses.py'),
238
+ 'fastllm.responses.AsyncResponses.prepare': ( 'responses.html#asyncresponses.prepare',
239
+ 'fastllm/responses.py'),
240
+ 'fastllm.responses.AsyncResponses.state': ( 'responses.html#asyncresponses.state',
241
+ 'fastllm/responses.py'),
242
+ 'fastllm.responses.ResponseState': ('responses.html#responsestate', 'fastllm/responses.py'),
243
+ 'fastllm.responses.ResponseState.__init__': ( 'responses.html#responsestate.__init__',
244
+ 'fastllm/responses.py'),
245
+ 'fastllm.responses.ResponseTurn': ('responses.html#responseturn', 'fastllm/responses.py'),
246
+ 'fastllm.responses.ResponseTurn.__init__': ( 'responses.html#responseturn.__init__',
247
+ 'fastllm/responses.py'),
248
+ 'fastllm.responses.ResponseTurn.provider_previous_id': ( 'responses.html#responseturn.provider_previous_id',
249
+ 'fastllm/responses.py'),
250
+ 'fastllm.responses.ResponsesError': ('responses.html#responseserror', 'fastllm/responses.py'),
251
+ 'fastllm.responses.ResponsesError.__init__': ( 'responses.html#responseserror.__init__',
252
+ 'fastllm/responses.py'),
253
+ 'fastllm.responses._ResponseStream': ('responses.html#_responsestream', 'fastllm/responses.py'),
254
+ 'fastllm.responses._ResponseStream.__init__': ( 'responses.html#_responsestream.__init__',
255
+ 'fastllm/responses.py'),
256
+ 'fastllm.responses._ResponseStream.done_events': ( 'responses.html#_responsestream.done_events',
257
+ 'fastllm/responses.py'),
258
+ 'fastllm.responses._ResponseStream.event': ( 'responses.html#_responsestream.event',
259
+ 'fastllm/responses.py'),
260
+ 'fastllm.responses._ResponseStream.part_events': ( 'responses.html#_responsestream.part_events',
261
+ 'fastllm/responses.py'),
262
+ 'fastllm.responses._ResponseStream.text_events': ( 'responses.html#_responsestream.text_events',
263
+ 'fastllm/responses.py'),
264
+ 'fastllm.responses._ResponseStream.tool_events': ( 'responses.html#_responsestream.tool_events',
265
+ 'fastllm/responses.py'),
266
+ 'fastllm.responses._arguments': ('responses.html#_arguments', 'fastllm/responses.py'),
267
+ 'fastllm.responses._msg_parts': ('responses.html#_msg_parts', 'fastllm/responses.py'),
268
+ 'fastllm.responses._new_id': ('responses.html#_new_id', 'fastllm/responses.py'),
269
+ 'fastllm.responses._response_base': ('responses.html#_response_base', 'fastllm/responses.py'),
270
+ 'fastllm.responses._text_content': ('responses.html#_text_content', 'fastllm/responses.py'),
271
+ 'fastllm.responses.normalize_call_ids': ('responses.html#normalize_call_ids', 'fastllm/responses.py'),
272
+ 'fastllm.responses.pending_response': ('responses.html#pending_response', 'fastllm/responses.py'),
273
+ 'fastllm.responses.response_event': ('responses.html#response_event', 'fastllm/responses.py'),
274
+ 'fastllm.responses.response_input': ('responses.html#response_input', 'fastllm/responses.py'),
275
+ 'fastllm.responses.response_object': ('responses.html#response_object', 'fastllm/responses.py'),
276
+ 'fastllm.responses.response_output': ('responses.html#response_output', 'fastllm/responses.py'),
277
+ 'fastllm.responses.response_usage': ('responses.html#response_usage', 'fastllm/responses.py')},
226
278
  'fastllm.streaming': { 'fastllm.streaming.Delta': ('streaming.html#delta', 'fastllm/streaming.py'),
227
279
  'fastllm.streaming.Delta.__eq__': ('streaming.html#delta.__eq__', 'fastllm/streaming.py'),
228
280
  'fastllm.streaming.Delta.__hash__': ('streaming.html#delta.__hash__', 'fastllm/streaming.py'),
@@ -279,6 +331,7 @@ d = { 'settings': { 'branch': 'main',
279
331
  'fastllm.types.model_prices_meta': ('types.html#model_prices_meta', 'fastllm/types.py'),
280
332
  'fastllm.types.payload_kwargs': ('types.html#payload_kwargs', 'fastllm/types.py'),
281
333
  'fastllm.types.price_tier': ('types.html#price_tier', 'fastllm/types.py'),
334
+ 'fastllm.types.provider_req': ('types.html#provider_req', 'fastllm/types.py'),
282
335
  'fastllm.types.register_model_info': ('types.html#register_model_info', 'fastllm/types.py'),
283
336
  'fastllm.types.resize_b64': ('types.html#resize_b64', 'fastllm/types.py'),
284
337
  'fastllm.types.tier_rate': ('types.html#tier_rate', 'fastllm/types.py'),
@@ -3,16 +3,13 @@
3
3
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/06_acomplete.ipynb.
4
4
 
5
5
  # %% auto #0
6
- __all__ = ['specs_path', 'ant_spec', 'oai_spec', 'gem_spec', 'vendor_mapping', 'api2spec', 'defaults', 'split_vendor',
7
- 'mk_client', 'ContextWindowExceededError', 'acomplete']
6
+ __all__ = ['vendor_mapping', 'defaults', 'split_vendor', 'mk_client', 'ContextWindowExceededError', 'acomplete']
8
7
 
9
8
  # %% ../nbs/06_acomplete.ipynb #f2f57253
10
9
  import asyncio,json,httpx2
11
- from importlib.resources import files
12
10
  from fastcore.utils import *
13
11
  from fastcore.meta import *
14
- from fastspec.spec import *
15
- from fastspec.oapi import *
12
+ from fasttransport.core import AsyncHttpCli
16
13
  from fastspec.errors import APIError
17
14
 
18
15
  from .types import *
@@ -24,12 +21,6 @@ from .openai_chat import *
24
21
  from .anthropic import *
25
22
  from .gemini import *
26
23
 
27
- # %% ../nbs/06_acomplete.ipynb #6d11ac28
28
- specs_path = files('fastllm') / 'specs'
29
- ant_spec = SpecParser.from_openapi(dict2obj(json.loads((specs_path/'anthropic.json').read_text())))
30
- oai_spec = SpecParser.from_openapi(dict2obj(json.loads((specs_path/'openai.with-code-samples.json').read_text())))
31
- gem_spec = SpecParser.from_discovery(dict2obj(json.loads((specs_path/'gemini.json').read_text())))
32
-
33
24
  # %% ../nbs/06_acomplete.ipynb #32ee2546
34
25
  _codex_path = os.getenv('CODEX_AUTH_PATH', '~/.codex/auth.json')
35
26
  _codex_json = _codex_path, ('tokens','access_token')
@@ -47,11 +38,8 @@ vendor_mapping = {
47
38
  "fireworks_ai": ('openai_chat', "https://api.fireworks.ai/inference/v1", "FIREWORKS_API_KEY"),
48
39
  "qwen": ('openai_chat', "https://dashscope.aliyuncs.com/compatible-mode/v1", "QWEN_API_KEY"),
49
40
  "minimax": ('anthropic', "https://api.minimax.io/anthropic", "MINIMAX_API_KEY"),
50
- "meta_ai": ('openai', "https://api.meta.ai/v1", "META_API_KEY")
51
- }
52
-
53
- # %% ../nbs/06_acomplete.ipynb #77d27ea7
54
- api2spec = {'openai':oai_spec, 'openai_chat':oai_spec, 'anthropic':ant_spec, 'gemini':gem_spec}
41
+ "meta_ai": ('openai', "https://api.meta.ai/v1", "META_API_KEY"),
42
+ "zai": ('openai_chat', "https://api.z.ai/api/paas/v4", "ZAI_API_KEY")}
55
43
 
56
44
  # %% ../nbs/06_acomplete.ipynb #e3ed40fb
57
45
  def split_vendor(model):
@@ -96,13 +84,11 @@ def mk_client(model=None, vendor_name=None, api_name=None, api_key=None, base_ur
96
84
  api_key = get_api_key(api_key, env_api_nm)
97
85
  except KeyError: raise ValueError(f"Unknown vendor '{vendor_name}', {err_msg}")
98
86
  elif base_url and api_key: vendor_name, api_name = ifnone(vendor_name, 'custom'), ifnone(api_name, 'openai_chat')
99
- elif (api_name:=infer_api_name(model)): base_url, vendor_name = None, api_name
87
+ elif (api_name:=infer_api_name(model)): base_url, vendor_name = vendor_mapping[api_name][1], api_name
100
88
  else: raise ValueError(f"Model {model} can't be auto resolved, {err_msg}")
101
89
  api = api_registry[api_name]
102
- spec, hdrs = api2spec[api_name], api.get_hdrs(api_key)
103
- cli = OpenAPIClient(spec, headers=merge(hdrs, ifnone(xtra_hdrs, {})), timeout=timeout)
104
- if base_url is not None:
105
- for op in cli.ops: op.base_url = base_url # pyright: ignore[reportAttributeAccessIssue]
90
+ hdrs = merge(api.get_hdrs(api_key), ifnone(xtra_hdrs, {}))
91
+ cli = AsyncHttpCli(base_url, base_headers=hdrs, timeout=timeout)
106
92
  return cli, api_name, vendor_name
107
93
 
108
94
  # %% ../nbs/06_acomplete.ipynb #df851a5c
@@ -113,8 +99,8 @@ def _is_ctx_exceeded(code, msg):
113
99
  if any(x in m for x in ("string_above_max_length", "invalid 'user'")): return False
114
100
  if str(code or "").lower() == "context_length_exceeded": return True
115
101
  return any(s in m for s in ("exceed context limit", "maximum context length", "maximum context limit",
116
- "longer than the model's context length", "input tokens exceed the configured limit",
117
- "exceeds the maximum number of tokens allowed", "prompt is too long", "exceeds the context window"))
102
+ "longer than the model's context length", "input tokens exceed the configured limit",
103
+ "exceeds the maximum number of tokens allowed", "prompt is too long", "exceeds the context window"))
118
104
 
119
105
  def _classify_error(exc):
120
106
  "Upgrade generic `APIError` to a specific subclass if applicable."
@@ -134,14 +120,13 @@ async def _classify_error_stream(gen):
134
120
  # %% ../nbs/06_acomplete.ipynb #f626a4e1
135
121
  defaults = SimpleNamespace(debug_mode=False)
136
122
 
137
- def _debug_print(model, api_name, vendor_name, payload, func):
123
+ def _debug_print(model, api_name, vendor_name, payload, url):
138
124
  "Pretty-print acomplete inputs when defaults.debug_mode is set"
139
125
  from pprint import pformat
140
126
  p = dict(payload)
141
- if defaults.debug_mode == 'brief' and 'tools' in p:
142
- p['tools'] = '; '.join(o.get('name', o.get('type', o)) for o in p['tools'])
127
+ if defaults.debug_mode == 'brief' and 'tools' in p: p['tools'] = '; '.join(o.get('name', o.get('type', o)) for o in p['tools'])
143
128
  print('━'*60)
144
- print(f"\033[1;36mfastllm debug\033[0m model={model} vendor={vendor_name} api={api_name} base_url={func.base_url} path={func.path}")
129
+ print(f"\033[1;36mfastllm debug\033[0m model={model} vendor={vendor_name} api={api_name} url={url}")
145
130
  print('─'*60)
146
131
  print(f"\033[1;33mpayload:\033[0m\n{pformat(p, width=120, sort_dicts=False)}")
147
132
  print('━'*60)
@@ -169,39 +154,36 @@ async def _retry_stream(mk_gen, retries=2, retry_delay=0.5):
169
154
 
170
155
  # %% ../nbs/06_acomplete.ipynb #2379ec94
171
156
  @delegates(payload_kwargs)
172
- async def acomplete(msgs, model, api_name=None, vendor_name=None, api_key=None,
173
- base_url=None, xtra_body=None, xtra_hdrs=None, stream=False,
174
- stop_callables=None, retries=2, retry_delay=0.5, **kwargs):
157
+ async def acomplete(msgs, model, api_name=None, vendor_name=None, api_key=None, base_url=None, endpoint=None, xtra_body=None, xtra_hdrs=None,
158
+ stream=False, previous_response_id=None, stop_callables=None, retries=2, retry_delay=0.5, **kwargs):
175
159
  "Unified completion across different APIs."
176
160
  if not vendor_name and not api_name and not (base_url and api_key):
177
161
  v, m = split_vendor(model)
178
162
  if v in vendor_mapping: vendor_name, model = v, m
179
163
  elif v: api_name, model = v, m # a registered transport api (e.g. claude_code): not an HTTP vendor
180
- if api_name == 'claude_code':
181
- if not stream: raise NotImplementedError("claude_code backend supports stream=True only for now")
182
- api = api_registry[api_name]
183
- payload = api.mk_payload(msgs, model, stream=stream, **kwargs)
184
- async def _mk_gen():
185
- async for o in api.acollect_stream(payload, model=model, vendor_name='claude_code', stop_callables=stop_callables): yield o
186
- return _retry_stream(_mk_gen, retries, retry_delay)
187
- cli, api_name, vendor_name = mk_client(model=model, vendor_name=vendor_name, api_name=api_name, api_key=api_key, base_url=base_url, xtra_hdrs=xtra_hdrs)
164
+ own = api_name in api_registry and not hasattr(api_registry[api_name], 'endpoint') if api_name else False
165
+ if own: vendor_name = ifnone(vendor_name, api_name) # an own-transport api: no HTTP client to build
166
+ else: cli, api_name, vendor_name = mk_client(model=model, vendor_name=vendor_name, api_name=api_name, api_key=api_key, base_url=base_url, xtra_hdrs=xtra_hdrs)
188
167
  api = api_registry[api_name]
168
+ if previous_response_id is not None:
169
+ if not getattr(api, 'supports_previous_response_id', False):
170
+ raise ValueError('previous_response_id is not supported by this transport')
171
+ kwargs['previous_response_id'] = previous_response_id
189
172
  payload = api.mk_payload(msgs, model, stream=stream, **kwargs)
190
- payload = merge(payload, ifnone(xtra_body, {}))
191
- if vendor_name == 'codex':
192
- for k in 'temperature max_tokens max_output_tokens max_completion_tokens metadata'.split(): payload.pop(k, None)
193
- payload['store'] = False
194
- if nested_idx(payload, 'messages', -1, 'role') == 'assistant':
195
- if vendor_name == 'deepseek' and 'v4' in model: payload['messages'][-1]['prefix'] = True
196
- if vendor_name == 'moonshot' and 'kimi' in model: payload['messages'][-1]['partial'] = True
197
- func = attrgetter(api.op_path[stream])(cli)
198
- if defaults.debug_mode: _debug_print(model, api_name, vendor_name, payload, func)
199
- async def _call(): return await func(**payload)
200
- if not stream:
201
- resp = await _retry_call(_call, retries, retry_delay)
202
- return mk_completion(resp, model=model, api_name=api_name, vendor_name=vendor_name)
203
-
173
+ xtra_body = ifnone(xtra_body, {})
174
+ if fix := getattr(api, 'fix_payload', None): fix(payload, model, vendor_name)
175
+ if not own:
176
+ ep = endpoint or api.endpoint
177
+ path,params = ep(model, stream) if callable(ep) else (ep, None)
178
+ if defaults.debug_mode: _debug_print(model, api_name, vendor_name, payload | xtra_body, None if own else cli._url(path))
179
+ async def _call(): return await provider_req(cli, path, payload | xtra_body, params=params, stream=stream)
204
180
  async def _mk_gen():
205
- resp = await _call()
181
+ resp = payload if own else await _call() # an own-transport api's `acollect_stream` makes the request itself, from the payload
206
182
  async for o in api.acollect_stream(resp, model=model, vendor_name=vendor_name, stop_callables=stop_callables): yield o
207
- return _retry_stream(_mk_gen, retries, retry_delay)
183
+ if stream or own:
184
+ result = _retry_stream(_mk_gen, retries, retry_delay)
185
+ if stream: return result
186
+ async for completion in result: pass # own-transport apis are streaming-only: drain for the completion
187
+ return completion
188
+ resp = await _retry_call(_call, retries, retry_delay)
189
+ return mk_completion(resp, model=model, api_name=api_name, vendor_name=vendor_name)
@@ -343,4 +343,4 @@ def cost(usage, m):
343
343
  # %% ../nbs/04_anthropic.ipynb #f7c0b989
344
344
  api_registry.register('anthropic', norm_tool_calls=norm_tool_calls, norm_parts=norm_parts, norm_finish=norm_finish, norm_usage=norm_usage,
345
345
  finalize_usage=finalize_usage, acollect_stream=acollect_stream, mk_payload=mk_payload, cost=cost, get_hdrs=get_hdrs,
346
- op_path=('messages.messages_post','messages.messages_post'))
346
+ endpoint='/v1/messages')
@@ -71,7 +71,7 @@ def _mk_tool_result(res):
71
71
  # %% ../nbs/07_chat.ipynb #a0fcc96e
72
72
  def _call_func(tc:ToolUse, tool_schemas, ns, callf):
73
73
  "Call tool function synchronously and return formatted result"
74
- fn, valid = tc.name, {nested_idx(o,'function','name') for o in tool_schemas or []}
74
+ fn, valid = tc.name, {fs[0] for o in tool_schemas or [] if (fs:=fn_schema(o))}
75
75
  if fn not in valid: return f"Tool not defined in tool_schemas: {fn}"
76
76
  else: return callf(fn, tc.arguments, ns=ns, raise_on_err=False)
77
77
 
@@ -92,14 +92,14 @@ async def structured(
92
92
  ):
93
93
  "Return the value of the tool call (generally used for structured outputs)"
94
94
  t = lite_mk_func(tool)
95
- r = await acomplete(msgs, m, system=sp, tools=[t], tool_choice=nested_idx(t, 'function', 'name'), **kwargs)
95
+ r = await acomplete(msgs, m, system=sp, tools=[t], tool_choice=fn_schema(t)[0], **kwargs)
96
96
  return tool(**r.tool_calls[0].arguments)
97
97
 
98
98
  # %% ../nbs/07_chat.ipynb #1fe8a9bc
99
99
  def _has_search(info): return bool(info.get('search_context_cost_per_query') or info.get('supports_web_search'))
100
100
 
101
101
  # %% ../nbs/07_chat.ipynb #2d78087b
102
- effort = AttrDict({o[0]:o for o in ('low','medium','high')})
102
+ effort = AttrDict({o[0]:o for o in ('none','low','medium','high')})
103
103
  effort['x'] = 'max'
104
104
 
105
105
  # %% ../nbs/07_chat.ipynb #dc17f844
@@ -125,10 +125,9 @@ class UsageStats:
125
125
  @classmethod
126
126
  def from_response(cls, r):
127
127
  u = r.usage
128
- return cls(
129
- model=r.model or '', prompt_tokens=u.prompt_tokens or 0, completion_tokens=u.completion_tokens or 0, total_tokens=u.total_tokens or 0,
130
- cached_tokens=u.cached_tokens or 0, cache_creation_tokens=u.cache_creation_tokens or 0, reasoning_tokens=u.reasoning_tokens or 0,
131
- web_search_requests=search_count(r), cost=r.cost)
128
+ return cls(model=r.model or '', prompt_tokens=u.prompt_tokens or 0, completion_tokens=u.completion_tokens or 0,
129
+ total_tokens=u.total_tokens or 0, cached_tokens=u.cached_tokens or 0, cache_creation_tokens=u.cache_creation_tokens or 0,
130
+ reasoning_tokens=u.reasoning_tokens or 0, web_search_requests=search_count(r), cost=r.cost)
132
131
 
133
132
  def __add__(self, other):
134
133
  if other is None: return self
@@ -170,7 +169,9 @@ class AsyncChat:
170
169
  vendor_name=None, # Vendor name, one of vendor_mapping which resolves api_base/api_key automatically
171
170
  api_key=None, # API key when model can't be resolved or vendor_name is not known or codex
172
171
  base_url=None, # API base url when model can't be resolved or vendor_name is not known
172
+ endpoint=None, # Override the transport's request path, for a server mounting Responses at a custom location
173
173
  extra_headers=None, # Extra HTTP headers for custom providers
174
+ use_previous_response_id=False, # Continue tool rounds with Responses IDs instead of replaying history
174
175
  markup=0, # Cost markup multiplier (e.g. 0.5 for 50%)
175
176
  showthink=False, # Stamp streamed thinking parts to display their text rather than 🧠 glyphs
176
177
  cbs:list=None, # Chat callbacks
@@ -187,6 +188,7 @@ class AsyncChat:
187
188
  elif ns is None: ns = globals()
188
189
  self.tool_schemas = [lite_mk_func(t) for t in tools] if tools else None
189
190
  self.use = UsageStats()
191
+ self.response_id,self._response_hist_idx = None,0
190
192
  self._turn_start = 0 # index into `hist` where the current turn began, for `full`
191
193
  self.last_req_use = None # usage of the latest request only; `use` accumulates across a turn's tool-call steps
192
194
  store_attr(but='cbs')
@@ -223,7 +225,10 @@ class AsyncChat:
223
225
  def _usrtools(tcs): return L(tcs).filter(lambda o: not o.server) if tcs else None
224
226
 
225
227
  # %% ../nbs/07_chat.ipynb #19b87f53
228
+ _effort_names = dict(none='n', low='l', medium='m', high='h', xhigh='x', max='x')
229
+
226
230
  def _think_kw(model, think, vendor_name):
231
+ think = _effort_names.get(think, think)
227
232
  if not think: return {}
228
233
  if 'opus-4-7' in model:
229
234
  e = 'xhigh' if think=='h' else effort.get(think)
@@ -250,6 +255,7 @@ def _prep_call(self:AsyncChat, search, max_tokens, kwargs, stream=False, think=N
250
255
  if self.vendor_name: kwargs['vendor_name'] = self.vendor_name
251
256
  if self.api_key: kwargs['api_key'] = self.api_key
252
257
  if self.base_url: kwargs['base_url'] = self.base_url
258
+ if self.endpoint: kwargs['endpoint'] = self.endpoint
253
259
  if self.extra_headers: kwargs['xtra_headers'] = self.extra_headers
254
260
  kwargs.update(_think_kw(self.model, think, self.vendor_name))
255
261
  return max_tokens
@@ -276,19 +282,23 @@ async def astream_with_complete(self, agen, postproc=noop):
276
282
  # %% ../nbs/07_chat.ipynb #a049cf52
277
283
  @patch
278
284
  @delegates(acomplete)
279
- async def _call(self:AsyncChat, msg=None, temp=None, think=None, search=None, stream=False, max_steps=2, step=1,
280
- final_prompt=None, tool_choice=None, max_tokens=None, n_workers=8, pause=0.001, tc_timeout=7200, **kwargs):
281
- if step>max_steps+1: return
282
- max_tokens = self._prep_call(search, max_tokens, kwargs, stream=stream, think=think)
283
- self.turn_sysp, self.turn_msgs = self._prep_msg(msg)
285
+ async def _call(self:AsyncChat, msg=None, step=1, search=None, tool_choice=None, initial_body=None, **kwargs):
286
+ "One model turn plus its tool round, recursing for the next; turn-constant options come from `turn_opts`"
287
+ t = AttrDict(self.turn_opts)
288
+ if step>t.max_steps+1: return
289
+ max_tokens = self._prep_call(search, t.max_tokens, kwargs, stream=t.stream, think=t.think)
290
+ self.turn_sysp, turn_msgs = self._prep_msg(msg)
291
+ self.turn_msgs = turn_msgs[self._response_hist_idx:] if self.response_id else turn_msgs
284
292
  async for o in self._call_cbs('after_msgs'): yield o
285
293
 
286
- self.turn_kwargs, self.stream = kwargs, stream
294
+ self.turn_kwargs, self.stream = kwargs, t.stream
295
+ kw = merge(kwargs, dict(xtra_body=merge(kwargs.get('xtra_body'), initial_body))) if initial_body else kwargs
287
296
  async for o in self._call_cbs('before_acomplete'): yield o
288
- res = await acomplete(self.turn_msgs, self.model, system=self.turn_sysp, stream=stream, tools=self.tool_schemas,
289
- tool_choice=tool_choice, max_tokens=int(max_tokens), temperature=None if think else ifnone(temp,self.temp),
290
- cache_idxs=self.cache_idxs if self.cache else [], ttl=self.ttl, **self.turn_kwargs)
291
- if stream:
297
+ res = await acomplete(self.turn_msgs, self.model, system=self.turn_sysp, stream=t.stream, tools=self.tool_schemas,
298
+ tool_choice=tool_choice, max_tokens=int(max_tokens), temperature=None if t.think else ifnone(t.temp,self.temp),
299
+ previous_response_id=self.response_id if self.use_previous_response_id else None,
300
+ cache_idxs=self.cache_idxs if self.cache else [], ttl=self.ttl, **kw)
301
+ if t.stream:
292
302
  res = astream_with_complete(res, postproc=postproc)
293
303
  async for chunk in res:
294
304
  if isinstance(chunk, Thinking) and self.showthink: chunk.showthink = True
@@ -296,6 +306,10 @@ async def _call(self:AsyncChat, msg=None, temp=None, think=None, search=None, st
296
306
  res = res.value
297
307
  self.turn_res, self.turn_msg = res, contents(res)
298
308
  self.hist.append(self.turn_msg)
309
+ if self.use_previous_response_id:
310
+ if res.response_id: self.response_id,self._response_hist_idx = res.response_id,len(self.hist)
311
+ elif res.tool_calls: raise ValueError('tool-call response requires a continuation-capable transport')
312
+ else: self.response_id,self._response_hist_idx = None,0
299
313
  async for o in self._call_cbs('after_acomplete'): yield o
300
314
  self._track(self.turn_res)
301
315
  yield res
@@ -303,46 +317,51 @@ async def _call(self:AsyncChat, msg=None, temp=None, think=None, search=None, st
303
317
  self.toolloop, self.prompt, tmsg = False, None, None
304
318
  async for o in self._call_cbs('before_tool_calls'): yield o
305
319
  if tcs := _usrtools(res.tool_calls):
306
- tres = await parallel_async(_alite_call_func, tcs, timeout=tc_timeout, n_workers=n_workers, pause=pause, **self.tcdict)
320
+ tres = await parallel_async(_alite_call_func, tcs, timeout=t.tc_timeout, n_workers=t.n_workers, pause=t.pause, **self.tcdict)
307
321
  tmsg = mk_tool_res_msg(tcs, tres)
308
322
  for r in tmsg.content: yield r
309
323
  self.hist.append(tmsg)
310
324
  yield Refresh()
311
- if step>=max_steps-1 or _has_stop(tmsg.content): self.prompt,tool_choice,search = mk_msg(final_prompt),'none',False
325
+ if step>=t.max_steps-1 or _has_stop(tmsg.content): self.prompt,tool_choice,search = mk_msg(t.final_prompt),'none',False
312
326
  self.toolloop = True
313
327
 
314
328
  async for o in self._call_cbs('after_tool_calls'): yield o
315
- if self.toolloop and step <= max_steps:
329
+ if self.toolloop and step <= t.max_steps:
316
330
  try:
317
- async for result in self._call(
318
- self.prompt, temp, think, search, stream, max_steps, step+1,
319
- final_prompt, tool_choice=tool_choice, **kwargs): yield result
331
+ async for result in self._call(self.prompt, step+1, search, tool_choice, **kwargs): yield result
320
332
  except ContextWindowExceededError:
321
333
  if tmsg is not None:
322
334
  for p in tmsg.content:
323
335
  if len(p.text)>1000: p.text = _cwe_msg + trunc_str(p.text, mx=1000)
324
- async for result in self._call(
325
- self.prompt, temp, think, search, stream, max_steps, step+1,
326
- final_prompt, tool_choice='none', **kwargs): yield result
336
+ async for result in self._call(self.prompt, step+1, search, 'none', **kwargs): yield result
327
337
 
328
338
  # %% ../nbs/07_chat.ipynb #1361515a
329
339
  @patch
330
- @delegates(AsyncChat._call)
340
+ @delegates(acomplete)
331
341
  async def __call__(
332
342
  self:AsyncChat,
333
343
  msg=None, # Message str, or list of multiple message parts
334
344
  temp=None, # Override temp set on chat initialization
335
- think=None, # Thinking (l,m,h)
345
+ think=None, # Thinking effort: l/m/h/x, or the full names
336
346
  search=None, # Override search set on chat initialization (l,m,h)
337
347
  stream=False, # Stream results
338
- max_steps=2, # Maximum number of tool calls
348
+ max_steps=2, # Maximum number of tool calls
339
349
  final_prompt=_final_prompt, # Final prompt when tool calls have ran out
350
+ initial_body=None, # Extra body fields sent with the first request only; tool-loop continuations use the standard payload
340
351
  return_all=False, # Returns all intermediate ModelResponses if not streaming and has tool calls
352
+ tool_choice=None, # Force ('required'/a tool name) or disable ('none') tool use
353
+ max_tokens=None, # Response token limit (default: the model's maximum)
354
+ n_workers=8, # Max concurrent tool executions
355
+ pause=0.001, # Pause between tool submissions
356
+ tc_timeout=7200, # Per-tool-call timeout in seconds
341
357
  **kwargs
342
358
  ):
359
+ self.response_id,self._response_hist_idx = None,0
343
360
  self.use = UsageStats()
344
361
  self._turn_start = len(self.hist)
345
- result_gen = self._call(msg, temp, think, search, stream, max_steps, 1, final_prompt, **kwargs)
362
+ self.turn_opts = dict(temp=temp, think=think, stream=stream, max_steps=max_steps, final_prompt=final_prompt,
363
+ max_tokens=max_tokens, n_workers=n_workers, pause=pause, tc_timeout=tc_timeout)
364
+ result_gen = self._call(msg, 1, search, tool_choice, initial_body, **kwargs)
346
365
  if stream or return_all: return result_gen
347
366
  async for res in result_gen: pass
348
367
  return res # normal chat behavior only return last msg
@@ -406,7 +425,7 @@ _lang2tool = dict(py='python', bash='bash')
406
425
  def _active_fence_langs(tool_schemas):
407
426
  "Return set of active fence langs whose mapped tool is registered"
408
427
  if not tool_schemas: return set()
409
- names = {nested_idx(t, 'function', 'name') for t in tool_schemas}
428
+ names = {fs[0] for t in tool_schemas if (fs:=fn_schema(t))}
410
429
  return {lang for lang, tname in _lang2tool.items() if tname in names}
411
430
 
412
431
  # %% ../nbs/07_chat.ipynb #72274cdc
@@ -415,7 +434,7 @@ async def run_fence_tool(lang, code, ns):
415
434
  tname = _lang2tool[lang]
416
435
  arg = dict(code=code) if lang == 'py' else dict(cmd=code)
417
436
  res = _mk_tool_result(await call_func_async(tname, arg, ns=ns, raise_on_err=False))
418
- return mk_result_fence(trunc_str(str(res)))
437
+ return mk_result_fence(trunc_str(tool_text(res)))
419
438
 
420
439
  # %% ../nbs/07_chat.ipynb #740ee3a4
421
440
  class FenceToolCallback(ChatCallback):