python-fastllm 0.0.1__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 -0
- fastllm/_modidx.py +245 -0
- fastllm/acomplete.py +122 -0
- fastllm/anthropic.py +298 -0
- fastllm/chat.py +622 -0
- fastllm/gemini.py +304 -0
- fastllm/openai_chat.py +219 -0
- fastllm/openai_responses.py +260 -0
- fastllm/specs/anthropic.json +1 -0
- fastllm/specs/anthropic.yml +15684 -0
- fastllm/specs/gemini.json +6951 -0
- fastllm/specs/openai.with-code-samples.json +1 -0
- fastllm/specs/openai.with-code-samples.yml +73650 -0
- fastllm/specs/spec_manifest.json +17 -0
- fastllm/streaming.py +162 -0
- fastllm/types.py +301 -0
- python_fastllm-0.0.1.dist-info/METADATA +395 -0
- python_fastllm-0.0.1.dist-info/RECORD +21 -0
- python_fastllm-0.0.1.dist-info/WHEEL +5 -0
- python_fastllm-0.0.1.dist-info/entry_points.txt +2 -0
- python_fastllm-0.0.1.dist-info/top_level.txt +1 -0
fastllm/anthropic.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/04_anthropic.ipynb.
|
|
2
|
+
|
|
3
|
+
# %% auto #0
|
|
4
|
+
__all__ = ['ant_tc_types', 'api_ns', 'norm_tool_call', 'norm_tool_calls', 'norm_usage', 'norm_finish', 'norm_parts',
|
|
5
|
+
'norm_sse_event', 'delta_index_fn', '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_file', 'denorm_tool_result', 'mk_payload',
|
|
8
|
+
'get_hdrs', 'cost']
|
|
9
|
+
|
|
10
|
+
# %% ../nbs/04_anthropic.ipynb #02afd3d7
|
|
11
|
+
import json
|
|
12
|
+
from collections import Counter
|
|
13
|
+
from fastcore.utils import *
|
|
14
|
+
from fastcore.meta import *
|
|
15
|
+
from fastspec.errors import api_error_from_event
|
|
16
|
+
|
|
17
|
+
from .types import *
|
|
18
|
+
from .streaming import *
|
|
19
|
+
from .streaming import mk_acollect_stream
|
|
20
|
+
|
|
21
|
+
# %% ../nbs/04_anthropic.ipynb #5656d1d6
|
|
22
|
+
ant_tc_types = ("tool_use", "server_tool_use", "mcp_tool_use")
|
|
23
|
+
|
|
24
|
+
def norm_tool_call(b):
|
|
25
|
+
if b.get("type") in ant_tc_types:
|
|
26
|
+
extra = {k:v for k,v in b.items() if k not in ("type","id","name","input")}
|
|
27
|
+
return ToolCall(id=b["id"], name=b["name"], arguments=b.get("input") or {}, server=b.get("type")!="tool_use", extra=extra)
|
|
28
|
+
|
|
29
|
+
def norm_tool_calls(resp, delta=False):
|
|
30
|
+
"Extract Anthropic tool_use blocks as normalized tool calls."
|
|
31
|
+
out = []
|
|
32
|
+
for b in resp.get("content", []):
|
|
33
|
+
if tc:= norm_tool_call(b): out.append(tc)
|
|
34
|
+
return out
|
|
35
|
+
|
|
36
|
+
# %% ../nbs/04_anthropic.ipynb #0a1e2a8e
|
|
37
|
+
def norm_usage(resp):
|
|
38
|
+
"Normalize Anthropic usage shape."
|
|
39
|
+
if not (usg:=resp.get("usage")): return None
|
|
40
|
+
cached = int(usg.get("cache_read_input_tokens", 0) or 0)
|
|
41
|
+
cache_creation = int(usg.get("cache_creation_input_tokens", 0) or 0)
|
|
42
|
+
pt = int(usg.get("input_tokens", 0) or 0) + cached + cache_creation
|
|
43
|
+
ct = int(usg.get("output_tokens", 0) or 0)
|
|
44
|
+
return Usage(prompt_tokens=pt, completion_tokens=ct, total_tokens=pt + ct,
|
|
45
|
+
cached_tokens=cached, cache_creation_tokens=cache_creation, raw=usg)
|
|
46
|
+
|
|
47
|
+
# %% ../nbs/04_anthropic.ipynb #7a8b1f8f
|
|
48
|
+
def norm_finish(resp, tcs=None):
|
|
49
|
+
"Canonicalize finish_reason to OpenAI Chat values: stop, tool_calls, length, content_filter."
|
|
50
|
+
reason = resp.get("stop_reason")
|
|
51
|
+
mp = dict(end_turn=FinishReason.stop, tool_use=FinishReason.tool_calls, max_tokens=FinishReason.length)
|
|
52
|
+
r = mp.get(reason, reason)
|
|
53
|
+
return FinishReason.tool_calls if r==FinishReason.stop and any(~L(tcs).attrgot('server')) else r
|
|
54
|
+
|
|
55
|
+
# %% ../nbs/04_anthropic.ipynb #20d2e803
|
|
56
|
+
def _ant_part_type(typ):
|
|
57
|
+
"Map Anthropic content block type to canonical PartType."
|
|
58
|
+
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)
|
|
59
|
+
if typ in ant_parts_mapping: return ant_parts_mapping[typ]
|
|
60
|
+
if typ.endswith('_tool_result'): return PartType.server_tool_result
|
|
61
|
+
return typ
|
|
62
|
+
|
|
63
|
+
def norm_parts(resp):
|
|
64
|
+
tcs = norm_tool_calls(resp)
|
|
65
|
+
tc_map = {tc.id: tc for tc in tcs}
|
|
66
|
+
parts = []
|
|
67
|
+
for b in resp.get("content", []):
|
|
68
|
+
typ = _ant_part_type(b.get("type", "text"))
|
|
69
|
+
if typ == PartType.thinking: parts.append(Part(type=PartType.thinking, text=b.get("thinking", ""), data=b))
|
|
70
|
+
elif typ == "tool_use":
|
|
71
|
+
if tc:=tc_map.get(b.get("id")):
|
|
72
|
+
tdata = {**tc.extra, 'id':tc.id, 'name':tc.name, 'arguments':tc.arguments, 'server':tc.server}
|
|
73
|
+
parts.append(Part(type=PartType.tool_use, data=tdata))
|
|
74
|
+
else: parts.append(Part(type=typ, text=b.get("text", ""), data=b))
|
|
75
|
+
return parts
|
|
76
|
+
|
|
77
|
+
# %% ../nbs/04_anthropic.ipynb #a3869e31
|
|
78
|
+
def norm_sse_event(ev, **kwargs):
|
|
79
|
+
typ = ev.get("type")
|
|
80
|
+
text, thinking, tcs, citations = None, None, [], None
|
|
81
|
+
if typ == "content_block_start":
|
|
82
|
+
cb = ev.get("content_block", {})
|
|
83
|
+
if cb.get("type", "").endswith("_tool_result"): return Delta(server_tool_result=cb, raw=ev, **kwargs)
|
|
84
|
+
if tc := norm_tool_call(cb): tcs = [tc]
|
|
85
|
+
elif typ == "content_block_delta":
|
|
86
|
+
d = ev.get("delta", {})
|
|
87
|
+
dtyp = d.get("type")
|
|
88
|
+
if dtyp == "text_delta": text = d.get("text")
|
|
89
|
+
elif dtyp == "thinking_delta": thinking = d.get("thinking")
|
|
90
|
+
elif dtyp == "input_json_delta":
|
|
91
|
+
tcs = [ToolCall(id=str(ev.get("index", "")), name="", arguments={"_delta": d.get("partial_json", '')})]
|
|
92
|
+
elif dtyp == "citations_delta":
|
|
93
|
+
citations = listify(d.get('citation',[]))
|
|
94
|
+
elif typ == "message_delta":
|
|
95
|
+
fin = norm_finish(ev.get('delta'))
|
|
96
|
+
return Delta(finish_reason=fin, usage=norm_usage(ev), raw=ev, **kwargs)
|
|
97
|
+
elif typ == "error": raise api_error_from_event(ev)
|
|
98
|
+
return Delta(text=text, thinking=thinking, tool_calls=tcs, citations=citations, raw=ev, **kwargs)
|
|
99
|
+
|
|
100
|
+
# %% ../nbs/04_anthropic.ipynb #33cd3bd3
|
|
101
|
+
def delta_index_fn(d, typ, last_typ, last_idx):
|
|
102
|
+
'Returns accumulation index for current delta and updated last idx'
|
|
103
|
+
return nested_idx(d, 'raw', 'index'), None
|
|
104
|
+
|
|
105
|
+
# %% ../nbs/04_anthropic.ipynb #c994adb1
|
|
106
|
+
@delegates(mk_acollect_stream, but=['index_fn', 'api_name'])
|
|
107
|
+
async def acollect_stream(resp, **kwargs):
|
|
108
|
+
res = mk_acollect_stream(norm_and_yield(resp, norm_sse_event), index_fn=delta_index_fn, api_name='anthropic', **kwargs)
|
|
109
|
+
async for o in res: yield o
|
|
110
|
+
|
|
111
|
+
# %% ../nbs/04_anthropic.ipynb #bb11ab62
|
|
112
|
+
def _ant_cc(block, p):
|
|
113
|
+
"Add cache_control to block if present in Part.data."
|
|
114
|
+
if (cc := (p.data or {}).get('cache_control')): block['cache_control'] = cc
|
|
115
|
+
return block
|
|
116
|
+
|
|
117
|
+
# %% ../nbs/04_anthropic.ipynb #6ec772cb
|
|
118
|
+
def denorm_tool_use(p:Part):
|
|
119
|
+
"Convert canonical tool_use Part to Anthropic tool_use content block."
|
|
120
|
+
d = p.data or {}
|
|
121
|
+
block = dict(type='tool_use', id=d.get('id',''), name=d.get('name',''), input=d.get('arguments') or {})
|
|
122
|
+
if 'caller' in d: block['caller'] = d['caller']
|
|
123
|
+
return _ant_cc(block, p)
|
|
124
|
+
|
|
125
|
+
def denorm_assistant(m:Msg):
|
|
126
|
+
"Convert canonical assistant Msg to Anthropic assistant message + synthetic tool results for non-Anthropic server tools."
|
|
127
|
+
blocks, srv_results, srv_call_id = [], [], None
|
|
128
|
+
for p in m.content:
|
|
129
|
+
if p.type == PartType.thinking:
|
|
130
|
+
if srv_call_id:
|
|
131
|
+
srv_results.append(dict(type='tool_result', tool_use_id=srv_call_id, content=p.text or ''))
|
|
132
|
+
srv_call_id = None
|
|
133
|
+
elif sig:=(p.data or {}).get('signature',''): blocks.append(dict(type='thinking', thinking=p.text or '', signature=sig))
|
|
134
|
+
else: blocks.append(_ant_cc(dict(type='text', text=p.text or ''), p))
|
|
135
|
+
elif p.type == PartType.text:
|
|
136
|
+
if srv_call_id:
|
|
137
|
+
srv_results.append(dict(type='tool_result', tool_use_id=srv_call_id, content=p.text or ''))
|
|
138
|
+
srv_call_id = None
|
|
139
|
+
else: blocks.append(_ant_cc(dict(type='text', text=p.text or ''), p))
|
|
140
|
+
elif p.type == PartType.tool_use:
|
|
141
|
+
if p.data.get('server') and (p.data.get('id','') or '').startswith('srvtoolu_'):
|
|
142
|
+
blocks.append(dict(type='server_tool_use', id=p.data['id'], name=p.data.get('name',''), input=p.data.get('arguments') or {}))
|
|
143
|
+
elif p.data.get('server'):
|
|
144
|
+
blocks.append(denorm_tool_use(p))
|
|
145
|
+
srv_call_id = p.data.get('id','')
|
|
146
|
+
else: blocks.append(denorm_tool_use(p))
|
|
147
|
+
elif p.type == PartType.server_tool_result: blocks.append(p.data if p.data else dict(type='server_tool_result'))
|
|
148
|
+
res = [dict(role='assistant', content=blocks)]
|
|
149
|
+
if srv_results: res.append(dict(role='user', content=srv_results))
|
|
150
|
+
return res
|
|
151
|
+
|
|
152
|
+
def denorm_tool(m:Msg):
|
|
153
|
+
"Convert canonical tool Msg to Anthropic user message with tool_result blocks."
|
|
154
|
+
blocks = [denorm_tool_result(p) for p in m.content if p.type == PartType.tool_result]
|
|
155
|
+
return [dict(role='user', content=blocks)]
|
|
156
|
+
|
|
157
|
+
def denorm_msgs(msgs:list[Msg]):
|
|
158
|
+
"Convert list of canonical Msgs to Anthropic messages."
|
|
159
|
+
res = []
|
|
160
|
+
for m in msgs:
|
|
161
|
+
if m.role == 'user': res.append(denorm_user(m))
|
|
162
|
+
elif m.role == 'assistant': res.extend(denorm_assistant(m))
|
|
163
|
+
elif m.role == 'tool': res.extend(denorm_tool(m))
|
|
164
|
+
return res
|
|
165
|
+
|
|
166
|
+
# %% ../nbs/04_anthropic.ipynb #f762a6f2
|
|
167
|
+
def denorm_tool_schs(tools):
|
|
168
|
+
"Convert canonical tools to Anthropic format."
|
|
169
|
+
out = []
|
|
170
|
+
for t in tools:
|
|
171
|
+
fn = fn_schema(t)
|
|
172
|
+
if fn is None: out.append(t); continue
|
|
173
|
+
name, desc, params = fn
|
|
174
|
+
out.append(dict(name=name, description=desc, input_schema=params))
|
|
175
|
+
return out
|
|
176
|
+
|
|
177
|
+
# %% ../nbs/04_anthropic.ipynb #6118d7ea
|
|
178
|
+
def denorm_tool_choice(v):
|
|
179
|
+
"Map canonical tool_choice to Anthropic format."
|
|
180
|
+
if v is None: return None
|
|
181
|
+
if v in ('auto',): return {'type': 'auto'}
|
|
182
|
+
if v in ('required', 'any', 'force'): return {'type': 'any'}
|
|
183
|
+
if v in ('none', 'off', 'disabled'): return {'type': 'none'}
|
|
184
|
+
return {'type': 'tool', 'name': v}
|
|
185
|
+
|
|
186
|
+
# %% ../nbs/04_anthropic.ipynb #310f1e75
|
|
187
|
+
def denorm_reasoning(v):
|
|
188
|
+
"Map canonical reasoning_effort to Anthropic adaptive thinking + output_config."
|
|
189
|
+
mp = dict(minimal='low', low='low', medium='medium', high='high', xhigh='xhigh', max='max')
|
|
190
|
+
err = ValueError(f"Invalid reasoning effort for Anthropic: {v}, accepted string values are: {list(mp)} and dicts are passthrough")
|
|
191
|
+
if v is None: return None
|
|
192
|
+
elif isinstance(v, dict): return v
|
|
193
|
+
elif isinstance(v, str) and v in mp: return {"thinking": {"type": "adaptive"}, "output_config": {"effort": mp.get(v)}}
|
|
194
|
+
raise err
|
|
195
|
+
|
|
196
|
+
# %% ../nbs/04_anthropic.ipynb #8fa9fbb8
|
|
197
|
+
def denorm_web_search(v):
|
|
198
|
+
"Map canonical web_search_options to Anthropic hosted web_search tool."
|
|
199
|
+
_max_uses = {"low": 1, "medium": 5, "high": 10}
|
|
200
|
+
t = {"type": "web_search_20260209", "name": "web_search"}
|
|
201
|
+
if (typ := (v or {}).get("type")): t["type"] = typ
|
|
202
|
+
if (s := (v or {}).get("search_context_size")):
|
|
203
|
+
t["max_uses"] = _max_uses.get(s, 5)
|
|
204
|
+
if (u := (v or {}).get("user_location", {}).get("approximate")):
|
|
205
|
+
t["user_location"] = {"type": "approximate", **u}
|
|
206
|
+
return t
|
|
207
|
+
|
|
208
|
+
# %% ../nbs/04_anthropic.ipynb #6b485275
|
|
209
|
+
def denorm_system(sp):
|
|
210
|
+
if isinstance(sp, Msg): sp = sp.content[0]
|
|
211
|
+
if isinstance(sp, Part):
|
|
212
|
+
block = dict(type='text', text=sp.text)
|
|
213
|
+
if (cc := (sp.data or {}).get('cache_control')): block['cache_control'] = cc
|
|
214
|
+
return [block]
|
|
215
|
+
else: return sp
|
|
216
|
+
|
|
217
|
+
# %% ../nbs/04_anthropic.ipynb #1b990cc9
|
|
218
|
+
def denorm_user(m:Msg):
|
|
219
|
+
"Convert canonical user Msg to Anthropic user message."
|
|
220
|
+
parts = []
|
|
221
|
+
for p in m.content:
|
|
222
|
+
if p.type == PartType.text: parts.append(_ant_cc({"type":"text", "text":p.text or ''}, p))
|
|
223
|
+
elif p.type == PartType.input_image: parts.append(_ant_cc(denorm_image(p), p))
|
|
224
|
+
elif p.type == PartType.input_audio: raise ValueError("Anthropic does not support audio input")
|
|
225
|
+
elif p.type == PartType.input_video: raise ValueError("Anthropic does not support video input")
|
|
226
|
+
elif p.type == PartType.input_file: parts.append(_ant_cc(denorm_file(p), p))
|
|
227
|
+
return dict(role='user', content=parts)
|
|
228
|
+
|
|
229
|
+
# %% ../nbs/04_anthropic.ipynb #edd87272
|
|
230
|
+
def denorm_image(p):
|
|
231
|
+
if (b64:=data_url(p.text)): return {"type": "image", "source": {"type": "base64", "media_type": b64[0], "data": b64[1]}}
|
|
232
|
+
return {"type": "image", "source": {"type": "url", "url": p.text}}
|
|
233
|
+
|
|
234
|
+
# %% ../nbs/04_anthropic.ipynb #fc6bbdfc
|
|
235
|
+
def denorm_file(p):
|
|
236
|
+
if (b64:=data_url(p.text)): return {"type": "document", "source": {"type": "base64", "media_type": b64[0], "data": b64[1]}}
|
|
237
|
+
return {"type": "document", "source": {"type": "url", "url": p.text}}
|
|
238
|
+
|
|
239
|
+
# %% ../nbs/04_anthropic.ipynb #37b63cc2
|
|
240
|
+
def denorm_tool_result(p:Part):
|
|
241
|
+
"Convert canonical tool_result Part to Anthropic tool_result content block."
|
|
242
|
+
d = p.data or {}
|
|
243
|
+
tid = d.get('id') or d.get('call_id','')
|
|
244
|
+
if isinstance(p.text, list):
|
|
245
|
+
blocks = []
|
|
246
|
+
for pp in p.text:
|
|
247
|
+
if pp.type == PartType.text: blocks.append({"type": "text", "text": pp.text or ""})
|
|
248
|
+
elif pp.type == PartType.input_image: blocks.append(denorm_image(pp))
|
|
249
|
+
elif pp.type == PartType.input_file: blocks.append(denorm_file(pp))
|
|
250
|
+
else: raise ValueError(f"Anthropic tool_result does not support {pp.type}")
|
|
251
|
+
return _ant_cc(dict(type='tool_result', tool_use_id=tid, content=blocks), p)
|
|
252
|
+
return _ant_cc(dict(type='tool_result', tool_use_id=tid, content=str(p.text)), p)
|
|
253
|
+
|
|
254
|
+
# %% ../nbs/04_anthropic.ipynb #587d4fe5
|
|
255
|
+
@delegates(payload_kwargs)
|
|
256
|
+
def mk_payload(msgs, model, **kwargs):
|
|
257
|
+
payload = dict(model=model, messages=denorm_msgs(msgs), max_tokens=kwargs.get('max_tokens') or 1024)
|
|
258
|
+
if kwargs.get('stream'): payload['stream'] = True
|
|
259
|
+
if sp:=kwargs.get('system'): payload['system'] = denorm_system(sp)
|
|
260
|
+
if tools:=kwargs.get('tools'): payload['tools'] = denorm_tool_schs(tools)
|
|
261
|
+
if tchc:=kwargs.get('tool_choice'): payload['tool_choice'] = denorm_tool_choice(tchc)
|
|
262
|
+
if thk:=kwargs.get('reasoning_effort'): payload.update(denorm_reasoning(thk))
|
|
263
|
+
if (wopts:=kwargs.get('web_search_options')) is not None:
|
|
264
|
+
payload.setdefault('tools', []).append(denorm_web_search(wopts))
|
|
265
|
+
if (temp:=kwargs.get('temperature')) is not None:
|
|
266
|
+
payload['temperature'] = temp
|
|
267
|
+
return payload
|
|
268
|
+
|
|
269
|
+
# %% ../nbs/04_anthropic.ipynb #60d52c1f
|
|
270
|
+
def get_hdrs(api_key=None):
|
|
271
|
+
return {"x-api-key": get_api_key(api_key, 'ANTHROPIC_API_KEY'), "anthropic-version": "2023-06-01"}
|
|
272
|
+
|
|
273
|
+
# %% ../nbs/04_anthropic.ipynb #0d03643a
|
|
274
|
+
def cost(usage, m):
|
|
275
|
+
raw = usage.raw
|
|
276
|
+
in_tok = raw['input_tokens']
|
|
277
|
+
cache_read = raw.get('cache_read_input_tokens', 0)
|
|
278
|
+
cc = raw.get('cache_creation', {}) or {}
|
|
279
|
+
cache_5m = cc.get('ephemeral_5m_input_tokens', 0)
|
|
280
|
+
cache_1h = cc.get('ephemeral_1h_input_tokens', 0)
|
|
281
|
+
cost = in_tok * m.input_cost_per_token
|
|
282
|
+
cost += raw['output_tokens'] * m.output_cost_per_token
|
|
283
|
+
cost += cache_read * m.get('cache_read_input_token_cost', 0)
|
|
284
|
+
cost += cache_5m * m.get('cache_creation_input_token_cost', 0)
|
|
285
|
+
cost += cache_1h * m.get('cache_creation_input_token_cost_above_1hr', 0)
|
|
286
|
+
return cost
|
|
287
|
+
|
|
288
|
+
# %% ../nbs/04_anthropic.ipynb #f7c0b989
|
|
289
|
+
api_ns = dict(norm_tool_calls=norm_tool_calls,
|
|
290
|
+
norm_parts=norm_parts,
|
|
291
|
+
norm_finish=norm_finish,
|
|
292
|
+
norm_usage=norm_usage,
|
|
293
|
+
acollect_stream=acollect_stream,
|
|
294
|
+
mk_payload=mk_payload,
|
|
295
|
+
cost=cost,
|
|
296
|
+
get_hdrs=get_hdrs,
|
|
297
|
+
op_path=('messages.messages_post','messages.messages_post'))
|
|
298
|
+
api_registry.register('anthropic', **api_ns)
|