python-fastllm 0.0.43__tar.gz → 0.0.45__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.43/python_fastllm.egg-info → python_fastllm-0.0.45}/PKG-INFO +27 -7
  2. python_fastllm-0.0.43/PKG-INFO → python_fastllm-0.0.45/README.md +25 -21
  3. python_fastllm-0.0.45/fastllm/__init__.py +5 -0
  4. {python_fastllm-0.0.43 → python_fastllm-0.0.45}/fastllm/_modidx.py +54 -1
  5. {python_fastllm-0.0.43 → python_fastllm-0.0.45}/fastllm/acomplete.py +36 -55
  6. {python_fastllm-0.0.43 → python_fastllm-0.0.45}/fastllm/anthropic.py +2 -2
  7. {python_fastllm-0.0.43 → python_fastllm-0.0.45}/fastllm/chat.py +51 -32
  8. {python_fastllm-0.0.43 → python_fastllm-0.0.45}/fastllm/gemini.py +13 -9
  9. {python_fastllm-0.0.43 → python_fastllm-0.0.45}/fastllm/openai_chat.py +29 -19
  10. {python_fastllm-0.0.43 → python_fastllm-0.0.45}/fastllm/openai_responses.py +46 -31
  11. python_fastllm-0.0.45/fastllm/responses.py +299 -0
  12. {python_fastllm-0.0.43 → python_fastllm-0.0.45}/fastllm/streaming.py +1 -1
  13. {python_fastllm-0.0.43 → python_fastllm-0.0.45}/fastllm/types.py +29 -8
  14. {python_fastllm-0.0.43 → python_fastllm-0.0.45}/pyproject.toml +1 -4
  15. python_fastllm-0.0.43/README.md → python_fastllm-0.0.45/python_fastllm.egg-info/PKG-INFO +41 -5
  16. {python_fastllm-0.0.43 → python_fastllm-0.0.45}/python_fastllm.egg-info/SOURCES.txt +1 -6
  17. {python_fastllm-0.0.43 → python_fastllm-0.0.45}/python_fastllm.egg-info/requires.txt +1 -1
  18. python_fastllm-0.0.43/fastllm/__init__.py +0 -1
  19. python_fastllm-0.0.43/fastllm/specs/anthropic.json +0 -1
  20. python_fastllm-0.0.43/fastllm/specs/anthropic.yml +0 -15684
  21. python_fastllm-0.0.43/fastllm/specs/gemini.json +0 -6951
  22. python_fastllm-0.0.43/fastllm/specs/openai.with-code-samples.json +0 -1
  23. python_fastllm-0.0.43/fastllm/specs/openai.with-code-samples.yml +0 -73650
  24. python_fastllm-0.0.43/fastllm/specs/spec_manifest.json +0 -17
  25. {python_fastllm-0.0.43 → python_fastllm-0.0.45}/fastllm/codex.py +0 -0
  26. {python_fastllm-0.0.43 → python_fastllm-0.0.45}/python_fastllm.egg-info/dependency_links.txt +0 -0
  27. {python_fastllm-0.0.43 → python_fastllm-0.0.45}/python_fastllm.egg-info/entry_points.txt +0 -0
  28. {python_fastllm-0.0.43 → python_fastllm-0.0.45}/python_fastllm.egg-info/top_level.txt +0 -0
  29. {python_fastllm-0.0.43 → python_fastllm-0.0.45}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fastllm
3
- Version: 0.0.43
3
+ Version: 0.0.45
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
@@ -11,7 +11,7 @@ Requires-Python: >=3.10
11
11
  Description-Content-Type: text/markdown
12
12
  Requires-Dist: fastcore>=2.1.18
13
13
  Requires-Dist: aidialog>=0.0.10
14
- Requires-Dist: fastspec>=0.2.1
14
+ Requires-Dist: fasttransport>=0.0.2
15
15
  Requires-Dist: pillow
16
16
 
17
17
  # fastllm
@@ -26,8 +26,7 @@ Clone and install locally into your `aai-ws` env
26
26
  ## Setup
27
27
 
28
28
  ``` python
29
- from fastllm.types import Completion
30
- from aidialog.msg_parts import Msg, Part, Text, Thinking, ToolUse, InputImage, mk_tool_res_msg
29
+ from aidialog.msg_parts import Msg, Part, Text, Thinking, ToolUse, InputImage, mk_tool_res_msg, Completion
31
30
  from fastllm.acomplete import acomplete
32
31
  import asyncio, json
33
32
 
@@ -178,7 +177,9 @@ print("Tool calls:", r1.tool_calls)
178
177
  ```
