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.
@@ -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)