python-fastllm 0.0.58__py3-none-any.whl → 0.0.60__py3-none-any.whl
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.
- fastllm/__init__.py +1 -1
- fastllm/_modidx.py +6 -1
- fastllm/acomplete.py +1 -1
- fastllm/anthropic.py +25 -20
- fastllm/chat.py +13 -9
- fastllm/gemini.py +31 -34
- fastllm/openai_chat.py +2 -0
- fastllm/openai_responses.py +7 -7
- fastllm/responses.py +19 -7
- fastllm/streaming.py +3 -4
- fastllm/types.py +9 -4
- python_fastllm-0.0.60.dist-info/METADATA +231 -0
- python_fastllm-0.0.60.dist-info/RECORD +18 -0
- python_fastllm-0.0.58.dist-info/METADATA +0 -373
- python_fastllm-0.0.58.dist-info/RECORD +0 -18
- {python_fastllm-0.0.58.dist-info → python_fastllm-0.0.60.dist-info}/WHEEL +0 -0
- {python_fastllm-0.0.58.dist-info → python_fastllm-0.0.60.dist-info}/entry_points.txt +0 -0
- {python_fastllm-0.0.58.dist-info → python_fastllm-0.0.60.dist-info}/top_level.txt +0 -0
fastllm/__init__.py
CHANGED
fastllm/_modidx.py
CHANGED
|
@@ -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.
|
|
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'),
|
fastllm/acomplete.py
CHANGED
fastllm/anthropic.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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":
|
|
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":
|
|
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
|
-
"
|
|
313
|
-
for m in msgs:
|
|
314
|
-
|
|
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
|
-
|
|
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')
|
fastllm/chat.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
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
|
-
|
|
116
|
-
if cnt :=
|
|
117
|
-
if cnt :=
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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)
|
fastllm/gemini.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
'
|
|
180
|
-
'
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
218
|
-
if v
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
330
|
-
|
|
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)
|
fastllm/openai_chat.py
CHANGED
fastllm/openai_responses.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Translate Responses API payloads, completed responses, and stream events to and from fastllm's shared message types"""
|
|
2
|
+
|
|
1
3
|
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/02_oai_responses.ipynb.
|
|
2
4
|
|
|
3
5
|
# %% auto #0
|
|
@@ -65,7 +67,7 @@ def norm_finish(resp, tcs=None):
|
|
|
65
67
|
|
|
66
68
|
# %% ../nbs/02_oai_responses.ipynb #bbfb99b9
|
|
67
69
|
def search_hits(parts):
|
|
68
|
-
"
|
|
70
|
+
"Fill empty server-tool results with the message's cited sources as `title: url` lines"
|
|
69
71
|
hits = {c['url']: c.get('title', '') for p in parts if isinstance(p, Text) for c in p.citations or [] if c.get('url')}
|
|
70
72
|
for p in parts:
|
|
71
73
|
if isinstance(p, ToolUse) and p.server and not p.text: p.text = MdStr('\n'.join(f"{t}: {u}" for u,t in hits.items()))
|
|
@@ -77,9 +79,7 @@ def norm_parts(resp):
|
|
|
77
79
|
for item in resp["output"]:
|
|
78
80
|
if (typ:=item["type"]) == "message":
|
|
79
81
|
for c in item["content"]:
|
|
80
|
-
if (ctyp := c["type"]) == "output_text":
|
|
81
|
-
c['citations'] = c.pop('annotations', [])
|
|
82
|
-
parts.append(Text(c['text'], citations=c['citations'], raw=c))
|
|
82
|
+
if (ctyp := c["type"]) == "output_text": parts.append(Text(c['text'], citations=list(c.get('annotations') or []), raw=c))
|
|
83
83
|
elif ctyp == "refusal": parts.append(Refusal(c['refusal'], raw=c))
|
|
84
84
|
elif typ == "reasoning":
|
|
85
85
|
for s in item["summary"]: parts.append(Thinking(s['text'], raw=item))
|
|
@@ -88,7 +88,7 @@ def norm_parts(resp):
|
|
|
88
88
|
|
|
89
89
|
# %% ../nbs/02_oai_responses.ipynb #2aaab962
|
|
90
90
|
def raw_msg(resp):
|
|
91
|
-
"
|
|
91
|
+
"Return the response output list for assistant replay, without copying"
|
|
92
92
|
return resp['output']
|
|
93
93
|
|
|
94
94
|
# %% ../nbs/02_oai_responses.ipynb #ef2d19f3
|
|
@@ -282,6 +282,7 @@ def cost(usage, m):
|
|
|
282
282
|
# %% ../nbs/02_oai_responses.ipynb #2efda2a1
|
|
283
283
|
def fix_payload(payload, model, vendor_name):
|
|
284
284
|
"Apply vendor request quirks to a built payload."
|
|
285
|
+
if model == 'gpt-6-astra': payload.pop('temperature', None)
|
|
285
286
|
if vendor_name == 'codex':
|
|
286
287
|
for k in 'temperature max_tokens max_output_tokens max_completion_tokens metadata'.split(): payload.pop(k, None)
|
|
287
288
|
payload['store'] = False
|
|
@@ -289,6 +290,5 @@ def fix_payload(payload, model, vendor_name):
|
|
|
289
290
|
# %% ../nbs/02_oai_responses.ipynb #07114b55
|
|
290
291
|
api_ns = dict(norm_tool_calls=norm_tool_calls, norm_parts=norm_parts, norm_finish=norm_finish, norm_usage=norm_usage,
|
|
291
292
|
supports_previous_response_id=True, raw_msg=raw_msg, collate_raw=collate_raw, acollect_stream=acollect_stream, mk_payload=mk_payload,
|
|
292
|
-
cost=cost, get_hdrs=get_hdrs, fix_payload=fix_payload,
|
|
293
|
-
endpoint='/responses')
|
|
293
|
+
cost=cost, get_hdrs=get_hdrs, fix_payload=fix_payload, endpoint='/responses')
|
|
294
294
|
api_registry.register('openai', **api_ns)
|
fastllm/responses.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"""Provider-independent Responses resources and streaming over FastLLM
|
|
2
2
|
|
|
3
|
-
`response_input`
|
|
3
|
+
`response_input` accepts a prompt string or an input-item array. It returns `(messages, instructions)`. System and developer text goes in the instructions; the remaining items become shared messages.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`normalize_call_ids` supplies missing client call IDs before we retain the history. Later tool results can refer to the same IDs that the client received.
|
|
6
6
|
|
|
7
|
-
`ResponseState`
|
|
7
|
+
`ResponseState` holds the public ID, model, full message history, and optional provider response ID. The host can store it under its own ownership and expiry rules. A provider ID lets the next call send just the new messages; without one, it needs the retained history.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
FastLLM calls stream internally. The helpers here turn their parts into Responses SSE frames; they don't decide storage, ownership, or billing policy."""
|
|
10
10
|
|
|
11
11
|
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/06a_responses.ipynb.
|
|
12
12
|
|
|
@@ -361,13 +361,25 @@ def done_events(self:_ResponseStream, comp):
|
|
|
361
361
|
if isinstance(part, ToolUse) and part.server:
|
|
362
362
|
events.append(self.event('response.output_item.done', output_index=self.next_index, item=server_call_item(part)))
|
|
363
363
|
self.next_index += 1
|
|
364
|
-
for
|
|
364
|
+
client_calls = [tc for tc in comp.tool_calls if not tc.server]
|
|
365
|
+
for i,tc in enumerate(client_calls):
|
|
365
366
|
if not tc.id and i < len(self.streamed_call_ids): tc.id = self.streamed_call_ids[i]
|
|
366
367
|
tc._response_item_id = self.tool_item_ids.get(tc.id)
|
|
367
368
|
for part in comp.message.content:
|
|
368
369
|
if isinstance(part, Text): part._response_item_id = self.message_id
|
|
369
370
|
return events
|
|
370
371
|
|
|
372
|
+
# %% ../nbs/06a_responses.ipynb #04a20345
|
|
373
|
+
def _server_call(result):
|
|
374
|
+
"A public server call with the host's completed result."
|
|
375
|
+
return ToolUse(id=result.id, name=result.name, arguments=result.arguments, server=True, text=result.text)
|
|
376
|
+
|
|
377
|
+
def _visible_parts(parts, results):
|
|
378
|
+
"Replace completed host calls without changing the provider's parts."
|
|
379
|
+
completed = {r.id:r for r in results}
|
|
380
|
+
return [_server_call(completed[p.id]) if isinstance(p, ToolUse) and p.id in completed else p for p in parts]
|
|
381
|
+
|
|
382
|
+
|
|
371
383
|
# %% ../nbs/06a_responses.ipynb #e10c928b
|
|
372
384
|
@patch
|
|
373
385
|
async def events(self:AsyncResponses, turn, finalize=None, stream=None):
|
|
@@ -385,11 +397,11 @@ async def events(self:AsyncResponses, turn, finalize=None, stream=None):
|
|
|
385
397
|
results = await self.server_results(turn, comp)
|
|
386
398
|
if not results or len(results) < len(comp.tool_calls): break
|
|
387
399
|
texts += [p for p in comp.message.content if isinstance(p, Text) or (isinstance(p, ToolUse) and p.server)]
|
|
388
|
-
texts += [
|
|
400
|
+
texts += [_server_call(r) for r in results]
|
|
389
401
|
turn = self.server_turn(turn, comp, results)
|
|
390
402
|
next_state = self.state(turn, comp, results)
|
|
391
403
|
comp = copy.copy(comp)
|
|
392
|
-
comp.message,comp.usage = Msg('assistant', texts + comp.message.content),usage
|
|
404
|
+
comp.message,comp.usage = Msg('assistant', texts + _visible_parts(comp.message.content, results)),usage
|
|
393
405
|
for event in state.done_events(comp): yield event
|
|
394
406
|
response = response_object(first.id, first.body, comp, first.previous.id if first.previous else None, first.created_at)
|
|
395
407
|
if finalize and (extra := await finalize(next_state, comp)): response['usage'].update(extra)
|
fastllm/streaming.py
CHANGED
|
@@ -184,7 +184,6 @@ async def mk_acollect_stream(it, index_fn, model=None, api_name=None, vendor_nam
|
|
|
184
184
|
if stop: fin = FinishReason.stop
|
|
185
185
|
fin = FinishReason.tool_calls if fin==FinishReason.stop and any(~L(tcs).attrgot('server')) else fin # recheck tool calls post collation
|
|
186
186
|
# tool calls and non-anthropic citations are yielded at the end
|
|
187
|
-
yield Completion(model,
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
raw={'deltas':deltas})
|
|
187
|
+
(((((((yield Completion(model,
|
|
188
|
+
message=Msg(role='assistant', content=parts, raw=api_registry[api_name].collate_raw(deltas) if api_name else None),
|
|
189
|
+
finish_reason=fin, usage=usg, api_name=api_name, vendor_name=vendor_name, raw={'deltas': deltas}))))))))
|
fastllm/types.py
CHANGED
|
@@ -7,7 +7,7 @@ __all__ = ['FinishReason', 'api_registry', 'model_prices_url', 'sample_img_url',
|
|
|
7
7
|
'sonn', 'sonn5', 'opus46', 'opus48', 'opus', 'opus5', 'fable', 'fable5', 'fable51', 'gpt54', 'gpt54m',
|
|
8
8
|
'gpt55', 'codex54', 'codex54m', 'codex55', 'codex53spark', 'model_info_registry', 'modern_llm',
|
|
9
9
|
'effort_codes', 'deepseek_v4_common', 'deepseek_v4_flash_prices', 'mimo_v25_common', 'codex_pricing', 'sol',
|
|
10
|
-
'terra', 'luna', 'gpt56s', 'Usage', 'APIRegistry', 'mk_completion', 'fn_schema', 'payload_kwargs',
|
|
10
|
+
'terra', 'luna', 'gpt56s', 'astra', 'Usage', 'APIRegistry', 'mk_completion', 'fn_schema', 'payload_kwargs',
|
|
11
11
|
'provider_req', 'get_api_key', 'wrap_typed', 'unwrap_typed', 'resize_b64', 'model_prices_meta',
|
|
12
12
|
'infer_api_name', 'get_model_meta', 'register_model_info', 'get_model_info', 'effort_code', 'effort_levels',
|
|
13
13
|
'resolve_effort', 'get_model_pricing', 'approx_pricing', 'is_deepseek_peak_hour', 'price_tier', 'tier_rate']
|
|
@@ -136,11 +136,11 @@ _typed_re = re.compile(r'^𝍁(\w+)𝍁(.+)$', re.S)
|
|
|
136
136
|
_typed_strs = {c.__name__: c for c in (StopResponse, FullResponse)}
|
|
137
137
|
|
|
138
138
|
def wrap_typed(s):
|
|
139
|
-
"
|
|
139
|
+
"Prefix a `str` subclass with `𝍁TypeName𝍁` for transmission as plain text"
|
|
140
140
|
return s if type(s) is str else f'𝍁{type(s).__name__}𝍁{s}'
|
|
141
141
|
|
|
142
142
|
def unwrap_typed(s):
|
|
143
|
-
"
|
|
143
|
+
"Decode a `𝍁TypeName𝍁` prefix, creating a `str` subclass for an unknown name"
|
|
144
144
|
if not (m := _typed_re.match(s)): return s
|
|
145
145
|
nm,body = m.groups()
|
|
146
146
|
return _typed_strs.setdefault(nm, type(nm, (str,), {}))(body)
|
|
@@ -317,11 +317,16 @@ for model in (haik45, sonn45, sonn46, sonn5, opus46, opus48, opus5, fable5, fabl
|
|
|
317
317
|
|
|
318
318
|
# %% ../nbs/00_types.ipynb #bb0c4c2a
|
|
319
319
|
sol,terra,luna = gpt56s = 'gpt-5.6-sol gpt-5.6-terra gpt-5.6-luna'.split()
|
|
320
|
+
astra = 'gpt-6-astra'
|
|
321
|
+
for model in (*gpt56s, astra): register_model_info(model, 'openai', base=model, max_input_tokens=272_000)
|
|
322
|
+
|
|
320
323
|
# Codex serves only the suffixed names; bare `gpt-5.6` is rejected with a ChatGPT account.
|
|
321
|
-
# Its window
|
|
324
|
+
# Its window accepts 371,331 input tokens and rejects 371,981, on all three.
|
|
322
325
|
for model in gpt56s:
|
|
323
326
|
register_model_info(model, 'codex', base=model, base_vendor_name='openai', max_input_tokens=371_000, **codex_pricing)
|
|
324
327
|
_flat_rates('codex', model)
|
|
328
|
+
register_model_info(astra, 'codex', base=astra, base_vendor_name='openai', **codex_pricing)
|
|
329
|
+
_flat_rates('codex', astra)
|
|
325
330
|
|
|
326
331
|
# %% ../nbs/00_types.ipynb #24cc47ec
|
|
327
332
|
def get_model_pricing(mn, vendor_name, million=True):
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-fastllm
|
|
3
|
+
Version: 0.0.60
|
|
4
|
+
Author-email: Kerem Turgutlu <keremturgutlu@gmail.com>
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Project-URL: Repository, https://github.com/AnswerDotAI/fastllm
|
|
7
|
+
Keywords: nbdev
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: fastcore>=2.1.18
|
|
13
|
+
Requires-Dist: aidialog>=0.0.27
|
|
14
|
+
Requires-Dist: fasttransport>=0.0.2
|
|
15
|
+
Requires-Dist: pillow
|
|
16
|
+
|
|
17
|
+
# fastllm
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->
|
|
21
|
+
|
|
22
|
+
`fastllm` provides a common async interface for Anthropic, OpenAI Responses and Chat, Gemini, and OpenAI-compatible providers such as Kimi. Define messages and tools in a shared format and call `acomplete`. Select the model and, where needed, its provider without rewriting the conversation or tool loop. Supported features vary by provider.
|
|
23
|
+
|
|
24
|
+
`AsyncResponses` provides a streaming Responses API interface over the same providers. It translates input items and events and keeps a canonical `ResponseState` between turns. For transports without provider continuation ids, it replays that state automatically.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
Run `pip install python-fastllm` or clone from github and install locally.
|
|
29
|
+
|
|
30
|
+
## Setup
|
|
31
|
+
|
|
32
|
+
``` python
|
|
33
|
+
from aidialog.msg_parts import Msg, Part, Text, Thinking, ToolUse, InputImage, mk_tool_res_msg, Completion
|
|
34
|
+
from fastllm.acomplete import acomplete
|
|
35
|
+
from fastllm.types import sample_img_url, sample_doms
|
|
36
|
+
import asyncio, json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
``` python
|
|
40
|
+
def user(text): return Msg(role='user', content=[Text(text)])
|
|
41
|
+
|
|
42
|
+
async def stream(msgs, model, max_think=0, **kw):
|
|
43
|
+
"Print streamed parts and return the final Completion; limit printed thinking deltas with max_think."
|
|
44
|
+
cnt = 0
|
|
45
|
+
async for o in await acomplete(msgs, model, stream=True, **kw):
|
|
46
|
+
if not isinstance(o, Part): continue
|
|
47
|
+
if isinstance(o, Thinking):
|
|
48
|
+
cnt += 1
|
|
49
|
+
if cnt > max_think: continue
|
|
50
|
+
print(o.formatted, end='', flush=True)
|
|
51
|
+
print()
|
|
52
|
+
return o
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
``` python
|
|
56
|
+
mtok = 1024
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Chat across providers
|
|
60
|
+
|
|
61
|
+
Use the same `acomplete` interface with Claude, GPT, Gemini, or Kimi. Set `model` and any provider-specific routing arguments:
|
|
62
|
+
|
|
63
|
+
``` python
|
|
64
|
+
models = [
|
|
65
|
+
('claude-sonnet-5', {}),
|
|
66
|
+
('gpt-4o-mini', {}),
|
|
67
|
+
('models/gemini-3-flash-preview', {}),
|
|
68
|
+
('accounts/fireworks/models/kimi-k3', dict(vendor_name='fireworks_ai'))
|
|
69
|
+
]
|
|
70
|
+
for name, kw in models:
|
|
71
|
+
r = await acomplete([user("Translate 'hello' into French. Return only the translation.")],
|
|
72
|
+
model=name, max_tokens=mtok, **kw)
|
|
73
|
+
text = ''.join(p.text for p in r.message.content if isinstance(p, Text))
|
|
74
|
+
print(f"{name}: {text.strip()}")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
claude-sonnet-5: Bonjour
|
|
78
|
+
gpt-4o-mini: Bonjour
|
|
79
|
+
models/gemini-3-flash-preview: Bonjour
|
|
80
|
+
accounts/fireworks/models/kimi-k3: Bonjour
|
|
81
|
+
|
|
82
|
+
## System prompts
|
|
83
|
+
|
|
84
|
+
Pass `system` to supply a system prompt. `fastllm` translates it to the provider’s field: Anthropic `system`, OpenAI Responses `instructions`, or Gemini `system_instruction`.
|
|
85
|
+
|
|
86
|
+
``` python
|
|
87
|
+
sys = "You are a pirate chef. Always respond in pirate speak and mention food. Use one short sentence."
|
|
88
|
+
|
|
89
|
+
print("Claude: ", end='')
|
|
90
|
+
r = await stream([user("What should I do today?")], model='claude-sonnet-5', system=sys, max_tokens=mtok)
|
|
91
|
+
|
|
92
|
+
print("Gemini: ", end='')
|
|
93
|
+
r = await stream([user("What should I do today?")], model='models/gemini-3-flash-preview', system=sys, max_tokens=mtok)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Claude: Arrr, chart a course fer the galley and cook up a hearty stew, matey!
|
|
97
|
+
Gemini: Sharpen yer cutlass and feast on spicy shark stew, ye scurvy dog!
|
|
98
|
+
|
|
99
|
+
## Tool calling
|
|
100
|
+
|
|
101
|
+
Define tools in the shared schema. `fastllm` translates it to the provider’s tool format. This example handles a tool request, adds the result to the history, and continues the conversation:
|
|
102
|
+
|
|
103
|
+
``` python
|
|
104
|
+
tools = [{"type": "function", "function": {
|
|
105
|
+
"name": "get_weather",
|
|
106
|
+
"description": "Get current weather for a city",
|
|
107
|
+
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
|
|
108
|
+
}}]
|
|
109
|
+
|
|
110
|
+
msgs = [user("What's the weather in Paris?")]
|
|
111
|
+
r1 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
|
|
112
|
+
print("Tool calls:", r1.tool_calls)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- ⏳ `get_weather(city="Paris")` ⏳
|
|
116
|
+
|
|
117
|
+
Tool calls: [ToolUse(raw={'caller': {'type': 'direct'}}, cache_control=None, id='toolu_01XacBpJHnsjtPWJWr55nA2m', name='get_weather', arguments={'city': 'Paris'}, server=False, text=None)]
|
|
118
|
+
|
|
119
|
+
``` python
|
|
120
|
+
msgs += [r1.message, mk_tool_res_msg(r1.tool_calls, ['22°C, sunny with light clouds']),
|
|
121
|
+
user("Should I bring a jacket? Answer in one short sentence.")]
|
|
122
|
+
r2 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
No, a jacket isn't necessary — it's a mild, sunny 22°C day.
|
|
126
|
+
|
|
127
|
+
## Tool Choice
|
|
128
|
+
|
|
129
|
+
Control whether the model must use tools, can’t use tools, or decides on its own:
|
|
130
|
+
|
|
131
|
+
``` python
|
|
132
|
+
r = await acomplete([user("Hello there!")], model='claude-sonnet-5',
|
|
133
|
+
tools=tools, tool_choice='required', max_tokens=mtok)
|
|
134
|
+
print("Required:", [tc.name for tc in r.tool_calls])
|
|
135
|
+
|
|
136
|
+
r = await acomplete([user("What's the weather?")], model='claude-sonnet-5',
|
|
137
|
+
tools=tools, tool_choice='none', max_tokens=mtok)
|
|
138
|
+
print("None:", r.tool_calls)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Required: ['get_weather']
|
|
142
|
+
None: []
|
|
143
|
+
|
|
144
|
+
## Thinking and extended reasoning
|
|
145
|
+
|
|
146
|
+
Set `reasoning_effort` to `low`, `medium`, or `high` for supported models. The adapters translate these values to provider-specific settings. Reasoning output uses `Thinking` parts; this example prints a short excerpt alongside the answer.
|
|
147
|
+
|
|
148
|
+
``` python
|
|
149
|
+
print("Claude: ", end='')
|
|
150
|
+
r = await stream([user("What is 127 × 849? Return just the number.")], model='claude-sonnet-4-6',
|
|
151
|
+
reasoning_effort='low', max_tokens=8192)
|
|
152
|
+
for p in r.message.content:
|
|
153
|
+
if isinstance(p, Thinking): print(f"Thinking excerpt: {p.text[:150]}...")
|
|
154
|
+
|
|
155
|
+
print("Kimi: ", end='')
|
|
156
|
+
r = await stream([user("What is 127 × 849? Return just the number.")],
|
|
157
|
+
model='accounts/fireworks/models/kimi-k3', vendor_name='fireworks_ai',
|
|
158
|
+
reasoning_effort='low', max_tokens=8192)
|
|
159
|
+
for p in r.message.content:
|
|
160
|
+
if isinstance(p, Thinking): print(f"Thinking excerpt: {p.text[:150]}...")
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Claude: 107823
|
|
164
|
+
Thinking excerpt: 127 × 849 = 127 × 800 + 127 × 49 = 101600 + 6223 = 107823...
|
|
165
|
+
Kimi: 107823
|
|
166
|
+
Thinking excerpt: 127 × 849 = 107823. Let me verify: 127*800=101600; 127*49=6223; total 107823. Return just number....
|
|
167
|
+
|
|
168
|
+
## Web Search (Server Tools)
|
|
169
|
+
|
|
170
|
+
OpenAI’s Responses API supports server-side web search. Server tool calls are normalized alongside regular tool calls:
|
|
171
|
+
|
|
172
|
+
``` python
|
|
173
|
+
ws_tools = [{"type": "web_search_preview"}]
|
|
174
|
+
r = await acomplete([user("What is the latest Python release? Reply with the version number only.")],
|
|
175
|
+
model='gpt-4o-mini', tools=ws_tools, max_tokens=512)
|
|
176
|
+
print("Server tools used:", [tc.name for tc in r.tool_calls if tc.server])
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Server tools used: ['web_search']
|
|
180
|
+
|
|
181
|
+
## Caching with Anthropic
|
|
182
|
+
|
|
183
|
+
Set a part’s `cache_control` to request prompt caching. Later requests can reuse eligible cached content. The example prints usage for two calls with the same system prompt:
|
|
184
|
+
|
|
185
|
+
``` python
|
|
186
|
+
long_ctx = "You are an expert on the solar system. " * 200
|
|
187
|
+
system = Text(long_ctx, cache_control={'type': 'ephemeral'})
|
|
188
|
+
|
|
189
|
+
r1 = await acomplete([user("What is Jupiter's mass? Answer in one sentence.")],
|
|
190
|
+
model='claude-sonnet-5', system=system, max_tokens=mtok)
|
|
191
|
+
print("First call: cache creation tokens", r1.usage.cache_creation_tokens)
|
|
192
|
+
|
|
193
|
+
r2 = await acomplete([user("What is Saturn's mass? Answer in one sentence.")],
|
|
194
|
+
model='claude-sonnet-5', system=system, max_tokens=mtok)
|
|
195
|
+
print("Second call: cache read tokens", r2.usage.cached_tokens)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
First call: cache creation tokens 2204
|
|
199
|
+
Second call: cache read tokens 2204
|
|
200
|
+
|
|
201
|
+
## Media inputs
|
|
202
|
+
|
|
203
|
+
Use `InputImage` with a model that supports images. The example sends the same image message to three providers:
|
|
204
|
+
|
|
205
|
+
``` python
|
|
206
|
+
img_msg = Msg(role='user', content=[
|
|
207
|
+
InputImage(sample_img_url),
|
|
208
|
+
Text("List three visible objects, using nouns only.")
|
|
209
|
+
])
|
|
210
|
+
|
|
211
|
+
for name, kw in [('claude-sonnet-5', {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {})]:
|
|
212
|
+
print(f"{name}: ", end='')
|
|
213
|
+
r = await stream([img_msg], model=name, max_tokens=mtok, **kw)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
claude-sonnet-5: Mountains, lake, trees
|
|
217
|
+
gpt-4o-mini: 1. Mountains
|
|
218
|
+
2. Trees
|
|
219
|
+
3. Lake
|
|
220
|
+
models/gemini-3-flash-preview: planks, lake, mountains
|
|
221
|
+
|
|
222
|
+
The media adapters support the following part types:
|
|
223
|
+
|
|
224
|
+
| Media part | Anthropic | OpenAI Responses | OpenAI Chat | Gemini |
|
|
225
|
+
|--------------|-----------|------------------|-------------|--------|
|
|
226
|
+
| `InputImage` | Yes | Yes | Yes | Yes |
|
|
227
|
+
| `InputAudio` | No | No | Yes | Yes |
|
|
228
|
+
| `InputVideo` | No | No | No | Yes |
|
|
229
|
+
| `InputFile` | Yes | Yes | Yes | Yes |
|
|
230
|
+
|
|
231
|
+
Media parts accept a URL or base64 data URL as their `text`. An unsupported combination raises `ValueError`.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
fastllm/__init__.py,sha256=RzCM0b2ATYsYmXlX_uPf0CguuCCIUz7Jl-gj65TszzE,131
|
|
2
|
+
fastllm/_modidx.py,sha256=iyQLdOYLOPp87AmgdFQhL7QUxXMKh-TooHd6swPFuMc,46958
|
|
3
|
+
fastllm/acomplete.py,sha256=2BTlr5ZZFslscSCquXes3OnwrzCmtbPxGCwZtLGdPkY,10788
|
|
4
|
+
fastllm/anthropic.py,sha256=N8gfoDftlH-WFVO3CED4OGTv5qDTDGBHzp4JevgOFss,19347
|
|
5
|
+
fastllm/chat.py,sha256=f92AOIpJipEi3GZPnTKinJvxMU2RLdu_qEKkAUjgtcU,25099
|
|
6
|
+
fastllm/codex.py,sha256=r6auZ8egia83aGkWcF4132AzW4SsBvKeDwg_CxyXj9E,5808
|
|
7
|
+
fastllm/gemini.py,sha256=56C3NJt8WLwx_XUWU-AElyfz_V5Zp2hIGmmkFhjFWn0,17527
|
|
8
|
+
fastllm/model_prices.json,sha256=pa0j96LZgklYii8howX443QbukUP1iFr3lBcucS-6Yo,2133018
|
|
9
|
+
fastllm/openai_chat.py,sha256=yGtUQbN8UBzMwDkBUALLWdlxYOcHWX78ZT2Ic8FCXWg,12489
|
|
10
|
+
fastllm/openai_responses.py,sha256=n0uzAULex61rXNWuzvxSBV9-qHpqSQfl-7KBNk_GO6w,15440
|
|
11
|
+
fastllm/responses.py,sha256=k5D3YS8zgvUBpk_qADPtvVD9CUOWl4iej_0qvi-GuVs,23166
|
|
12
|
+
fastllm/streaming.py,sha256=dQ7ZFc4JEtPanoKXizww91rmYXTODTisQJb1FlSfWMU,8765
|
|
13
|
+
fastllm/types.py,sha256=dWFeU3UGA1Z6y5Sn4lmFZfhJe2ROw7skOtg_1W9wgZE,19618
|
|
14
|
+
python_fastllm-0.0.60.dist-info/METADATA,sha256=IL1bXk4TbnCuK8kw9D7DhmIJsiDZa8gLlUFkou29iAU,9136
|
|
15
|
+
python_fastllm-0.0.60.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
16
|
+
python_fastllm-0.0.60.dist-info/entry_points.txt,sha256=WkWd04F3xVYFP5bsexaQe9Se6uelwE5ULoTtHWreO0s,133
|
|
17
|
+
python_fastllm-0.0.60.dist-info/top_level.txt,sha256=F8qodL7nEGUHGmzzqfhNKCTIr1i0D6cvudOnm3z7o0Y,8
|
|
18
|
+
python_fastllm-0.0.60.dist-info/RECORD,,
|
|
@@ -1,373 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: python-fastllm
|
|
3
|
-
Version: 0.0.58
|
|
4
|
-
Author-email: Kerem Turgutlu <keremturgutlu@gmail.com>
|
|
5
|
-
License: Apache-2.0
|
|
6
|
-
Project-URL: Repository, https://github.com/AnswerDotAI/fastllm
|
|
7
|
-
Keywords: nbdev
|
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
-
Requires-Python: >=3.10
|
|
11
|
-
Description-Content-Type: text/markdown
|
|
12
|
-
Requires-Dist: fastcore>=2.1.18
|
|
13
|
-
Requires-Dist: aidialog>=0.0.27
|
|
14
|
-
Requires-Dist: fasttransport>=0.0.2
|
|
15
|
-
Requires-Dist: pillow
|
|
16
|
-
|
|
17
|
-
# fastllm
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->
|
|
21
|
-
|
|
22
|
-
## Install
|
|
23
|
-
|
|
24
|
-
Run `pip install python-fastllm` or clone from github and install locally.
|
|
25
|
-
|
|
26
|
-
## Setup
|
|
27
|
-
|
|
28
|
-
``` python
|
|
29
|
-
from aidialog.msg_parts import Msg, Part, Text, Thinking, ToolUse, InputImage, mk_tool_res_msg, Completion
|
|
30
|
-
from fastllm.acomplete import acomplete
|
|
31
|
-
from fastllm.types import sample_img_url, sample_doms
|
|
32
|
-
import asyncio, json
|
|
33
|
-
|
|
34
|
-
# Helpers
|
|
35
|
-
def user(text): return Msg(role='user', content=[Text(text)])
|
|
36
|
-
|
|
37
|
-
async def stream(msgs, model, max_think=10, **kw):
|
|
38
|
-
"Stream a response, printing each item's `formatted` (🧠 per thinking delta). Returns the final Completion."
|
|
39
|
-
cnt = 0
|
|
40
|
-
async for o in await acomplete(msgs, model, stream=True, **kw):
|
|
41
|
-
if not isinstance(o, Part): continue
|
|
42
|
-
if isinstance(o, Thinking):
|
|
43
|
-
cnt += 1
|
|
44
|
-
if cnt > max_think: continue
|
|
45
|
-
print(o.formatted, end='', flush=True)
|
|
46
|
-
print()
|
|
47
|
-
return o
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
``` python
|
|
51
|
-
mtok = 1024
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
## Chat — One Interface, Every Provider
|
|
55
|
-
|
|
56
|
-
The same `acomplete` call works with Claude, GPT, Gemini, and Kimi. Just change the model name:
|
|
57
|
-
|
|
58
|
-
``` python
|
|
59
|
-
models = [
|
|
60
|
-
('claude-sonnet-5', {}),
|
|
61
|
-
('gpt-4o-mini', {}),
|
|
62
|
-
('models/gemini-3-flash-preview', {}),
|
|
63
|
-
('accounts/fireworks/models/kimi-k2p5', dict(vendor_name='fireworks_ai'))
|
|
64
|
-
]
|
|
65
|
-
for name, kw in models:
|
|
66
|
-
r = await acomplete([user("Say 'hello' in French.")], model=name, max_tokens=mtok, **kw)
|
|
67
|
-
print(f"{name:>30s} → {r.message.content[0].text.strip()}")
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
claude-sonnet-4-20250514 → Bonjour!
|
|
71
|
-
gpt-4o-mini → In French, "hello" is said as "bonjour."
|
|
72
|
-
models/gemini-3-flash-preview → Bonjour.
|
|
73
|
-
accounts/fireworks/models/kimi-k2p5 → The user is asking me to say "hello" in French. This is a very straightforward request. The common ways to say hello in French are:
|
|
74
|
-
|
|
75
|
-
1. "Bonjour" - the standard, formal way to say hello/good day
|
|
76
|
-
2. "Salut" - informal way to say hi/hello (also used for goodbye)
|
|
77
|
-
3. "Bonsoir" - good evening
|
|
78
|
-
4. "Coucou" - very informal, cute way to say hi
|
|
79
|
-
|
|
80
|
-
Since the user just asked for "hello" without specifying context, "Bonjour" is the most appropriate and standard answer. I should provide the most common translation and perhaps mention the informal alternative for completeness.
|
|
81
|
-
|
|
82
|
-
The response should be simple and direct.
|
|
83
|
-
|
|
84
|
-
## System Prompts
|
|
85
|
-
|
|
86
|
-
Pass a system prompt to any provider — `fastllm` maps it to each API’s native mechanism (Anthropic `system`, OpenAI `instructions`, Gemini `system_instruction`):
|
|
87
|
-
|
|
88
|
-
``` python
|
|
89
|
-
sys = "You are a pirate chef. Always respond in pirate speak and mention food."
|
|
90
|
-
|
|
91
|
-
print("Claude: ", end='')
|
|
92
|
-
r = await stream([user("What should I do today?")], model='claude-sonnet-5', system=sys, max_tokens=mtok)
|
|
93
|
-
|
|
94
|
-
print("Gemini: ", end='')
|
|
95
|
-
r = await stream([user("What should I do today?")], model='models/gemini-3-flash-preview', system=sys, max_tokens=mtok)
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
Claude: Ahoy there, me hearty! *tips chef's hat with a feathered plume*
|
|
99
|
-
|
|
100
|
-
Ye be askin' what to do on this fine day, eh? Well, shiver me spatulas, I've got some tasty suggestions for ye!
|
|
101
|
-
|
|
102
|
-
First, ye should be raidin' yer galley (that be yer kitchen, landlubber!) and whip up some grub fit for a crew! Perhaps some hearty sea biscuits with honey, or a fine fish stew that'll warm yer bones like treasure warms the heart!
|
|
103
|
-
|
|
104
|
-
Then, if the weather be fair, take yerself outside and feel that salty breeze on yer face while ye munch on some portable provisions - maybe some dried fruits and nuts, perfect for any seafarin' adventure!
|
|
105
|
-
|
|
106
|
-
And if ye be feelin' ambitious, batten down the hatches and try cookin' somethin' new! A spicy jambalaya or some coconut rice that'll transport ye straight to the Caribbean seas!
|
|
107
|
-
|
|
108
|
-
Remember, me matey - a pirate's day ain't complete without good food in yer belly and the spirit of adventure in yer heart! Now off with ye, and may yer meals be as bountiful as buried treasure!
|
|
109
|
-
|
|
110
|
-
*waves wooden spoon like a cutlass*
|
|
111
|
-
|
|
112
|
-
Arrr, what say ye? Does any of this grub sound temptin' to yer taste buds?
|
|
113
|
-
Gemini: Ahoy there, ye scurvy bilge-rat! If ye be lookin' for a way to spend yer daylight hours, I’ve got just the plan for a hungry soul like yerself.
|
|
114
|
-
|
|
115
|
-
First, ye should sharpen yer cutlass—not for fightin' off the Royal Navy, mind ye, but for slicin' through a thick slab of **salted pork** and some **ripe mangoes**! A dull blade is the mark of a lazy cook, and we'll have none of that on this vessel.
|
|
116
|
-
|
|
117
|
-
Once yer steel is keen, head down to the shoreline and see if ye can't scavenge some **clams** or a **fat crustacean** from the tide pools. If ye find a crab the size of a cannonball, we'll toss it in the pot with some stolen spices and a splash of grog to make a **bisque** that’ll make yer toes curl!
|
|
118
|
-
|
|
119
|
-
And if the wind be light, ye can spend the afternoon scrubbin' the barnacles off the hull—it builds an appetite for a massive bowl of **hardtack and lobscouse stew**.
|
|
120
|
-
|
|
121
|
-
Now, quit yer lollygaggin' and get to the galley! There be **onions** that won't peel themselves, and I'll be hornswoggled if I let a single clove of **garlic** go to waste! Arrr!
|
|
122
|
-
|
|
123
|
-
## Tool Calling — Define Once, Use Anywhere
|
|
124
|
-
|
|
125
|
-
Define tools in a single canonical format. `fastllm` translates to each provider’s native tool schema automatically. Here Claude requests the tool, receives the result, and answers:
|
|
126
|
-
|
|
127
|
-
``` python
|
|
128
|
-
tools = [{"type": "function", "function": {
|
|
129
|
-
"name": "get_weather",
|
|
130
|
-
"description": "Get current weather for a city",
|
|
131
|
-
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
|
|
132
|
-
}}]
|
|
133
|
-
|
|
134
|
-
# Turn 1: Claude requests the tool
|
|
135
|
-
msgs = [user("What's the weather in Paris?")]
|
|
136
|
-
r1 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
|
|
137
|
-
print("Tool calls:", r1.tool_calls)
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
I'll check the current weather in Paris for you.
|
|
141
|
-
- ⏳ `get_weather(city="Paris")` ⏳
|
|
142
|
-
|
|
143
|
-
Tool calls: [ToolUse(raw={'caller': {'type': 'direct'}}, cache_control=None, id='toolu_01RMN1WM7vPBT3ovv5Ex6VzC', name='get_weather', arguments={'city': 'Paris'}, server=False, text=None)]
|
|
144
|
-
|
|
145
|
-
``` python
|
|
146
|
-
# Provide the tool result, then continue the same conversation
|
|
147
|
-
msgs += [r1.message, mk_tool_res_msg(r1.tool_calls, ['22°C, sunny with light clouds']), user("Should I bring a jacket?")]
|
|
148
|
-
print("Claude: ", end='')
|
|
149
|
-
r2 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
GPT: With the temperature at 22°C and sunny with light clouds, a jacket isn't necessary for most people. However, if you tend to feel cold easily or if you plan to be out in the evening when it might cool down a bit, it could be a good idea to bring a light jacket.
|
|
153
|
-
|
|
154
|
-
## Tool Choice
|
|
155
|
-
|
|
156
|
-
Control whether the model must use tools, can’t use tools, or decides on its own:
|
|
157
|
-
|
|
158
|
-
``` python
|
|
159
|
-
# Force the model to call a tool (even for a greeting)
|
|
160
|
-
r = await stream([user("Hello there!")], model='claude-sonnet-5', tools=tools, tool_choice='required', max_tokens=mtok)
|
|
161
|
-
print("Forced:", r.tool_calls)
|
|
162
|
-
|
|
163
|
-
# Prevent tool use (model must answer directly)
|
|
164
|
-
print("\nNo tools: ", end='')
|
|
165
|
-
r = await stream([user("What's the weather?")], model='claude-sonnet-5', tools=tools, tool_choice='none', max_tokens=mtok)
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
- ⏳ `get_weather(city="<UNKNOWN>")` ⏳
|
|
169
|
-
|
|
170
|
-
Forced: [ToolUse(raw={'caller': {'type': 'direct'}}, cache_control=None, id='toolu_01U7tAXjXAtwjp6xNSMbsyPU', name='get_weather', arguments={'city': '<UNKNOWN>'}, server=False, text=None)]
|
|
171
|
-
|
|
172
|
-
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?
|
|
173
|
-
|
|
174
|
-
## Thinking / Extended Reasoning
|
|
175
|
-
|
|
176
|
-
Enable model reasoning with `reasoning_effort`. The canonical values (`low`, `medium`, `high`) map to each provider’s native budget system. Thinking tokens stream as 🧠:
|
|
177
|
-
|
|
178
|
-
``` python
|
|
179
|
-
# Claude with thinking
|
|
180
|
-
print("Claude: ", end='')
|
|
181
|
-
r = await stream([user("What is 127 × 849?")], model='claude-sonnet-4-6', reasoning_effort='low', max_tokens=8192)
|
|
182
|
-
for p in r.message.content:
|
|
183
|
-
if isinstance(p, Thinking): print(f"\n🧠 {p.text[:150]}...")
|
|
184
|
-
|
|
185
|
-
# Kimi with thinking — same interface
|
|
186
|
-
print("\nKimi: ", end='')
|
|
187
|
-
r = await stream([user("What is 127 × 849?")], model='accounts/fireworks/models/kimi-k2p5', vendor_name='fireworks_ai', reasoning_effort='low', max_tokens=8192)
|
|
188
|
-
for p in r.message.content:
|
|
189
|
-
if isinstance(p, Thinking): print(f"\n🧠 {p.text[:150]}...")
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
Claude: 🧠🧠🧠## Calculating 127 × 849
|
|
193
|
-
|
|
194
|
-
**Breaking it down:**
|
|
195
|
-
- 127 × 800 = 101,600
|
|
196
|
-
- 127 × 40 = 5,080
|
|
197
|
-
- 127 × 9 = 1,143
|
|
198
|
-
|
|
199
|
-
**Adding the parts:**
|
|
200
|
-
101,600 + 5,080 + 1,143 = **107,823**
|
|
201
|
-
|
|
202
|
-
🧠 127 × 849:
|
|
203
|
-
127 × 800 = 101,600
|
|
204
|
-
127 × 49 = 127 × 50 - 127 = 6350 - 127 = 6223
|
|
205
|
-
Total: 107,823...
|
|
206
|
-
|
|
207
|
-
Kimi: 🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠**107,823**
|
|
208
|
-
|
|
209
|
-
Here's the calculation:
|
|
210
|
-
- 127 × 800 = 101,600
|
|
211
|
-
- 127 × 40 = 5,080
|
|
212
|
-
- 127 × 9 = 1,143
|
|
213
|
-
- **Total: 101,600 + 5,080 + 1,143 = 107,823**
|
|
214
|
-
|
|
215
|
-
🧠 The user is asking for the product of 127 and 849. I need to calculate 127 × 849.
|
|
216
|
-
|
|
217
|
-
Let me calculate this step by step.
|
|
218
|
-
|
|
219
|
-
Method 1: Standard multiplicat...
|
|
220
|
-
|
|
221
|
-
## Web Search (Server Tools)
|
|
222
|
-
|
|
223
|
-
OpenAI’s Responses API supports server-side web search. Server tool calls are normalized alongside regular tool calls:
|
|
224
|
-
|
|
225
|
-
``` python
|
|
226
|
-
ws_tools = [{"type": "web_search_preview"}]
|
|
227
|
-
print("GPT + web search: ", end='')
|
|
228
|
-
r = await stream([user("What is the latest Python release?")], model='gpt-4o-mini', tools=ws_tools, max_tokens=512)
|
|
229
|
-
print(f"\nServer tools used: {[tc.name for tc in r.tool_calls if tc.server]}")
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
GPT + web search:
|
|
233
|
-
|
|
234
|
-
```json {.tool}
|
|
235
|
-
{
|
|
236
|
-
"id": "ws_0f48f4b49d3a4937006a2c04cb944481a3882056257ae43956",
|
|
237
|
-
"name": "web_search",
|
|
238
|
-
"args": {
|
|
239
|
-
"type": "search",
|
|
240
|
-
"queries": [
|
|
241
|
-
"latest Python release October 2023"
|
|
242
|
-
],
|
|
243
|
-
"query": "latest Python release October 2023"
|
|
244
|
-
},
|
|
245
|
-
"result": "Server tool call executed.",
|
|
246
|
-
"server": true
|
|
247
|
-
}
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
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))
|
|
251
|
-
|
|
252
|
-
Python 3.14 introduced several significant features, including:
|
|
253
|
-
|
|
254
|
-
- **PEP 779**: Official support for free-threaded Python, allowing threads to run more concurrently.
|
|
255
|
-
- **PEP 649**: Deferred evaluation of annotations, improving the semantics of using annotations.
|
|
256
|
-
- **PEP 750**: Template string literals (t-strings) for custom string processing, using the familiar syntax of f-strings.
|
|
257
|
-
- **PEP 734**: Support for multiple interpreters in the standard library.
|
|
258
|
-
- **PEP 784**: A new module `compression.zstd` providing support for the Zstandard compression algorithm.
|
|
259
|
-
|
|
260
|
-
For a comprehensive list of changes and improvements in Python 3.14, you can refer to the official release notes. ([docs.python.org](https://docs.python.org/it/3.14/whatsnew/3.14.html?utm_source=openai))
|
|
261
|
-
|
|
262
|
-
You can download the latest version of Python from the official Python website. ([python.org](https://www.python.org/downloads/?lang=python&utm_source=openai))
|
|
263
|
-
|
|
264
|
-
Server tools used: ['web_search']
|
|
265
|
-
|
|
266
|
-
## Caching (Anthropic)
|
|
267
|
-
|
|
268
|
-
Anthropic supports prompt caching via `cache_control`. Set it on a part’s `cache_control` — repeat calls with the same cached content save tokens:
|
|
269
|
-
|
|
270
|
-
``` python
|
|
271
|
-
# Cache a long system prompt (must be >1024 tokens for Anthropic caching)
|
|
272
|
-
long_ctx = "You are an expert on the solar system. " * 200
|
|
273
|
-
system = Text(long_ctx, cache_control={'type': 'ephemeral'})
|
|
274
|
-
|
|
275
|
-
print("Call 1: ", end='')
|
|
276
|
-
r1 = await stream([user("What is Jupiter's mass?")], model='claude-sonnet-5', system=system, max_tokens=mtok)
|
|
277
|
-
print(f"Usage: {r1.usage}")
|
|
278
|
-
|
|
279
|
-
print("Call 2: ", end='')
|
|
280
|
-
r2 = await stream([user("How about Saturn?")], model='claude-sonnet-5', system=system, max_tokens=mtok)
|
|
281
|
-
print(f"Usage: {r2.usage}")
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
Call 1: Jupiter's mass is approximately 1.898 × 10²⁷ kilograms (or 1,898,000,000,000,000,000,000,000,000 kg).
|
|
285
|
-
|
|
286
|
-
To put this in perspective:
|
|
287
|
-
- Jupiter is about 318 times more massive than Earth
|
|
288
|
-
- It contains more than twice the mass of all other planets in our solar system combined
|
|
289
|
-
- Jupiter's mass is about 1/1047th the mass of the Sun
|
|
290
|
-
|
|
291
|
-
This enormous mass gives Jupiter its strong gravitational influence, which helps it act as a "cosmic vacuum cleaner" by capturing asteroids and comets that might otherwise threaten the inner planets.
|
|
292
|
-
Usage: Usage(prompt_tokens=1814, completion_tokens=145, total_tokens=1959, cached_tokens=0, cache_creation_tokens=1802, reasoning_tokens=0, raw={'input_tokens': 12, 'cache_creation_input_tokens': 1802, 'cache_read_input_tokens': 0, 'output_tokens': 145})
|
|
293
|
-
Call 2: Saturn is truly one of the most spectacular planets in our solar system! Here are some key facts about this gas giant:
|
|
294
|
-
|
|
295
|
-
**Basic Characteristics:**
|
|
296
|
-
- Sixth planet from the Sun
|
|
297
|
-
- Second largest planet (after Jupiter)
|
|
298
|
-
- Composed primarily of hydrogen and helium
|
|
299
|
-
- Has the lowest density of any planet - it would actually float in water!
|
|
300
|
-
|
|
301
|
-
**Famous Ring System:**
|
|
302
|
-
- Most prominent and extensive ring system in the solar system
|
|
303
|
-
- Made primarily of ice particles and rocky debris
|
|
304
|
-
- Rings are organized into distinct sections (A, B, C rings are the main ones)
|
|
305
|
-
- Ring particles range from tiny ice crystals to house-sized chunks
|
|
306
|
-
|
|
307
|
-
**Moons:**
|
|
308
|
-
- Has 146 confirmed moons (the most of any planet)
|
|
309
|
-
- Titan is its largest moon - larger than Mercury and has a thick atmosphere
|
|
310
|
-
- Enceladus has geysers of water ice erupting from its south pole
|
|
311
|
-
- Many other fascinating moons like Iapetus, Mimas, and Dione
|
|
312
|
-
|
|
313
|
-
**Physical Features:**
|
|
314
|
-
- Takes about 29.5 Earth years to orbit the Sun
|
|
315
|
-
- A day on Saturn is only about 10.7 hours
|
|
316
|
-
- Has extreme winds reaching up to 1,800 km/h
|
|
317
|
-
- Beautiful hexagonal storm at its north pole
|
|
318
|
-
|
|
319
|
-
**Exploration:**
|
|
320
|
-
- Visited by Pioneer 11, Voyager 1 & 2, and most notably the Cassini mission (2004-2017)
|
|
321
|
-
|
|
322
|
-
Is there a particular aspect of Saturn you'd like to know more about?
|
|
323
|
-
Usage: Usage(prompt_tokens=1812, completion_tokens=337, total_tokens=2149, cached_tokens=1802, cache_creation_tokens=0, reasoning_tokens=0, raw={'input_tokens': 10, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 1802, 'output_tokens': 337})
|
|
324
|
-
|
|
325
|
-
## Media Inputs
|
|
326
|
-
|
|
327
|
-
Send images to any provider that supports them. The canonical `input_image` part works everywhere:
|
|
328
|
-
|
|
329
|
-
``` python
|
|
330
|
-
img_msg = Msg(role='user', content=[
|
|
331
|
-
InputImage(sample_img_url),
|
|
332
|
-
Text("What do you see in this image?")
|
|
333
|
-
])
|
|
334
|
-
|
|
335
|
-
for name, kw in [('claude-sonnet-5', {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {})]:
|
|
336
|
-
print(f"{name:>30s}: ", end='')
|
|
337
|
-
r = await stream([img_msg], model=name, max_tokens=mtok, **kw)
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
claude-sonnet-4-20250514: I see a beautiful, serene landscape photograph taken from what appears to be a wooden dock or platform. The image shows:
|
|
341
|
-
|
|
342
|
-
- A perfectly still lake that creates mirror-like reflections
|
|
343
|
-
- Snow-capped mountains in the background under a partly cloudy sky
|
|
344
|
-
- Dense forests of evergreen trees (likely pine or fir) lining the shoreline
|
|
345
|
-
- The mountains, trees, and sky all reflected clearly in the calm water
|
|
346
|
-
- Wooden planks in the foreground, suggesting the photo was taken from a dock or wooden viewing platform
|
|
347
|
-
- The lighting appears soft and atmospheric, possibly during golden hour
|
|
348
|
-
- The overall scene has a very peaceful, pristine wilderness quality typical of places like the Canadian Rockies, Alaska, or similar mountainous lake regions
|
|
349
|
-
|
|
350
|
-
The composition creates a sense of depth and tranquility, with the wooden platform in the foreground leading the eye toward the expansive natural landscape beyond.
|
|
351
|
-
gpt-4o-mini: The image features a serene landscape with a body of water reflecting surrounding trees and mountains. In the foreground, there is a wooden deck or platform, enhancing the picturesque view. The scene is calm and depicts natural beauty, characterized by lush greenery, a clear sky, and distant snow-capped mountains.
|
|
352
|
-
models/gemini-3-flash-preview: This image depicts a serene and majestic landscape, viewed from the perspective of someone standing on a wooden deck.
|
|
353
|
-
|
|
354
|
-
Here is a breakdown of what is visible:
|
|
355
|
-
|
|
356
|
-
* **Foreground:** In the immediate foreground are weathered brown wooden planks of a deck or pier. The planks run diagonally, drawing the eye toward the center of the image.
|
|
357
|
-
* **Midground (The Lake):** Beyond the deck is a large, still lake. The water is a deep blue and acts as a perfect mirror, reflecting the trees and mountains above with startling clarity.
|
|
358
|
-
* **Shoreline:** Along the far edge of the lake is a dense line of lush green evergreen trees. A thin, bright yellow or golden strip of vegetation runs along the very edge of the water.
|
|
359
|
-
* **Background (Mountains):** Towering over the entire scene are massive, rugged mountains. The peaks are covered in white snow or glaciers and appear in shades of light blue and grey due to a soft, atmospheric haze.
|
|
360
|
-
* **Sky:** The sky is a pale, bright blue, almost white in some areas, with a few soft clouds visible in the upper right corner.
|
|
361
|
-
|
|
362
|
-
The overall mood of the image is peaceful, cool-toned, and tranquil.
|
|
363
|
-
|
|
364
|
-
`fastllm` supports four media part classes. Provider support varies:
|
|
365
|
-
|
|
366
|
-
| Media part | Anthropic | OpenAI Responses | OpenAI Chat | Gemini |
|
|
367
|
-
|--------------|-----------|------------------|-------------|--------|
|
|
368
|
-
| `InputImage` | ✅ | ✅ | ✅ | ✅ |
|
|
369
|
-
| `InputAudio` | ❌ | ❌ (coming soon) | ✅ | ✅ |
|
|
370
|
-
| `InputVideo` | ❌ | ❌ | ❌ | ✅ |
|
|
371
|
-
| `InputFile` | ✅ | ✅ | ✅ | ✅ |
|
|
372
|
-
|
|
373
|
-
All media parts accept either a URL or a base64 data URL as their `text`. Unsupported combinations raise a clear `ValueError`.
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
fastllm/__init__.py,sha256=uDoIIuBjxZKcbyRrdFaaGJlfJwWkr9dF7dNA3b04A9k,131
|
|
2
|
-
fastllm/_modidx.py,sha256=qzUtLbjOche-GNbeNun3VRIQdcm5g4dUOCLJ6T9fG-w,46374
|
|
3
|
-
fastllm/acomplete.py,sha256=CusLue35xiaswpmTZyLkJj6t4CVsNDeeY3Sk7hzVNoY,10777
|
|
4
|
-
fastllm/anthropic.py,sha256=ktH-EuQMQnp8O2_6VmSrvvDCIe2QTeSR7jNpWfqf3L4,18884
|
|
5
|
-
fastllm/chat.py,sha256=klji3SOYs5xyzrSFbNhy3uoQ2TSZ3a-8HTjvdRtaG5Q,25036
|
|
6
|
-
fastllm/codex.py,sha256=r6auZ8egia83aGkWcF4132AzW4SsBvKeDwg_CxyXj9E,5808
|
|
7
|
-
fastllm/gemini.py,sha256=vpY_f4Jqn344l9CANm6_c7Cmhjq2URXxWhdVv_LswSg,17615
|
|
8
|
-
fastllm/model_prices.json,sha256=pa0j96LZgklYii8howX443QbukUP1iFr3lBcucS-6Yo,2133018
|
|
9
|
-
fastllm/openai_chat.py,sha256=n5INfDCsAZIiKUImWeaX6SJ_OAJ_uMhw20HUkgFgdgM,12379
|
|
10
|
-
fastllm/openai_responses.py,sha256=NrvzNCKkSQabbAMnxYCSPdROaT1u8Vp2eoRzbzAA-4M,15345
|
|
11
|
-
fastllm/responses.py,sha256=9gh-UlhfswCcP0RCzLuPT5meWNh9mF_OIivzCzZU94I,22726
|
|
12
|
-
fastllm/streaming.py,sha256=DrqNJkMhyntFKokY6GF6oW3CjELrk5XWmnxwcApJUyM,8770
|
|
13
|
-
fastllm/types.py,sha256=OznlKQZUx2DrLE_O-sbIBsT6q6EBeVDlH9kXxhsGBSE,19445
|
|
14
|
-
python_fastllm-0.0.58.dist-info/METADATA,sha256=scQa4XA1Oh28m6HO6knQtPmOwV_E0ppqFdXuMH8GTA8,18537
|
|
15
|
-
python_fastllm-0.0.58.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
16
|
-
python_fastllm-0.0.58.dist-info/entry_points.txt,sha256=WkWd04F3xVYFP5bsexaQe9Se6uelwE5ULoTtHWreO0s,133
|
|
17
|
-
python_fastllm-0.0.58.dist-info/top_level.txt,sha256=F8qodL7nEGUHGmzzqfhNKCTIr1i0D6cvudOnm3z7o0Y,8
|
|
18
|
-
python_fastllm-0.0.58.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|