python-fastllm 0.0.59__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 +6 -7
- fastllm/responses.py +19 -7
- fastllm/streaming.py +3 -4
- fastllm/types.py +2 -2
- {python_fastllm-0.0.59.dist-info → python_fastllm-0.0.60.dist-info}/METADATA +1 -1
- python_fastllm-0.0.60.dist-info/RECORD +18 -0
- python_fastllm-0.0.59.dist-info/RECORD +0 -18
- {python_fastllm-0.0.59.dist-info → python_fastllm-0.0.60.dist-info}/WHEEL +0 -0
- {python_fastllm-0.0.59.dist-info → python_fastllm-0.0.60.dist-info}/entry_points.txt +0 -0
- {python_fastllm-0.0.59.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
|
|
@@ -290,6 +290,5 @@ def fix_payload(payload, model, vendor_name):
|
|
|
290
290
|
# %% ../nbs/02_oai_responses.ipynb #07114b55
|
|
291
291
|
api_ns = dict(norm_tool_calls=norm_tool_calls, norm_parts=norm_parts, norm_finish=norm_finish, norm_usage=norm_usage,
|
|
292
292
|
supports_previous_response_id=True, raw_msg=raw_msg, collate_raw=collate_raw, acollect_stream=acollect_stream, mk_payload=mk_payload,
|
|
293
|
-
cost=cost, get_hdrs=get_hdrs, fix_payload=fix_payload,
|
|
294
|
-
endpoint='/responses')
|
|
293
|
+
cost=cost, get_hdrs=get_hdrs, fix_payload=fix_payload, endpoint='/responses')
|
|
295
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
|
@@ -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)
|
|
@@ -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,18 +0,0 @@
|
|
|
1
|
-
fastllm/__init__.py,sha256=6tGHvtWA9S06PKl5eFmjR7B9sGVZhtZsXqaK_d39b7k,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=Jbu8ddPC3rqlLgubOuOva4EWEtN6dk47MbwBfB8d9ng,15409
|
|
11
|
-
fastllm/responses.py,sha256=9gh-UlhfswCcP0RCzLuPT5meWNh9mF_OIivzCzZU94I,22726
|
|
12
|
-
fastllm/streaming.py,sha256=DrqNJkMhyntFKokY6GF6oW3CjELrk5XWmnxwcApJUyM,8770
|
|
13
|
-
fastllm/types.py,sha256=W8t38EN1ZAzZaZ8QMi-Vin3fMxEpRhEZ8pm6fM5G3NA,19673
|
|
14
|
-
python_fastllm-0.0.59.dist-info/METADATA,sha256=fCzlAUhirh1MHNym6IjGbVAlG0ouKo0EAP9n5DVF1m0,9136
|
|
15
|
-
python_fastllm-0.0.59.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
16
|
-
python_fastllm-0.0.59.dist-info/entry_points.txt,sha256=WkWd04F3xVYFP5bsexaQe9Se6uelwE5ULoTtHWreO0s,133
|
|
17
|
-
python_fastllm-0.0.59.dist-info/top_level.txt,sha256=F8qodL7nEGUHGmzzqfhNKCTIr1i0D6cvudOnm3z7o0Y,8
|
|
18
|
-
python_fastllm-0.0.59.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|