python-fastllm 0.0.59__tar.gz → 0.0.61__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 (23) hide show
  1. {python_fastllm-0.0.59 → python_fastllm-0.0.61}/PKG-INFO +16 -29
  2. {python_fastllm-0.0.59 → python_fastllm-0.0.61}/README.md +15 -28
  3. {python_fastllm-0.0.59 → python_fastllm-0.0.61}/fastllm/__init__.py +1 -1
  4. {python_fastllm-0.0.59 → python_fastllm-0.0.61}/fastllm/_modidx.py +6 -1
  5. {python_fastllm-0.0.59 → python_fastllm-0.0.61}/fastllm/acomplete.py +2 -1
  6. {python_fastllm-0.0.59 → python_fastllm-0.0.61}/fastllm/anthropic.py +25 -20
  7. {python_fastllm-0.0.59 → python_fastllm-0.0.61}/fastllm/chat.py +13 -9
  8. {python_fastllm-0.0.59 → python_fastllm-0.0.61}/fastllm/gemini.py +31 -34
  9. {python_fastllm-0.0.59 → python_fastllm-0.0.61}/fastllm/model_prices.json +6517 -470
  10. {python_fastllm-0.0.59 → python_fastllm-0.0.61}/fastllm/openai_chat.py +3 -1
  11. {python_fastllm-0.0.59 → python_fastllm-0.0.61}/fastllm/openai_responses.py +6 -7
  12. {python_fastllm-0.0.59 → python_fastllm-0.0.61}/fastllm/responses.py +19 -7
  13. {python_fastllm-0.0.59 → python_fastllm-0.0.61}/fastllm/streaming.py +3 -4
  14. {python_fastllm-0.0.59 → python_fastllm-0.0.61}/fastllm/types.py +22 -23
  15. {python_fastllm-0.0.59 → python_fastllm-0.0.61}/python_fastllm.egg-info/PKG-INFO +16 -29
  16. {python_fastllm-0.0.59 → python_fastllm-0.0.61}/fastllm/codex.py +0 -0
  17. {python_fastllm-0.0.59 → python_fastllm-0.0.61}/pyproject.toml +0 -0
  18. {python_fastllm-0.0.59 → python_fastllm-0.0.61}/python_fastllm.egg-info/SOURCES.txt +0 -0
  19. {python_fastllm-0.0.59 → python_fastllm-0.0.61}/python_fastllm.egg-info/dependency_links.txt +0 -0
  20. {python_fastllm-0.0.59 → python_fastllm-0.0.61}/python_fastllm.egg-info/entry_points.txt +0 -0
  21. {python_fastllm-0.0.59 → python_fastllm-0.0.61}/python_fastllm.egg-info/requires.txt +0 -0
  22. {python_fastllm-0.0.59 → python_fastllm-0.0.61}/python_fastllm.egg-info/top_level.txt +0 -0
  23. {python_fastllm-0.0.59 → python_fastllm-0.0.61}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fastllm
3
- Version: 0.0.59
3
+ Version: 0.0.61
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
@@ -61,15 +61,11 @@ mtok = 1024
61
61
  Use the same `acomplete` interface with Claude, GPT, Gemini, or Kimi. Set `model` and any provider-specific routing arguments:
62
62
 
63
63
  ``` python
64
- models = [
65
- ('claude-sonnet-5', {}),
66
- ('gpt-4o-mini', {}),
67
- ('models/gemini-3-flash-preview', {}),
68
- ('accounts/fireworks/models/kimi-k3', dict(vendor_name='fireworks_ai'))
69
- ]
64
+ models = [('claude-sonnet-5', {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {}),
65
+ ('accounts/fireworks/models/kimi-k3', dict(vendor_name='fireworks_ai'))]
70
66
  for name, kw in models:
71
67
  r = await acomplete([user("Translate 'hello' into French. Return only the translation.")],
72
- model=name, max_tokens=mtok, **kw)
68
+ model=name, max_tokens=mtok, **kw)
73
69
  text = ''.join(p.text for p in r.message.content if isinstance(p, Text))
74
70
  print(f"{name}: {text.strip()}")
75
71
  ```
@@ -101,11 +97,8 @@ r = await stream([user("What should I do today?")], model='models/gemini-3-flash
101
97
  Define tools in the shared schema. `fastllm` translates it to the provider’s tool format. This example handles a tool request, adds the result to the history, and continues the conversation:
102
98
 
103
99
  ``` python
104
- tools = [{"type": "function", "function": {
105
- "name": "get_weather",
106
- "description": "Get current weather for a city",
107
- "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
108
- }}]
100
+ tools = [{"type": "function", "function": dict(name="get_weather", description="Get current weather for a city",
101
+ parameters=dict(type="object", properties={"city": {"type": "string"}}, required=["city"]))}]
109
102
 
110
103
  msgs = [user("What's the weather in Paris?")]
111
104
  r1 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
@@ -118,7 +111,7 @@ print("Tool calls:", r1.tool_calls)
118
111
 
119
112
  ``` python
120
113
  msgs += [r1.message, mk_tool_res_msg(r1.tool_calls, ['22°C, sunny with light clouds']),
121
- user("Should I bring a jacket? Answer in one short sentence.")]
114
+ user("Should I bring a jacket? Answer in one short sentence.")]
122
115
  r2 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
123
116
  ```
124
117
 
@@ -129,12 +122,10 @@ r2 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
129
122
  Control whether the model must use tools, can’t use tools, or decides on its own:
130
123
 
131
124
  ``` python