179
178
 
180
179
  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'}})]
180
+ - `get_weather(city="Paris")`
181
+
182
+ Tool calls: [ToolUse(raw={'caller': {'type': 'direct'}}, cache_control=None, id='toolu_01RMN1WM7vPBT3ovv5Ex6VzC', name='get_weather', arguments={'city': 'Paris'}, server=False, text=None)]
182
183
 
183
184
  ``` python
184
185
  # Provide the tool result
@@ -218,8 +219,9 @@ print("\nNo tools: ", end='')
218
219
  r = await stream([user("What's the weather?")], model='claude-sonnet-4-20250514', tools=tools, tool_choice='none', max_tokens=mtok)
219
220
  ```
220
221
 
222
+ - ⏳ `get_weather(city="<UNKNOWN>")` ⏳
221
223
 
222
- Forced: [ToolCall(id='toolu_01U7tAXjXAtwjp6xNSMbsyPU', name='get_weather', arguments={'city': '<UNKNOWN>'}, server=False, extra={'caller': {'type': 'direct'}})]
224
+ Forced: [ToolUse(raw={'caller': {'type': 'direct'}}, cache_control=None, id='toolu_01U7tAXjXAtwjp6xNSMbsyPU', name='get_weather', arguments={'city': '<UNKNOWN>'}, server=False, text=None)]
223
225
 
224
226
  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
227
 
