python-fastllm 0.0.49__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 CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  - `fastllm.responses`: Provider-independent Responses resources and streaming over FastLLM"""
4
4
 
5
- __version__ = "0.0.49"
5
+ __version__ = "0.0.50"
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',
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', 'acollect_stream', 'denorm_tool_use',
6
- 'denorm_assistant', 'denorm_tool', 'denorm_msgs', 'denorm_tool_schs', 'denorm_tool_choice',
7
- 'denorm_reasoning', 'denorm_web_search', 'denorm_system', 'denorm_user', 'denorm_image', 'denorm_file',
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, ServerToolResult,
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
- "Adjust usage using finalized Anthropic content parts."
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
- if typ in ant_parts_mapping: return ant_parts_mapping[typ]
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.server_tool_result: parts.append(ServerToolResult(b.get("text", ""), raw=b))
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
- block = dict(type='tool_use', id=_sanid(p.id or ''), name=p.name or '', input=p.arguments or {})
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
- "Convert canonical assistant Msg to Anthropic assistant message + synthetic tool results for non-Anthropic server tools."
154
- blocks, srv_results, srv_call_id = [], [], None
155
- for p in m.content:
156
- if isinstance(p, (Thinking, Text)):
157
- if srv_call_id:
158
- srv_results.append(dict(type='tool_result', tool_use_id=srv_call_id, content=p.text or ''))
159
- srv_call_id = None
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 [dict(role='user', content=blocks)]
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.extend(denorm_assistant(m))
185
- elif m.role == 'tool': res.extend(denorm_tool(m))
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
@@ -346,5 +350,6 @@ def cost(usage, m):
346
350
 
347
351
  # %% ../nbs/04_anthropic.ipynb #f7c0b989
348
352
  api_registry.register('anthropic', norm_tool_calls=norm_tool_calls, norm_parts=norm_parts, norm_finish=norm_finish, norm_usage=norm_usage,
349
- finalize_usage=finalize_usage, acollect_stream=acollect_stream, mk_payload=mk_payload, cost=cost, get_hdrs=get_hdrs,
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,
350
355
  endpoint='/v1/messages')
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', 'norm_sse_event', 'delta_index_fn',
5
- 'acollect_stream', 'denorm_tool_use', 'denorm_assistant', 'denorm_tool', 'denorm_msgs', 'denorm_tool_schs',
6
- 'denorm_tool_choice', 'denorm_reasoning', 'denorm_web_search', 'denorm_system', 'denorm_user',
7
- 'denorm_image', 'denorm_audio', 'denorm_video', 'denorm_file', 'denorm_tool_result', 'mk_payload',
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
- "Extract Gemini functionCall parts as normalized tool calls."
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 not (fc:=p.get("functionCall")): continue
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))
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"] = 1
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
- part = dict(functionCall=fc)
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
- "Convert canonical assistant Msg to Gemini model content."
127
- parts = []
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):
@@ -301,6 +317,8 @@ api_ns = dict(norm_tool_calls=norm_tool_calls,
301
317
  norm_finish=norm_finish,
302
318
  norm_usage=norm_usage,
303
319
  acollect_stream=acollect_stream,
320
+ raw_msg=raw_msg,
321
+ collate_raw=collate_raw,
304
322
  mk_payload=mk_payload,
305
323
  cost=cost,
306
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', 'acollect_stream',
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
- "Convert canonical assistant Msg to OpenAI Chat assistant message + synthetic tool responses for server tools."
89
- tcs, srv_responses, texts = [], [], []
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
- thinking = [p for p in m.content if isinstance(p, Thinking)]
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.extend(denorm_assistant(m))
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
 
@@ -226,6 +239,6 @@ def fix_payload(payload, model, vendor_name):
226
239
 
227
240
  # %% ../nbs/03_oai_chat.ipynb #e2b0908e
228
241
  api_ns = dict(norm_tool_calls=norm_tool_calls, norm_parts=norm_parts, norm_finish=norm_finish, norm_usage=norm_usage,
229
- 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,
230
243
  endpoint='/chat/completions')
231
244
  api_registry.register('openai_chat', **api_ns)
@@ -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', 'denorm_msgs',
6
- 'denorm_tool_schs', 'denorm_tool_choice', 'denorm_reasoning', 'denorm_web_search', 'denorm_system',
7
- 'denorm_user', 'denorm_image', 'denorm_video', 'denorm_file', 'denorm_tool_result', 'mk_payload', 'get_hdrs',
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, srv_call_id = [], None
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
- items.append(denorm_tool_use(p))
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
@@ -280,6 +278,7 @@ def fix_payload(payload, model, vendor_name):
280
278
 
281
279
  # %% ../nbs/02_oai_responses.ipynb #07114b55
282
280
  api_ns = dict(norm_tool_calls=norm_tool_calls, norm_parts=norm_parts, norm_finish=norm_finish, norm_usage=norm_usage,
283
- supports_previous_response_id=True, acollect_stream=acollect_stream, mk_payload=mk_payload, cost=cost, get_hdrs=get_hdrs, fix_payload=fix_payload,
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,
284
283
  endpoint='/responses')
285
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, provider_response_reusable=True): store_attr()
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, comp.response_id_reusable)
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, ServerToolResult, mk_part, fence_call_re, Completion
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, **kwargs): self[name] = SimpleNamespace(finalize_usage=finalize_usage, **kwargs)
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fastllm
3
- Version: 0.0.49
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.10
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
- Clone and install locally into your `aai-ws` env
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(img_url),
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=udUKGtrdGuO2q1CvwAXrPId3ToCsgVd8MI9FczPGC08,131
2
- fastllm/_modidx.py,sha256=UExnE14vuPnwPFZ1RcoK2iJawaLmlYJDsOKbpPx-F7o,42832
3
- fastllm/acomplete.py,sha256=aP1ff9a7WKNhBd1kwKE2TJ-YyihLcdhd9-1Izi4wQP4,10772
4
- fastllm/anthropic.py,sha256=RQpOpwIpXxpZe5httO7zgWautF7lJEPSzotcUP-eDVE,17265
5
- fastllm/chat.py,sha256=klji3SOYs5xyzrSFbNhy3uoQ2TSZ3a-8HTjvdRtaG5Q,25036
6
- fastllm/codex.py,sha256=r6auZ8egia83aGkWcF4132AzW4SsBvKeDwg_CxyXj9E,5808
7
- fastllm/gemini.py,sha256=d0nLkxPr5k3X6A8CZNaAuiRrWxmJrc6FfNylJoKNpIc,15291
8
- fastllm/openai_chat.py,sha256=mUOyos5bdmXukPRMD_sEDiVt4WXDRBzvYiCQskQUHPI,11769
9
- fastllm/openai_responses.py,sha256=pWg1rA53u5A0aTvnBczxvTK-aBIHm01U9FeIg3-lLbE,14618
10
- fastllm/responses.py,sha256=jcbKUwbEIsy7ngNcHUtv2bfYeDgqSysK_qWM-0LC9Mg,18767
11
- fastllm/streaming.py,sha256=8qVtWeZ7NlP-9rup_4Ho0ol2dx3-XuGcIMXpZ1GYdEo,9079
12
- fastllm/types.py,sha256=IYHA3Nfs2ghXUhgYy2bbbCAcfqU-guTOjI5gF3NW5NY,19260
13
- python_fastllm-0.0.49.dist-info/METADATA,sha256=VYlgLNW8x53M1VvmBcW7qyq3-A6FF5a7Twh_z8-q3l0,21143
14
- python_fastllm-0.0.49.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
15
- python_fastllm-0.0.49.dist-info/entry_points.txt,sha256=WkWd04F3xVYFP5bsexaQe9Se6uelwE5ULoTtHWreO0s,133
16
- python_fastllm-0.0.49.dist-info/top_level.txt,sha256=F8qodL7nEGUHGmzzqfhNKCTIr1i0D6cvudOnm3z7o0Y,8
17
- python_fastllm-0.0.49.dist-info/RECORD,,