132
- r = await acomplete([user("Hello there!")], model='claude-sonnet-5',
133
- tools=tools, tool_choice='required', max_tokens=mtok)
125
+ r = await acomplete([user('Hello there!')], model='claude-sonnet-5', tools=tools, tool_choice='required', max_tokens=mtok)
134
126
  print("Required:", [tc.name for tc in r.tool_calls])
135
127
 
136
- r = await acomplete([user("What's the weather?")], model='claude-sonnet-5',
137
- tools=tools, tool_choice='none', max_tokens=mtok)
128
+ r = await acomplete([user("What's the weather?")], model='claude-sonnet-5', tools=tools, tool_choice='none', max_tokens=mtok)
138
129
  print("None:", r.tool_calls)
139
130
  ```
140
131
 
@@ -148,14 +139,13 @@ Set `reasoning_effort` to `low`, `medium`, or `high` for supported models. The a
148
139
  ``` python
149
140
  print("Claude: ", end='')
150
141
  r = await stream([user("What is 127 × 849? Return just the number.")], model='claude-sonnet-4-6',
151
- reasoning_effort='low', max_tokens=8192)
142
+ reasoning_effort='low', max_tokens=8192)
152
143
  for p in r.message.content:
153
144
  if isinstance(p, Thinking): print(f"Thinking excerpt: {p.text[:150]}...")
154
145
 
155
146
  print("Kimi: ", end='')
156
- r = await stream([user("What is 127 × 849? Return just the number.")],
157
- model='accounts/fireworks/models/kimi-k3', vendor_name='fireworks_ai',
158
- reasoning_effort='low', max_tokens=8192)
147
+ r = await stream([user('What is 127 × 849? Return just the number.')], model='accounts/fireworks/models/kimi-k3',
148
+ vendor_name='fireworks_ai', reasoning_effort='low', max_tokens=8192)
159
149
  for p in r.message.content:
160
150
  if isinstance(p, Thinking): print(f"Thinking excerpt: {p.text[:150]}...")
161
151
  ```
@@ -172,7 +162,7 @@ OpenAI’s Responses API supports server-side web search. Server tool calls are
172
162
  ``` python
173
163
  ws_tools = [{"type": "web_search_preview"}]
174
164
  r = await acomplete([user("What is the latest Python release? Reply with the version number only.")],
175
- model='gpt-4o-mini', tools=ws_tools, max_tokens=512)
165
+ model='gpt-4o-mini', tools=ws_tools, max_tokens=512)
176
166
  print("Server tools used:", [tc.name for tc in r.tool_calls if tc.server])
177
167
  ```
178
168
 
@@ -187,11 +177,11 @@ long_ctx = "You are an expert on the solar system. " * 200
187
177
  system = Text(long_ctx, cache_control={'type': 'ephemeral'})
188
178
 
189
179
  r1 = await acomplete([user("What is Jupiter's mass? Answer in one sentence.")],
190
- model='claude-sonnet-5', system=system, max_tokens=mtok)
180
+ model='claude-sonnet-5', system=system, max_tokens=mtok)
191
181
  print("First call: cache creation tokens", r1.usage.cache_creation_tokens)
192
182
 
193
183
  r2 = await acomplete([user("What is Saturn's mass? Answer in one sentence.")],
194
- model='claude-sonnet-5', system=system, max_tokens=mtok)
184
+ model='claude-sonnet-5', system=system, max_tokens=mtok)
195
185
  print("Second call: cache read tokens", r2.usage.cached_tokens)
196
186
  ```
197
187
 
@@ -203,10 +193,7 @@ print("Second call: cache read tokens", r2.usage.cached_tokens)
203
193
  Use `InputImage` with a model that supports images. The example sends the same image message to three providers:
204
194
 
205
195
  ``` python
206
- img_msg = Msg(role='user', content=[
207
- InputImage(sample_img_url),
208
- Text("List three visible objects, using nouns only.")
209
- ])
196
+ img_msg = Msg(role='user', content=[InputImage(sample_img_url), Text('List three visible objects, using nouns only.')])
210
197
 
211
198
  for name, kw in [('claude-sonnet-5', {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {})]:
212
199
  print(f"{name}: ", end='')
@@ -45,15 +45,11 @@ mtok = 1024
45
45
  Use the same `acomplete` interface with Claude, GPT, Gemini, or Kimi. Set `model` and any provider-specific routing arguments:
46
46
 
47
47
  ``` python
48
- models = [
49
- ('claude-sonnet-5', {}),
50
- ('gpt-4o-mini', {}),
51
- ('models/gemini-3-flash-preview', {}),
52
- ('accounts/fireworks/models/kimi-k3', dict(vendor_name='fireworks_ai'))
53
- ]
48
+ models = [('claude-sonnet-5', {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {}),
49
+ ('accounts/fireworks/models/kimi-k3', dict(vendor_name='fireworks_ai'))]
54
50
  for name, kw in models:
55
51
  r = await acomplete([user("Translate 'hello' into French. Return only the translation.")],
56
- model=name, max_tokens=mtok, **kw)
52
+ model=name, max_tokens=mtok, **kw)
57
53
  text = ''.join(p.text for p in r.message.content if isinstance(p, Text))
58
54
  print(f"{name}: {text.strip()}")
