python-fastllm 0.0.49__py3-none-any.whl → 0.0.51__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 +19 -6
- fastllm/anthropic.py +59 -44
- fastllm/gemini.py +37 -19
- fastllm/openai_chat.py +31 -18
- fastllm/openai_responses.py +23 -24
- fastllm/responses.py +21 -2
- fastllm/streaming.py +3 -8
- fastllm/types.py +12 -10
- {python_fastllm-0.0.49.dist-info → python_fastllm-0.0.51.dist-info}/METADATA +18 -73
- python_fastllm-0.0.51.dist-info/RECORD +17 -0
- python_fastllm-0.0.49.dist-info/RECORD +0 -17
- {python_fastllm-0.0.49.dist-info → python_fastllm-0.0.51.dist-info}/WHEEL +0 -0
- {python_fastllm-0.0.49.dist-info → python_fastllm-0.0.51.dist-info}/entry_points.txt +0 -0
- {python_fastllm-0.0.49.dist-info → python_fastllm-0.0.51.dist-info}/top_level.txt +0 -0
fastllm/__init__.py
CHANGED
fastllm/_modidx.py
CHANGED
|
@@ -25,6 +25,7 @@ d = { 'settings': { 'branch': 'main',
|
|
|
25
25
|
'fastllm.anthropic._sanid': ('anthropic.html#_sanid', 'fastllm/anthropic.py'),
|
|
26
26
|
'fastllm.anthropic.acollect_stream': ('anthropic.html#acollect_stream', 'fastllm/anthropic.py'),
|
|
27
27
|
'fastllm.anthropic.apply_cache_idxs': ('anthropic.html#apply_cache_idxs', 'fastllm/anthropic.py'),
|
|
28
|
+
'fastllm.anthropic.collate_raw': ('anthropic.html#collate_raw', 'fastllm/anthropic.py'),
|
|
28
29
|
'fastllm.anthropic.cost': ('anthropic.html#cost', 'fastllm/anthropic.py'),
|
|
29
30
|
'fastllm.anthropic.delta_index_fn': ('anthropic.html#delta_index_fn', 'fastllm/anthropic.py'),
|
|
30
31
|
'fastllm.anthropic.denorm_assistant': ('anthropic.html#denorm_assistant', 'fastllm/anthropic.py'),
|
|
@@ -49,7 +50,9 @@ d = { 'settings': { 'branch': 'main',
|
|
|
49
50
|
'fastllm.anthropic.norm_tool_call': ('anthropic.html#norm_tool_call', 'fastllm/anthropic.py'),
|
|
50
51
|
'fastllm.anthropic.norm_tool_calls': ('anthropic.html#norm_tool_calls', 'fastllm/anthropic.py'),
|
|
51
52
|
'fastllm.anthropic.norm_tr_parts': ('anthropic.html#norm_tr_parts', 'fastllm/anthropic.py'),
|
|
52
|
-
'fastllm.anthropic.norm_usage': ('anthropic.html#norm_usage', 'fastllm/anthropic.py')
|
|
53
|
+
'fastllm.anthropic.norm_usage': ('anthropic.html#norm_usage', 'fastllm/anthropic.py'),
|
|
54
|
+
'fastllm.anthropic.raw_msg': ('anthropic.html#raw_msg', 'fastllm/anthropic.py'),
|
|
55
|
+
'fastllm.anthropic.search_hits': ('anthropic.html#search_hits', 'fastllm/anthropic.py')},
|
|
53
56
|
'fastllm.chat': { 'fastllm.chat.AsyncChat': ('chat.html#asyncchat', 'fastllm/chat.py'),
|
|
54
57
|
'fastllm.chat.AsyncChat.__call__': ('chat.html#asyncchat.__call__', 'fastllm/chat.py'),
|
|
55
58
|
'fastllm.chat.AsyncChat.__init__': ('chat.html#asyncchat.__init__', 'fastllm/chat.py'),
|
|
@@ -139,6 +142,7 @@ d = { 'settings': { 'branch': 'main',
|
|
|
139
142
|
'fastllm.gemini': { 'fastllm.gemini._gem_filter_sch': ('gemini.html#_gem_filter_sch', 'fastllm/gemini.py'),
|
|
140
143
|
'fastllm.gemini._gem_part_type': ('gemini.html#_gem_part_type', 'fastllm/gemini.py'),
|
|
141
144
|
'fastllm.gemini.acollect_stream': ('gemini.html#acollect_stream', 'fastllm/gemini.py'),
|
|
145
|
+
'fastllm.gemini.collate_raw': ('gemini.html#collate_raw', 'fastllm/gemini.py'),
|
|
142
146
|
'fastllm.gemini.cost': ('gemini.html#cost', 'fastllm/gemini.py'),
|
|
143
147
|
'fastllm.gemini.delta_index_fn': ('gemini.html#delta_index_fn', 'fastllm/gemini.py'),
|
|
144
148
|
'fastllm.gemini.denorm_assistant': ('gemini.html#denorm_assistant', 'fastllm/gemini.py'),
|
|
@@ -163,8 +167,10 @@ d = { 'settings': { 'branch': 'main',
|
|
|
163
167
|
'fastllm.gemini.norm_parts': ('gemini.html#norm_parts', 'fastllm/gemini.py'),
|
|
164
168
|
'fastllm.gemini.norm_sse_event': ('gemini.html#norm_sse_event', 'fastllm/gemini.py'),
|
|
165
169
|
'fastllm.gemini.norm_tool_calls': ('gemini.html#norm_tool_calls', 'fastllm/gemini.py'),
|
|
166
|
-
'fastllm.gemini.norm_usage': ('gemini.html#norm_usage', 'fastllm/gemini.py')
|
|
170
|
+
'fastllm.gemini.norm_usage': ('gemini.html#norm_usage', 'fastllm/gemini.py'),
|
|
171
|
+
'fastllm.gemini.raw_msg': ('gemini.html#raw_msg', 'fastllm/gemini.py')},
|
|
167
172
|
'fastllm.openai_chat': { 'fastllm.openai_chat.acollect_stream': ('oai_chat.html#acollect_stream', 'fastllm/openai_chat.py'),
|
|
173
|
+
'fastllm.openai_chat.collate_raw': ('oai_chat.html#collate_raw', 'fastllm/openai_chat.py'),
|
|
168
174
|
'fastllm.openai_chat.cost': ('oai_chat.html#cost', 'fastllm/openai_chat.py'),
|
|
169
175
|
'fastllm.openai_chat.delta_index_fn': ('oai_chat.html#delta_index_fn', 'fastllm/openai_chat.py'),
|
|
170
176
|
'fastllm.openai_chat.denorm_assistant': ('oai_chat.html#denorm_assistant', 'fastllm/openai_chat.py'),
|
|
@@ -189,14 +195,15 @@ d = { 'settings': { 'branch': 'main',
|
|
|
189
195
|
'fastllm.openai_chat.norm_finish': ('oai_chat.html#norm_finish', 'fastllm/openai_chat.py'),
|
|
190
196
|
'fastllm.openai_chat.norm_parts': ('oai_chat.html#norm_parts', 'fastllm/openai_chat.py'),
|
|
191
197
|
'fastllm.openai_chat.norm_sse_event': ('oai_chat.html#norm_sse_event', 'fastllm/openai_chat.py'),
|
|
192
|
-
'fastllm.openai_chat.norm_tool_calls': ('oai_chat.html#norm_tool_calls', 'fastllm/openai_chat.py')
|
|
198
|
+
'fastllm.openai_chat.norm_tool_calls': ('oai_chat.html#norm_tool_calls', 'fastllm/openai_chat.py'),
|
|
199
|
+
'fastllm.openai_chat.raw_msg': ('oai_chat.html#raw_msg', 'fastllm/openai_chat.py')},
|
|
193
200
|
'fastllm.openai_responses': { 'fastllm.openai_responses.Completion.response_id': ( 'oai_responses.html#completion.response_id',
|
|
194
201
|
'fastllm/openai_responses.py'),
|
|
195
|
-
'fastllm.openai_responses.Completion.response_id_reusable': ( 'oai_responses.html#completion.response_id_reusable',
|
|
196
|
-
'fastllm/openai_responses.py'),
|
|
197
202
|
'fastllm.openai_responses._sanid': ('oai_responses.html#_sanid', 'fastllm/openai_responses.py'),
|
|
198
203
|
'fastllm.openai_responses.acollect_stream': ( 'oai_responses.html#acollect_stream',
|
|
199
204
|
'fastllm/openai_responses.py'),
|
|
205
|
+
'fastllm.openai_responses.collate_raw': ( 'oai_responses.html#collate_raw',
|
|
206
|
+
'fastllm/openai_responses.py'),
|
|
200
207
|
'fastllm.openai_responses.cost': ('oai_responses.html#cost', 'fastllm/openai_responses.py'),
|
|
201
208
|
'fastllm.openai_responses.delta_index_fn': ( 'oai_responses.html#delta_index_fn',
|
|
202
209
|
'fastllm/openai_responses.py'),
|
|
@@ -245,13 +252,19 @@ d = { 'settings': { 'branch': 'main',
|
|
|
245
252
|
'fastllm.openai_responses.norm_tool_calls': ( 'oai_responses.html#norm_tool_calls',
|
|
246
253
|
'fastllm/openai_responses.py'),
|
|
247
254
|
'fastllm.openai_responses.norm_usage': ( 'oai_responses.html#norm_usage',
|
|
248
|
-
'fastllm/openai_responses.py')
|
|
255
|
+
'fastllm/openai_responses.py'),
|
|
256
|
+
'fastllm.openai_responses.raw_msg': ( 'oai_responses.html#raw_msg',
|
|
257
|
+
'fastllm/openai_responses.py')},
|
|
249
258
|
'fastllm.responses': { 'fastllm.responses.AsyncResponses': ('responses.html#asyncresponses', 'fastllm/responses.py'),
|
|
259
|
+
'fastllm.responses.AsyncResponses.__init__': ( 'responses.html#asyncresponses.__init__',
|
|
260
|
+
'fastllm/responses.py'),
|
|
250
261
|
'fastllm.responses.AsyncResponses.call': ('responses.html#asyncresponses.call', 'fastllm/responses.py'),
|
|
251
262
|
'fastllm.responses.AsyncResponses.events': ( 'responses.html#asyncresponses.events',
|
|
252
263
|
'fastllm/responses.py'),
|
|
253
264
|
'fastllm.responses.AsyncResponses.prepare': ( 'responses.html#asyncresponses.prepare',
|
|
254
265
|
'fastllm/responses.py'),
|
|
266
|
+
'fastllm.responses.AsyncResponses.serve': ( 'responses.html#asyncresponses.serve',
|
|
267
|
+
'fastllm/responses.py'),
|
|
255
268
|
'fastllm.responses.AsyncResponses.server_results': ( 'responses.html#asyncresponses.server_results',
|
|
256
269
|
'fastllm/responses.py'),
|
|
257
270
|
'fastllm.responses.AsyncResponses.server_turn': ( 'responses.html#asyncresponses.server_turn',
|
fastllm/anthropic.py
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/04_anthropic.ipynb.
|
|
2
2
|
|
|
3
3
|
# %% auto #0
|
|
4
|
-
__all__ = ['ant_tc_types', 'norm_tool_call', 'norm_tool_calls', 'norm_usage', 'finalize_usage', 'norm_finish', '
|
|
5
|
-
'
|
|
6
|
-
'denorm_assistant', 'denorm_tool', 'denorm_msgs', 'denorm_tool_schs',
|
|
7
|
-
'denorm_reasoning', 'denorm_web_search', 'denorm_system', 'denorm_user',
|
|
8
|
-
'denorm_tool_result', 'apply_cache_idxs', 'mk_payload', 'get_hdrs', 'cost']
|
|
4
|
+
__all__ = ['ant_tc_types', 'norm_tool_call', 'norm_tool_calls', 'norm_usage', 'finalize_usage', 'norm_finish', 'search_hits',
|
|
5
|
+
'norm_parts', 'raw_msg', 'norm_tr_parts', 'norm_sse_event', 'delta_index_fn', 'collate_raw',
|
|
6
|
+
'acollect_stream', 'denorm_tool_use', 'denorm_assistant', 'denorm_tool', 'denorm_msgs', 'denorm_tool_schs',
|
|
7
|
+
'denorm_tool_choice', 'denorm_reasoning', 'denorm_web_search', 'denorm_system', 'denorm_user',
|
|
8
|
+
'denorm_image', 'denorm_file', 'denorm_tool_result', 'apply_cache_idxs', 'mk_payload', 'get_hdrs', 'cost']
|
|
9
9
|
|
|
10
10
|
# %% ../nbs/04_anthropic.ipynb #02afd3d7
|
|
11
|
-
import json
|
|
11
|
+
import json, copy
|
|
12
12
|
from collections import Counter
|
|
13
13
|
from fastcore.utils import *
|
|
14
14
|
from fastcore.meta import *
|
|
15
15
|
from fasttransport.errors import api_error_from_event
|
|
16
16
|
|
|
17
17
|
from .types import *
|
|
18
|
-
from aidialog.msg_parts import (Part, PartType, mk_part, Msg, Text, Thinking, ToolUse, ToolResult, tool_text,
|
|
18
|
+
from aidialog.msg_parts import (Part, PartType, mk_part, Msg, Completion, Text, Thinking, ToolUse, ToolResult, tool_text,
|
|
19
19
|
InputImage, InputAudio, InputVideo, InputFile, data_url)
|
|
20
20
|
from .streaming import *
|
|
21
21
|
from .streaming import mk_acollect_stream
|
|
@@ -47,11 +47,11 @@ def norm_usage(resp):
|
|
|
47
47
|
cached_tokens=cached, cache_creation_tokens=cache_creation, reasoning_tokens=0, raw=usg)
|
|
48
48
|
|
|
49
49
|
def finalize_usage(usg, parts):
|
|
50
|
-
"
|
|
50
|
+
"Split reasoning tokens out of `output_tokens`: the reported `thinking_tokens`, else an estimate from the thinking text."
|
|
51
51
|
if not usg: return usg
|
|
52
52
|
rc = '\n'.join(p.text or '' for p in parts if isinstance(p, Thinking))
|
|
53
53
|
ct = int(usg.raw.get('output_tokens', usg.completion_tokens) or 0)
|
|
54
|
-
rt = min(int(len(rc.split())*1.5), ct) if rc else 0
|
|
54
|
+
rt = int(nested_idx(usg.raw, 'output_tokens_details', 'thinking_tokens') or 0) or (min(int(len(rc.split())*1.5), ct) if rc else 0)
|
|
55
55
|
return Usage(prompt_tokens=usg.prompt_tokens, completion_tokens=ct-rt, total_tokens=usg.prompt_tokens+ct,
|
|
56
56
|
cached_tokens=usg.cached_tokens, cache_creation_tokens=usg.cache_creation_tokens, reasoning_tokens=rt, raw=usg.raw)
|
|
57
57
|
|
|
@@ -67,20 +67,31 @@ def norm_finish(resp, tcs=None):
|
|
|
67
67
|
def _ant_part_type(typ):
|
|
68
68
|
"Map Anthropic content block type to canonical PartType."
|
|
69
69
|
ant_parts_mapping = dict(text=PartType.text, thinking=PartType.thinking, redacted_thinking=PartType.thinking, tool_use=PartType.tool_use, server_tool_use=PartType.tool_use, mcp_tool_use=PartType.tool_use)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
return ant_parts_mapping.get(typ, typ)
|
|
71
|
+
|
|
72
|
+
def search_hits(parts, blocks):
|
|
73
|
+
"Set each server call's `text` to the `title: url` lines of its result block, when the response has one"
|
|
74
|
+
calls = {p.id: p for p in parts if isinstance(p, ToolUse)}
|
|
75
|
+
for b in blocks:
|
|
76
|
+
if b.get('type', '').endswith('_tool_result') and (tc := calls.get(b.get('tool_use_id'))):
|
|
77
|
+
tc.text = '\n'.join(f"{r.get('title', '')}: {r.get('url', '')}" for r in b.get('content') or [] if isinstance(r, dict) and r.get('url'))
|
|
78
|
+
return parts
|
|
73
79
|
|
|
74
80
|
def norm_parts(resp):
|
|
81
|
+
"Canonical parts of a response; a server tool's result block lives in the raw message, and as the call's `text`"
|
|
75
82
|
parts = []
|
|
76
83
|
for b in resp.get("content", []):
|
|
77
84
|
typ = _ant_part_type(b.get("type", "text"))
|
|
78
85
|
if typ == PartType.thinking: parts.append(Thinking(b.get("thinking", ""), raw=b))
|
|
79
86
|
elif typ == PartType.tool_use:
|
|
80
87
|
if tc := norm_tool_call(b): parts.append(tc)
|
|
81
|
-
elif typ == PartType.
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
elif typ == PartType.text: parts.append(Text(b.get("text", ""), raw=b))
|
|
89
|
+
return search_hits(parts, resp.get("content", []))
|
|
90
|
+
|
|
91
|
+
# %% ../nbs/04_anthropic.ipynb #f24e52db
|
|
92
|
+
def raw_msg(resp):
|
|
93
|
+
"The assistant message as the API sent it"
|
|
94
|
+
return dict(role='assistant', content=resp['content'])
|
|
84
95
|
|
|
85
96
|
# %% ../nbs/04_anthropic.ipynb #5d1440b7
|
|
86
97
|
def norm_tr_parts(content):
|
|
@@ -103,7 +114,6 @@ def norm_sse_event(ev, **kwargs):
|
|
|
103
114
|
text, thinking, tcs, citations = None, None, [], None
|
|
104
115
|
if typ == "content_block_start":
|
|
105
116
|
cb = ev.get("content_block", {})
|
|
106
|
-
if cb.get("type", "").endswith("_tool_result"): return Delta(server_tool_result=cb, raw=ev, **kwargs)
|
|
107
117
|
if tc := norm_tool_call(cb):
|
|
108
118
|
if not tc.arguments: tc.arguments = {'_delta': ''}
|
|
109
119
|
tcs = [tc]
|
|
@@ -127,11 +137,30 @@ def delta_index_fn(d, typ, last_typ, last_idx):
|
|
|
127
137
|
'Returns accumulation index for current delta and updated last idx'
|
|
128
138
|
return nested_idx(d, 'raw', 'index'), None
|
|
129
139
|
|
|
140
|
+
# %% ../nbs/04_anthropic.ipynb #de900e34
|
|
141
|
+
def collate_raw(deltas):
|
|
142
|
+
"The assistant message rebuilt from a stream's events"
|
|
143
|
+
blocks = {}
|
|
144
|
+
for ev in L(deltas).attrgot('raw'):
|
|
145
|
+
typ,i = ev.get('type'),ev.get('index')
|
|
146
|
+
if typ == 'content_block_start': blocks[i] = copy.deepcopy(ev['content_block'])
|
|
147
|
+
elif typ == 'content_block_delta':
|
|
148
|
+
d,b = ev['delta'],blocks[i]
|
|
149
|
+
if d['type'] == 'text_delta': b['text'] += d['text']
|
|
150
|
+
elif d['type'] == 'thinking_delta': b['thinking'] += d['thinking']
|
|
151
|
+
elif d['type'] == 'signature_delta': b['signature'] = d['signature']
|
|
152
|
+
elif d['type'] == 'input_json_delta': b['_json'] = b.get('_json', '') + d['partial_json']
|
|
153
|
+
elif d['type'] == 'citations_delta': b['citations'] = (b.get('citations') or []) + [d['citation']]
|
|
154
|
+
elif typ == 'content_block_stop' and '_json' in blocks[i]: blocks[i]['input'] = json.loads(blocks[i].pop('_json') or '{}')
|
|
155
|
+
return dict(role='assistant', content=list(blocks.values()))
|
|
156
|
+
|
|
130
157
|
# %% ../nbs/04_anthropic.ipynb #c994adb1
|
|
131
158
|
@delegates(mk_acollect_stream, but=['index_fn', 'api_name'])
|
|
132
159
|
async def acollect_stream(resp, **kwargs):
|
|
133
160
|
res = mk_acollect_stream(norm_and_yield(resp, norm_sse_event), index_fn=delta_index_fn, api_name='anthropic', **kwargs)
|
|
134
|
-
async for o in res:
|
|
161
|
+
async for o in res:
|
|
162
|
+
if isinstance(o, Completion): search_hits(o.message.content, o.message.raw['content'])
|
|
163
|
+
yield o
|
|
135
164
|
|
|
136
165
|
# %% ../nbs/04_anthropic.ipynb #bb11ab62
|
|
137
166
|
def _ant_cc(block, p):
|
|
@@ -145,44 +174,29 @@ def _sanid(id_str): return re.sub(r'[^a-zA-Z0-9_-]', '_', id_str or '')
|
|
|
145
174
|
# %% ../nbs/04_anthropic.ipynb #6ec772cb
|
|
146
175
|
def denorm_tool_use(p:ToolUse):
|
|
147
176
|
"Convert canonical tool_use Part to Anthropic tool_use content block."
|
|
148
|
-
|
|
149
|
-
if caller := (p.raw or {}).get('caller'): block['caller'] = caller
|
|
150
|
-
return _ant_cc(block, p)
|
|
177
|
+
return _ant_cc(dict(type='tool_use', id=_sanid(p.id or ''), name=p.name or '', input=p.arguments or {}), p)
|
|
151
178
|
|
|
152
179
|
def denorm_assistant(m:Msg):
|
|
153
|
-
"
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
elif isinstance(p, Thinking) and (sig:=(p.raw or {}).get('signature','')):
|
|
161
|
-
blocks.append(dict(type='thinking', thinking=p.text or '', signature=sig))
|
|
162
|
-
else: blocks.append(_ant_cc(dict(type='text', text=p.text or ''), p))
|
|
163
|
-
elif isinstance(p, ToolUse):
|
|
164
|
-
if p.server and (p.id or '').startswith('srvtoolu_'):
|
|
165
|
-
blocks.append(dict(type='server_tool_use', id=p.id, name=p.name or '', input=p.arguments or {}))
|
|
166
|
-
else:
|
|
167
|
-
blocks.append(denorm_tool_use(p))
|
|
168
|
-
if p.server: srv_call_id = p.id or ''
|
|
169
|
-
elif isinstance(p, ServerToolResult): blocks.append(p.raw or dict(type='server_tool_result'))
|
|
170
|
-
res = [dict(role='assistant', content=blocks)]
|
|
171
|
-
if srv_results: res.append(dict(role='user', content=srv_results))
|
|
172
|
-
return res
|
|
180
|
+
"The message as the API sent it, else its text and tool calls; a cache mark on the last part is copied onto the last raw block"
|
|
181
|
+
if m.raw:
|
|
182
|
+
cc = [p.cache_control for p in m.content if p.cache_control]
|
|
183
|
+
if not cc: return m.raw
|
|
184
|
+
return m.raw | dict(content=m.raw['content'][:-1] + [m.raw['content'][-1] | dict(cache_control=cc[-1])])
|
|
185
|
+
blocks = [_ant_cc(dict(type='text', text=p.text or ''), p) if isinstance(p, Text) else denorm_tool_use(p) for p in m.content if isinstance(p, (Text, ToolUse))]
|
|
186
|
+
return dict(role='assistant', content=blocks)
|
|
173
187
|
|
|
174
188
|
def denorm_tool(m:Msg):
|
|
175
189
|
"Convert canonical tool Msg to Anthropic user message with tool_result blocks."
|
|
176
190
|
blocks = [denorm_tool_result(p) for p in m.content if isinstance(p, ToolResult)]
|
|
177
|
-
return
|
|
191
|
+
return dict(role='user', content=blocks)
|
|
178
192
|
|
|
179
193
|
def denorm_msgs(msgs:list[Msg]):
|
|
180
194
|
"Convert list of canonical Msgs to Anthropic messages."
|
|
181
195
|
res = []
|
|
182
196
|
for m in msgs:
|
|
183
197
|
if m.role == 'user': res.append(denorm_user(m))
|
|
184
|
-
elif m.role == 'assistant': res.
|
|
185
|
-
elif m.role == 'tool': res.
|
|
198
|
+
elif m.role == 'assistant': res.append(denorm_assistant(m))
|
|
199
|
+
elif m.role == 'tool': res.append(denorm_tool(m))
|
|
186
200
|
return res
|
|
187
201
|
|
|
188
202
|
# %% ../nbs/04_anthropic.ipynb #f762a6f2
|
|
@@ -346,5 +360,6 @@ def cost(usage, m):
|
|
|
346
360
|
|
|
347
361
|
# %% ../nbs/04_anthropic.ipynb #f7c0b989
|
|
348
362
|
api_registry.register('anthropic', norm_tool_calls=norm_tool_calls, norm_parts=norm_parts, norm_finish=norm_finish, norm_usage=norm_usage,
|
|
349
|
-
finalize_usage=finalize_usage,
|
|
363
|
+
finalize_usage=finalize_usage, raw_msg=raw_msg, collate_raw=collate_raw, acollect_stream=acollect_stream, mk_payload=mk_payload,
|
|
364
|
+
cost=cost, get_hdrs=get_hdrs,
|
|
350
365
|
endpoint='/v1/messages')
|
fastllm/gemini.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/05_gemini.ipynb.
|
|
2
2
|
|
|
3
3
|
# %% auto #0
|
|
4
|
-
__all__ = ['api_ns', 'endpoint', 'norm_tool_calls', 'norm_usage', 'norm_finish', 'norm_parts', '
|
|
5
|
-
'
|
|
6
|
-
'
|
|
7
|
-
'
|
|
8
|
-
'get_hdrs', 'cost']
|
|
4
|
+
__all__ = ['api_ns', 'endpoint', 'norm_tool_calls', 'norm_usage', 'norm_finish', 'norm_parts', 'raw_msg', 'norm_sse_event',
|
|
5
|
+
'delta_index_fn', 'collate_raw', 'acollect_stream', 'denorm_tool_use', 'denorm_assistant', 'denorm_tool',
|
|
6
|
+
'denorm_msgs', 'denorm_tool_schs', 'denorm_tool_choice', 'denorm_reasoning', 'denorm_web_search',
|
|
7
|
+
'denorm_system', 'denorm_user', 'denorm_image', 'denorm_audio', 'denorm_video', 'denorm_file',
|
|
8
|
+
'denorm_tool_result', 'mk_payload', 'get_hdrs', 'cost']
|
|
9
9
|
|
|
10
10
|
# %% ../nbs/05_gemini.ipynb #02afd3d7
|
|
11
11
|
import json
|
|
@@ -27,12 +27,14 @@ def endpoint(model, stream):
|
|
|
27
27
|
|
|
28
28
|
# %% ../nbs/05_gemini.ipynb #f1eb32f3
|
|
29
29
|
def norm_tool_calls(resp):
|
|
30
|
-
"
|
|
30
|
+
"Tool calls of a response: `functionCall` parts, and a `toolCall` part for a search the API ran itself"
|
|
31
31
|
out = []
|
|
32
32
|
for i,p in enumerate(nested_idx(resp, 'candidates', 0, 'content', 'parts') or []):
|
|
33
|
-
if
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
if fc := p.get("functionCall"):
|
|
34
|
+
raw = {k:v for k,v in p.items() if k != "functionCall"}
|
|
35
|
+
out.append(ToolUse(id=fc.get("id", f"call_{i}"), name=fc.get("name", ""), arguments=fc.get("args") or {}, raw=raw))
|
|
36
|
+
elif tc := p.get("toolCall"):
|
|
37
|
+
out.append(ToolUse(id=tc.get("id", f"call_{i}"), name=tc.get("toolType", "").lower(), arguments=tc.get("args") or {}, server=True))
|
|
36
38
|
return out
|
|
37
39
|
|
|
38
40
|
# %% ../nbs/05_gemini.ipynb #90504163
|
|
@@ -48,7 +50,7 @@ def norm_usage(resp):
|
|
|
48
50
|
cand = nested_idx(resp, 'candidates', 0) or {}
|
|
49
51
|
stu = {}
|
|
50
52
|
if any("executableCode" in p for p in parts): stu["code_execution"] = sum(1 for p in parts if "executableCode" in p)
|
|
51
|
-
if "groundingMetadata" in cand: stu["google_search"] =
|
|
53
|
+
if n := ("groundingMetadata" in cand) + sum(1 for p in parts if 'toolCall' in p): stu["google_search"] = n
|
|
52
54
|
if stu: usg['server_tool_use'] = stu
|
|
53
55
|
return Usage(prompt_tokens=pt, completion_tokens=ct, total_tokens=tt,
|
|
54
56
|
cached_tokens=cached, reasoning_tokens=reasoning, raw=usg)
|
|
@@ -76,6 +78,7 @@ def norm_parts(resp):
|
|
|
76
78
|
tc_map = {tc.id: tc for tc in norm_tool_calls(resp)}
|
|
77
79
|
parts = []
|
|
78
80
|
for p in nested_idx(c0, 'content', 'parts') or []:
|
|
81
|
+
if 'toolResponse' in p: continue
|
|
79
82
|
typ = _gem_part_type(p)
|
|
80
83
|
if typ == PartType.tool_use:
|
|
81
84
|
fc = p.get('functionCall') or p.get('toolCall') or {}
|
|
@@ -88,6 +91,11 @@ def norm_parts(resp):
|
|
|
88
91
|
if isinstance(p, Text): p.citations = citations
|
|
89
92
|
return parts
|
|
90
93
|
|
|
94
|
+
# %% ../nbs/05_gemini.ipynb #3acc9538
|
|
95
|
+
def raw_msg(resp):
|
|
96
|
+
"The model content as the API sent it"
|
|
97
|
+
return nested_idx(resp, 'candidates', 0, 'content')
|
|
98
|
+
|
|
91
99
|
# %% ../nbs/05_gemini.ipynb #9a5024ee
|
|
92
100
|
def norm_sse_event(ev, **kwargs):
|
|
93
101
|
"Normalize Gemini stream event into Delta."
|
|
@@ -107,6 +115,19 @@ def delta_index_fn(d, typ, last_typ, last_idx):
|
|
|
107
115
|
if not (last_typ or last_idx): return 0,0
|
|
108
116
|
return last_idx + 1, last_idx + 1
|
|
109
117
|
|
|
118
|
+
# %% ../nbs/05_gemini.ipynb #33506d56
|
|
119
|
+
def collate_raw(deltas):
|
|
120
|
+
"The model content rebuilt from a stream's chunks"
|
|
121
|
+
parts = []
|
|
122
|
+
for ev in L(deltas).attrgot('raw'):
|
|
123
|
+
for p in nested_idx(ev, 'candidates', 0, 'content', 'parts') or []:
|
|
124
|
+
last = parts[-1] if parts else None
|
|
125
|
+
if 'text' in p and last and 'text' in last and last.get('thought') == p.get('thought') and 'thoughtSignature' not in last:
|
|
126
|
+
last['text'] += p['text']
|
|
127
|
+
if 'thoughtSignature' in p: last['thoughtSignature'] = p['thoughtSignature']
|
|
128
|
+
else: parts.append(dict(p))
|
|
129
|
+
return dict(role='model', parts=parts)
|
|
130
|
+
|
|
110
131
|
# %% ../nbs/05_gemini.ipynb #328af4d5
|
|
111
132
|
@delegates(mk_acollect_stream, but=['index_fn', 'api_name'])
|
|
112
133
|
async def acollect_stream(resp, **kwargs):
|
|
@@ -118,17 +139,12 @@ def denorm_tool_use(p:ToolUse):
|
|
|
118
139
|
"Convert canonical tool_use Part to Gemini functionCall part."
|
|
119
140
|
fc = dict(name=p.name or '', args=p.arguments or {})
|
|
120
141
|
if p.id: fc['id'] = p.id
|
|
121
|
-
|
|
122
|
-
part['thoughtSignature'] = (p.raw or {}).get('thoughtSignature', 'skip_thought_signature_validator')
|
|
123
|
-
return part
|
|
142
|
+
return dict(functionCall=fc)
|
|
124
143
|
|
|
125
144
|
def denorm_assistant(m:Msg):
|
|
126
|
-
"
|
|
127
|
-
|
|
128
|
-
for p in m.content
|
|
129
|
-
if isinstance(p, Thinking): parts.append(dict(text=p.text or '', thought=True))
|
|
130
|
-
elif isinstance(p, ToolUse): parts.append(denorm_tool_use(p))
|
|
131
|
-
elif isinstance(p, Text): parts.append(dict(text=p.text or ''))
|
|
145
|
+
"The content as the API sent it, else its text and tool calls"
|
|
146
|
+
if m.raw: return m.raw
|
|
147
|
+
parts = [dict(text=p.text or '') if isinstance(p, Text) else denorm_tool_use(p) for p in m.content if isinstance(p, (Text, ToolUse))]
|
|
132
148
|
return dict(role='model', parts=parts)
|
|
133
149
|
|
|
134
150
|
def denorm_tool(m:Msg):
|
|
@@ -301,6 +317,8 @@ api_ns = dict(norm_tool_calls=norm_tool_calls,
|
|
|
301
317
|
norm_finish=norm_finish,
|
|
302
318
|
norm_usage=norm_usage,
|
|
303
319
|
acollect_stream=acollect_stream,
|
|
320
|
+
raw_msg=raw_msg,
|
|
321
|
+
collate_raw=collate_raw,
|
|
304
322
|
mk_payload=mk_payload,
|
|
305
323
|
cost=cost,
|
|
306
324
|
get_hdrs=get_hdrs,
|
fastllm/openai_chat.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/03_oai_chat.ipynb.
|
|
2
2
|
|
|
3
3
|
# %% auto #0
|
|
4
|
-
__all__ = ['api_ns', 'norm_tool_calls', 'norm_finish', 'norm_parts', 'norm_sse_event', 'delta_index_fn', '
|
|
5
|
-
'denorm_tool_use', 'denorm_assistant', 'denorm_tool', 'denorm_msgs', 'denorm_tool_schs',
|
|
4
|
+
__all__ = ['api_ns', 'norm_tool_calls', 'norm_finish', 'norm_parts', 'raw_msg', 'norm_sse_event', 'delta_index_fn', 'collate_raw',
|
|
5
|
+
'acollect_stream', 'denorm_tool_use', 'denorm_assistant', 'denorm_tool', 'denorm_msgs', 'denorm_tool_schs',
|
|
6
6
|
'denorm_tool_choice', 'denorm_reasoning', 'denorm_web_search', 'denorm_system', 'denorm_user',
|
|
7
7
|
'denorm_image', 'denorm_audio', 'denorm_file', 'denorm_tool_result', 'mk_payload', 'get_hdrs', 'cost',
|
|
8
8
|
'fix_payload']
|
|
@@ -50,6 +50,11 @@ def norm_parts(resp):
|
|
|
50
50
|
if ref := msg.get('refusal'): parts.append(Refusal(ref))
|
|
51
51
|
return parts + norm_tool_calls(resp)
|
|
52
52
|
|
|
53
|
+
# %% ../nbs/03_oai_chat.ipynb #a30e4324
|
|
54
|
+
def raw_msg(resp):
|
|
55
|
+
"The assistant message as the API sent it"
|
|
56
|
+
return nested_idx(resp, 'choices', 0, 'message')
|
|
57
|
+
|
|
53
58
|
# %% ../nbs/03_oai_chat.ipynb #9baac40e
|
|
54
59
|
def norm_sse_event(ev, **kwargs):
|
|
55
60
|
"Normalize a chat completion stream event."
|
|
@@ -73,6 +78,23 @@ def delta_index_fn(d, typ, last_typ, last_idx):
|
|
|
73
78
|
if typ == last_typ: return last_idx, last_idx
|
|
74
79
|
return last_idx + 1, last_idx + 1
|
|
75
80
|
|
|
81
|
+
# %% ../nbs/03_oai_chat.ipynb #b7a19b8d
|
|
82
|
+
def collate_raw(deltas):
|
|
83
|
+
"The assistant message rebuilt from a stream's chunks"
|
|
84
|
+
msg, tcs = dict(role='assistant', content=None), {}
|
|
85
|
+
for ev in L(deltas).attrgot('raw'):
|
|
86
|
+
d = nested_idx(ev, 'choices', 0, 'delta') or {}
|
|
87
|
+
for k in ('content', 'reasoning_content', 'refusal'):
|
|
88
|
+
if d.get(k): msg[k] = (msg.get(k) or '') + d[k]
|
|
89
|
+
for tc in d.get('tool_calls') or []:
|
|
90
|
+
t = tcs.setdefault(tc['index'], dict(id=tc.get('id'), type='function', function=dict(name='', arguments='')))
|
|
91
|
+
if tc.get('id'): t['id'] = tc['id']
|
|
92
|
+
fn = tc.get('function') or {}
|
|
93
|
+
t['function']['name'] += fn.get('name') or ''
|
|
94
|
+
t['function']['arguments'] += fn.get('arguments') or ''
|
|
95
|
+
if tcs: msg['tool_calls'] = [tcs[i] for i in sorted(tcs)]
|
|
96
|
+
return msg
|
|
97
|
+
|
|
76
98
|
# %% ../nbs/03_oai_chat.ipynb #e7f8965b
|
|
77
99
|
@delegates(mk_acollect_stream, but=['index_fn', 'api_name'])
|
|
78
100
|
async def acollect_stream(resp, **kwargs):
|
|
@@ -85,21 +107,12 @@ def denorm_tool_use(p:ToolUse):
|
|
|
85
107
|
return dict(id=p.id, type='function', function=dict(name=p.name, arguments=json.dumps(p.arguments)))
|
|
86
108
|
|
|
87
109
|
def denorm_assistant(m:Msg):
|
|
88
|
-
"
|
|
89
|
-
|
|
90
|
-
for p in m.content
|
|
91
|
-
if isinstance(p, ToolUse):
|
|
92
|
-
tcs.append(denorm_tool_use(p))
|
|
93
|
-
if p.server:
|
|
94
|
-
srv_txt = f"[Server tool `{p.name}` executed successfully, results are generated]"
|
|
95
|
-
srv_responses.append(dict(role='tool', tool_call_id=p.id, content=srv_txt))
|
|
96
|
-
elif isinstance(p, Text): texts.append(p)
|
|
97
|
-
msg = dict(role='assistant')
|
|
110
|
+
"The message as the API sent it, else its text and tool calls"
|
|
111
|
+
if m.raw: return m.raw
|
|
112
|
+
msg, texts = dict(role='assistant'), [p for p in m.content if isinstance(p, Text)]
|
|
98
113
|
if texts: msg['content'] = texts[0].text if len(texts)==1 else [dict(type='text', text=p.text or '') for p in texts]
|
|
99
|
-
if tcs: msg['tool_calls'] = tcs
|
|
100
|
-
|
|
101
|
-
if thinking: msg['reasoning_content'] = ''.join(p.text or '' for p in thinking)
|
|
102
|
-
return [msg] + srv_responses
|
|
114
|
+
if tcs := [denorm_tool_use(p) for p in m.content if isinstance(p, ToolUse)]: msg['tool_calls'] = tcs
|
|
115
|
+
return msg
|
|
103
116
|
|
|
104
117
|
def denorm_tool(m:Msg):
|
|
105
118
|
"Convert canonical tool Msg to list of OpenAI Chat tool messages."
|
|
@@ -110,7 +123,7 @@ def denorm_msgs(msgs:list[Msg]):
|
|
|
110
123
|
res = []
|
|
111
124
|
for m in msgs:
|
|
112
125
|
if m.role == 'user': res.append(denorm_user(m))
|
|
113
|
-
elif m.role == 'assistant': res.
|
|
126
|
+
elif m.role == 'assistant': res.append(denorm_assistant(m))
|
|
114
127
|
elif m.role == 'tool': res.extend(denorm_tool(m))
|
|
115
128
|
return res
|
|
116
129
|
|
|
@@ -226,6 +239,6 @@ def fix_payload(payload, model, vendor_name):
|
|
|
226
239
|
|
|
227
240
|
# %% ../nbs/03_oai_chat.ipynb #e2b0908e
|
|
228
241
|
api_ns = dict(norm_tool_calls=norm_tool_calls, norm_parts=norm_parts, norm_finish=norm_finish, norm_usage=norm_usage,
|
|
229
|
-
acollect_stream=acollect_stream, mk_payload=mk_payload, cost=cost, get_hdrs=get_hdrs, fix_payload=fix_payload,
|
|
242
|
+
raw_msg=raw_msg, collate_raw=collate_raw, acollect_stream=acollect_stream, mk_payload=mk_payload, cost=cost, get_hdrs=get_hdrs, fix_payload=fix_payload,
|
|
230
243
|
endpoint='/chat/completions')
|
|
231
244
|
api_registry.register('openai_chat', **api_ns)
|
fastllm/openai_responses.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/02_oai_responses.ipynb.
|
|
2
2
|
|
|
3
3
|
# %% auto #0
|
|
4
|
-
__all__ = ['api_ns', 'norm_tool_call', 'norm_tool_calls', 'norm_usage', 'norm_finish', 'norm_parts', 'norm_sse_event',
|
|
5
|
-
'delta_index_fn', 'acollect_stream', 'denorm_tool_use', 'denorm_tool', 'denorm_assistant',
|
|
6
|
-
'denorm_tool_schs', 'denorm_tool_choice', 'denorm_reasoning', 'denorm_web_search',
|
|
7
|
-
'denorm_user', 'denorm_image', 'denorm_video', 'denorm_file', 'denorm_tool_result',
|
|
8
|
-
'cost', 'fix_payload']
|
|
4
|
+
__all__ = ['api_ns', 'norm_tool_call', 'norm_tool_calls', 'norm_usage', 'norm_finish', 'norm_parts', 'raw_msg', 'norm_sse_event',
|
|
5
|
+
'delta_index_fn', 'collate_raw', 'acollect_stream', 'denorm_tool_use', 'denorm_tool', 'denorm_assistant',
|
|
6
|
+
'denorm_msgs', 'denorm_tool_schs', 'denorm_tool_choice', 'denorm_reasoning', 'denorm_web_search',
|
|
7
|
+
'denorm_system', 'denorm_user', 'denorm_image', 'denorm_video', 'denorm_file', 'denorm_tool_result',
|
|
8
|
+
'mk_payload', 'get_hdrs', 'cost', 'fix_payload']
|
|
9
9
|
|
|
10
10
|
# %% ../nbs/02_oai_responses.ipynb #591f55b5
|
|
11
11
|
import json
|
|
@@ -78,6 +78,11 @@ def norm_parts(resp):
|
|
|
78
78
|
elif typ == "function_call" or typ.endswith("_call"): parts.append(norm_tool_call(item))
|
|
79
79
|
return parts
|
|
80
80
|
|
|
81
|
+
# %% ../nbs/02_oai_responses.ipynb #2aaab962
|
|
82
|
+
def raw_msg(resp):
|
|
83
|
+
"The output items as the API sent them"
|
|
84
|
+
return resp['output']
|
|
85
|
+
|
|
81
86
|
# %% ../nbs/02_oai_responses.ipynb #ef2d19f3
|
|
82
87
|
@patch(as_prop=True)
|
|
83
88
|
def response_id(self:Completion):
|
|
@@ -89,11 +94,6 @@ def response_id(self:Completion):
|
|
|
89
94
|
for d in reversed(self.raw.get('deltas', [])):
|
|
90
95
|
if rid := nested_idx(d.raw, 'response', 'id'): return rid
|
|
91
96
|
|
|
92
|
-
@patch(as_prop=True)
|
|
93
|
-
def response_id_reusable(self:Completion):
|
|
94
|
-
"Can the provider continuation identifier be consumed more than once?"
|
|
95
|
-
return self.raw.get('response_id_reusable', True)
|
|
96
|
-
|
|
97
97
|
# %% ../nbs/02_oai_responses.ipynb #7cd48aa5
|
|
98
98
|
def norm_sse_event(ev, **kwargs):
|
|
99
99
|
"Normalize OpenAI Responses API stream event into Delta."
|
|
@@ -107,6 +107,7 @@ def norm_sse_event(ev, **kwargs):
|
|
|
107
107
|
item = ev.get("item", {})
|
|
108
108
|
itype = item.get("type", "")
|
|
109
109
|
if itype == "function_call" or itype.endswith("_call"): return Delta(tool_calls=[norm_tool_call(item)], raw=ev, **kwargs)
|
|
110
|
+
return Delta(raw=ev, **kwargs)
|
|
110
111
|
if typ in ("response.completed", "response.incomplete"):
|
|
111
112
|
resp = ev.get("response", {})
|
|
112
113
|
return Delta(finish_reason=norm_finish(resp), usage=norm_usage(resp), raw=ev, **kwargs)
|
|
@@ -118,6 +119,11 @@ def delta_index_fn(d, typ, last_typ, last_idx):
|
|
|
118
119
|
idx = nested_idx(d, 'raw', 'output_index')
|
|
119
120
|
return idx, idx
|
|
120
121
|
|
|
122
|
+
# %% ../nbs/02_oai_responses.ipynb #689e31b0
|
|
123
|
+
def collate_raw(deltas):
|
|
124
|
+
"The output items rebuilt from a stream's events"
|
|
125
|
+
return [d.raw['item'] for d in deltas if d.raw.get('type') == 'response.output_item.done']
|
|
126
|
+
|
|
121
127
|
# %% ../nbs/02_oai_responses.ipynb #021b5257
|
|
122
128
|
@delegates(mk_acollect_stream, but=['index_fn', 'api_name'])
|
|
123
129
|
async def acollect_stream(resp, **kwargs):
|
|
@@ -137,20 +143,12 @@ def denorm_tool(m:Msg): return [denorm_tool_result(p) for p in m.content if isin
|
|
|
137
143
|
|
|
138
144
|
# %% ../nbs/02_oai_responses.ipynb #67c8de13
|
|
139
145
|
def denorm_assistant(m:Msg):
|
|
140
|
-
items
|
|
146
|
+
"The output items as the API sent them, else the message's text and tool calls as items"
|
|
147
|
+
if m.raw: return m.raw
|
|
148
|
+
items = []
|
|
141
149
|
for p in m.content:
|
|
142
|
-
if isinstance(p, ToolUse):
|
|
143
|
-
|
|
144
|
-
# TODO: Server tcs are excluded during deserialization in lisette, so this
|
|
145
|
-
# is not used. Anthropic checks for `srvtoolu` and requires the full tool result metadata
|
|
146
|
-
# which is a lot to store.
|
|
147
|
-
# Instead of reconstructing server tool calls as actual tool uses we can make it a regular msg
|
|
148
|
-
# telling AI a server tool was executed
|
|
149
|
-
if p.server:
|
|
150
|
-
srv_txt = f"[Server tool `{p.name}` executed successfully, results are generated below]"
|
|
151
|
-
items.append(dict(type='function_call_output', call_id=p.id, output=srv_txt))
|
|
152
|
-
elif isinstance(p, (Text, Thinking)):
|
|
153
|
-
items.append(dict(type="message", role="assistant", content=[dict(type="output_text", text=p.text)]))
|
|
150
|
+
if isinstance(p, ToolUse): items.append(denorm_tool_use(p))
|
|
151
|
+
elif isinstance(p, Text): items.append(dict(type="message", role="assistant", content=[dict(type="output_text", text=p.text or '')]))
|
|
154
152
|
return items
|
|
155
153
|
|
|
156
154
|
# %% ../nbs/02_oai_responses.ipynb #64883b99
|
|
@@ -280,6 +278,7 @@ def fix_payload(payload, model, vendor_name):
|
|
|
280
278
|
|
|
281
279
|
# %% ../nbs/02_oai_responses.ipynb #07114b55
|
|
282
280
|
api_ns = dict(norm_tool_calls=norm_tool_calls, norm_parts=norm_parts, norm_finish=norm_finish, norm_usage=norm_usage,
|
|
283
|
-
supports_previous_response_id=True,
|
|
281
|
+
supports_previous_response_id=True, raw_msg=raw_msg, collate_raw=collate_raw, acollect_stream=acollect_stream, mk_payload=mk_payload,
|
|
282
|
+
cost=cost, get_hdrs=get_hdrs, fix_payload=fix_payload,
|
|
284
283
|
endpoint='/responses')
|
|
285
284
|
api_registry.register('openai', **api_ns)
|
fastllm/responses.py
CHANGED
|
@@ -143,7 +143,7 @@ def response_object(rid, body, comp, previous_response_id=None, created_at=None)
|
|
|
143
143
|
# %% ../nbs/06a_responses.ipynb #73a2fc16
|
|
144
144
|
class ResponseState(BasicRepr):
|
|
145
145
|
"Canonical history and optional provider continuation metadata."
|
|
146
|
-
def __init__(self, id, model, history, provider_response_id=None
|
|
146
|
+
def __init__(self, id, model, history, provider_response_id=None): store_attr()
|
|
147
147
|
|
|
148
148
|
# %% ../nbs/06a_responses.ipynb #c9ca3744
|
|
149
149
|
class ResponseTurn(BasicRepr):
|
|
@@ -160,6 +160,7 @@ class ResponseTurn(BasicRepr):
|
|
|
160
160
|
# %% ../nbs/06a_responses.ipynb #a648297f
|
|
161
161
|
class AsyncResponses:
|
|
162
162
|
"Run individual Responses turns over FastLLM."
|
|
163
|
+
def __init__(self): self.tasks = set() # served turns still running
|
|
163
164
|
def prepare(self, body, previous=None, server_tools=None, **kwargs):
|
|
164
165
|
if not isinstance(body, dict): raise ResponsesError('Request body must be an object')
|
|
165
166
|
model = body.get('model')
|
|
@@ -203,7 +204,7 @@ def state(self:AsyncResponses, turn, comp, results=()):
|
|
|
203
204
|
"Build the next continuation state; `results` are server tool results answering `comp`'s calls."
|
|
204
205
|
normalize_call_ids(comp)
|
|
205
206
|
history = (*turn.history, comp.message, *([Msg('tool', list(results))] if results else []))
|
|
206
|
-
return ResponseState(turn.id, turn.body['model'], history, comp.response_id
|
|
207
|
+
return ResponseState(turn.id, turn.body['model'], history, comp.response_id)
|
|
207
208
|
|
|
208
209
|
|
|
209
210
|
# %% ../nbs/06a_responses.ipynb #7615b274
|
|
@@ -343,3 +344,21 @@ async def events(self:AsyncResponses, turn, finalize=None, stream=None):
|
|
|
343
344
|
except ResponsesError as exc: yield state.event('error', code=exc.code, message=str(exc), param=exc.param)
|
|
344
345
|
except APIError as exc: yield state.event('error', code=exc.code or 'provider_error', message=str(exc), param=None)
|
|
345
346
|
|
|
347
|
+
|
|
348
|
+
# %% ../nbs/06a_responses.ipynb #cc32ce40
|
|
349
|
+
@patch
|
|
350
|
+
def serve(self:AsyncResponses, turn, finalize=None, stream=None, timeout=None):
|
|
351
|
+
"The frames of `events`, from a task that outlives its reader, so a disconnected client still gets its turn finalized"
|
|
352
|
+
q = asyncio.Queue()
|
|
353
|
+
async def _run():
|
|
354
|
+
try:
|
|
355
|
+
async with asyncio.timeout(timeout):
|
|
356
|
+
async for frame in self.events(turn, finalize=finalize, stream=stream): q.put_nowait(frame)
|
|
357
|
+
except TimeoutError: q.put_nowait(response_event('error', 0, code='timeout', message=f'turn exceeded {timeout}s', param=None))
|
|
358
|
+
finally: q.put_nowait(None)
|
|
359
|
+
t = asyncio.create_task(_run())
|
|
360
|
+
self.tasks.add(t)
|
|
361
|
+
t.add_done_callback(self.tasks.discard)
|
|
362
|
+
async def _frames():
|
|
363
|
+
while (frame := await q.get()) is not None: yield frame
|
|
364
|
+
return _frames()
|
fastllm/streaming.py
CHANGED
|
@@ -12,13 +12,12 @@ from fastcore.utils import *
|
|
|
12
12
|
from fastcore.meta import delegates
|
|
13
13
|
from fasttransport.errors import *
|
|
14
14
|
from .types import *
|
|
15
|
-
from aidialog.msg_parts import Part, PartType, Msg, Text, Thinking, ToolUse, ToolResult,
|
|
15
|
+
from aidialog.msg_parts import Part, PartType, Msg, Text, Thinking, ToolUse, ToolResult, mk_part, fence_call_re, Completion
|
|
16
16
|
|
|
17
17
|
# %% ../nbs/01_streaming.ipynb #400d628a
|
|
18
18
|
class Delta(BasicRepr):
|
|
19
19
|
"Normalized streaming delta event."
|
|
20
|
-
def __init__(self, text='', thinking='', refusal='', tool_calls=None, citations=None,
|
|
21
|
-
server_tool_result=None, finish_reason=None, usage=None, raw=None):
|
|
20
|
+
def __init__(self, text='', thinking='', refusal='', tool_calls=None, citations=None, finish_reason=None, usage=None, raw=None):
|
|
22
21
|
if tool_calls is None: tool_calls = []
|
|
23
22
|
if citations is None: citations = []
|
|
24
23
|
if raw is None: raw = {}
|
|
@@ -172,11 +171,7 @@ async def mk_acollect_stream(it, index_fn, model=None, api_name=None, vendor_nam
|
|
|
172
171
|
except json.JSONDecodeError: continue
|
|
173
172
|
acc.arguments = args
|
|
174
173
|
yield acc
|
|
175
|
-
# Server tool results for anthropic are yielded in d.server_tool_result by checking injected dummy `_delta`
|
|
176
174
|
if acc.server: yield ToolResult(id=acc.id, name=acc.name, arguments=args, server=True, text="Server tool call executed.")
|
|
177
|
-
if d.server_tool_result:
|
|
178
|
-
idx = _fidx(d, 'server_tool_result')
|
|
179
|
-
part_accum.parts[idx] = ServerToolResult(raw=d.server_tool_result)
|
|
180
175
|
r = _proc(d, 'refusal')
|
|
181
176
|
if r[0]: yield _mk_delta_part('refusal', r[0]['refusal'])
|
|
182
177
|
if d.finish_reason: fin = d.finish_reason
|
|
@@ -190,6 +185,6 @@ async def mk_acollect_stream(it, index_fn, model=None, api_name=None, vendor_nam
|
|
|
190
185
|
fin = FinishReason.tool_calls if fin==FinishReason.stop and any(~L(tcs).attrgot('server')) else fin # recheck tool calls post collation
|
|
191
186
|
# tool calls and non-anthropic citations are yielded at the end
|
|
192
187
|
yield Completion(model,
|
|
193
|
-
message=Msg(role="assistant", content=parts),
|
|
188
|
+
message=Msg(role="assistant", content=parts, raw=api_registry[api_name].collate_raw(deltas) if api_name else None),
|
|
194
189
|
finish_reason=fin, usage=usg, api_name=api_name, vendor_name=vendor_name,
|
|
195
190
|
raw={'deltas':deltas})
|
fastllm/types.py
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_types.ipynb.
|
|
4
4
|
|
|
5
5
|
# %% auto #0
|
|
6
|
-
__all__ = ['FinishReason', 'api_registry', 'model_prices_url', 'sample_img_url', '
|
|
7
|
-
'opus46', 'opus48', 'opus', 'opus5', 'fable', 'fable5', 'fable51', 'gpt54', 'gpt54m',
|
|
8
|
-
'codex54m', 'codex55', 'codex53spark', 'model_info_registry', 'modern_llm',
|
|
9
|
-
'deepseek_v4_common', 'deepseek_v4_flash_prices', 'mimo_v25_common', 'codex_pricing', 'sol',
|
|
10
|
-
'gpt56s', 'Usage', 'APIRegistry', 'mk_completion', 'fn_schema', 'payload_kwargs',
|
|
11
|
-
'get_api_key', 'wrap_typed', 'unwrap_typed', 'resize_b64', 'model_prices_meta',
|
|
12
|
-
'get_model_meta', 'register_model_info', 'get_model_info', 'effort_code', 'effort_levels',
|
|
13
|
-
'get_model_pricing', 'approx_pricing', 'is_deepseek_peak_hour', 'price_tier', 'tier_rate']
|
|
6
|
+
__all__ = ['FinishReason', 'api_registry', 'model_prices_url', 'sample_img_url', 'sample_doms', 'haik45', 'sonn45', 'sonn46',
|
|
7
|
+
'sonn', 'sonn5', 'opus46', 'opus48', 'opus', 'opus5', 'fable', 'fable5', 'fable51', 'gpt54', 'gpt54m',
|
|
8
|
+
'gpt55', 'codex54', 'codex54m', 'codex55', 'codex53spark', 'model_info_registry', 'modern_llm',
|
|
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',
|
|
11
|
+
'provider_req', 'get_api_key', 'wrap_typed', 'unwrap_typed', 'resize_b64', 'model_prices_meta',
|
|
12
|
+
'infer_api_name', 'get_model_meta', 'register_model_info', 'get_model_info', 'effort_code', 'effort_levels',
|
|
13
|
+
'resolve_effort', 'get_model_pricing', 'approx_pricing', 'is_deepseek_peak_hour', 'price_tier', 'tier_rate']
|
|
14
14
|
|
|
15
15
|
# %% ../nbs/00_types.ipynb #b4d047fd
|
|
16
16
|
import httpx2, base64, io
|
|
@@ -68,7 +68,8 @@ FinishReason = str_enum('finish_reason', 'stop', 'tool_calls', 'length', 'conten
|
|
|
68
68
|
# %% ../nbs/00_types.ipynb #fc681c52
|
|
69
69
|
class APIRegistry(dict):
|
|
70
70
|
_eps = None
|
|
71
|
-
def register(self, name, finalize_usage=noop,
|
|
71
|
+
def register(self, name, finalize_usage=noop, raw_msg=noop, collate_raw=noop, **kwargs):
|
|
72
|
+
self[name] = SimpleNamespace(finalize_usage=finalize_usage, raw_msg=raw_msg, collate_raw=collate_raw, **kwargs)
|
|
72
73
|
def _load(self, name):
|
|
73
74
|
"Import the plugin providing api `name` from the `fastllm.apis` entry-point group; its import registers the api."
|
|
74
75
|
if self._eps is None: self._eps = {o.name:o for o in entry_points(group='fastllm.apis')}
|
|
@@ -90,7 +91,7 @@ def mk_completion(resp, model, api_name, vendor_name):
|
|
|
90
91
|
tcs = api.norm_tool_calls(resp)
|
|
91
92
|
parts = api.norm_parts(resp)
|
|
92
93
|
usg = api.finalize_usage(api.norm_usage(resp), parts)
|
|
93
|
-
return Completion(model=model, message=Msg(role="assistant", content=parts), finish_reason=api.norm_finish(resp, tcs),
|
|
94
|
+
return Completion(model=model, message=Msg(role="assistant", content=parts, raw=api.raw_msg(resp)), finish_reason=api.norm_finish(resp, tcs),
|
|
94
95
|
usage=usg, api_name=api_name, vendor_name=vendor_name, raw=resp)
|
|
95
96
|
|
|
96
97
|
# %% ../nbs/00_types.ipynb #8a8e468b
|
|
@@ -169,6 +170,7 @@ def model_prices_meta():
|
|
|
169
170
|
|
|
170
171
|
# %% ../nbs/00_types.ipynb #019882a3
|
|
171
172
|
sample_img_url = "https://img.magnific.com/free-photo/mountain-range-body-water_53876-139760.jpg?semt=ais_hybrid&w=740&q=80"
|
|
173
|
+
sample_doms = ('img.magnific.com', 'arxiv.org', 'openaiassets.blob.core.windows.net') # hosts of the sample media the lessons fetch, for cachy
|
|
172
174
|
|
|
173
175
|
# %% ../nbs/00_types.ipynb #68e488d8
|
|
174
176
|
def infer_api_name(model):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-fastllm
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.51
|
|
4
4
|
Author-email: Kerem Turgutlu <keremturgutlu@gmail.com>
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Project-URL: Repository, https://github.com/AnswerDotAI/fastllm
|
|
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
10
10
|
Requires-Python: >=3.10
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
Requires-Dist: fastcore>=2.1.18
|
|
13
|
-
Requires-Dist: aidialog>=0.0.
|
|
13
|
+
Requires-Dist: aidialog>=0.0.25
|
|
14
14
|
Requires-Dist: fasttransport>=0.0.2
|
|
15
15
|
Requires-Dist: pillow
|
|
16
16
|
|
|
@@ -21,13 +21,14 @@ Requires-Dist: pillow
|
|
|
21
21
|
|
|
22
22
|
## Install
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
Run `pip install python-fastllm` or clone from github and install locally.
|
|
25
25
|
|
|
26
26
|
## Setup
|
|
27
27
|
|
|
28
28
|
``` python
|
|
29
29
|
from aidialog.msg_parts import Msg, Part, Text, Thinking, ToolUse, InputImage, mk_tool_res_msg, Completion
|
|
30
30
|
from fastllm.acomplete import acomplete
|
|
31
|
+
from fastllm.types import sample_img_url, sample_doms
|
|
31
32
|
import asyncio, json
|
|
32
33
|
|
|
33
34
|
# Helpers
|
|
@@ -56,7 +57,7 @@ The same `acomplete` call works with Claude, GPT, Gemini, and Kimi. Just change
|
|
|
56
57
|
|
|
57
58
|
``` python
|
|
58
59
|
models = [
|
|
59
|
-
('claude-sonnet-
|
|
60
|
+
('claude-sonnet-5', {}),
|
|
60
61
|
('gpt-4o-mini', {}),
|
|
61
62
|
('models/gemini-3-flash-preview', {}),
|
|
62
63
|
('accounts/fireworks/models/kimi-k2p5', dict(vendor_name='fireworks_ai'))
|
|
@@ -80,46 +81,6 @@ for name, kw in models:
|
|
|
80
81
|
|
|
81
82
|
The response should be simple and direct.
|
|
82
83
|
|
|
83
|
-
## Multi-Turn — Swap Providers Mid-Conversation
|
|
84
|
-
|
|
85
|
-
Build a conversation with one provider, then seamlessly continue it with another. `fastllm` translates between every provider’s native format automatically:
|
|
86
|
-
|
|
87
|
-
``` python
|
|
88
|
-
# Turn 1: Claude starts the conversation
|
|
89
|
-
msgs = [user("Name the 3 largest planets in our solar system. One sentence.")]
|
|
90
|
-
print("Claude: ", end='')
|
|
91
|
-
r1 = await stream(msgs, model='claude-sonnet-4-20250514', max_tokens=mtok)
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
Claude: The three largest planets in our solar system are Jupiter, Saturn, and Neptune.
|
|
95
|
-
|
|
96
|
-
``` python
|
|
97
|
-
# Turn 2: Switch to GPT — just change the model string
|
|
98
|
-
msgs += [r1.message, user("Which one has the most moons?")]
|
|
99
|
-
print("GPT: ", end='')
|
|
100
|
-
r2 = await stream(msgs, model='gpt-4o-mini', max_tokens=mtok)
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
GPT: As of now, Saturn has the most moons, with over 80 confirmed moons.
|
|
104
|
-
|
|
105
|
-
``` python
|
|
106
|
-
# Turn 3: Switch to Gemini — same msgs, different provider
|
|
107
|
-
msgs += [r2.message, user("Summarize our conversation in one sentence.")]
|
|
108
|
-
print("Gemini: ", end='')
|
|
109
|
-
r3 = await stream(msgs, model='models/gemini-3-flash-preview', max_tokens=mtok)
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
Gemini: The conversation identified the three largest planets in the solar system and noted that Saturn currently has the most moons.
|
|
113
|
-
|
|
114
|
-
``` python
|
|
115
|
-
# Turn 4: Switch to Kimi — works the same way
|
|
116
|
-
msgs += [r3.message, user("Thanks! What's one surprising fact about Saturn?")]
|
|
117
|
-
print("Kimi: ", end='')
|
|
118
|
-
r4 = await stream(msgs, model='accounts/fireworks/models/kimi-k2p5', vendor_name='fireworks_ai', max_tokens=mtok)
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
Kimi: 🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠Saturn is less dense than water, meaning it would theoretically float if you could find a bathtub large enough to hold it.
|
|
122
|
-
|
|
123
84
|
## System Prompts
|
|
124
85
|
|
|
125
86
|
Pass a system prompt to any provider — `fastllm` maps it to each API’s native mechanism (Anthropic `system`, OpenAI `instructions`, Gemini `system_instruction`):
|
|
@@ -128,7 +89,7 @@ Pass a system prompt to any provider — `fastllm` maps it to each API’s nativ
|
|
|
128
89
|
sys = "You are a pirate chef. Always respond in pirate speak and mention food."
|
|
129
90
|
|
|
130
91
|
print("Claude: ", end='')
|
|
131
|
-
r = await stream([user("What should I do today?")], model='claude-sonnet-
|
|
92
|
+
r = await stream([user("What should I do today?")], model='claude-sonnet-5', system=sys, max_tokens=mtok)
|
|
132
93
|
|
|
133
94
|
print("Gemini: ", end='')
|
|
134
95
|
r = await stream([user("What should I do today?")], model='models/gemini-3-flash-preview', system=sys, max_tokens=mtok)
|
|
@@ -161,7 +122,7 @@ r = await stream([user("What should I do today?")], model='models/gemini-3-flash
|
|
|
161
122
|
|
|
162
123
|
## Tool Calling — Define Once, Use Anywhere
|
|
163
124
|
|
|
164
|
-
Define tools in a single canonical format. `fastllm` translates to each provider’s native tool schema automatically. Here
|
|
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:
|
|
165
126
|
|
|
166
127
|
``` python
|
|
167
128
|
tools = [{"type": "function", "function": {
|
|
@@ -172,7 +133,7 @@ tools = [{"type": "function", "function": {
|
|
|
172
133
|
|
|
173
134
|
# Turn 1: Claude requests the tool
|
|
174
135
|
msgs = [user("What's the weather in Paris?")]
|
|
175
|
-
r1 = await stream(msgs, model='claude-sonnet-
|
|
136
|
+
r1 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
|
|
176
137
|
print("Tool calls:", r1.tool_calls)
|
|
177
138
|
```
|
|
178
139
|
|
|
@@ -182,41 +143,26 @@ print("Tool calls:", r1.tool_calls)
|
|
|
182
143
|
Tool calls: [ToolUse(raw={'caller': {'type': 'direct'}}, cache_control=None, id='toolu_01RMN1WM7vPBT3ovv5Ex6VzC', name='get_weather', arguments={'city': 'Paris'}, server=False, text=None)]
|
|
183
144
|
|
|
184
145
|
``` python
|
|
185
|
-
# Provide the tool result
|
|
186
|
-
msgs += [r1.message, mk_tool_res_msg(r1.tool_calls, ['22°C, sunny with light clouds'])]
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
msgs.append(user("Should I bring a jacket?"))
|
|
190
|
-
print("GPT: ", end='')
|
|
191
|
-
r2 = await stream(msgs, model='gpt-4o-mini', tools=tools, max_tokens=mtok)
|
|
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)
|
|
192
150
|
```
|
|
193
151
|
|
|
194
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.
|
|
195
153
|
|
|
196
|
-
``` python
|
|
197
|
-
# Turn 3: Gemini sees the full cross-provider tool history
|
|
198
|
-
msgs += [r2.message, user("How about tomorrow — will it rain?")]
|
|
199
|
-
r3 = await stream(msgs, model='models/gemini-3-flash-preview', tools=tools, max_tokens=mtok, web_search_options={})
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
Tomorrow in Paris (Saturday, June 13), the weather is expected to be mostly pleasant and sunny.
|
|
203
|
-
|
|
204
|
-
While there is a small chance of light, passing showers (around a 20-25% chance), most forecasts indicate a dry day with mostly clear or scattered clouds. Temperatures will likely reach a comfortable high of around 22°C to 27°C (72°F to 80°F), making it a great day for being outdoors.
|
|
205
|
-
|
|
206
|
-
So, while you might see a stray shower, you likely won't need to worry about heavy rain!
|
|
207
|
-
|
|
208
154
|
## Tool Choice
|
|
209
155
|
|
|
210
156
|
Control whether the model must use tools, can’t use tools, or decides on its own:
|
|
211
157
|
|
|
212
158
|
``` python
|
|
213
159
|
# Force the model to call a tool (even for a greeting)
|
|
214
|
-
r = await stream([user("Hello there!")], model='claude-sonnet-
|
|
160
|
+
r = await stream([user("Hello there!")], model='claude-sonnet-5', tools=tools, tool_choice='required', max_tokens=mtok)
|
|
215
161
|
print("Forced:", r.tool_calls)
|
|
216
162
|
|
|
217
163
|
# Prevent tool use (model must answer directly)
|
|
218
164
|
print("\nNo tools: ", end='')
|
|
219
|
-
r = await stream([user("What's the weather?")], model='claude-sonnet-
|
|
165
|
+
r = await stream([user("What's the weather?")], model='claude-sonnet-5', tools=tools, tool_choice='none', max_tokens=mtok)
|
|
220
166
|
```
|
|
221
167
|
|
|
222
168
|
- ⏳ `get_weather(city="<UNKNOWN>")` ⏳
|
|
@@ -327,11 +273,11 @@ long_ctx = "You are an expert on the solar system. " * 200
|
|
|
327
273
|
system = Text(long_ctx, cache_control={'type': 'ephemeral'})
|
|
328
274
|
|
|
329
275
|
print("Call 1: ", end='')
|
|
330
|
-
r1 = await stream([user("What is Jupiter's mass?")], model='claude-sonnet-
|
|
276
|
+
r1 = await stream([user("What is Jupiter's mass?")], model='claude-sonnet-5', system=system, max_tokens=mtok)
|
|
331
277
|
print(f"Usage: {r1.usage}")
|
|
332
278
|
|
|
333
279
|
print("Call 2: ", end='')
|
|
334
|
-
r2 = await stream([user("How about Saturn?")], model='claude-sonnet-
|
|
280
|
+
r2 = await stream([user("How about Saturn?")], model='claude-sonnet-5', system=system, max_tokens=mtok)
|
|
335
281
|
print(f"Usage: {r2.usage}")
|
|
336
282
|
```
|
|
337
283
|
|
|
@@ -381,13 +327,12 @@ print(f"Usage: {r2.usage}")
|
|
|
381
327
|
Send images to any provider that supports them. The canonical `input_image` part works everywhere:
|
|
382
328
|
|
|
383
329
|
``` python
|
|
384
|
-
img_url = "https://img.freepik.com/free-photo/mountain-range-body-water_53876-139760.jpg?semt=ais_hybrid&w=740&q=80"
|
|
385
330
|
img_msg = Msg(role='user', content=[
|
|
386
|
-
InputImage(
|
|
331
|
+
InputImage(sample_img_url),
|
|
387
332
|
Text("What do you see in this image?")
|
|
388
333
|
])
|
|
389
334
|
|
|
390
|
-
for name, kw in [('claude-sonnet-
|
|
335
|
+
for name, kw in [('claude-sonnet-5', {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {})]:
|
|
391
336
|
print(f"{name:>30s}: ", end='')
|
|
392
337
|
r = await stream([img_msg], model=name, max_tokens=mtok, **kw)
|
|
393
338
|
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
fastllm/__init__.py,sha256=9Ir0J8R-z0_JnZDe2PsJohUmaNENqlehYHPTO3403g8,131
|
|
2
|
+
fastllm/_modidx.py,sha256=fQ6pg5H_5Pp8l7k12Fb5ca6uWmQewKtGtZBDi8hCuFg,44277
|
|
3
|
+
fastllm/acomplete.py,sha256=aP1ff9a7WKNhBd1kwKE2TJ-YyihLcdhd9-1Izi4wQP4,10772
|
|
4
|
+
fastllm/anthropic.py,sha256=k0QeuRnl_CalfCq74WXN6llRQqRiAQ9d8ohn_0YYC7A,18386
|
|
5
|
+
fastllm/chat.py,sha256=klji3SOYs5xyzrSFbNhy3uoQ2TSZ3a-8HTjvdRtaG5Q,25036
|
|
6
|
+
fastllm/codex.py,sha256=r6auZ8egia83aGkWcF4132AzW4SsBvKeDwg_CxyXj9E,5808
|
|
7
|
+
fastllm/gemini.py,sha256=zbxNmI1cMUJJ10xIfBqPqh38hAQ0exsFKkhTXvG2-eE,16281
|
|
8
|
+
fastllm/openai_chat.py,sha256=n5INfDCsAZIiKUImWeaX6SJ_OAJ_uMhw20HUkgFgdgM,12379
|
|
9
|
+
fastllm/openai_responses.py,sha256=zsaOjkQUZ-7iuGPc-UpvnM6oAyqW09BDYwZ_ma4-1Qo,14346
|
|
10
|
+
fastllm/responses.py,sha256=mFFaBpKNsq85sJjXIK5rYKb6WO7nlImEfowqDPf_yzA,19633
|
|
11
|
+
fastllm/streaming.py,sha256=i_FU_Mt-3F2lBEImYav8I5pEjUSKYk3JFIW3lkiXEb8,8813
|
|
12
|
+
fastllm/types.py,sha256=BIZHwlzsbkZELjAib2oSFRab8gKBa216DNNeE_P68go,19525
|
|
13
|
+
python_fastllm-0.0.51.dist-info/METADATA,sha256=Btmnr0P4mLlxL6J39LJEBg6Zw4KkTLu1-1o4ulP3RFY,18537
|
|
14
|
+
python_fastllm-0.0.51.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
15
|
+
python_fastllm-0.0.51.dist-info/entry_points.txt,sha256=WkWd04F3xVYFP5bsexaQe9Se6uelwE5ULoTtHWreO0s,133
|
|
16
|
+
python_fastllm-0.0.51.dist-info/top_level.txt,sha256=F8qodL7nEGUHGmzzqfhNKCTIr1i0D6cvudOnm3z7o0Y,8
|
|
17
|
+
python_fastllm-0.0.51.dist-info/RECORD,,
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
fastllm/__init__.py,sha256=udUKGtrdGuO2q1CvwAXrPId3ToCsgVd8MI9FczPGC08,131
|
|
2
|
-
fastllm/_modidx.py,sha256=UExnE14vuPnwPFZ1RcoK2iJawaLmlYJDsOKbpPx-F7o,42832
|
|
3
|
-
fastllm/acomplete.py,sha256=aP1ff9a7WKNhBd1kwKE2TJ-YyihLcdhd9-1Izi4wQP4,10772
|
|
4
|
-
fastllm/anthropic.py,sha256=RQpOpwIpXxpZe5httO7zgWautF7lJEPSzotcUP-eDVE,17265
|
|
5
|
-
fastllm/chat.py,sha256=klji3SOYs5xyzrSFbNhy3uoQ2TSZ3a-8HTjvdRtaG5Q,25036
|
|
6
|
-
fastllm/codex.py,sha256=r6auZ8egia83aGkWcF4132AzW4SsBvKeDwg_CxyXj9E,5808
|
|
7
|
-
fastllm/gemini.py,sha256=d0nLkxPr5k3X6A8CZNaAuiRrWxmJrc6FfNylJoKNpIc,15291
|
|
8
|
-
fastllm/openai_chat.py,sha256=mUOyos5bdmXukPRMD_sEDiVt4WXDRBzvYiCQskQUHPI,11769
|
|
9
|
-
fastllm/openai_responses.py,sha256=pWg1rA53u5A0aTvnBczxvTK-aBIHm01U9FeIg3-lLbE,14618
|
|
10
|
-
fastllm/responses.py,sha256=jcbKUwbEIsy7ngNcHUtv2bfYeDgqSysK_qWM-0LC9Mg,18767
|
|
11
|
-
fastllm/streaming.py,sha256=8qVtWeZ7NlP-9rup_4Ho0ol2dx3-XuGcIMXpZ1GYdEo,9079
|
|
12
|
-
fastllm/types.py,sha256=IYHA3Nfs2ghXUhgYy2bbbCAcfqU-guTOjI5gF3NW5NY,19260
|
|
13
|
-
python_fastllm-0.0.49.dist-info/METADATA,sha256=VYlgLNW8x53M1VvmBcW7qyq3-A6FF5a7Twh_z8-q3l0,21143
|
|
14
|
-
python_fastllm-0.0.49.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
15
|
-
python_fastllm-0.0.49.dist-info/entry_points.txt,sha256=WkWd04F3xVYFP5bsexaQe9Se6uelwE5ULoTtHWreO0s,133
|
|
16
|
-
python_fastllm-0.0.49.dist-info/top_level.txt,sha256=F8qodL7nEGUHGmzzqfhNKCTIr1i0D6cvudOnm3z7o0Y,8
|
|
17
|
-
python_fastllm-0.0.49.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|