@@ -281,7 +283,25 @@ r = await stream([user("What is the latest Python release?")], model='gpt-4o-min
281
283
  print(f"\nServer tools used: {[tc.name for tc in r.tool_calls if tc.server]}")
282
284
  ```
283
285
 
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))
286
+ GPT + web search:
287
+
288
+ ```json {.tool}
289
+ {
290
+ "id": "ws_0f48f4b49d3a4937006a2c04cb944481a3882056257ae43956",
291
+ "name": "web_search",
292
+ "args": {
293
+ "type": "search",
294
+ "queries": [
295
+ "latest Python release October 2023"
296
+ ],
297
+ "query": "latest Python release October 2023"
298
+ },
299
+ "result": "Server tool call executed.",
300
+ "server": true
301
+ }
302
+ ```
303
+
304
+ 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
305
 
286
306
  Python 3.14 introduced several significant features, including:
287
307
 
@@ -1,19 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: python-fastllm
3
- Version: 0.0.43
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.45"
@@ -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,17 +3,14 @@
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 *
16
- from fastspec.errors import APIError
12
+ from fasttransport.core import AsyncHttpCli
13
+ from fasttransport.errors import APIError
17
14
 
18
15
  from .types import *
19
16
  from aidialog.msg_parts import (Part, PartType, Msg, Text, Thinking, ToolUse, ToolResult, InputImage, InputAudio, InputVideo,
@@ -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')
@@ -48,11 +39,7 @@ vendor_mapping = {
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
41
  "meta_ai": ('openai', "https://api.meta.ai/v1", "META_API_KEY"),
51
- "zai": ('openai_chat', "https://api.z.ai/api/paas/v4", "ZAI_API_KEY")
52
- }
53
-
54
- # %% ../nbs/06_acomplete.ipynb #77d27ea7
55
- api2spec = {'openai':oai_spec, 'openai_chat':oai_spec, 'anthropic':ant_spec, 'gemini':gem_spec}
42
+ "zai": ('openai_chat', "https://api.z.ai/api/paas/v4", "ZAI_API_KEY")}
56
43
 
57
44
  # %% ../nbs/06_acomplete.ipynb #e3ed40fb
58
45
  def split_vendor(model):
@@ -97,13 +84,11 @@ def mk_client(model=None, vendor_name=None, api_name=None, api_key=None, base_ur
97
84
  api_key = get_api_key(api_key, env_api_nm)
98
85
  except KeyError: raise ValueError(f"Unknown vendor '{vendor_name}', {err_msg}")
99
86
  elif base_url and api_key: vendor_name, api_name = ifnone(vendor_name, 'custom'), ifnone(api_name, 'openai_chat')
100
- 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
101
88
  else: raise ValueError(f"Model {model} can't be auto resolved, {err_msg}")
102
89
  api = api_registry[api_name]
103
- spec, hdrs = api2spec[api_name], api.get_hdrs(api_key)
104
- cli = OpenAPIClient(spec, headers=merge(hdrs, ifnone(xtra_hdrs, {})), timeout=timeout)
105
- if base_url is not None:
106
- 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)
107
92
  return cli, api_name, vendor_name
108
93
 
109
94
  # %% ../nbs/06_acomplete.ipynb #df851a5c
@@ -114,8 +99,8 @@ def _is_ctx_exceeded(code, msg):
114
99
  if any(x in m for x in ("string_above_max_length", "invalid 'user'")): return False
115
100
  if str(code or "").lower() == "context_length_exceeded": return True
116
101
  return any(s in m for s in ("exceed context limit", "maximum context length", "maximum context limit",
117
- "longer than the model's context length", "input tokens exceed the configured limit",
118
- "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"))
119
104
 
120
105
  def _classify_error(exc):
121
106
  "Upgrade generic `APIError` to a specific subclass if applicable."
@@ -135,14 +120,13 @@ async def _classify_error_stream(gen):
135
120
  # %% ../nbs/06_acomplete.ipynb #f626a4e1
136
121
  defaults = SimpleNamespace(debug_mode=False)
137
122
 
138
- def _debug_print(model, api_name, vendor_name, payload, func):
123
+ def _debug_print(model, api_name, vendor_name, payload, url):
139
124
  "Pretty-print acomplete inputs when defaults.debug_mode is set"
140
125
  from pprint import pformat
141
126
  p = dict(payload)
142
- if defaults.debug_mode == 'brief' and 'tools' in p:
143
- 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'])
144
128
  print('━'*60)
145
- 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}")
146
130
  print('─'*60)
147
131
  print(f"\033[1;33mpayload:\033[0m\n{pformat(p, width=120, sort_dicts=False)}")
148
132
  print('━'*60)
@@ -170,39 +154,36 @@ async def _retry_stream(mk_gen, retries=2, retry_delay=0.5):
170
154
 
171
155
  # %% ../nbs/06_acomplete.ipynb #2379ec94
172
156
  @delegates(payload_kwargs)
173
- async def acomplete(msgs, model, api_name=None, vendor_name=None, api_key=None,
174
- base_url=None, xtra_body=None, xtra_hdrs=None, stream=False,
175
- 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):
176
159
  "Unified completion across different APIs."
177
160
  if not vendor_name and not api_name and not (base_url and api_key):
178
161
  v, m = split_vendor(model)
179
162
  if v in vendor_mapping: vendor_name, model = v, m
180
163
  elif v: api_name, model = v, m # a registered transport api (e.g. claude_code): not an HTTP vendor
181
- if api_name == 'claude_code':
182
- if not stream: raise NotImplementedError("claude_code backend supports stream=True only for now")
183
- api = api_registry[api_name]
184
- payload = api.mk_payload(msgs, model, stream=stream, **kwargs)
185
- async def _mk_gen():
186
- async for o in api.acollect_stream(payload, model=model, vendor_name='claude_code', stop_callables=stop_callables): yield o
187
- return _retry_stream(_mk_gen, retries, retry_delay)
188
- 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)
189
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
190
172
  payload = api.mk_payload(msgs, model, stream=stream, **kwargs)
191
- payload = merge(payload, ifnone(xtra_body, {}))
192
- if vendor_name == 'codex':
193
- for k in 'temperature max_tokens max_output_tokens max_completion_tokens metadata'.split(): payload.pop(k, None)
194
- payload['store'] = False
195
- if nested_idx(payload, 'messages', -1, 'role') == 'assistant':
196
- if vendor_name == 'deepseek' and 'v4' in model: payload['messages'][-1]['prefix'] = True
197
- if vendor_name == 'moonshot' and 'kimi' in model: payload['messages'][-1]['partial'] = True
198
- func = attrgetter(api.op_path[stream])(cli)
199
- if defaults.debug_mode: _debug_print(model, api_name, vendor_name, payload, func)
200
- async def _call(): return await func(**payload)
201
- if not stream:
202
- resp = await _retry_call(_call, retries, retry_delay)
203
- return mk_completion(resp, model=model, api_name=api_name, vendor_name=vendor_name)
204
-
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)
205
180
  async def _mk_gen():
206
- 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
207
182
  async for o in api.acollect_stream(resp, model=model, vendor_name=vendor_name, stop_callables=stop_callables): yield o
208
- 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)
@@ -12,7 +12,7 @@ import json
12
12
  from collections import Counter
13
13
  from fastcore.utils import *
14
14
  from fastcore.meta import *
15
- from fastspec.errors import api_error_from_event
15
+ from fasttransport.errors import api_error_from_event
16
16
 
17
17
  from .types import *
18
18
  from aidialog.msg_parts import (Part, PartType, mk_part, Msg, Text, Thinking, ToolUse, ToolResult, tool_text, ServerToolResult,
@@ -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')