59
55
  ```
@@ -85,11 +81,8 @@ r = await stream([user("What should I do today?")], model='models/gemini-3-flash
85
81
  Define tools in the shared schema. `fastllm` translates it to the provider’s tool format. This example handles a tool request, adds the result to the history, and continues the conversation:
86
82
 
87
83
  ``` python
88
- tools = [{"type": "function", "function": {
89
- "name": "get_weather",
90
- "description": "Get current weather for a city",
91
- "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
92
- }}]
84
+ tools = [{"type": "function", "function": dict(name="get_weather", description="Get current weather for a city",
85
+ parameters=dict(type="object", properties={"city": {"type": "string"}}, required=["city"]))}]
93
86
 
94
87
  msgs = [user("What's the weather in Paris?")]
95
88
  r1 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
@@ -102,7 +95,7 @@ print("Tool calls:", r1.tool_calls)
102
95
 
103
96
  ``` python
104
97
  msgs += [r1.message, mk_tool_res_msg(r1.tool_calls, ['22°C, sunny with light clouds']),
105
- user("Should I bring a jacket? Answer in one short sentence.")]
98
+ user("Should I bring a jacket? Answer in one short sentence.")]
106
99
  r2 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
107
100
  ```
108
101
 
@@ -113,12 +106,10 @@ r2 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
113
106
  Control whether the model must use tools, can’t use tools, or decides on its own:
114
107
 
115
108
  ``` python
116
- r = await acomplete([user("Hello there!")], model='claude-sonnet-5',
117
- tools=tools, tool_choice='required', max_tokens=mtok)
109
+ r = await acomplete([user('Hello there!')], model='claude-sonnet-5', tools=tools, tool_choice='required', max_tokens=mtok)
118
110
  print("Required:", [tc.name for tc in r.tool_calls])
119
111
 
120
- r = await acomplete([user("What's the weather?")], model='claude-sonnet-5',
121
- tools=tools, tool_choice='none', max_tokens=mtok)
112
+ r = await acomplete([user("What's the weather?")], model='claude-sonnet-5', tools=tools, tool_choice='none', max_tokens=mtok)
122
113
  print("None:", r.tool_calls)
123
114
  ```
124
115
 
@@ -132,14 +123,13 @@ Set `reasoning_effort` to `low`, `medium`, or `high` for supported models. The a
132
123
  ``` python
133
124
  print("Claude: ", end='')
134
125
  r = await stream([user("What is 127 × 849? Return just the number.")], model='claude-sonnet-4-6',
135
- reasoning_effort='low', max_tokens=8192)
126
+ reasoning_effort='low', max_tokens=8192)
136
127
  for p in r.message.content:
137
128
  if isinstance(p, Thinking): print(f"Thinking excerpt: {p.text[:150]}...")
138
129
 
139
130
  print("Kimi: ", end='')
140
- r = await stream([user("What is 127 × 849? Return just the number.")],
141
- model='accounts/fireworks/models/kimi-k3', vendor_name='fireworks_ai',
142
- reasoning_effort='low', max_tokens=8192)
131
+ r = await stream([user('What is 127 × 849? Return just the number.')], model='accounts/fireworks/models/kimi-k3',
132
+ vendor_name='fireworks_ai', reasoning_effort='low', max_tokens=8192)
143
133
  for p in r.message.content:
144
134
  if isinstance(p, Thinking): print(f"Thinking excerpt: {p.text[:150]}...")
145
135
  ```
@@ -156,7 +146,7 @@ OpenAI’s Responses API supports server-side web search. Server tool calls are
156
146
  ``` python
157
147
  ws_tools = [{"type": "web_search_preview"}]
158
148
  r = await acomplete([user("What is the latest Python release? Reply with the version number only.")],
159
- model='gpt-4o-mini', tools=ws_tools, max_tokens=512)
149
+ model='gpt-4o-mini', tools=ws_tools, max_tokens=512)
160
150
  print("Server tools used:", [tc.name for tc in r.tool_calls if tc.server])
161
151
  ```
162
152
 
@@ -171,11 +161,11 @@ long_ctx = "You are an expert on the solar system. " * 200
171
161
  system = Text(long_ctx, cache_control={'type': 'ephemeral'})
172
162
 
173
163
  r1 = await acomplete([user("What is Jupiter's mass? Answer in one sentence.")],
174
- model='claude-sonnet-5', system=system, max_tokens=mtok)
164
+ model='claude-sonnet-5', system=system, max_tokens=mtok)
175
165
  print("First call: cache creation tokens", r1.usage.cache_creation_tokens)
176
166
 
177
167
  r2 = await acomplete([user("What is Saturn's mass? Answer in one sentence.")],
178
- model='claude-sonnet-5', system=system, max_tokens=mtok)
168
+ model='claude-sonnet-5', system=system, max_tokens=mtok)
179
169
  print("Second call: cache read tokens", r2.usage.cached_tokens)
180
170
  ```
181
171
 
@@ -187,10 +177,7 @@ print("Second call: cache read tokens", r2.usage.cached_tokens)
187
177
  Use `InputImage` with a model that supports images. The example sends the same image message to three providers:
188
178
 
189
179
  ``` python
190
- img_msg = Msg(role='user', content=[
191
- InputImage(sample_img_url),
192
- Text("List three visible objects, using nouns only.")
193
- ])
180
+ img_msg = Msg(role='user', content=[InputImage(sample_img_url), Text('List three visible objects, using nouns only.')])
194
181
 
