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
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/02_oai_responses.ipynb.
|
|
2
|
+
|
|
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', 'denorm_msgs',
|
|
6
|
+
'denorm_tool_schs', 'denorm_tool_choice', 'denorm_reasoning', 'denorm_web_search', 'denorm_system',
|
|
7
|
+
'denorm_user', 'denorm_image', 'denorm_file', 'denorm_tool_result', 'mk_payload', 'get_hdrs', 'cost']
|
|
8
|
+
|
|
9
|
+
# %% ../nbs/02_oai_responses.ipynb #591f55b5
|
|
10
|
+
import json
|
|
11
|
+
from collections import Counter
|
|
12
|
+
from fastcore.utils import *
|
|
13
|
+
from fastcore.meta import *
|
|
14
|
+
from fastspec.errors import api_error_from_event
|
|
15
|
+
|
|
16
|
+
from .types import *
|
|
17
|
+
from .streaming import *
|
|
18
|
+
from .streaming import mk_acollect_stream
|
|
19
|
+
|
|
20
|
+
# %% ../nbs/02_oai_responses.ipynb #479243ae
|
|
21
|
+
def norm_tool_call(item):
|
|
22
|
+
typ = item.get("type", "")
|
|
23
|
+
if typ == "function_call":
|
|
24
|
+
extra = {k:v for k,v in item.items() if k not in ("id","name","arguments")}
|
|
25
|
+
return ToolCall(id=item["id"], name=item["name"], arguments=json.loads(item["arguments"]), extra=extra)
|
|
26
|
+
if typ.endswith("_call"): # web_search_call, file_search_call, etc.
|
|
27
|
+
name = typ.removesuffix("_call")
|
|
28
|
+
extra = {k:v for k,v in item.items() if k not in ("id")}
|
|
29
|
+
return ToolCall(id=item.get("id",""), name=name, arguments=item.get("action", {}), server=True, extra=extra)
|
|
30
|
+
|
|
31
|
+
def norm_tool_calls(resp):
|
|
32
|
+
"Extract Responses API tool call items as normalized tool calls."
|
|
33
|
+
out = []
|
|
34
|
+
for item in resp.get('output', []):
|
|
35
|
+
if tc := norm_tool_call(item): out.append(tc)
|
|
36
|
+
return out
|
|
37
|
+
|
|
38
|
+
# %% ../nbs/02_oai_responses.ipynb #a199aab4
|
|
39
|
+
def norm_usage(resp):
|
|
40
|
+
"Normalize OpenAI usage shape(s)."
|
|
41
|
+
if not (usg:=resp.get("usage")): return None
|
|
42
|
+
pt = int(usg.get("prompt_tokens", usg.get("input_tokens", 0)) or 0)
|
|
43
|
+
ct = int(usg.get("completion_tokens", usg.get("output_tokens", 0)) or 0)
|
|
44
|
+
tt = int(usg.get("total_tokens", pt + ct) or (pt + ct))
|
|
45
|
+
pd = usg.get("prompt_tokens_details") or usg.get("input_tokens_details") or {}
|
|
46
|
+
cd = usg.get("completion_tokens_details") or usg.get("output_tokens_details") or {}
|
|
47
|
+
cached = int(pd.get("cached_tokens", 0) or 0)
|
|
48
|
+
reasoning = int(cd.get("reasoning_tokens", 0) or 0)
|
|
49
|
+
server_tool_use = dict(Counter(o['type'] for o in resp.get('output', []) if o.get('type') != 'function_call' and o.get('type', '').endswith('_call')))
|
|
50
|
+
if server_tool_use: usg['server_tool_use'] = server_tool_use
|
|
51
|
+
return Usage(prompt_tokens=pt, completion_tokens=ct, total_tokens=tt,
|
|
52
|
+
cached_tokens=cached, reasoning_tokens=reasoning, raw=usg)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# %% ../nbs/02_oai_responses.ipynb #95102df4
|
|
56
|
+
def norm_finish(resp, tcs=None):
|
|
57
|
+
"Canonicalize finish_reason to OpenAI Chat values: stop, tool_calls, length, content_filter."
|
|
58
|
+
reason = resp.get("status")
|
|
59
|
+
mp = dict(completed=FinishReason.stop, incomplete=FinishReason.length, failed=FinishReason.content_filter)
|
|
60
|
+
r = mp.get(reason, reason)
|
|
61
|
+
return FinishReason.tool_calls if r==FinishReason.stop and any(~L(tcs).attrgot('server')) else r
|
|
62
|
+
|
|
63
|
+
# %% ../nbs/02_oai_responses.ipynb #4ef27720
|
|
64
|
+
def norm_parts(resp):
|
|
65
|
+
parts = []
|
|
66
|
+
for item in resp["output"]:
|
|
67
|
+
if (typ:=item["type"]) == "message":
|
|
68
|
+
for c in item["content"]:
|
|
69
|
+
if (ctyp := c["type"]) == "output_text":
|
|
70
|
+
c['citations'] = c.pop('annotations', [])
|
|
71
|
+
parts.append(Part(type=PartType.text, text=c['text'], data=c))
|
|
72
|
+
elif ctyp == "refusal":
|
|
73
|
+
parts.append(Part(type=PartType.refusal, text=c['refusal'], data=c))
|
|
74
|
+
elif typ == "reasoning":
|
|
75
|
+
for s in item["summary"]: parts.append(Part(type=PartType.thinking, text=s['text'], data=item))
|
|
76
|
+
elif typ == "function_call" or typ.endswith("_call"):
|
|
77
|
+
tc = norm_tool_call(item)
|
|
78
|
+
tdata = {**tc.extra, 'id':tc.id, 'name':tc.name, 'arguments':tc.arguments, 'server':tc.server}
|
|
79
|
+
parts.append(Part(type=PartType.tool_use, data=tdata))
|
|
80
|
+
return parts
|
|
81
|
+
|
|
82
|
+
# %% ../nbs/02_oai_responses.ipynb #7cd48aa5
|
|
83
|
+
def norm_sse_event(ev, **kwargs):
|
|
84
|
+
"Normalize OpenAI Responses API stream event into Delta."
|
|
85
|
+
typ = ev.get("type")
|
|
86
|
+
if typ == "response.output_text.delta": return Delta(text=ev.get("delta"), raw=ev, **kwargs)
|
|
87
|
+
if typ == "response.reasoning_text.delta": return Delta(thinking=ev.get("delta",""), raw=ev, **kwargs)
|
|
88
|
+
if typ == "response.reasoning_summary_text.delta": return Delta(thinking=ev.get("delta",""), raw=ev, **kwargs)
|
|
89
|
+
if typ == "response.output_text.annotation.added": return Delta(citations=[ev.get("annotation")], raw=ev, **kwargs)
|
|
90
|
+
if typ == "response.output_item.done":
|
|
91
|
+
item = ev.get("item", {})
|
|
92
|
+
itype = item.get("type", "")
|
|
93
|
+
if itype == "function_call" or itype.endswith("_call"):
|
|
94
|
+
return Delta(tool_calls=[norm_tool_call(item)], raw=ev, **kwargs)
|
|
95
|
+
if typ in ("response.completed", "response.incomplete"):
|
|
96
|
+
resp = ev.get("response", {})
|
|
97
|
+
return Delta(finish_reason=norm_finish(resp), usage=norm_usage(resp), raw=ev, **kwargs)
|
|
98
|
+
if typ == "error": raise api_error_from_event(ev)
|
|
99
|
+
|
|
100
|
+
# %% ../nbs/02_oai_responses.ipynb #66370799
|
|
101
|
+
def delta_index_fn(d, typ, last_typ, last_idx):
|
|
102
|
+
'Returns accumulation index for current delta and updated last idx'
|
|
103
|
+
idx = nested_idx(d, 'raw', 'output_index')
|
|
104
|
+
return idx, idx
|
|
105
|
+
|
|
106
|
+
# %% ../nbs/02_oai_responses.ipynb #021b5257
|
|
107
|
+
@delegates(mk_acollect_stream, but=['index_fn', 'api_name'])
|
|
108
|
+
async def acollect_stream(resp, **kwargs):
|
|
109
|
+
res = mk_acollect_stream(norm_and_yield(resp, norm_sse_event), index_fn=delta_index_fn, api_name='openai', **kwargs)
|
|
110
|
+
async for o in res: yield o
|
|
111
|
+
|
|
112
|
+
# %% ../nbs/02_oai_responses.ipynb #b746c82b
|
|
113
|
+
def denorm_tool_use(p:Part):
|
|
114
|
+
"Convert canonical tool_use Part back to OpenAI Responses function_call item."
|
|
115
|
+
return dict(type='function_call', call_id=p.data.get('id'), name=p.data.get('name'), arguments=json.dumps(p.data.get('arguments', '{}')))
|
|
116
|
+
|
|
117
|
+
# %% ../nbs/02_oai_responses.ipynb #8f42adf7
|
|
118
|
+
def denorm_tool(m:Msg):
|
|
119
|
+
items = []
|
|
120
|
+
for part in m.content:
|
|
121
|
+
if part.type == PartType.tool_result: items.append(denorm_tool_result(part))
|
|
122
|
+
return items
|
|
123
|
+
|
|
124
|
+
# %% ../nbs/02_oai_responses.ipynb #67c8de13
|
|
125
|
+
def denorm_assistant(m:Msg):
|
|
126
|
+
items, srv_call_id = [], None
|
|
127
|
+
for p in m.content:
|
|
128
|
+
if p.type == PartType.tool_use:
|
|
129
|
+
items.append(denorm_tool_use(p))
|
|
130
|
+
# TODO: Server tcs are excluded during deserialization in lisette, so this
|
|
131
|
+
# is not used. Anthropic checks for `srvtoolu` and requires the full tool result metadata
|
|
132
|
+
# which is a lot to store.
|
|
133
|
+
# Instead of reconstructing server tool calls as actual tool uses we can make it a regular msg
|
|
134
|
+
# telling AI a server tool was executed
|
|
135
|
+
if p.data.get('server'):
|
|
136
|
+
srv_txt = f"[Server tool `{p.data['name']}` executed successfully, results are generated below]"
|
|
137
|
+
items.append(dict(type='function_call_output', call_id=p.data['id'], output=srv_txt))
|
|
138
|
+
elif p.type in (PartType.text, PartType.thinking):
|
|
139
|
+
items.append(dict(type="message", role="assistant", content=[dict(type="output_text", text=p.text)]))
|
|
140
|
+
return items
|
|
141
|
+
|
|
142
|
+
# %% ../nbs/02_oai_responses.ipynb #64883b99
|
|
143
|
+
def denorm_msgs(msgs:list[Msg]):
|
|
144
|
+
res = []
|
|
145
|
+
for m in msgs:
|
|
146
|
+
if m.role == 'user': res.append(denorm_user(m))
|
|
147
|
+
elif m.role == 'assistant': res.extend(denorm_assistant(m))
|
|
148
|
+
elif m.role == 'tool': res.extend(denorm_tool(m))
|
|
149
|
+
return res
|
|
150
|
+
|
|
151
|
+
# %% ../nbs/02_oai_responses.ipynb #f280226d
|
|
152
|
+
def denorm_tool_schs(tools):
|
|
153
|
+
"Convert canonical tools to OpenAI Responses format."
|
|
154
|
+
out = []
|
|
155
|
+
for t in tools:
|
|
156
|
+
fn = fn_schema(t)
|
|
157
|
+
if fn is None: out.append(t); continue
|
|
158
|
+
name, desc, params = fn
|
|
159
|
+
out.append(dict(type='function', name=name, description=desc, parameters=params))
|
|
160
|
+
return out
|
|
161
|
+
|
|
162
|
+
# %% ../nbs/02_oai_responses.ipynb #a6bc189c
|
|
163
|
+
def denorm_tool_choice(v):
|
|
164
|
+
"Map canonical tool_choice to OpenAI Responses format."
|
|
165
|
+
_tc_modes = {'auto', 'required', 'any', 'force', 'none', 'off', 'disabled'}
|
|
166
|
+
if v is None: return None
|
|
167
|
+
if v in _tc_modes: return v if v in ('auto','none','required') else {'auto':'auto','any':'required','force':'required','off':'none','disabled':'none'}[v]
|
|
168
|
+
return {'type': 'function', 'name': v}
|
|
169
|
+
|
|
170
|
+
# %% ../nbs/02_oai_responses.ipynb #b1dcf1d3
|
|
171
|
+
def denorm_reasoning(v):
|
|
172
|
+
"Map canonical reasoning_effort to OpenAI Responses reasoning param."
|
|
173
|
+
if v is None: return None
|
|
174
|
+
return {'effort': v} if isinstance(v, str) else v
|
|
175
|
+
|
|
176
|
+
# %% ../nbs/02_oai_responses.ipynb #e11c15c0
|
|
177
|
+
def denorm_web_search(v):
|
|
178
|
+
"Map canonical web_search_options to OpenAI Responses web_search_preview tool."
|
|
179
|
+
t = {"type": "web_search_preview"}
|
|
180
|
+
if (typ := (v or {}).get("type")): t["type"] = typ
|
|
181
|
+
if (s := (v or {}).get("search_context_size")): t["search_context_size"] = s
|
|
182
|
+
if (u := (v or {}).get("user_location")): t["user_location"] = u
|
|
183
|
+
return t
|
|
184
|
+
|
|
185
|
+
# %% ../nbs/02_oai_responses.ipynb #18346f93
|
|
186
|
+
def denorm_system(sp): return sys_text(part_txt(sp))
|
|
187
|
+
|
|
188
|
+
# %% ../nbs/02_oai_responses.ipynb #8c06da0c
|
|
189
|
+
def denorm_user(m:Msg):
|
|
190
|
+
"Convert canonical user Msg to OpenAI Responses input message."
|
|
191
|
+
parts = []
|
|
192
|
+
for p in m.content:
|
|
193
|
+
if p.type == PartType.text: parts.append({"type": "input_text", "text": p.text or ""})
|
|
194
|
+
elif p.type == PartType.input_image: parts.append(denorm_image(p))
|
|
195
|
+
elif p.type == PartType.input_audio: raise ValueError("OpenAI Responses API does not support audio input; Coming Soon.")
|
|
196
|
+
elif p.type == PartType.input_video: raise ValueError("OpenAI Responses API does not support video input")
|
|
197
|
+
elif p.type == PartType.input_file: parts.append(denorm_file(p))
|
|
198
|
+
return dict(type='message', role='user', content=parts)
|
|
199
|
+
|
|
200
|
+
# %% ../nbs/02_oai_responses.ipynb #4ced282f
|
|
201
|
+
def denorm_image(p): return {"type": "input_image", "image_url": p.text}
|
|
202
|
+
|
|
203
|
+
# %% ../nbs/02_oai_responses.ipynb #6e79a133
|
|
204
|
+
def denorm_file(p):
|
|
205
|
+
if (b64:=data_url(p.text)): return {"type": "input_file", "file_data": p.text, "filename": f"upload.{b64[0].split('/')[-1]}"}
|
|
206
|
+
return {"type": "input_file", "file_url": p.text}
|
|
207
|
+
|
|
208
|
+
# %% ../nbs/02_oai_responses.ipynb #145b1c79
|
|
209
|
+
def denorm_tool_result(m:Part):
|
|
210
|
+
"Convert canonical tool result back to OpenAI Responses function_call_output item."
|
|
211
|
+
cid = m.data.get('id', '') or m.data.get('call_id')
|
|
212
|
+
if isinstance(m.text, list):
|
|
213
|
+
out = []
|
|
214
|
+
for p in m.text:
|
|
215
|
+
if p.type == PartType.text: out.append({"type": "input_text", "text": p.text or ""})
|
|
216
|
+
elif p.type == PartType.input_image: out.append(denorm_image(p))
|
|
217
|
+
elif p.type == PartType.input_file: out.append(denorm_file(p))
|
|
218
|
+
else: raise ValueError(f"OpenAI Responses tool_result does not support {p.type}")
|
|
219
|
+
return dict(type='function_call_output', call_id=cid, output=out)
|
|
220
|
+
return dict(type='function_call_output', call_id=cid, output=str(m.text))
|
|
221
|
+
|
|
222
|
+
# %% ../nbs/02_oai_responses.ipynb #9b0f81a4
|
|
223
|
+
@delegates(payload_kwargs)
|
|
224
|
+
def mk_payload(msgs, model, **kwargs):
|
|
225
|
+
payload = dict(model=model, input=denorm_msgs(msgs))
|
|
226
|
+
if stream:=kwargs.get('stream'): payload['stream'] = True
|
|
227
|
+
if sp:=kwargs.get('system'): payload['instructions'] = denorm_system(sp)
|
|
228
|
+
if mt:=kwargs.get('max_tokens'): payload['max_output_tokens'] = mt
|
|
229
|
+
if tools:=kwargs.get('tools'): payload['tools'] = denorm_tool_schs(tools)
|
|
230
|
+
if tchc:=kwargs.get('tool_choice'): payload['tool_choice'] = denorm_tool_choice(tchc)
|
|
231
|
+
if thk:=kwargs.get('reasoning_effort'): payload['reasoning'] = denorm_reasoning(thk)
|
|
232
|
+
if (wopts:=kwargs.get('web_search_options')) is not None:
|
|
233
|
+
payload.setdefault('tools', []).append(denorm_web_search(wopts))
|
|
234
|
+
if (temp:=kwargs.get('temperature')) is not None: payload['temperature'] = temp
|
|
235
|
+
return payload
|
|
236
|
+
|
|
237
|
+
# %% ../nbs/02_oai_responses.ipynb #529fd4bc
|
|
238
|
+
def get_hdrs(api_key=None):
|
|
239
|
+
return {"Authorization": f"Bearer {get_api_key(api_key, 'OPENAI_API_KEY')}"}
|
|
240
|
+
|
|
241
|
+
# %% ../nbs/02_oai_responses.ipynb #a907ffa8
|
|
242
|
+
def cost(usage, m):
|
|
243
|
+
raw = usage.raw
|
|
244
|
+
cached = raw.get('input_tokens_details', {}).get('cached_tokens', 0)
|
|
245
|
+
in_txt = raw['input_tokens'] - cached
|
|
246
|
+
cost = in_txt * m.input_cost_per_token + raw['output_tokens'] * m.output_cost_per_token
|
|
247
|
+
cost += cached * m.get('cache_read_input_token_cost', 0)
|
|
248
|
+
return cost
|
|
249
|
+
|
|
250
|
+
# %% ../nbs/02_oai_responses.ipynb #07114b55
|
|
251
|
+
api_ns = dict(norm_tool_calls=norm_tool_calls,
|
|
252
|
+
norm_parts=norm_parts,
|
|
253
|
+
norm_finish=norm_finish,
|
|
254
|
+
norm_usage=norm_usage,
|
|
255
|
+
acollect_stream=acollect_stream,
|
|
256
|
+
mk_payload=mk_payload,
|
|
257
|
+
cost=cost,
|
|
258
|
+
get_hdrs=get_hdrs,
|
|
259
|
+
op_path=('responses.create_response','responses.create_response'))
|
|
260
|
+
api_registry.register('openai', **api_ns)
|