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/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)