195
182
  for name, kw in [('claude-sonnet-5', {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {})]:
196
183
  print(f"{name}: ", end='')
@@ -2,4 +2,4 @@
2
2
 
3
3
  - `fastllm.responses`: Provider-independent Responses resources and streaming over FastLLM"""
4
4
 
5
- __version__ = "0.0.59"
5
+ __version__ = "0.0.61"
@@ -22,6 +22,8 @@ d = { 'settings': { 'branch': 'main',
22
22
  'fastllm.anthropic': { 'fastllm.anthropic._add_cache_control': ('anthropic.html#_add_cache_control', 'fastllm/anthropic.py'),
23
23
  'fastllm.anthropic._ant_cc': ('anthropic.html#_ant_cc', 'fastllm/anthropic.py'),
24
24
  'fastllm.anthropic._ant_part_type': ('anthropic.html#_ant_part_type', 'fastllm/anthropic.py'),
25
+ 'fastllm.anthropic._clear_cache_control': ( 'anthropic.html#_clear_cache_control',
26
+ 'fastllm/anthropic.py'),
25
27
  'fastllm.anthropic._sanid': ('anthropic.html#_sanid', 'fastllm/anthropic.py'),
26
28
  'fastllm.anthropic._url_cite': ('anthropic.html#_url_cite', 'fastllm/anthropic.py'),
27
29
  'fastllm.anthropic.acollect_stream': ('anthropic.html#acollect_stream', 'fastllm/anthropic.py'),
@@ -140,7 +142,8 @@ d = { 'settings': { 'branch': 'main',
140
142
  'fastllm.codex.main': ('codex.html#main', 'fastllm/codex.py'),
141
143
  'fastllm.codex.mk_jwt': ('codex.html#mk_jwt', 'fastllm/codex.py'),
142
144
  'fastllm.codex.token_expired': ('codex.html#token_expired', 'fastllm/codex.py')},
143
- 'fastllm.gemini': { 'fastllm.gemini._fn_calls': ('gemini.html#_fn_calls', 'fastllm/gemini.py'),
145
+ 'fastllm.gemini': { 'fastllm.gemini._fn_call': ('gemini.html#_fn_call', 'fastllm/gemini.py'),
146
+ 'fastllm.gemini._fn_calls': ('gemini.html#_fn_calls', 'fastllm/gemini.py'),
144
147
  'fastllm.gemini._gem_filter_sch': ('gemini.html#_gem_filter_sch', 'fastllm/gemini.py'),
145
148
  'fastllm.gemini._gem_part_type': ('gemini.html#_gem_part_type', 'fastllm/gemini.py'),
146
149
  'fastllm.gemini._url_cite': ('gemini.html#_url_cite', 'fastllm/gemini.py'),
@@ -315,7 +318,9 @@ d = { 'settings': { 'branch': 'main',
315
318
  'fastllm.responses._msg_parts': ('responses.html#_msg_parts', 'fastllm/responses.py'),
316
319
  'fastllm.responses._new_id': ('responses.html#_new_id', 'fastllm/responses.py'),
317
320
  'fastllm.responses._response_base': ('responses.html#_response_base', 'fastllm/responses.py'),
321
+ 'fastllm.responses._server_call': ('responses.html#_server_call', 'fastllm/responses.py'),
318
322
  'fastllm.responses._text_content': ('responses.html#_text_content', 'fastllm/responses.py'),
323
+ 'fastllm.responses._visible_parts': ('responses.html#_visible_parts', 'fastllm/responses.py'),
319
324
  'fastllm.responses.normalize_call_ids': ('responses.html#normalize_call_ids', 'fastllm/responses.py'),
320
325
  'fastllm.responses.pending_response': ('responses.html#pending_response', 'fastllm/responses.py'),
321
326
  'fastllm.responses.response_event': ('responses.html#response_event', 'fastllm/responses.py'),
@@ -1,4 +1,4 @@
1
- """A high level unified function make api calls"""
1
+ """A unified async API for model completions and streaming"""
2
2
 
3
3
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/06_acomplete.ipynb.
4
4
 
@@ -33,6 +33,7 @@ vendor_mapping = {
33
33
  "codex": ('openai', 'https://chatgpt.com/backend-api/codex', 'CODEX_AUTH_TOKEN', _codex_json),
34
34
  "moonshot": ('openai_chat', "https://api.moonshot.ai/v1", "MOONSHOT_API_KEY"),
35
35
  "deepseek": ('openai_chat', "https://api.deepseek.com/v1", "DEEPSEEK_API_KEY"),
36
+ "inception": ('openai_chat', "https://api.inceptionlabs.ai/v1", "INCEPTION_API_KEY"),
36
37
  "mimo": ('anthropic', "https://api.xiaomimimo.com/anthropic", "MIMO_API_KEY"),
37
38
  "openrouter": ('openai_chat', "https://openrouter.ai/api/v1", "OPENROUTER_API_KEY"),
38
39
  "together": ('openai_chat', "https://api.together.xyz/v1", "TOGETHER_API_KEY"),
@@ -1,3 +1,5 @@
1
+ """Translate Anthropic Messages API requests, responses, and streams to and from fastllm's shared message types, with tool replay, media, and prompt caching"""
2
+
1
3
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/04_anthropic.ipynb.
2
4
 
3
5
  # %% auto #0
@@ -16,7 +18,7 @@ from fasttransport.errors import api_error_from_event
16
18
 
17
19
  from .types import *
18
20
  from aidialog.msg_parts import (MdStr, Part, PartType, mk_part, Msg, Completion, Text, Thinking, ToolUse, ToolResult, tool_text,
19
- InputImage, InputAudio, InputVideo, InputFile, data_url)
21
+ InputImage, InputAudio, InputVideo, InputFile, data_url)
20
22
  from .streaming import *
