python-fastllm 0.0.48__py3-none-any.whl → 0.0.50__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 +15 -7
- fastllm/acomplete.py +11 -10
- fastllm/anthropic.py +52 -43
- fastllm/chat.py +3 -1
- fastllm/gemini.py +41 -20
- fastllm/openai_chat.py +35 -19
- fastllm/openai_responses.py +27 -25
- fastllm/responses.py +2 -2
- fastllm/streaming.py +3 -8
- fastllm/types.py +9 -7
- {python_fastllm-0.0.48.dist-info → python_fastllm-0.0.50.dist-info}/METADATA +6 -5
- python_fastllm-0.0.50.dist-info/RECORD +17 -0
- python_fastllm-0.0.48.dist-info/RECORD +0 -17
- {python_fastllm-0.0.48.dist-info → python_fastllm-0.0.50.dist-info}/WHEEL +0 -0
- {python_fastllm-0.0.48.dist-info → python_fastllm-0.0.50.dist-info}/entry_points.txt +0 -0
- {python_fastllm-0.0.48.dist-info → python_fastllm-0.0.50.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,8 @@ 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')},
|
|
53
55
|
'fastllm.chat': { 'fastllm.chat.AsyncChat': ('chat.html#asyncchat', 'fastllm/chat.py'),
|
|
54
56
|
'fastllm.chat.AsyncChat.__call__': ('chat.html#asyncchat.__call__', 'fastllm/chat.py'),
|
|
55
57
|
'fastllm.chat.AsyncChat.__init__': ('chat.html#asyncchat.__init__', 'fastllm/chat.py'),
|
|
@@ -139,6 +141,7 @@ d = { 'settings': { 'branch': 'main',
|
|
|
139
141
|
'fastllm.gemini': { 'fastllm.gemini._gem_filter_sch': ('gemini.html#_gem_filter_sch', 'fastllm/gemini.py'),
|
|
140
142
|
'fastllm.gemini._gem_part_type': ('gemini.html#_gem_part_type', 'fastllm/gemini.py'),
|
|
141
143
|
'fastllm.gemini.acollect_stream': ('gemini.html#acollect_stream', 'fastllm/gemini.py'),
|
|
144
|
+
'fastllm.gemini.collate_raw': ('gemini.html#collate_raw', 'fastllm/gemini.py'),
|
|
142
145
|
'fastllm.gemini.cost': ('gemini.html#cost', 'fastllm/gemini.py'),
|
|
143
146
|
'fastllm.gemini.delta_index_fn': ('gemini.html#delta_index_fn', 'fastllm/gemini.py'),
|
|
144
147
|
'fastllm.gemini.denorm_assistant': ('gemini.html#denorm_assistant', 'fastllm/gemini.py'),
|
|
@@ -163,8 +166,10 @@ d = { 'settings': { 'branch': 'main',
|
|
|
163
166
|
'fastllm.gemini.norm_parts': ('gemini.html#norm_parts', 'fastllm/gemini.py'),
|
|
164
167
|
'fastllm.gemini.norm_sse_event': ('gemini.html#norm_sse_event', 'fastllm/gemini.py'),
|
|
165
168
|
'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')
|
|
169
|
+
'fastllm.gemini.norm_usage': ('gemini.html#norm_usage', 'fastllm/gemini.py'),
|
|
170
|
+
'fastllm.gemini.raw_msg': ('gemini.html#raw_msg', 'fastllm/gemini.py')},
|
|
167
171
|
'fastllm.openai_chat': { 'fastllm.openai_chat.acollect_stream': ('oai_chat.html#acollect_stream', 'fastllm/openai_chat.py'),
|
|
172
|
+
'fastllm.openai_chat.collate_raw': ('oai_chat.html#collate_raw', 'fastllm/openai_chat.py'),
|
|
168
173
|
'fastllm.openai_chat.cost': ('oai_chat.html#cost', 'fastllm/openai_chat.py'),
|
|
169
174
|
'fastllm.openai_chat.delta_index_fn': ('oai_chat.html#delta_index_fn', 'fastllm/openai_chat.py'),
|
|
170
175
|
'fastllm.openai_chat.denorm_assistant': ('oai_chat.html#denorm_assistant', 'fastllm/openai_chat.py'),
|
|
@@ -189,14 +194,15 @@ d = { 'settings': { 'branch': 'main',
|
|
|
189
194
|
'fastllm.openai_chat.norm_finish': ('oai_chat.html#norm_finish', 'fastllm/openai_chat.py'),
|
|
190
195
|
'fastllm.openai_chat.norm_parts': ('oai_chat.html#norm_parts', 'fastllm/openai_chat.py'),
|
|
191
196
|
'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')
|
|
197
|
+
'fastllm.openai_chat.norm_tool_calls': ('oai_chat.html#norm_tool_calls', 'fastllm/openai_chat.py'),
|
|
198
|
+
'fastllm.openai_chat.raw_msg': ('oai_chat.html#raw_msg', 'fastllm/openai_chat.py')},
|
|
193
199
|
'fastllm.openai_responses': { 'fastllm.openai_responses.Completion.response_id': ( 'oai_responses.html#completion.response_id',
|
|
194
200
|
'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
201
|
'fastllm.openai_responses._sanid': ('oai_responses.html#_sanid', 'fastllm/openai_responses.py'),
|
|
198
202
|
'fastllm.openai_responses.acollect_stream': ( 'oai_responses.html#acollect_stream',
|
|
199
203
|
'fastllm/openai_responses.py'),
|
|
204
|
+
'fastllm.openai_responses.collate_raw': ( 'oai_responses.html#collate_raw',
|
|
205
|
+
'fastllm/openai_responses.py'),
|
|
200
206
|
'fastllm.openai_responses.cost': ('oai_responses.html#cost', 'fastllm/openai_responses.py'),
|
|
201
207
|
'fastllm.openai_responses.delta_index_fn': ( 'oai_responses.html#delta_index_fn',
|
|
202
208
|
'fastllm/openai_responses.py'),
|
|
@@ -245,7 +251,9 @@ d = { 'settings': { 'branch': 'main',
|
|
|
245
251
|
'fastllm.openai_responses.norm_tool_calls': ( 'oai_responses.html#norm_tool_calls',
|
|
246
252
|
'fastllm/openai_responses.py'),
|
|
247
253
|
'fastllm.openai_responses.norm_usage': ( 'oai_responses.html#norm_usage',
|
|
248
|
-
'fastllm/openai_responses.py')
|
|
254
|
+
'fastllm/openai_responses.py'),
|
|
255
|
+
'fastllm.openai_responses.raw_msg': ( 'oai_responses.html#raw_msg',
|
|
256
|
+
'fastllm/openai_responses.py')},
|
|
249
257
|
'fastllm.responses': { 'fastllm.responses.AsyncResponses': ('responses.html#asyncresponses', 'fastllm/responses.py'),
|
|
250
258
|
'fastllm.responses.AsyncResponses.call': ('responses.html#asyncresponses.call', 'fastllm/responses.py'),
|
|
251
259
|
'fastllm.responses.AsyncResponses.events': ( 'responses.html#asyncresponses.events',
|
|
@@ -338,7 +346,7 @@ d = { 'settings': { 'branch': 'main',
|
|
|
338
346
|
'fastllm.types.Usage.__add__': ('types.html#usage.__add__', 'fastllm/types.py'),
|
|
339
347
|
'fastllm.types.Usage.__eq__': ('types.html#usage.__eq__', 'fastllm/types.py'),
|
|
340
348
|
'fastllm.types.Usage.__init__': ('types.html#usage.__init__', 'fastllm/types.py'),
|
|
341
|
-
'fastllm.types.
|
|
349
|
+
'fastllm.types._flat_rates': ('types.html#_flat_rates', 'fastllm/types.py'),
|
|
342
350
|
'fastllm.types.approx_pricing': ('types.html#approx_pricing', 'fastllm/types.py'),
|
|
343
351
|
'fastllm.types.effort_code': ('types.html#effort_code', 'fastllm/types.py'),
|
|
344
352
|
'fastllm.types.effort_levels': ('types.html#effort_levels', 'fastllm/types.py'),
|
fastllm/acomplete.py
CHANGED
|
@@ -67,10 +67,10 @@ def _codex_mtime_policy():
|
|
|
67
67
|
|
|
68
68
|
# %% ../nbs/06_acomplete.ipynb #79075d95
|
|
69
69
|
@flexicache(_codex_mtime_policy())
|
|
70
|
-
def mk_client(model=None, vendor_name=None, api_name=None, api_key=None, base_url=None, xtra_hdrs=None,
|
|
70
|
+
def mk_client(model=None, vendor_name=None, api_name=None, api_key=None, oauth_token=None, base_url=None, xtra_hdrs=None,
|
|
71
71
|
timeout=httpx2.Timeout(connect=30, read=300, write=30, pool=10)):
|
|
72
72
|
err_msg = f"please pass a valid one vendor: {', '.join(list(vendor_mapping))} or pass `api_name`,`base_url` and `api_key`"
|
|
73
|
-
if not vendor_name and not api_name and not (base_url and api_key):
|
|
73
|
+
if not vendor_name and not api_name and not (base_url and (api_key or oauth_token)):
|
|
74
74
|
v, m = split_vendor(model)
|
|
75
75
|
if v in vendor_mapping: vendor_name, model = v, m
|
|
76
76
|
if vendor_name:
|
|
@@ -78,17 +78,17 @@ def mk_client(model=None, vendor_name=None, api_name=None, api_key=None, base_ur
|
|
|
78
78
|
try:
|
|
79
79
|
api_name, base_url, env_api_nm, *auth_json = vendor_mapping[vendor_name]
|
|
80
80
|
base_url = override_base_url or base_url
|
|
81
|
-
if auth_json and not api_key and not os.getenv(env_api_nm):
|
|
81
|
+
if auth_json and not api_key and not oauth_token and not os.getenv(env_api_nm):
|
|
82
82
|
fn,keys = auth_json[0] # pyright: ignore[reportAssignmentType]
|
|
83
83
|
auth_fn = Path(fn).expanduser()
|
|
84
84
|
if auth_fn.exists(): api_key = nested_idx(json.loads(auth_fn.read_text()), *keys)
|
|
85
|
-
api_key = get_api_key(api_key, env_api_nm)
|
|
85
|
+
if not oauth_token: api_key = get_api_key(api_key, env_api_nm)
|
|
86
86
|
except KeyError: raise ValueError(f"Unknown vendor '{vendor_name}', {err_msg}")
|
|
87
|
-
elif base_url and api_key: vendor_name, api_name = ifnone(vendor_name, 'custom'), ifnone(api_name, 'openai_chat')
|
|
87
|
+
elif base_url and (api_key or oauth_token): vendor_name, api_name = ifnone(vendor_name, 'custom'), ifnone(api_name, 'openai_chat')
|
|
88
88
|
elif (api_name:=infer_api_name(model)): base_url, vendor_name = vendor_mapping[api_name][1], api_name
|
|
89
89
|
else: raise ValueError(f"Model {model} can't be auto resolved, {err_msg}")
|
|
90
90
|
api = api_registry[api_name]
|
|
91
|
-
hdrs = merge(api.get_hdrs(api_key), ifnone(xtra_hdrs, {}))
|
|
91
|
+
hdrs = merge(api.get_hdrs(api_key, oauth_token), ifnone(xtra_hdrs, {}))
|
|
92
92
|
cli = AsyncHttpCli(base_url, base_headers=hdrs, timeout=timeout)
|
|
93
93
|
return cli, api_name, vendor_name
|
|
94
94
|
|
|
@@ -155,18 +155,19 @@ async def _retry_stream(mk_gen, retries=2, retry_delay=0.5):
|
|
|
155
155
|
|
|
156
156
|
# %% ../nbs/06_acomplete.ipynb #2379ec94
|
|
157
157
|
@delegates(payload_kwargs)
|
|
158
|
-
async def acomplete(msgs, model, api_name=None, vendor_name=None, api_key=None, base_url=None, endpoint=None, xtra_body=None, xtra_hdrs=None,
|
|
158
|
+
async def acomplete(msgs, model, api_name=None, vendor_name=None, api_key=None, oauth_token=None, base_url=None, endpoint=None, xtra_body=None, xtra_hdrs=None,
|
|
159
159
|
stream=False, previous_response_id=None, stop_callables=None, retries=2, retry_delay=0.5, **kwargs):
|
|
160
160
|
"Unified completion across different APIs."
|
|
161
|
-
if
|
|
161
|
+
if api_key and oauth_token: raise ValueError('Pass `api_key` or `oauth_token`, not both: a request has one identity')
|
|
162
|
+
if not vendor_name and not api_name and not (base_url and (api_key or oauth_token)):
|
|
162
163
|
v, m = split_vendor(model)
|
|
163
164
|
if v in vendor_mapping: vendor_name, model = v, m
|
|
164
165
|
elif v: api_name, model = v, m # a registered transport api (e.g. claude_code): not an HTTP vendor
|
|
165
166
|
own = api_name in api_registry and not hasattr(api_registry[api_name], 'endpoint') if api_name else False
|
|
166
167
|
if own:
|
|
167
168
|
vendor_name = ifnone(vendor_name, api_name) # an own-transport api: no HTTP client to build
|
|
168
|
-
if
|
|
169
|
-
else: cli, api_name, vendor_name = mk_client(model=model, vendor_name=vendor_name, api_name=api_name, api_key=api_key, base_url=base_url, xtra_hdrs=xtra_hdrs)
|
|
169
|
+
if oauth_token: kwargs['oauth_token'] = oauth_token # its `mk_payload` applies the user's token itself
|
|
170
|
+
else: cli, api_name, vendor_name = mk_client(model=model, vendor_name=vendor_name, api_name=api_name, api_key=api_key, oauth_token=oauth_token, base_url=base_url, xtra_hdrs=xtra_hdrs)
|
|
170
171
|
api = api_registry[api_name]
|
|
171
172
|
if previous_response_id is not None:
|
|
172
173
|
if not getattr(api, 'supports_previous_response_id', False):
|
fastllm/anthropic.py
CHANGED
|
@@ -2,20 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
# %% auto #0
|
|
4
4
|
__all__ = ['ant_tc_types', 'norm_tool_call', 'norm_tool_calls', 'norm_usage', 'finalize_usage', 'norm_finish', 'norm_parts',
|
|
5
|
-
'norm_tr_parts', 'norm_sse_event', 'delta_index_fn', '
|
|
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']
|
|
5
|
+
'raw_msg', 'norm_tr_parts', 'norm_sse_event', 'delta_index_fn', 'collate_raw', 'acollect_stream',
|
|
6
|
+
'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, 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,21 +67,24 @@ 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
|
-
if typ.endswith('_tool_result'): return PartType.server_tool_result
|
|
72
|
-
return typ
|
|
70
|
+
return ant_parts_mapping.get(typ, typ)
|
|
73
71
|
|
|
74
72
|
def norm_parts(resp):
|
|
73
|
+
"Canonical parts of a response; a server tool's result block lives only in the raw message"
|
|
75
74
|
parts = []
|
|
76
75
|
for b in resp.get("content", []):
|
|
77
76
|
typ = _ant_part_type(b.get("type", "text"))
|
|
78
77
|
if typ == PartType.thinking: parts.append(Thinking(b.get("thinking", ""), raw=b))
|
|
79
78
|
elif typ == PartType.tool_use:
|
|
80
79
|
if tc := norm_tool_call(b): parts.append(tc)
|
|
81
|
-
elif typ == PartType.
|
|
82
|
-
else: parts.append(Text(b.get("text", ""), raw=b))
|
|
80
|
+
elif typ == PartType.text: parts.append(Text(b.get("text", ""), raw=b))
|
|
83
81
|
return parts
|
|
84
82
|
|
|
83
|
+
# %% ../nbs/04_anthropic.ipynb #f24e52db
|
|
84
|
+
def raw_msg(resp):
|
|
85
|
+
"The assistant message as the API sent it"
|
|
86
|
+
return dict(role='assistant', content=resp['content'])
|
|
87
|
+
|
|
85
88
|
# %% ../nbs/04_anthropic.ipynb #5d1440b7
|
|
86
89
|
def norm_tr_parts(content):
|
|
87
90
|
"Normalize an Anthropic `tool_result` content str or block list to canonical `Part`s"
|
|
@@ -103,7 +106,6 @@ def norm_sse_event(ev, **kwargs):
|
|
|
103
106
|
text, thinking, tcs, citations = None, None, [], None
|
|
104
107
|
if typ == "content_block_start":
|
|
105
108
|
cb = ev.get("content_block", {})
|
|
106
|
-
if cb.get("type", "").endswith("_tool_result"): return Delta(server_tool_result=cb, raw=ev, **kwargs)
|
|
107
109
|
if tc := norm_tool_call(cb):
|
|
108
110
|
if not tc.arguments: tc.arguments = {'_delta': ''}
|
|
109
111
|
tcs = [tc]
|
|
@@ -127,6 +129,23 @@ def delta_index_fn(d, typ, last_typ, last_idx):
|
|
|
127
129
|
'Returns accumulation index for current delta and updated last idx'
|
|
128
130
|
return nested_idx(d, 'raw', 'index'), None
|
|
129
131
|
|
|
132
|
+
# %% ../nbs/04_anthropic.ipynb #de900e34
|
|
133
|
+
def collate_raw(deltas):
|
|
134
|
+
"The assistant message rebuilt from a stream's events"
|
|
135
|
+
blocks = {}
|
|
136
|
+
for ev in L(deltas).attrgot('raw'):
|
|
137
|
+
typ,i = ev.get('type'),ev.get('index')
|
|
138
|
+
if typ == 'content_block_start': blocks[i] = copy.deepcopy(ev['content_block'])
|
|
139
|
+
elif typ == 'content_block_delta':
|
|
140
|
+
d,b = ev['delta'],blocks[i]
|
|
141
|
+
if d['type'] == 'text_delta': b['text'] += d['text']
|
|
142
|
+
elif d['type'] == 'thinking_delta': b['thinking'] += d['thinking']
|
|
143
|
+
elif d['type'] == 'signature_delta': b['signature'] = d['signature']
|
|
144
|
+
elif d['type'] == 'input_json_delta': b['_json'] = b.get('_json', '') + d['partial_json']
|
|
145
|
+
elif d['type'] == 'citations_delta': b['citations'] = (b.get('citations') or []) + [d['citation']]
|
|
146
|
+
elif typ == 'content_block_stop' and '_json' in blocks[i]: blocks[i]['input'] = json.loads(blocks[i].pop('_json') or '{}')
|
|
147
|
+
return dict(role='assistant', content=list(blocks.values()))
|
|
148
|
+
|
|
130
149
|
# %% ../nbs/04_anthropic.ipynb #c994adb1
|
|
131
150
|
@delegates(mk_acollect_stream, but=['index_fn', 'api_name'])
|
|
132
151
|
async def acollect_stream(resp, **kwargs):
|
|
@@ -145,44 +164,29 @@ def _sanid(id_str): return re.sub(r'[^a-zA-Z0-9_-]', '_', id_str or '')
|
|
|
145
164
|
# %% ../nbs/04_anthropic.ipynb #6ec772cb
|
|
146
165
|
def denorm_tool_use(p:ToolUse):
|
|
147
166
|
"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)
|
|
167
|
+
return _ant_cc(dict(type='tool_use', id=_sanid(p.id or ''), name=p.name or '', input=p.arguments or {}), p)
|
|
151
168
|
|
|
152
169
|
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
|
|
170
|
+
"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"
|
|
171
|
+
if m.raw:
|
|
172
|
+
cc = [p.cache_control for p in m.content if p.cache_control]
|
|
173
|
+
if not cc: return m.raw
|
|
174
|
+
return m.raw | dict(content=m.raw['content'][:-1] + [m.raw['content'][-1] | dict(cache_control=cc[-1])])
|
|
175
|
+
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))]
|
|
176
|
+
return dict(role='assistant', content=blocks)
|
|
173
177
|
|
|
174
178
|
def denorm_tool(m:Msg):
|
|
175
179
|
"Convert canonical tool Msg to Anthropic user message with tool_result blocks."
|
|
176
180
|
blocks = [denorm_tool_result(p) for p in m.content if isinstance(p, ToolResult)]
|
|
177
|
-
return
|
|
181
|
+
return dict(role='user', content=blocks)
|
|
178
182
|
|
|
179
183
|
def denorm_msgs(msgs:list[Msg]):
|
|
180
184
|
"Convert list of canonical Msgs to Anthropic messages."
|
|
181
185
|
res = []
|
|
182
186
|
for m in msgs:
|
|
183
187
|
if m.role == 'user': res.append(denorm_user(m))
|
|
184
|
-
elif m.role == 'assistant': res.
|
|
185
|
-
elif m.role == 'tool': res.
|
|
188
|
+
elif m.role == 'assistant': res.append(denorm_assistant(m))
|
|
189
|
+
elif m.role == 'tool': res.append(denorm_tool(m))
|
|
186
190
|
return res
|
|
187
191
|
|
|
188
192
|
# %% ../nbs/04_anthropic.ipynb #f762a6f2
|
|
@@ -322,8 +326,12 @@ def mk_payload(msgs, model, **kwargs):
|
|
|
322
326
|
return payload
|
|
323
327
|
|
|
324
328
|
# %% ../nbs/04_anthropic.ipynb #60d52c1f
|
|
325
|
-
def get_hdrs(api_key=None):
|
|
326
|
-
|
|
329
|
+
def get_hdrs(api_key=None, oauth_token=None):
|
|
330
|
+
"An OAuth token travels as a bearer under Anthropic's OAuth beta; an API key as `x-api-key`"
|
|
331
|
+
hdrs = {"anthropic-version": "2023-06-01"}
|
|
332
|
+
if oauth_token: return hdrs | {"Authorization": f"Bearer {oauth_token}", "anthropic-beta": "oauth-2025-04-20"}
|
|
333
|
+
return hdrs | {"x-api-key": get_api_key(api_key, 'ANTHROPIC_API_KEY')}
|
|
334
|
+
|
|
327
335
|
|
|
328
336
|
# %% ../nbs/04_anthropic.ipynb #0d03643a
|
|
329
337
|
def cost(usage, m):
|
|
@@ -342,5 +350,6 @@ def cost(usage, m):
|
|
|
342
350
|
|
|
343
351
|
# %% ../nbs/04_anthropic.ipynb #f7c0b989
|
|
344
352
|
api_registry.register('anthropic', norm_tool_calls=norm_tool_calls, norm_parts=norm_parts, norm_finish=norm_finish, norm_usage=norm_usage,
|
|
345
|
-
finalize_usage=finalize_usage,
|
|
353
|
+
finalize_usage=finalize_usage, raw_msg=raw_msg, collate_raw=collate_raw, acollect_stream=acollect_stream, mk_payload=mk_payload,
|
|
354
|
+
cost=cost, get_hdrs=get_hdrs,
|
|
346
355
|
endpoint='/v1/messages')
|
fastllm/chat.py
CHANGED
|
@@ -168,6 +168,7 @@ class AsyncChat:
|
|
|
168
168
|
api_name=None, # API to use, one of ApiName: openai (responses), openai_chat, anthropic, gemini
|
|
169
169
|
vendor_name=None, # Vendor name, one of vendor_mapping which resolves api_base/api_key automatically
|
|
170
170
|
api_key=None, # API key when model can't be resolved or vendor_name is not known or codex
|
|
171
|
+
oauth_token=None, # A subscription's OAuth token (Codex, Claude Code) in place of an API key
|
|
171
172
|
base_url=None, # API base url when model can't be resolved or vendor_name is not known
|
|
172
173
|
endpoint=None, # Override the transport's request path, for a server mounting Responses at a custom location
|
|
173
174
|
extra_headers=None, # Extra HTTP headers for custom providers
|
|
@@ -178,7 +179,7 @@ class AsyncChat:
|
|
|
178
179
|
default_cbs=True # Whether to include default callbacks
|
|
179
180
|
):
|
|
180
181
|
"LiteLLM chat client."
|
|
181
|
-
if not vendor_name and not api_name and not (base_url and api_key):
|
|
182
|
+
if not vendor_name and not api_name and not (base_url and (api_key or oauth_token)):
|
|
182
183
|
v, model = split_vendor(model)
|
|
183
184
|
if v in vendor_mapping: vendor_name = v
|
|
184
185
|
elif v: api_name = v # a registered transport api (e.g. claude_code)
|
|
@@ -248,6 +249,7 @@ def _prep_call(self:AsyncChat, search, max_tokens, kwargs, stream=False, think=N
|
|
|
248
249
|
if self.api_name: kwargs['api_name'] = self.api_name
|
|
249
250
|
if self.vendor_name: kwargs['vendor_name'] = self.vendor_name
|
|
250
251
|
if self.api_key: kwargs['api_key'] = self.api_key
|
|
252
|
+
if self.oauth_token: kwargs['oauth_token'] = self.oauth_token
|
|
251
253
|
if self.base_url: kwargs['base_url'] = self.base_url
|
|
252
254
|
if self.endpoint: kwargs['endpoint'] = self.endpoint
|
|
253
255
|
if self.extra_headers: kwargs['xtra_hdrs'] = self.extra_headers
|
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):
|
|
@@ -267,9 +283,12 @@ def mk_payload(msgs, model, **kwargs):
|
|
|
267
283
|
return payload
|
|
268
284
|
|
|
269
285
|
# %% ../nbs/05_gemini.ipynb #60d52c1f
|
|
270
|
-
def get_hdrs(api_key=None):
|
|
286
|
+
def get_hdrs(api_key=None, oauth_token=None):
|
|
287
|
+
"Bearer auth for an OAuth token; otherwise the API key header"
|
|
288
|
+
if oauth_token: return {"Authorization": f"Bearer {oauth_token}"}
|
|
271
289
|
return {"x-goog-api-key": get_api_key(api_key, 'GEMINI_API_KEY')}
|
|
272
290
|
|
|
291
|
+
|
|
273
292
|
# %% ../nbs/05_gemini.ipynb #4ee3891f
|
|
274
293
|
def cost(usage, m):
|
|
275
294
|
raw = usage.raw
|
|
@@ -298,6 +317,8 @@ api_ns = dict(norm_tool_calls=norm_tool_calls,
|
|
|
298
317
|
norm_finish=norm_finish,
|
|
299
318
|
norm_usage=norm_usage,
|
|
300
319
|
acollect_stream=acollect_stream,
|
|
320
|
+
raw_msg=raw_msg,
|
|
321
|
+
collate_raw=collate_raw,
|
|
301
322
|
mk_payload=mk_payload,
|
|
302
323
|
cost=cost,
|
|
303
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
|
|
|
@@ -198,7 +211,10 @@ def mk_payload(msgs, model, **kwargs):
|
|
|
198
211
|
return payload
|
|
199
212
|
|
|
200
213
|
# %% ../nbs/03_oai_chat.ipynb #16e813d2
|
|
201
|
-
def get_hdrs(api_key=None
|
|
214
|
+
def get_hdrs(api_key=None, oauth_token=None):
|
|
215
|
+
"Bearer auth: an OAuth access token, or else an API key from the argument or the environment"
|
|
216
|
+
return {"Authorization": f"Bearer {oauth_token or get_api_key(api_key, 'OPENAI_API_KEY')}"}
|
|
217
|
+
|
|
202
218
|
|
|
203
219
|
# %% ../nbs/03_oai_chat.ipynb #f89e2bf6
|
|
204
220
|
def cost(usage, m):
|
|
@@ -223,6 +239,6 @@ def fix_payload(payload, model, vendor_name):
|
|
|
223
239
|
|
|
224
240
|
# %% ../nbs/03_oai_chat.ipynb #e2b0908e
|
|
225
241
|
api_ns = dict(norm_tool_calls=norm_tool_calls, norm_parts=norm_parts, norm_finish=norm_finish, norm_usage=norm_usage,
|
|
226
|
-
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,
|
|
227
243
|
endpoint='/chat/completions')
|
|
228
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
|
|
@@ -256,7 +254,10 @@ def mk_payload(msgs, model, **kwargs):
|
|
|
256
254
|
return payload
|
|
257
255
|
|
|
258
256
|
# %% ../nbs/02_oai_responses.ipynb #529fd4bc
|
|
259
|
-
def get_hdrs(api_key=None
|
|
257
|
+
def get_hdrs(api_key=None, oauth_token=None):
|
|
258
|
+
"Bearer auth: an OAuth access token, or else an API key from the argument or the environment"
|
|
259
|
+
return {"Authorization": f"Bearer {oauth_token or get_api_key(api_key, 'OPENAI_API_KEY')}"}
|
|
260
|
+
|
|
260
261
|
|
|
261
262
|
# %% ../nbs/02_oai_responses.ipynb #a907ffa8
|
|
262
263
|
def cost(usage, m):
|
|
@@ -277,6 +278,7 @@ def fix_payload(payload, model, vendor_name):
|
|
|
277
278
|
|
|
278
279
|
# %% ../nbs/02_oai_responses.ipynb #07114b55
|
|
279
280
|
api_ns = dict(norm_tool_calls=norm_tool_calls, norm_parts=norm_parts, norm_finish=norm_finish, norm_usage=norm_usage,
|
|
280
|
-
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,
|
|
281
283
|
endpoint='/responses')
|
|
282
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):
|
|
@@ -203,7 +203,7 @@ def state(self:AsyncResponses, turn, comp, results=()):
|
|
|
203
203
|
"Build the next continuation state; `results` are server tool results answering `comp`'s calls."
|
|
204
204
|
normalize_call_ids(comp)
|
|
205
205
|
history = (*turn.history, comp.message, *([Msg('tool', list(results))] if results else []))
|
|
206
|
-
return ResponseState(turn.id, turn.body['model'], history, comp.response_id
|
|
206
|
+
return ResponseState(turn.id, turn.body['model'], history, comp.response_id)
|
|
207
207
|
|
|
208
208
|
|
|
209
209
|
# %% ../nbs/06a_responses.ipynb #7615b274
|
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
|
@@ -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
|
|
@@ -300,14 +301,14 @@ register_model_info('muse-spark-1.1', vendor_name='meta_ai', **modern_llm, max_i
|
|
|
300
301
|
codex_pricing = dict(input_cost_per_token = 0.10/1_000_000, output_cost_per_token = 0.50/1_000_000,
|
|
301
302
|
cache_creation_input_token_cost = 0.10/1_000_000, cache_read_input_token_cost = 0.10/1_000_000)
|
|
302
303
|
|
|
303
|
-
def
|
|
304
|
-
"
|
|
304
|
+
def _flat_rates(vendor_name, model):
|
|
305
|
+
"Keep only the flat subscription rates: drop the upstream context, cache-TTL and service-tier variants that would otherwise apply"
|
|
305
306
|
info = model_info_registry[vendor_name, model]
|
|
306
|
-
for k in [k for k in info if
|
|
307
|
+
for k in [k for k in info if 'cost' in k and k not in codex_pricing and k != 'search_context_cost_per_query']: del info[k]
|
|
307
308
|
|
|
308
309
|
for model in (codex54, codex54m, codex55):
|
|
309
310
|
register_model_info(model, 'codex', base=model, base_vendor_name='chatgpt', supports_web_search=True, max_input_tokens=256000, **codex_pricing)
|
|
310
|
-
|
|
311
|
+
_flat_rates('codex', model)
|
|
311
312
|
|
|
312
313
|
register_model_info(codex53spark, 'codex', **codex_pricing,
|
|
313
314
|
supports_vision=False, supports_image_input=False, supports_web_search=True, supports_reasoning=True, supports_function_calling=True,
|
|
@@ -318,6 +319,7 @@ register_model_info(codex53spark, 'codex', **codex_pricing,
|
|
|
318
319
|
register_model_info(fable51, 'anthropic', base=fable5)
|
|
319
320
|
for model in (haik45, sonn45, sonn46, sonn5, opus46, opus48, opus5, fable5, fable51):
|
|
320
321
|
register_model_info(model, 'claude_code', base=model, base_vendor_name='anthropic', **codex_pricing)
|
|
322
|
+
_flat_rates('claude_code', model)
|
|
321
323
|
|
|
322
324
|
# %% ../nbs/00_types.ipynb #bb0c4c2a
|
|
323
325
|
sol,terra,luna = gpt56s = 'gpt-5.6-sol gpt-5.6-terra gpt-5.6-luna'.split()
|
|
@@ -325,7 +327,7 @@ sol,terra,luna = gpt56s = 'gpt-5.6-sol gpt-5.6-terra gpt-5.6-luna'.split()
|
|
|
325
327
|
# Its window is smaller than the API's: 371,331 input tokens is accepted and 371,981 is not, on all three.
|
|
326
328
|
for model in gpt56s:
|
|
327
329
|
register_model_info(model, 'codex', base=model, base_vendor_name='openai', max_input_tokens=371_000, **codex_pricing)
|
|
328
|
-
|
|
330
|
+
_flat_rates('codex', model)
|
|
329
331
|
|
|
330
332
|
# %% ../nbs/00_types.ipynb #24cc47ec
|
|
331
333
|
def get_model_pricing(mn, vendor_name, million=True):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-fastllm
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.50
|
|
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.24
|
|
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
|
|
31
32
|
import asyncio, json
|
|
32
33
|
|
|
33
34
|
# Helpers
|
|
@@ -219,6 +220,7 @@ print("\nNo tools: ", end='')
|
|
|
219
220
|
r = await stream([user("What's the weather?")], model='claude-sonnet-4-20250514', tools=tools, tool_choice='none', max_tokens=mtok)
|
|
220
221
|
```
|
|
221
222
|
|
|
223
|
+
|
|
222
224
|
- ⏳ `get_weather(city="<UNKNOWN>")` ⏳
|
|
223
225
|
|
|
224
226
|
Forced: [ToolUse(raw={'caller': {'type': 'direct'}}, cache_control=None, id='toolu_01U7tAXjXAtwjp6xNSMbsyPU', name='get_weather', arguments={'city': '<UNKNOWN>'}, server=False, text=None)]
|
|
@@ -381,9 +383,8 @@ print(f"Usage: {r2.usage}")
|
|
|
381
383
|
Send images to any provider that supports them. The canonical `input_image` part works everywhere:
|
|
382
384
|
|
|
383
385
|
``` 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
386
|
img_msg = Msg(role='user', content=[
|
|
386
|
-
InputImage(
|
|
387
|
+
InputImage(sample_img_url),
|
|
387
388
|
Text("What do you see in this image?")
|
|
388
389
|
])
|
|
389
390
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
fastllm/__init__.py,sha256=49eWk0NL1c-ZrseVvC8ayVNYEyk7v6rCK0Q5Z4EQB64,131
|
|
2
|
+
fastllm/_modidx.py,sha256=o9foY201ZEAz0AR-9VkRUdpALNA8Jv6WYDs797YRyyw,43700
|
|
3
|
+
fastllm/acomplete.py,sha256=aP1ff9a7WKNhBd1kwKE2TJ-YyihLcdhd9-1Izi4wQP4,10772
|
|
4
|
+
fastllm/anthropic.py,sha256=0mmTjn9ZoEP6bBJki3YaCE1Ck97Q5qT06BZNGH1OMtg,17706
|
|
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=kRKXISv2c36wIM8EOF68p7c41VZ_Rpn9LdDmByPRf-Q,18707
|
|
11
|
+
fastllm/streaming.py,sha256=i_FU_Mt-3F2lBEImYav8I5pEjUSKYk3JFIW3lkiXEb8,8813
|
|
12
|
+
fastllm/types.py,sha256=MR8ukG3cuBXTbExBKHsZCa1GzCzzJ1Fgu-L9yxOnjtk,19365
|
|
13
|
+
python_fastllm-0.0.50.dist-info/METADATA,sha256=BTPOAD6e-hgHew_i7ITJMwqfY6C0t-H58y1cmEQKAjo,21101
|
|
14
|
+
python_fastllm-0.0.50.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
15
|
+
python_fastllm-0.0.50.dist-info/entry_points.txt,sha256=WkWd04F3xVYFP5bsexaQe9Se6uelwE5ULoTtHWreO0s,133
|
|
16
|
+
python_fastllm-0.0.50.dist-info/top_level.txt,sha256=F8qodL7nEGUHGmzzqfhNKCTIr1i0D6cvudOnm3z7o0Y,8
|
|
17
|
+
python_fastllm-0.0.50.dist-info/RECORD,,
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
fastllm/__init__.py,sha256=ioW91VJRYfSiEMjolhjzoNX1vPYfkbDKdrHXp79B9cQ,131
|
|
2
|
-
fastllm/_modidx.py,sha256=jF1SdJd1qbfI_ksSmsrjKUgRHGrANOpxyqTn_hcZ2LY,42836
|
|
3
|
-
fastllm/acomplete.py,sha256=Fb1GWkU6F7-bllbEEp7TPOzqmoWp309i5sJqYJbU6PM,10478
|
|
4
|
-
fastllm/anthropic.py,sha256=udojlLuritp90JQlH-dGHcWRpzgnO1mZYFYtbt2F5IA,17015
|
|
5
|
-
fastllm/chat.py,sha256=W8PPpZ95Az7wzLgQ15u3HMroWBVXDoaUz871f0x7cEk,24842
|
|
6
|
-
fastllm/codex.py,sha256=r6auZ8egia83aGkWcF4132AzW4SsBvKeDwg_CxyXj9E,5808
|
|
7
|
-
fastllm/gemini.py,sha256=DveWJNd78Pf2omC_t46iOzT84a-Lo3kWxchCKR3WsCA,15135
|
|
8
|
-
fastllm/openai_chat.py,sha256=f11U-iaxGbBq0-Rjz17GhA1Vame5n9kMqsk1gkfDtIQ,11633
|
|
9
|
-
fastllm/openai_responses.py,sha256=MUIBZY447tJ4Lc6saBPrVolXe8cdQdMK58YOtaJUHYk,14482
|
|
10
|
-
fastllm/responses.py,sha256=jcbKUwbEIsy7ngNcHUtv2bfYeDgqSysK_qWM-0LC9Mg,18767
|
|
11
|
-
fastllm/streaming.py,sha256=8qVtWeZ7NlP-9rup_4Ho0ol2dx3-XuGcIMXpZ1GYdEo,9079
|
|
12
|
-
fastllm/types.py,sha256=3qXyiS431AlSSn8GyGCNDflZxMWPeIWirp8TIyhIu_A,19146
|
|
13
|
-
python_fastllm-0.0.48.dist-info/METADATA,sha256=meHtTkX0PRsmljM1a72zXLGwlFHm1IKkjroJKnAmc1g,21143
|
|
14
|
-
python_fastllm-0.0.48.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
15
|
-
python_fastllm-0.0.48.dist-info/entry_points.txt,sha256=WkWd04F3xVYFP5bsexaQe9Se6uelwE5ULoTtHWreO0s,133
|
|
16
|
-
python_fastllm-0.0.48.dist-info/top_level.txt,sha256=F8qodL7nEGUHGmzzqfhNKCTIr1i0D6cvudOnm3z7o0Y,8
|
|
17
|
-
python_fastllm-0.0.48.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|