21
23
  from .streaming import mk_acollect_stream
22
24
 
@@ -44,8 +46,8 @@ def norm_usage(resp):
44
46
  pt = int(usg.get("input_tokens", 0) or 0) + cached + cache_creation
45
47
  ct = int(usg.get("output_tokens", 0) or 0)
46
48
  return Usage(prompt_tokens=pt, completion_tokens=ct, total_tokens=pt + ct,
47
- cached_tokens=cached, cache_creation_tokens=cache_creation, reasoning_tokens=0, raw=usg,
48
- search_queries=int(nested_idx(usg, 'server_tool_use', 'web_search_requests') or 0))
49
+ cached_tokens=cached, cache_creation_tokens=cache_creation, reasoning_tokens=0, raw=usg,
50
+ search_queries=int(nested_idx(usg, 'server_tool_use', 'web_search_requests') or 0))
49
51
 
50
52
  def finalize_usage(usg, parts):
51
53
  "Split reasoning tokens out of `output_tokens`: the reported `thinking_tokens`, else an estimate from the thinking text."
@@ -110,7 +112,9 @@ def norm_tr_parts(content):
110
112
  if typ=='text': parts.append(Text(b.get('text',''), raw=b))
111
113
  elif typ=='image':
112
114
  s = b['source']
113
- parts.append(InputImage(f"data:{s['media_type']};base64,{s['data']}", mime=s['media_type'], raw=b))
115
+ if s['type']=='url': parts.append(InputImage(s['url'], raw=b))
116
+ elif s['type']=='base64': parts.append(InputImage(f"data:{s['media_type']};base64,{s['data']}", mime=s['media_type'], raw=b))
117
+ else: raise ValueError(f"Unsupported Anthropic image source type: {s['type']!r}")
114
118
  else: parts.append(mk_part(**b))
115
119
  return parts
116
120
 
@@ -238,13 +242,11 @@ def denorm_reasoning(v):
238
242
  # %% ../nbs/04_anthropic.ipynb #8fa9fbb8
239
243
  def denorm_web_search(v):
240
244
  "Map canonical web_search_options to Anthropic hosted web_search tool."
241
- _max_uses = {"low": 1, "medium": 5, "high": 10}
245
+ _max_uses = dict(low=1, medium=5, high=10)
242
246
  t = {"type": "web_search_20250305", "name": "web_search"}
243
247
  if (typ := (v or {}).get("type")): t["type"] = typ
244
- if (s := (v or {}).get("search_context_size")):
245
- t["max_uses"] = _max_uses.get(s, 5)
246
- if (u := (v or {}).get("user_location", {}).get("approximate")):
247
- t["user_location"] = {"type": "approximate", **u}
248
+ if (s := (v or {}).get("search_context_size")): t["max_uses"] = _max_uses.get(s, 5)
249
+ if (u := (v or {}).get("user_location", {}).get("approximate")): t["user_location"] = {"type": "approximate", **u}
248
250
  return t
249
251
 
250
252
  # %% ../nbs/04_anthropic.ipynb #6b485275
@@ -269,12 +271,12 @@ def denorm_user(m:Msg):
269
271
  def denorm_image(p, max_sz=None):
270
272
  if (b64:=data_url(p.text)):
271
273
  data = resize_b64(b64[1], max_sz) if max_sz else b64[1]
272
- return {"type": "image", "source": {"type": "base64", "media_type": b64[0], "data": data}}
274
+ return {"type": "image", "source": dict(type="base64", media_type=b64[0], data=data)}
273
275
  return {"type": "image", "source": {"type": "url", "url": p.text}}
274
276
 
275
277
  # %% ../nbs/04_anthropic.ipynb #fc6bbdfc
276
278
  def denorm_file(p):
277
- if (b64:=data_url(p.text)): return {"type": "document", "source": {"type": "base64", "media_type": b64[0], "data": b64[1]}}
279
+ if (b64:=data_url(p.text)): return {"type": "document", "source": dict(type="base64", media_type=b64[0], data=b64[1])}
278
280
  return {"type": "document", "source": {"type": "url", "url": p.text}}
279
281
 
280
282
  # %% ../nbs/04_anthropic.ipynb #37b63cc2
@@ -303,15 +305,21 @@ def _add_cache_control(
303
305
  return msg
304
306
 
305
307
  # %% ../nbs/04_anthropic.ipynb #6bc52ef5
308
+ def _clear_cache_control(obj):
309
+ "Clear canonical cache marks from a message or part."
310
+ if isinstance(obj, Msg):
311
+ for p in obj.content: _clear_cache_control(p)
312
+ elif isinstance(obj, Part): obj.cache_control = None
313
+
306
314
  def apply_cache_idxs(
307
315
  msgs, # Canonical Msgs to mark
308
316
  cache_idxs=[-1], # Breakpoint idxs into `msgs` (tool-role msgs excluded); 0 is `sp` when given
309
317
  sp=None, # System prompt: str, Msg, or Part
310
318
  ttl=None # Cache TTL: '5m' (default) or '1h'
311
319
  ):
312
- "Strip all cache breakpoints from `msgs`, then mark `cache_idxs`; returns `sp`, marked if 0 is included"
313
- for m in msgs:
314
- for p in m.content: p.cache_control = None
320
+ "Replace cache breakpoints in `msgs` and `sp`, then return the system prompt."
321
+ for m in msgs: _clear_cache_control(m)
322
+ _clear_cache_control(sp)
315
323
  if sp:
316
324
  if 0 in cache_idxs:
317
325
  if isinstance(sp, str): sp = Text(sp)
@@ -335,10 +343,8 @@ def mk_payload(msgs, model, **kwargs):
335
343
  if tools:=kwargs.get('tools'): payload['tools'] = denorm_tool_schs(tools)
336
344
  if tchc:=kwargs.get('tool_choice'): payload['tool_choice'] = denorm_tool_choice(tchc)
337
345
  if thk:=kwargs.get('reasoning_effort'): payload.update(denorm_reasoning(thk))
338
- if (wopts:=kwargs.get('web_search_options')) is not None:
339
- payload.setdefault('tools', []).append(denorm_web_search(wopts))
340
- if (temp:=kwargs.get('temperature')) is not None:
341
- payload['temperature'] = temp
346
+ if (wopts:=kwargs.get('web_search_options')) is not None: payload.setdefault('tools', []).append(denorm_web_search(wopts))
347
+ if (temp:=kwargs.get('temperature')) is not None: payload['temperature'] = temp
342
348
  return payload
343
349
 
344
350
  # %% ../nbs/04_anthropic.ipynb #60d52c1f
@@ -367,5 +373,4 @@ def cost(usage, m):
367
373
  # %% ../nbs/04_anthropic.ipynb #f7c0b989
368
374
  api_registry.register('anthropic', norm_tool_calls=norm_tool_calls, norm_parts=norm_parts, norm_finish=norm_finish, norm_usage=norm_usage,
369
375
  finalize_usage=finalize_usage, raw_msg=raw_msg, collate_raw=collate_raw, acollect_stream=acollect_stream, mk_payload=mk_payload,
370
- cost=cost, get_hdrs=get_hdrs,
371
- endpoint='/v1/messages')
376
+ cost=cost, get_hdrs=get_hdrs, endpoint='/v1/messages')
@@ -1,4 +1,4 @@
1
- """High level chat api for fastllm similar to lisette"""
1
+ """A high-level chat API for fastllm, similar to Lisette"""
2
2
 
3
3
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/07_chat.ipynb.
4
4
 
@@ -112,9 +112,12 @@ _cwe_msg = "ContextWindowExceededError: Do no more tool calls and complete your
112
112
 
113
113
  # %% ../nbs/07_chat.ipynb #05c20a94
114
114
  def search_count(r):
115
- if cnt := nested_idx(r.usage.raw, 'server_tool_use', 'web_search_requests'): return cnt # Anthropic
116
- if cnt := nested_idx(r.usage.raw, 'server_tool_use', 'google_search'): return cnt # Gemini
117
- if cnt := nested_idx(r.usage.raw, 'web_search_requests'): return cnt # streaming with `include_usage`
115
+ raw = r.usage.raw or {}
116
+ if (cnt := raw.get('web_search_requests')) is not None: return cnt # host aggregate
117
+ if cnt := r.usage.search_queries: return cnt
118
+ legacy = raw.get('server_tool_use') or {}
119
+ if (cnt := legacy.get('web_search_requests')) is not None: return cnt # Anthropic
120
+ if (cnt := legacy.get('google_search')) is not None: return cnt # Gemini
118
121
  return 0
119
122
 
120
123
  # %% ../nbs/07_chat.ipynb #61395e0d
@@ -288,8 +291,9 @@ async def _call(self:AsyncChat, msg=None, step=1, search=None, tool_choice=None,
288
291
  async for o in self._call_cbs('after_msgs'): yield o
289
292
 
290
293
  self.turn_kwargs, self.stream = kwargs, t.stream
291
- kw = merge(kwargs, dict(xtra_body=merge(kwargs.get('xtra_body'), initial_body))) if initial_body else kwargs
292
294
  async for o in self._call_cbs('before_acomplete'): yield o
295
+ kw = self.turn_kwargs
296
+ if initial_body: kw = merge(kw, dict(xtra_body=merge(kw.get('xtra_body'), initial_body)))
293
297
  res = await acomplete(self.turn_msgs, self.model, system=self.turn_sysp, stream=t.stream, tools=self.tool_schemas,
294
298
  tool_choice=tool_choice, max_tokens=int(max_tokens), temperature=None if t.think else ifnone(t.temp,self.temp),
295
299
  previous_response_id=self.response_id if self.use_previous_response_id else None,
@@ -413,7 +417,7 @@ class StopReasonCallback(ChatCallback):
413
417
  action,msg = _handle_stop_reason(self.turn_res)
414
418
  if action != 'warning': return
415
419
  add_warning(self.chat.turn_res, msg)
416
- if self.stream: yield dict(text=f"warning: {msg}\n\n")
420
+ if self.stream: yield Text(f'\n\nwarning: {msg}\n\n')
417
421
 
418
422
  # %% ../nbs/07_chat.ipynb #aa7630b2
419
423
  _lang2tool = dict(py='python', bash='bash')
@@ -493,11 +497,11 @@ defaults.chat_callbacks = [FenceToolCallback, ToolReminderCallback, StopReasonCa
493
497
 
494
498
  # %% ../nbs/07_chat.ipynb #ead4b56d
495
499
  class StreamAccum:
496
- "Accumulate a formatted stream: append each item's `formatted`, swap in `chat.full()` on `Refresh`"
500
+ "Accumulate formatted parts; refresh from `chat.full()` on `Refresh` or `Completion`"
497
501
  def __init__(self, chat=None, mx=2000): self.chat,self.mx,self.txt,self.last = chat,mx,'',None
498
502
  def __call__(self, o):
499
503
  "Feed one stream item; True if the display text changed"
500
- if isinstance(o, Refresh):
504
+ if isinstance(o, (Refresh, Completion)):
501
505
  if self.chat is None: return False
502
506
  self.txt,self.last = self.chat.full(mx=self.mx)+'\n\n',None
503
507
  elif f := getattr(o, 'formatted', ''):
@@ -509,7 +513,7 @@ class StreamAccum:
509
513
 
510
514
  # %% ../nbs/07_chat.ipynb #944bcd25
511
515
  async def adisplay_stream(rs, chat=None, mx=2000):
512
- "Use IPython.display to markdown display the response stream, refreshing from `chat.full()` on `Refresh`."
516
+ "Display a stream as Markdown, refreshing from `chat.full()` on `Refresh` or `Completion`."
513
517
  try: from IPython.display import display, Markdown
514
518
  except ModuleNotFoundError: raise ModuleNotFoundError("This function requires ipython. Please run `pip install ipython` to use.")
515
519
  acc,h = StreamAccum(chat, mx=mx), display(Markdown(' '), display_id=True)
@@ -1,3 +1,5 @@
1
+ """Translate Gemini generateContent requests, responses, and streams to and from fastllm's shared message types, including tools, thought signatures, search grounding, and media"""
2
+
1
3
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/05_gemini.ipynb.
2
4
 
3
5
  # %% auto #0
@@ -16,7 +18,7 @@ from fasttransport.errors import api_error_from_event
16
18
 
17
19
  from .types import *
18
20
  from aidialog.msg_parts import (MdStr, Part, PartType, Msg, Completion, Text, Thinking, ToolUse, ToolResult, tool_text,
19
- InputImage, InputAudio, InputVideo, InputFile, sys_text, part_txt, data_url, url_mime)
21
+ InputImage, InputAudio, InputVideo, InputFile, sys_text, part_txt, data_url, url_mime)
20
22
  from .streaming import *
21
23
  from .streaming import mk_acollect_stream
22
24
 
@@ -41,9 +43,12 @@ def grounding(cand):
41
43
  return cites, call
42
44
 
43
45
  # %% ../nbs/05_gemini.ipynb #48de3125
44
- def _fn_calls(parts):
45
- return [ToolUse(id=fc.get("id", f"call_{i}"), name=fc.get("name", ""), arguments=fc.get("args") or {}, raw={k:v for k,v in p.items() if k != "functionCall"})
46
- for i,p in enumerate(parts) if (fc := p.get("functionCall"))]
46
+ def _fn_call(p, i):
47
+ fc = p['functionCall']
48
+ return ToolUse(id=fc.get("id", f"call_{i}"), name=fc.get("name", ""), arguments=fc.get("args") or {},
49
+ raw={k:v for k,v in p.items() if k != "functionCall"})
50
+
51
+ def _fn_calls(parts): return [_fn_call(p, i) for i,p in enumerate(parts) if p.get('functionCall')]
47
52
 
48
53
  def norm_tool_calls(resp):
49
54
  "Tool calls of a response: the `google_search` call its grounding describes, then its `functionCall` parts"
@@ -67,7 +72,7 @@ def norm_usage(resp):
67
72
  if call := grounding(cand)[1]: stu['google_search'] = len(call.arguments['queries'] or [1])
68
73
  if stu: usg['server_tool_use'] = stu
69
74
  return Usage(prompt_tokens=pt, completion_tokens=ct, total_tokens=tt,
70
- cached_tokens=cached, reasoning_tokens=reasoning, search_queries=stu.get('google_search', 0), raw=usg)
75
+ cached_tokens=cached, reasoning_tokens=reasoning, search_queries=stu.get('google_search', 0), raw=usg)
71
76
 
72
77
  # %% ../nbs/05_gemini.ipynb #7a8b1f8f
73
78
  def norm_finish(resp, tcs=None):
@@ -90,13 +95,11 @@ def norm_parts(resp):
90
95
  "Normalize Gemini generateContent response; a search's `toolCall` and `toolResponse` parts give way to the call `grounding` builds"
91
96
  c0 = nested_idx(resp, 'candidates', 0) or {}
92
97
  cites,call = grounding(c0)
93
- tc_map = {tc.id: tc for tc in norm_tool_calls(resp)}
94
98
  parts = [call] if call else []
95
- for p in nested_idx(c0, 'content', 'parts') or []:
99
+ for i,p in enumerate(nested_idx(c0, 'content', 'parts') or []):
96
100
  if 'toolCall' in p or 'toolResponse' in p: continue
97
101
  typ = _gem_part_type(p)
98
- if typ == PartType.tool_use:
99
- if tc := tc_map.get(p['functionCall'].get('id')): parts.append(tc)
102
+ if typ == PartType.tool_use: parts.append(_fn_call(p, i))
100
103
  elif typ == PartType.thinking: parts.append(Thinking(p.get("text",""), raw=p))
101
104
  elif typ == PartType.tool_result: parts.append(ToolResult(text=p.get("text",""), raw=p))
102
105
  else: parts.append(Text(p.get("text",""), raw=p))
@@ -175,15 +178,20 @@ def denorm_msgs(msgs:list[Msg]):
175
178
  return res
176
179
 
177
180
  # %% ../nbs/05_gemini.ipynb #adb525db
178
- _valid_gemini_sch = {'type', 'format', 'title', 'description', 'nullable', 'default',
179
- 'items', 'minItems', 'maxItems', 'enum', 'properties', 'propertyOrdering',
180
- 'required', 'minProperties', 'maxProperties', 'minimum', 'maximum',
181
- 'minLength', 'maxLength', 'pattern', 'example', 'anyOf'}
181
+ _valid_gemini_sch = {'type', 'format', 'title', 'description', 'nullable', 'default', 'items', 'minItems', 'maxItems', 'enum', 'properties',
182
+ 'propertyOrdering', 'required', 'minProperties', 'maxProperties', 'minimum', 'maximum', 'minLength', 'maxLength', 'pattern', 'example',
183
+ 'anyOf'}
182
184
 
183
185
  def _gem_filter_sch(s):
184
186
  if isinstance(s, list): return [_gem_filter_sch(x) for x in s]
185
187
  if not isinstance(s, dict): return s
186
- return {k: _gem_filter_sch(v) for k,v in s.items() if k in _valid_gemini_sch}
188
+ res = {}
189
+ for k,v in s.items():
190
+ if k not in _valid_gemini_sch: continue
191
+ if k == 'properties': res[k] = {name:_gem_filter_sch(prop) for name,prop in v.items()}
192
+ elif k in ('items', 'anyOf'): res[k] = _gem_filter_sch(v)
193
+ else: res[k] = v
194
+ return res
187
195
 
188
196
  def denorm_tool_schs(tools):
189
197
  "Convert canonical tools to Gemini format."
@@ -192,8 +200,7 @@ def denorm_tool_schs(tools):
192
200
  fn = fn_schema(t)
193
201
  if fn is None: other.append(t); continue
194
202
  name, desc, params = fn
195
- params = {k:v for k,v in params.items() if k in _valid_gemini_sch}
196
- params['properties'] = {k:_gem_filter_sch(v) for k,v in params['properties'].items()}
203
+ params = _gem_filter_sch(params)
197
204
  fn_decls.append(dict(name=name, description=desc, parameters=params))
198
205
  out = other[:]
199
206
  if fn_decls: out.insert(0, dict(functionDeclarations=fn_decls))
@@ -214,13 +221,11 @@ _gem_think_levels = dict(minimal='low', low='low', medium='medium', high='high'
214
221
 
215
222
  def denorm_reasoning(v, model=''):
216
223
  "Map canonical reasoning_effort to Gemini thinkingConfig (uses thinkingLevel for Gemini 3+)."
217
- err = ValueError(f"Invalid reasoning effort for Gemini: {v}, accepted string values are: {list(_gem_think_budgets)} and dicts are passthrough")
218
- if v is None: return None
219
- elif isinstance(v, dict): return v
220
- elif isinstance(v, str) and v in _gem_think_budgets:
221
- # defaults to includeThoughts same as litellm
222
- if 'gemini-3' in model: return {'thinkingLevel': _gem_think_levels.get(v, 'medium'), 'includeThoughts': True}
223
- return {'thinkingBudget': _gem_think_budgets.get(str(v).lower(), 1024), 'includeThoughts': True}
224
+ if v is None or isinstance(v, dict): return v
225
+ if not isinstance(v, str) or v not in _gem_think_budgets:
226
+ raise ValueError(f"Invalid reasoning effort for Gemini: {v!r}; expected one of {list(_gem_think_budgets)}, a dict, or None")
227
+ if 'gemini-3' in model: return {'thinkingLevel': _gem_think_levels[v], 'includeThoughts': True}
228
+ return {'thinkingBudget': _gem_think_budgets[v], 'includeThoughts': True}
224
229
 
225
230
  # %% ../nbs/05_gemini.ipynb #8fa9fbb8
226
231
  def denorm_web_search(v): return {"googleSearch": {}}
@@ -325,15 +330,7 @@ def cost(usage, m):
325
330
  return cost
326
331
 
327
332
  # %% ../nbs/05_gemini.ipynb #f7c0b989
328
- api_ns = dict(norm_tool_calls=norm_tool_calls,
329
- norm_parts=norm_parts,
330
- norm_finish=norm_finish,
331
- norm_usage=norm_usage,
332
- acollect_stream=acollect_stream,
333
- raw_msg=raw_msg,
334
- collate_raw=collate_raw,
335
- mk_payload=mk_payload,
336
- cost=cost,
337
- get_hdrs=get_hdrs,
338
- endpoint=endpoint)
333
+ api_ns = dict(norm_tool_calls=norm_tool_calls, norm_parts=norm_parts, norm_finish=norm_finish, norm_usage=norm_usage,
334
+ acollect_stream=acollect_stream, raw_msg=raw_msg, collate_raw=collate_raw, mk_payload=mk_payload, cost=cost, get_hdrs=get_hdrs,
335
+ endpoint=endpoint)
339
336
  api_registry.register('gemini', **api_ns)