python-fastllm 0.0.58__py3-none-any.whl → 0.0.60__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.58"
5
+ __version__ = "0.0.60"
fastllm/_modidx.py CHANGED
@@ -22,6 +22,8 @@ d = { 'settings': { 'branch': 'main',
22
22
  'fastllm.anthropic': { 'fastllm.anthropic._add_cache_control': ('anthropic.html#_add_cache_control', 'fastllm/anthropic.py'),
23
23
  'fastllm.anthropic._ant_cc': ('anthropic.html#_ant_cc', 'fastllm/anthropic.py'),
24
24
  'fastllm.anthropic._ant_part_type': ('anthropic.html#_ant_part_type', 'fastllm/anthropic.py'),
25
+ 'fastllm.anthropic._clear_cache_control': ( 'anthropic.html#_clear_cache_control',
26
+ 'fastllm/anthropic.py'),
25
27
  'fastllm.anthropic._sanid': ('anthropic.html#_sanid', 'fastllm/anthropic.py'),
26
28
  'fastllm.anthropic._url_cite': ('anthropic.html#_url_cite', 'fastllm/anthropic.py'),
27
29
  'fastllm.anthropic.acollect_stream': ('anthropic.html#acollect_stream', 'fastllm/anthropic.py'),
@@ -140,7 +142,8 @@ d = { 'settings': { 'branch': 'main',
140
142
  'fastllm.codex.main': ('codex.html#main', 'fastllm/codex.py'),
141
143
  'fastllm.codex.mk_jwt': ('codex.html#mk_jwt', 'fastllm/codex.py'),
142
144
  'fastllm.codex.token_expired': ('codex.html#token_expired', 'fastllm/codex.py')},
143
- 'fastllm.gemini': { 'fastllm.gemini._fn_calls': ('gemini.html#_fn_calls', 'fastllm/gemini.py'),
145
+ 'fastllm.gemini': { 'fastllm.gemini._fn_call': ('gemini.html#_fn_call', 'fastllm/gemini.py'),
146
+ 'fastllm.gemini._fn_calls': ('gemini.html#_fn_calls', 'fastllm/gemini.py'),
144
147
  'fastllm.gemini._gem_filter_sch': ('gemini.html#_gem_filter_sch', 'fastllm/gemini.py'),
145
148
  'fastllm.gemini._gem_part_type': ('gemini.html#_gem_part_type', 'fastllm/gemini.py'),
146
149
  'fastllm.gemini._url_cite': ('gemini.html#_url_cite', 'fastllm/gemini.py'),
@@ -315,7 +318,9 @@ d = { 'settings': { 'branch': 'main',
315
318
  'fastllm.responses._msg_parts': ('responses.html#_msg_parts', 'fastllm/responses.py'),
316
319
  'fastllm.responses._new_id': ('responses.html#_new_id', 'fastllm/responses.py'),
317
320
  'fastllm.responses._response_base': ('responses.html#_response_base', 'fastllm/responses.py'),
321
+ 'fastllm.responses._server_call': ('responses.html#_server_call', 'fastllm/responses.py'),
318
322
  'fastllm.responses._text_content': ('responses.html#_text_content', 'fastllm/responses.py'),
323
+ 'fastllm.responses._visible_parts': ('responses.html#_visible_parts', 'fastllm/responses.py'),
319
324
  'fastllm.responses.normalize_call_ids': ('responses.html#normalize_call_ids', 'fastllm/responses.py'),
320
325
  'fastllm.responses.pending_response': ('responses.html#pending_response', 'fastllm/responses.py'),
321
326
  'fastllm.responses.response_event': ('responses.html#response_event', 'fastllm/responses.py'),
fastllm/acomplete.py CHANGED
@@ -1,4 +1,4 @@
1
- """A high level unified function make api calls"""
1
+ """A unified async API for model completions and streaming"""
2
2
 
3
3
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/06_acomplete.ipynb.
4
4
 
fastllm/anthropic.py CHANGED
@@ -1,3 +1,5 @@
1
+ """Translate Anthropic Messages API requests, responses, and streams to and from fastllm's shared message types, with tool replay, media, and prompt caching"""
2
+
1
3
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/04_anthropic.ipynb.
2
4
 
3
5
  # %% auto #0
@@ -16,7 +18,7 @@ from fasttransport.errors import api_error_from_event
16
18
 
17
19
  from .types import *
18
20
  from aidialog.msg_parts import (MdStr, Part, PartType, mk_part, Msg, Completion, Text, Thinking, ToolUse, ToolResult, tool_text,
19
- InputImage, InputAudio, InputVideo, InputFile, data_url)
21
+ InputImage, InputAudio, InputVideo, InputFile, data_url)
20
22
  from .streaming import *
21
23
  from .streaming import mk_acollect_stream
22
24
 
@@ -44,8 +46,8 @@ def norm_usage(resp):
44
46
  pt = int(usg.get("input_tokens", 0) or 0) + cached + cache_creation
45
47
  ct = int(usg.get("output_tokens", 0) or 0)
46
48
  return Usage(prompt_tokens=pt, completion_tokens=ct, total_tokens=pt + ct,
47
- cached_tokens=cached, cache_creation_tokens=cache_creation, reasoning_tokens=0, raw=usg,
48
- search_queries=int(nested_idx(usg, 'server_tool_use', 'web_search_requests') or 0))
49
+ cached_tokens=cached, cache_creation_tokens=cache_creation, reasoning_tokens=0, raw=usg,
50
+ search_queries=int(nested_idx(usg, 'server_tool_use', 'web_search_requests') or 0))
49
51
 
50
52
  def finalize_usage(usg, parts):
51
53
  "Split reasoning tokens out of `output_tokens`: the reported `thinking_tokens`, else an estimate from the thinking text."
@@ -110,7 +112,9 @@ def norm_tr_parts(content):
110
112
  if typ=='text': parts.append(Text(b.get('text',''), raw=b))
111
113
  elif typ=='image':
112
114
  s = b['source']
113
- parts.append(InputImage(f"data:{s['media_type']};base64,{s['data']}", mime=s['media_type'], raw=b))
115
+ if s['type']=='url': parts.append(InputImage(s['url'], raw=b))
116
+ elif s['type']=='base64': parts.append(InputImage(f"data:{s['media_type']};base64,{s['data']}", mime=s['media_type'], raw=b))
117
+ else: raise ValueError(f"Unsupported Anthropic image source type: {s['type']!r}")
114
118
  else: parts.append(mk_part(**b))
115
119
  return parts
116
120
 
@@ -238,13 +242,11 @@ def denorm_reasoning(v):
238
242
  # %% ../nbs/04_anthropic.ipynb #8fa9fbb8
239
243
  def denorm_web_search(v):
240
244
  "Map canonical web_search_options to Anthropic hosted web_search tool."
241
- _max_uses = {"low": 1, "medium": 5, "high": 10}
245
+ _max_uses = dict(low=1, medium=5, high=10)
242
246
  t = {"type": "web_search_20250305", "name": "web_search"}
243
247
  if (typ := (v or {}).get("type")): t["type"] = typ
244
- if (s := (v or {}).get("search_context_size")):
245
- t["max_uses"] = _max_uses.get(s, 5)
246
- if (u := (v or {}).get("user_location", {}).get("approximate")):
247
- t["user_location"] = {"type": "approximate", **u}
248
+ if (s := (v or {}).get("search_context_size")): t["max_uses"] = _max_uses.get(s, 5)
249
+ if (u := (v or {}).get("user_location", {}).get("approximate")): t["user_location"] = {"type": "approximate", **u}
248
250
  return t
249
251
 
250
252
  # %% ../nbs/04_anthropic.ipynb #6b485275
@@ -269,12 +271,12 @@ def denorm_user(m:Msg):
269
271
  def denorm_image(p, max_sz=None):
270
272
  if (b64:=data_url(p.text)):
271
273
  data = resize_b64(b64[1], max_sz) if max_sz else b64[1]
272
- return {"type": "image", "source": {"type": "base64", "media_type": b64[0], "data": data}}
274
+ return {"type": "image", "source": dict(type="base64", media_type=b64[0], data=data)}
273
275
  return {"type": "image", "source": {"type": "url", "url": p.text}}
274
276
 
275
277
  # %% ../nbs/04_anthropic.ipynb #fc6bbdfc
276
278
  def denorm_file(p):
277
- if (b64:=data_url(p.text)): return {"type": "document", "source": {"type": "base64", "media_type": b64[0], "data": b64[1]}}
279
+ if (b64:=data_url(p.text)): return {"type": "document", "source": dict(type="base64", media_type=b64[0], data=b64[1])}
278
280
  return {"type": "document", "source": {"type": "url", "url": p.text}}
279
281
 
280
282
  # %% ../nbs/04_anthropic.ipynb #37b63cc2
@@ -303,15 +305,21 @@ def _add_cache_control(
303
305
  return msg
304
306
 
305
307
  # %% ../nbs/04_anthropic.ipynb #6bc52ef5
308
+ def _clear_cache_control(obj):
309
+ "Clear canonical cache marks from a message or part."
310
+ if isinstance(obj, Msg):
311
+ for p in obj.content: _clear_cache_control(p)
312
+ elif isinstance(obj, Part): obj.cache_control = None
313
+
306
314
  def apply_cache_idxs(
307
315
  msgs, # Canonical Msgs to mark
308
316
  cache_idxs=[-1], # Breakpoint idxs into `msgs` (tool-role msgs excluded); 0 is `sp` when given
309
317
  sp=None, # System prompt: str, Msg, or Part
310
318
  ttl=None # Cache TTL: '5m' (default) or '1h'
311
319
  ):
312
- "Strip all cache breakpoints from `msgs`, then mark `cache_idxs`; returns `sp`, marked if 0 is included"
313
- for m in msgs:
314
- for p in m.content: p.cache_control = None
320
+ "Replace cache breakpoints in `msgs` and `sp`, then return the system prompt."
321
+ for m in msgs: _clear_cache_control(m)
322
+ _clear_cache_control(sp)
315
323
  if sp:
316
324
  if 0 in cache_idxs:
317
325
  if isinstance(sp, str): sp = Text(sp)
@@ -335,10 +343,8 @@ def mk_payload(msgs, model, **kwargs):
335
343
  if tools:=kwargs.get('tools'): payload['tools'] = denorm_tool_schs(tools)
336
344
  if tchc:=kwargs.get('tool_choice'): payload['tool_choice'] = denorm_tool_choice(tchc)
337
345
  if thk:=kwargs.get('reasoning_effort'): payload.update(denorm_reasoning(thk))
338
- if (wopts:=kwargs.get('web_search_options')) is not None:
339
- payload.setdefault('tools', []).append(denorm_web_search(wopts))
340
- if (temp:=kwargs.get('temperature')) is not None:
341
- payload['temperature'] = temp
346
+ if (wopts:=kwargs.get('web_search_options')) is not None: payload.setdefault('tools', []).append(denorm_web_search(wopts))
347
+ if (temp:=kwargs.get('temperature')) is not None: payload['temperature'] = temp
342
348
  return payload
343
349
 
344
350
  # %% ../nbs/04_anthropic.ipynb #60d52c1f
@@ -367,5 +373,4 @@ def cost(usage, m):
367
373
  # %% ../nbs/04_anthropic.ipynb #f7c0b989
368
374
  api_registry.register('anthropic', norm_tool_calls=norm_tool_calls, norm_parts=norm_parts, norm_finish=norm_finish, norm_usage=norm_usage,
369
375
  finalize_usage=finalize_usage, raw_msg=raw_msg, collate_raw=collate_raw, acollect_stream=acollect_stream, mk_payload=mk_payload,
370
- cost=cost, get_hdrs=get_hdrs,
371
- endpoint='/v1/messages')
376
+ cost=cost, get_hdrs=get_hdrs, endpoint='/v1/messages')
fastllm/chat.py CHANGED
@@ -1,4 +1,4 @@
1
- """High level chat api for fastllm similar to lisette"""
1
+ """A high-level chat API for fastllm, similar to Lisette"""
2
2
 
3
3
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/07_chat.ipynb.
4
4
 
@@ -112,9 +112,12 @@ _cwe_msg = "ContextWindowExceededError: Do no more tool calls and complete your
112
112
 
113
113
  # %% ../nbs/07_chat.ipynb #05c20a94
114
114
  def search_count(r):
115
- if cnt := nested_idx(r.usage.raw, 'server_tool_use', 'web_search_requests'): return cnt # Anthropic
116
- if cnt := nested_idx(r.usage.raw, 'server_tool_use', 'google_search'): return cnt # Gemini
117
- if cnt := nested_idx(r.usage.raw, 'web_search_requests'): return cnt # streaming with `include_usage`
115
+ raw = r.usage.raw or {}
116
+ if (cnt := raw.get('web_search_requests')) is not None: return cnt # host aggregate
117
+ if cnt := r.usage.search_queries: return cnt
118
+ legacy = raw.get('server_tool_use') or {}
119
+ if (cnt := legacy.get('web_search_requests')) is not None: return cnt # Anthropic
120
+ if (cnt := legacy.get('google_search')) is not None: return cnt # Gemini
118
121
  return 0
119
122
 
120
123
  # %% ../nbs/07_chat.ipynb #61395e0d
@@ -288,8 +291,9 @@ async def _call(self:AsyncChat, msg=None, step=1, search=None, tool_choice=None,
288
291
  async for o in self._call_cbs('after_msgs'): yield o
289
292
 
290
293
  self.turn_kwargs, self.stream = kwargs, t.stream
291
- kw = merge(kwargs, dict(xtra_body=merge(kwargs.get('xtra_body'), initial_body))) if initial_body else kwargs
292
294
  async for o in self._call_cbs('before_acomplete'): yield o
295
+ kw = self.turn_kwargs
296
+ if initial_body: kw = merge(kw, dict(xtra_body=merge(kw.get('xtra_body'), initial_body)))
293
297
  res = await acomplete(self.turn_msgs, self.model, system=self.turn_sysp, stream=t.stream, tools=self.tool_schemas,
294
298
  tool_choice=tool_choice, max_tokens=int(max_tokens), temperature=None if t.think else ifnone(t.temp,self.temp),
295
299
  previous_response_id=self.response_id if self.use_previous_response_id else None,
@@ -413,7 +417,7 @@ class StopReasonCallback(ChatCallback):
413
417
  action,msg = _handle_stop_reason(self.turn_res)
414
418
  if action != 'warning': return
415
419
  add_warning(self.chat.turn_res, msg)
416
- if self.stream: yield dict(text=f"warning: {msg}\n\n")
420
+ if self.stream: yield Text(f'\n\nwarning: {msg}\n\n')
417
421
 
418
422
  # %% ../nbs/07_chat.ipynb #aa7630b2
419
423
  _lang2tool = dict(py='python', bash='bash')
@@ -493,11 +497,11 @@ defaults.chat_callbacks = [FenceToolCallback, ToolReminderCallback, StopReasonCa
493
497
 
494
498
  # %% ../nbs/07_chat.ipynb #ead4b56d
495
499
  class StreamAccum:
496
- "Accumulate a formatted stream: append each item's `formatted`, swap in `chat.full()` on `Refresh`"
500
+ "Accumulate formatted parts; refresh from `chat.full()` on `Refresh` or `Completion`"
497
501
  def __init__(self, chat=None, mx=2000): self.chat,self.mx,self.txt,self.last = chat,mx,'',None
498
502
  def __call__(self, o):
499
503
  "Feed one stream item; True if the display text changed"
500
- if isinstance(o, Refresh):
504
+ if isinstance(o, (Refresh, Completion)):
501
505
  if self.chat is None: return False
502
506
  self.txt,self.last = self.chat.full(mx=self.mx)+'\n\n',None
503
507
  elif f := getattr(o, 'formatted', ''):
@@ -509,7 +513,7 @@ class StreamAccum:
509
513
 
510
514
  # %% ../nbs/07_chat.ipynb #944bcd25
511
515
  async def adisplay_stream(rs, chat=None, mx=2000):
512
- "Use IPython.display to markdown display the response stream, refreshing from `chat.full()` on `Refresh`."
516
+ "Display a stream as Markdown, refreshing from `chat.full()` on `Refresh` or `Completion`."
513
517
  try: from IPython.display import display, Markdown
514
518
  except ModuleNotFoundError: raise ModuleNotFoundError("This function requires ipython. Please run `pip install ipython` to use.")
515
519
  acc,h = StreamAccum(chat, mx=mx), display(Markdown(' '), display_id=True)
fastllm/gemini.py CHANGED
@@ -1,3 +1,5 @@
1
+ """Translate Gemini generateContent requests, responses, and streams to and from fastllm's shared message types, including tools, thought signatures, search grounding, and media"""
2
+
1
3
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/05_gemini.ipynb.
2
4
 
3
5
  # %% auto #0
@@ -16,7 +18,7 @@ from fasttransport.errors import api_error_from_event
16
18
 
17
19
  from .types import *
18
20
  from aidialog.msg_parts import (MdStr, Part, PartType, Msg, Completion, Text, Thinking, ToolUse, ToolResult, tool_text,
19
- InputImage, InputAudio, InputVideo, InputFile, sys_text, part_txt, data_url, url_mime)
21
+ InputImage, InputAudio, InputVideo, InputFile, sys_text, part_txt, data_url, url_mime)
20
22
  from .streaming import *
21
23
  from .streaming import mk_acollect_stream
22
24
 
@@ -41,9 +43,12 @@ def grounding(cand):
41
43
  return cites, call
42
44
 
43
45
  # %% ../nbs/05_gemini.ipynb #48de3125
44
- def _fn_calls(parts):
45
- return [ToolUse(id=fc.get("id", f"call_{i}"), name=fc.get("name", ""), arguments=fc.get("args") or {}, raw={k:v for k,v in p.items() if k != "functionCall"})
46
- for i,p in enumerate(parts) if (fc := p.get("functionCall"))]
46
+ def _fn_call(p, i):
47
+ fc = p['functionCall']
48
+ return ToolUse(id=fc.get("id", f"call_{i}"), name=fc.get("name", ""), arguments=fc.get("args") or {},
49
+ raw={k:v for k,v in p.items() if k != "functionCall"})
50
+
51
+ def _fn_calls(parts): return [_fn_call(p, i) for i,p in enumerate(parts) if p.get('functionCall')]
47
52
 
48
53
  def norm_tool_calls(resp):
49
54
  "Tool calls of a response: the `google_search` call its grounding describes, then its `functionCall` parts"
@@ -67,7 +72,7 @@ def norm_usage(resp):
67
72
  if call := grounding(cand)[1]: stu['google_search'] = len(call.arguments['queries'] or [1])
68
73
  if stu: usg['server_tool_use'] = stu
69
74
  return Usage(prompt_tokens=pt, completion_tokens=ct, total_tokens=tt,
70
- cached_tokens=cached, reasoning_tokens=reasoning, search_queries=stu.get('google_search', 0), raw=usg)
75
+ cached_tokens=cached, reasoning_tokens=reasoning, search_queries=stu.get('google_search', 0), raw=usg)
71
76
 
72
77
  # %% ../nbs/05_gemini.ipynb #7a8b1f8f
73
78
  def norm_finish(resp, tcs=None):
@@ -90,13 +95,11 @@ def norm_parts(resp):
90
95
  "Normalize Gemini generateContent response; a search's `toolCall` and `toolResponse` parts give way to the call `grounding` builds"
91
96
  c0 = nested_idx(resp, 'candidates', 0) or {}
92
97
  cites,call = grounding(c0)
93
- tc_map = {tc.id: tc for tc in norm_tool_calls(resp)}
94
98
  parts = [call] if call else []
95
- for p in nested_idx(c0, 'content', 'parts') or []:
99
+ for i,p in enumerate(nested_idx(c0, 'content', 'parts') or []):
96
100
  if 'toolCall' in p or 'toolResponse' in p: continue
97
101
  typ = _gem_part_type(p)
98
- if typ == PartType.tool_use:
99
- if tc := tc_map.get(p['functionCall'].get('id')): parts.append(tc)
102
+ if typ == PartType.tool_use: parts.append(_fn_call(p, i))
100
103
  elif typ == PartType.thinking: parts.append(Thinking(p.get("text",""), raw=p))
101
104
  elif typ == PartType.tool_result: parts.append(ToolResult(text=p.get("text",""), raw=p))
102
105
  else: parts.append(Text(p.get("text",""), raw=p))
@@ -175,15 +178,20 @@ def denorm_msgs(msgs:list[Msg]):
175
178
  return res
176
179
 
177
180
  # %% ../nbs/05_gemini.ipynb #adb525db
178
- _valid_gemini_sch = {'type', 'format', 'title', 'description', 'nullable', 'default',
179
- 'items', 'minItems', 'maxItems', 'enum', 'properties', 'propertyOrdering',
180
- 'required', 'minProperties', 'maxProperties', 'minimum', 'maximum',
181
- 'minLength', 'maxLength', 'pattern', 'example', 'anyOf'}
181
+ _valid_gemini_sch = {'type', 'format', 'title', 'description', 'nullable', 'default', 'items', 'minItems', 'maxItems', 'enum', 'properties',
182
+ 'propertyOrdering', 'required', 'minProperties', 'maxProperties', 'minimum', 'maximum', 'minLength', 'maxLength', 'pattern', 'example',
183
+ 'anyOf'}
182
184
 
183
185
  def _gem_filter_sch(s):
184
186
  if isinstance(s, list): return [_gem_filter_sch(x) for x in s]
185
187
  if not isinstance(s, dict): return s
186
- return {k: _gem_filter_sch(v) for k,v in s.items() if k in _valid_gemini_sch}
188
+ res = {}
189
+ for k,v in s.items():
190
+ if k not in _valid_gemini_sch: continue
191
+ if k == 'properties': res[k] = {name:_gem_filter_sch(prop) for name,prop in v.items()}
192
+ elif k in ('items', 'anyOf'): res[k] = _gem_filter_sch(v)
193
+ else: res[k] = v
194
+ return res
187
195
 
188
196
  def denorm_tool_schs(tools):
189
197
  "Convert canonical tools to Gemini format."
@@ -192,8 +200,7 @@ def denorm_tool_schs(tools):
192
200
  fn = fn_schema(t)
193
201
  if fn is None: other.append(t); continue
194
202
  name, desc, params = fn
195
- params = {k:v for k,v in params.items() if k in _valid_gemini_sch}
196
- params['properties'] = {k:_gem_filter_sch(v) for k,v in params['properties'].items()}
203
+ params = _gem_filter_sch(params)
197
204
  fn_decls.append(dict(name=name, description=desc, parameters=params))
198
205
  out = other[:]
199
206
  if fn_decls: out.insert(0, dict(functionDeclarations=fn_decls))
@@ -214,13 +221,11 @@ _gem_think_levels = dict(minimal='low', low='low', medium='medium', high='high'
214
221
 
215
222
  def denorm_reasoning(v, model=''):
216
223
  "Map canonical reasoning_effort to Gemini thinkingConfig (uses thinkingLevel for Gemini 3+)."
217
- err = ValueError(f"Invalid reasoning effort for Gemini: {v}, accepted string values are: {list(_gem_think_budgets)} and dicts are passthrough")
218
- if v is None: return None
219
- elif isinstance(v, dict): return v
220
- elif isinstance(v, str) and v in _gem_think_budgets:
221
- # defaults to includeThoughts same as litellm
222
- if 'gemini-3' in model: return {'thinkingLevel': _gem_think_levels.get(v, 'medium'), 'includeThoughts': True}
223
- return {'thinkingBudget': _gem_think_budgets.get(str(v).lower(), 1024), 'includeThoughts': True}
224
+ if v is None or isinstance(v, dict): return v
225
+ if not isinstance(v, str) or v not in _gem_think_budgets:
226
+ raise ValueError(f"Invalid reasoning effort for Gemini: {v!r}; expected one of {list(_gem_think_budgets)}, a dict, or None")
227
+ if 'gemini-3' in model: return {'thinkingLevel': _gem_think_levels[v], 'includeThoughts': True}
228
+ return {'thinkingBudget': _gem_think_budgets[v], 'includeThoughts': True}
224
229
 
225
230
  # %% ../nbs/05_gemini.ipynb #8fa9fbb8
226
231
  def denorm_web_search(v): return {"googleSearch": {}}
@@ -325,15 +330,7 @@ def cost(usage, m):
325
330
  return cost
326
331
 
327
332
  # %% ../nbs/05_gemini.ipynb #f7c0b989
328
- api_ns = dict(norm_tool_calls=norm_tool_calls,
329
- norm_parts=norm_parts,
330
- norm_finish=norm_finish,
331
- norm_usage=norm_usage,
332
- acollect_stream=acollect_stream,
333
- raw_msg=raw_msg,
334
- collate_raw=collate_raw,
335
- mk_payload=mk_payload,
336
- cost=cost,
337
- get_hdrs=get_hdrs,
338
- endpoint=endpoint)
333
+ api_ns = dict(norm_tool_calls=norm_tool_calls, norm_parts=norm_parts, norm_finish=norm_finish, norm_usage=norm_usage,
334
+ acollect_stream=acollect_stream, raw_msg=raw_msg, collate_raw=collate_raw, mk_payload=mk_payload, cost=cost, get_hdrs=get_hdrs,
335
+ endpoint=endpoint)
339
336
  api_registry.register('gemini', **api_ns)
fastllm/openai_chat.py CHANGED
@@ -1,3 +1,5 @@
1
+ """Translate Chat Completions requests, responses, and streams to and from fastllm's shared message types"""
2
+
1
3
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/03_oai_chat.ipynb.
2
4
 
3
5
  # %% auto #0
@@ -1,3 +1,5 @@
1
+ """Translate Responses API payloads, completed responses, and stream events to and from fastllm's shared message types"""
2
+
1
3
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/02_oai_responses.ipynb.
2
4
 
3
5
  # %% auto #0
@@ -65,7 +67,7 @@ def norm_finish(resp, tcs=None):
65
67
 
66
68
  # %% ../nbs/02_oai_responses.ipynb #bbfb99b9
67
69
  def search_hits(parts):
68
- "OpenAI reports no result for a hosted search, so a server call's empty `text` becomes the message's cited sources, one `title: url` line each"
70
+ "Fill empty server-tool results with the message's cited sources as `title: url` lines"
69
71
  hits = {c['url']: c.get('title', '') for p in parts if isinstance(p, Text) for c in p.citations or [] if c.get('url')}
70
72
  for p in parts:
71
73
  if isinstance(p, ToolUse) and p.server and not p.text: p.text = MdStr('\n'.join(f"{t}: {u}" for u,t in hits.items()))
@@ -77,9 +79,7 @@ def norm_parts(resp):
77
79
  for item in resp["output"]:
78
80
  if (typ:=item["type"]) == "message":
79
81
  for c in item["content"]:
80
- if (ctyp := c["type"]) == "output_text":
81
- c['citations'] = c.pop('annotations', [])
82
- parts.append(Text(c['text'], citations=c['citations'], raw=c))
82
+ if (ctyp := c["type"]) == "output_text": parts.append(Text(c['text'], citations=list(c.get('annotations') or []), raw=c))
83
83
  elif ctyp == "refusal": parts.append(Refusal(c['refusal'], raw=c))
84
84
  elif typ == "reasoning":
85
85
  for s in item["summary"]: parts.append(Thinking(s['text'], raw=item))
@@ -88,7 +88,7 @@ def norm_parts(resp):
88
88
 
89
89
  # %% ../nbs/02_oai_responses.ipynb #2aaab962
90
90
  def raw_msg(resp):
91
- "The output items as the API sent them"
91
+ "Return the response output list for assistant replay, without copying"
92
92
  return resp['output']
93
93
 
94
94
  # %% ../nbs/02_oai_responses.ipynb #ef2d19f3
@@ -282,6 +282,7 @@ def cost(usage, m):
282
282
  # %% ../nbs/02_oai_responses.ipynb #2efda2a1
283
283
  def fix_payload(payload, model, vendor_name):
284
284
  "Apply vendor request quirks to a built payload."
285
+ if model == 'gpt-6-astra': payload.pop('temperature', None)
285
286
  if vendor_name == 'codex':
286
287
  for k in 'temperature max_tokens max_output_tokens max_completion_tokens metadata'.split(): payload.pop(k, None)
287
288
  payload['store'] = False
@@ -289,6 +290,5 @@ def fix_payload(payload, model, vendor_name):
289
290
  # %% ../nbs/02_oai_responses.ipynb #07114b55
290
291
  api_ns = dict(norm_tool_calls=norm_tool_calls, norm_parts=norm_parts, norm_finish=norm_finish, norm_usage=norm_usage,
291
292
  supports_previous_response_id=True, raw_msg=raw_msg, collate_raw=collate_raw, acollect_stream=acollect_stream, mk_payload=mk_payload,
292
- cost=cost, get_hdrs=get_hdrs, fix_payload=fix_payload,
293
- endpoint='/responses')
293
+ cost=cost, get_hdrs=get_hdrs, fix_payload=fix_payload, endpoint='/responses')
294
294
  api_registry.register('openai', **api_ns)
fastllm/responses.py CHANGED
@@ -1,12 +1,12 @@
1
1
  """Provider-independent Responses resources and streaming over FastLLM
2
2
 
3
- `response_input` converts a plain prompt or standard input-item array to canonical messages. System and developer messages become provider instructions; adjacent function outputs remain one tool message so a parallel batch reaches the next model turn together.
3
+ `response_input` accepts a prompt string or an input-item array. It returns `(messages, instructions)`. System and developer text goes in the instructions; the remaining items become shared messages.
4
4
 
5
- FastLLM completions become ordinary assistant-message and function-call items. Call ids are normalized before the canonical history is retained, so a later tool result always refers to the same id the client saw.
5
+ `normalize_call_ids` supplies missing client call IDs before we retain the history. Later tool results can refer to the same IDs that the client received.
6
6
 
7
- `ResponseState` is the complete provider-independent continuation value. A host may store it under any ownership and expiry policy. When a provider id exists, the next turn sends only new items; otherwise it replays canonical history.
7
+ `ResponseState` holds the public ID, model, full message history, and optional provider response ID. The host can store it under its own ownership and expiry rules. A provider ID lets the next call send just the new messages; without one, it needs the retained history.
8
8
 
9
- Every request is streamed. `_ResponseStream` only tracks the standard event grammar for one inference; it contains no provider, persistence, or billing policy."""
9
+ FastLLM calls stream internally. The helpers here turn their parts into Responses SSE frames; they don't decide storage, ownership, or billing policy."""
10
10
 
11
11
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/06a_responses.ipynb.
12
12
 
@@ -361,13 +361,25 @@ def done_events(self:_ResponseStream, comp):
361
361
  if isinstance(part, ToolUse) and part.server:
362
362
  events.append(self.event('response.output_item.done', output_index=self.next_index, item=server_call_item(part)))
363
363
  self.next_index += 1
364
- for i,tc in enumerate(comp.tool_calls):
364
+ client_calls = [tc for tc in comp.tool_calls if not tc.server]
365
+ for i,tc in enumerate(client_calls):
365
366
  if not tc.id and i < len(self.streamed_call_ids): tc.id = self.streamed_call_ids[i]
366
367
  tc._response_item_id = self.tool_item_ids.get(tc.id)
367
368
  for part in comp.message.content:
368
369
  if isinstance(part, Text): part._response_item_id = self.message_id
369
370
  return events
370
371
 
372
+ # %% ../nbs/06a_responses.ipynb #04a20345
373
+ def _server_call(result):
374
+ "A public server call with the host's completed result."
375
+ return ToolUse(id=result.id, name=result.name, arguments=result.arguments, server=True, text=result.text)
376
+
377
+ def _visible_parts(parts, results):
378
+ "Replace completed host calls without changing the provider's parts."
379
+ completed = {r.id:r for r in results}
380
+ return [_server_call(completed[p.id]) if isinstance(p, ToolUse) and p.id in completed else p for p in parts]
381
+
382
+
371
383
  # %% ../nbs/06a_responses.ipynb #e10c928b
372
384
  @patch
373
385
  async def events(self:AsyncResponses, turn, finalize=None, stream=None):
@@ -385,11 +397,11 @@ async def events(self:AsyncResponses, turn, finalize=None, stream=None):
385
397
  results = await self.server_results(turn, comp)
386
398
  if not results or len(results) < len(comp.tool_calls): break
387
399
  texts += [p for p in comp.message.content if isinstance(p, Text) or (isinstance(p, ToolUse) and p.server)]
388
- texts += [ToolUse(id=r.id, name=r.name, arguments=r.arguments, server=True, text=r.text) for r in results]
400
+ texts += [_server_call(r) for r in results]
389
401
  turn = self.server_turn(turn, comp, results)
390
402
  next_state = self.state(turn, comp, results)
391
403
  comp = copy.copy(comp)
392
- comp.message,comp.usage = Msg('assistant', texts + comp.message.content),usage
404
+ comp.message,comp.usage = Msg('assistant', texts + _visible_parts(comp.message.content, results)),usage
393
405
  for event in state.done_events(comp): yield event
394
406
  response = response_object(first.id, first.body, comp, first.previous.id if first.previous else None, first.created_at)
395
407
  if finalize and (extra := await finalize(next_state, comp)): response['usage'].update(extra)
fastllm/streaming.py CHANGED
@@ -184,7 +184,6 @@ async def mk_acollect_stream(it, index_fn, model=None, api_name=None, vendor_nam
184
184
  if stop: fin = FinishReason.stop
185
185
  fin = FinishReason.tool_calls if fin==FinishReason.stop and any(~L(tcs).attrgot('server')) else fin # recheck tool calls post collation
186
186
  # tool calls and non-anthropic citations are yielded at the end
187
- yield Completion(model,
188
- message=Msg(role="assistant", content=parts, raw=api_registry[api_name].collate_raw(deltas) if api_name else None),
189
- finish_reason=fin, usage=usg, api_name=api_name, vendor_name=vendor_name,
190
- raw={'deltas':deltas})
187
+ (((((((yield Completion(model,
188
+ message=Msg(role='assistant', content=parts, raw=api_registry[api_name].collate_raw(deltas) if api_name else None),
189
+ finish_reason=fin, usage=usg, api_name=api_name, vendor_name=vendor_name, raw={'deltas': deltas}))))))))
fastllm/types.py CHANGED
@@ -7,7 +7,7 @@ __all__ = ['FinishReason', 'api_registry', 'model_prices_url', 'sample_img_url',
7
7
  'sonn', 'sonn5', 'opus46', 'opus48', 'opus', 'opus5', 'fable', 'fable5', 'fable51', 'gpt54', 'gpt54m',
8
8
  'gpt55', 'codex54', 'codex54m', 'codex55', 'codex53spark', 'model_info_registry', 'modern_llm',
9
9
  'effort_codes', 'deepseek_v4_common', 'deepseek_v4_flash_prices', 'mimo_v25_common', 'codex_pricing', 'sol',
10
- 'terra', 'luna', 'gpt56s', 'Usage', 'APIRegistry', 'mk_completion', 'fn_schema', 'payload_kwargs',
10
+ 'terra', 'luna', 'gpt56s', 'astra', 'Usage', 'APIRegistry', 'mk_completion', 'fn_schema', 'payload_kwargs',
11
11
  'provider_req', 'get_api_key', 'wrap_typed', 'unwrap_typed', 'resize_b64', 'model_prices_meta',
12
12
  'infer_api_name', 'get_model_meta', 'register_model_info', 'get_model_info', 'effort_code', 'effort_levels',
13
13
  'resolve_effort', 'get_model_pricing', 'approx_pricing', 'is_deepseek_peak_hour', 'price_tier', 'tier_rate']
@@ -136,11 +136,11 @@ _typed_re = re.compile(r'^𝍁(\w+)𝍁(.+)$', re.S)
136
136
  _typed_strs = {c.__name__: c for c in (StopResponse, FullResponse)}
137
137
 
138
138
  def wrap_typed(s):
139
- "Mark `s` with a `𝍁TypeName𝍁` prefix when it is a `str` subclass, so the type survives a plain-string wire"
139
+ "Prefix a `str` subclass with `𝍁TypeName𝍁` for transmission as plain text"
140
140
  return s if type(s) is str else f'𝍁{type(s).__name__}𝍁{s}'
141
141
 
142
142
  def unwrap_typed(s):
143
- "Rebuild the named `str` subclass from a `𝍁`-marked string; unknown names get a minted ephemeral type"
143
+ "Decode a `𝍁TypeName𝍁` prefix, creating a `str` subclass for an unknown name"
144
144
  if not (m := _typed_re.match(s)): return s
145
145
  nm,body = m.groups()
146
146
  return _typed_strs.setdefault(nm, type(nm, (str,), {}))(body)
@@ -317,11 +317,16 @@ for model in (haik45, sonn45, sonn46, sonn5, opus46, opus48, opus5, fable5, fabl
317
317
 
318
318
  # %% ../nbs/00_types.ipynb #bb0c4c2a
319
319
  sol,terra,luna = gpt56s = 'gpt-5.6-sol gpt-5.6-terra gpt-5.6-luna'.split()
320
+ astra = 'gpt-6-astra'
321
+ for model in (*gpt56s, astra): register_model_info(model, 'openai', base=model, max_input_tokens=272_000)
322
+
320
323
  # Codex serves only the suffixed names; bare `gpt-5.6` is rejected with a ChatGPT account.
321
- # Its window is smaller than the API's: 371,331 input tokens is accepted and 371,981 is not, on all three.
324
+ # Its window accepts 371,331 input tokens and rejects 371,981, on all three.
322
325
  for model in gpt56s:
323
326
  register_model_info(model, 'codex', base=model, base_vendor_name='openai', max_input_tokens=371_000, **codex_pricing)
324
327
  _flat_rates('codex', model)
328
+ register_model_info(astra, 'codex', base=astra, base_vendor_name='openai', **codex_pricing)
329
+ _flat_rates('codex', astra)
325
330
 
326
331
  # %% ../nbs/00_types.ipynb #24cc47ec
327
332
  def get_model_pricing(mn, vendor_name, million=True):
@@ -0,0 +1,231 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-fastllm
3
+ Version: 0.0.60
4
+ Author-email: Kerem Turgutlu <keremturgutlu@gmail.com>
5
+ License: Apache-2.0
6
+ Project-URL: Repository, https://github.com/AnswerDotAI/fastllm
7
+ Keywords: nbdev
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: fastcore>=2.1.18
13
+ Requires-Dist: aidialog>=0.0.27
14
+ Requires-Dist: fasttransport>=0.0.2
15
+ Requires-Dist: pillow
16
+
17
+ # fastllm
18
+
19
+
20
+ <!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->
21
+
22
+ `fastllm` provides a common async interface for Anthropic, OpenAI Responses and Chat, Gemini, and OpenAI-compatible providers such as Kimi. Define messages and tools in a shared format and call `acomplete`. Select the model and, where needed, its provider without rewriting the conversation or tool loop. Supported features vary by provider.
23
+
24
+ `AsyncResponses` provides a streaming Responses API interface over the same providers. It translates input items and events and keeps a canonical `ResponseState` between turns. For transports without provider continuation ids, it replays that state automatically.
25
+
26
+ ## Install
27
+
28
+ Run `pip install python-fastllm` or clone from github and install locally.
29
+
30
+ ## Setup
31
+
32
+ ``` python
33
+ from aidialog.msg_parts import Msg, Part, Text, Thinking, ToolUse, InputImage, mk_tool_res_msg, Completion
34
+ from fastllm.acomplete import acomplete
35
+ from fastllm.types import sample_img_url, sample_doms
36
+ import asyncio, json
37
+ ```
38
+
39
+ ``` python
40
+ def user(text): return Msg(role='user', content=[Text(text)])
41
+
42
+ async def stream(msgs, model, max_think=0, **kw):
43
+ "Print streamed parts and return the final Completion; limit printed thinking deltas with max_think."
44
+ cnt = 0
45
+ async for o in await acomplete(msgs, model, stream=True, **kw):
46
+ if not isinstance(o, Part): continue
47
+ if isinstance(o, Thinking):
48
+ cnt += 1
49
+ if cnt > max_think: continue
50
+ print(o.formatted, end='', flush=True)
51
+ print()
52
+ return o
53
+ ```
54
+
55
+ ``` python
56
+ mtok = 1024
57
+ ```
58
+
59
+ ## Chat across providers
60
+
61
+ Use the same `acomplete` interface with Claude, GPT, Gemini, or Kimi. Set `model` and any provider-specific routing arguments:
62
+
63
+ ``` python
64
+ models = [
65
+ ('claude-sonnet-5', {}),
66
+ ('gpt-4o-mini', {}),
67
+ ('models/gemini-3-flash-preview', {}),
68
+ ('accounts/fireworks/models/kimi-k3', dict(vendor_name='fireworks_ai'))
69
+ ]
70
+ for name, kw in models:
71
+ r = await acomplete([user("Translate 'hello' into French. Return only the translation.")],
72
+ model=name, max_tokens=mtok, **kw)
73
+ text = ''.join(p.text for p in r.message.content if isinstance(p, Text))
74
+ print(f"{name}: {text.strip()}")
75
+ ```
76
+
77
+ claude-sonnet-5: Bonjour
78
+ gpt-4o-mini: Bonjour
79
+ models/gemini-3-flash-preview: Bonjour
80
+ accounts/fireworks/models/kimi-k3: Bonjour
81
+
82
+ ## System prompts
83
+
84
+ Pass `system` to supply a system prompt. `fastllm` translates it to the provider’s field: Anthropic `system`, OpenAI Responses `instructions`, or Gemini `system_instruction`.
85
+
86
+ ``` python
87
+ sys = "You are a pirate chef. Always respond in pirate speak and mention food. Use one short sentence."
88
+
89
+ print("Claude: ", end='')
90
+ r = await stream([user("What should I do today?")], model='claude-sonnet-5', system=sys, max_tokens=mtok)
91
+
92
+ print("Gemini: ", end='')
93
+ r = await stream([user("What should I do today?")], model='models/gemini-3-flash-preview', system=sys, max_tokens=mtok)
94
+ ```
95
+
96
+ Claude: Arrr, chart a course fer the galley and cook up a hearty stew, matey!
97
+ Gemini: Sharpen yer cutlass and feast on spicy shark stew, ye scurvy dog!
98
+
99
+ ## Tool calling
100
+
101
+ Define tools in the shared schema. `fastllm` translates it to the provider’s tool format. This example handles a tool request, adds the result to the history, and continues the conversation:
102
+
103
+ ``` python
104
+ tools = [{"type": "function", "function": {
105
+ "name": "get_weather",
106
+ "description": "Get current weather for a city",
107
+ "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
108
+ }}]
109
+
110
+ msgs = [user("What's the weather in Paris?")]
111
+ r1 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
112
+ print("Tool calls:", r1.tool_calls)
113
+ ```
114
+
115
+ - ⏳ `get_weather(city="Paris")` ⏳
116
+
117
+ Tool calls: [ToolUse(raw={'caller': {'type': 'direct'}}, cache_control=None, id='toolu_01XacBpJHnsjtPWJWr55nA2m', name='get_weather', arguments={'city': 'Paris'}, server=False, text=None)]
118
+
119
+ ``` python
120
+ msgs += [r1.message, mk_tool_res_msg(r1.tool_calls, ['22°C, sunny with light clouds']),
121
+ user("Should I bring a jacket? Answer in one short sentence.")]
122
+ r2 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
123
+ ```
124
+
125
+ No, a jacket isn't necessary — it's a mild, sunny 22°C day.
126
+
127
+ ## Tool Choice
128
+
129
+ Control whether the model must use tools, can’t use tools, or decides on its own:
130
+
131
+ ``` python
132
+ r = await acomplete([user("Hello there!")], model='claude-sonnet-5',
133
+ tools=tools, tool_choice='required', max_tokens=mtok)
134
+ print("Required:", [tc.name for tc in r.tool_calls])
135
+
136
+ r = await acomplete([user("What's the weather?")], model='claude-sonnet-5',
137
+ tools=tools, tool_choice='none', max_tokens=mtok)
138
+ print("None:", r.tool_calls)
139
+ ```
140
+
141
+ Required: ['get_weather']
142
+ None: []
143
+
144
+ ## Thinking and extended reasoning
145
+
146
+ Set `reasoning_effort` to `low`, `medium`, or `high` for supported models. The adapters translate these values to provider-specific settings. Reasoning output uses `Thinking` parts; this example prints a short excerpt alongside the answer.
147
+
148
+ ``` python
149
+ print("Claude: ", end='')
150
+ r = await stream([user("What is 127 × 849? Return just the number.")], model='claude-sonnet-4-6',
151
+ reasoning_effort='low', max_tokens=8192)
152
+ for p in r.message.content:
153
+ if isinstance(p, Thinking): print(f"Thinking excerpt: {p.text[:150]}...")
154
+
155
+ print("Kimi: ", end='')
156
+ r = await stream([user("What is 127 × 849? Return just the number.")],
157
+ model='accounts/fireworks/models/kimi-k3', vendor_name='fireworks_ai',
158
+ reasoning_effort='low', max_tokens=8192)
159
+ for p in r.message.content:
160
+ if isinstance(p, Thinking): print(f"Thinking excerpt: {p.text[:150]}...")
161
+ ```
162
+
163
+ Claude: 107823
164
+ Thinking excerpt: 127 × 849 = 127 × 800 + 127 × 49 = 101600 + 6223 = 107823...
165
+ Kimi: 107823
166
+ Thinking excerpt: 127 × 849 = 107823. Let me verify: 127*800=101600; 127*49=6223; total 107823. Return just number....
167
+
168
+ ## Web Search (Server Tools)
169
+
170
+ OpenAI’s Responses API supports server-side web search. Server tool calls are normalized alongside regular tool calls:
171
+
172
+ ``` python
173
+ ws_tools = [{"type": "web_search_preview"}]
174
+ r = await acomplete([user("What is the latest Python release? Reply with the version number only.")],
175
+ model='gpt-4o-mini', tools=ws_tools, max_tokens=512)
176
+ print("Server tools used:", [tc.name for tc in r.tool_calls if tc.server])
177
+ ```
178
+
179
+ Server tools used: ['web_search']
180
+
181
+ ## Caching with Anthropic
182
+
183
+ Set a part’s `cache_control` to request prompt caching. Later requests can reuse eligible cached content. The example prints usage for two calls with the same system prompt:
184
+
185
+ ``` python
186
+ long_ctx = "You are an expert on the solar system. " * 200
187
+ system = Text(long_ctx, cache_control={'type': 'ephemeral'})
188
+
189
+ r1 = await acomplete([user("What is Jupiter's mass? Answer in one sentence.")],
190
+ model='claude-sonnet-5', system=system, max_tokens=mtok)
191
+ print("First call: cache creation tokens", r1.usage.cache_creation_tokens)
192
+
193
+ r2 = await acomplete([user("What is Saturn's mass? Answer in one sentence.")],
194
+ model='claude-sonnet-5', system=system, max_tokens=mtok)
195
+ print("Second call: cache read tokens", r2.usage.cached_tokens)
196
+ ```
197
+
198
+ First call: cache creation tokens 2204
199
+ Second call: cache read tokens 2204
200
+
201
+ ## Media inputs
202
+
203
+ Use `InputImage` with a model that supports images. The example sends the same image message to three providers:
204
+
205
+ ``` python
206
+ img_msg = Msg(role='user', content=[
207
+ InputImage(sample_img_url),
208
+ Text("List three visible objects, using nouns only.")
209
+ ])
210
+
211
+ for name, kw in [('claude-sonnet-5', {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {})]:
212
+ print(f"{name}: ", end='')
213
+ r = await stream([img_msg], model=name, max_tokens=mtok, **kw)
214
+ ```
215
+
216
+ claude-sonnet-5: Mountains, lake, trees
217
+ gpt-4o-mini: 1. Mountains
218
+ 2. Trees
219
+ 3. Lake
220
+ models/gemini-3-flash-preview: planks, lake, mountains
221
+
222
+ The media adapters support the following part types:
223
+
224
+ | Media part | Anthropic | OpenAI Responses | OpenAI Chat | Gemini |
225
+ |--------------|-----------|------------------|-------------|--------|
226
+ | `InputImage` | Yes | Yes | Yes | Yes |
227
+ | `InputAudio` | No | No | Yes | Yes |
228
+ | `InputVideo` | No | No | No | Yes |
229
+ | `InputFile` | Yes | Yes | Yes | Yes |
230
+
231
+ Media parts accept a URL or base64 data URL as their `text`. An unsupported combination raises `ValueError`.
@@ -0,0 +1,18 @@
1
+ fastllm/__init__.py,sha256=RzCM0b2ATYsYmXlX_uPf0CguuCCIUz7Jl-gj65TszzE,131
2
+ fastllm/_modidx.py,sha256=iyQLdOYLOPp87AmgdFQhL7QUxXMKh-TooHd6swPFuMc,46958
3
+ fastllm/acomplete.py,sha256=2BTlr5ZZFslscSCquXes3OnwrzCmtbPxGCwZtLGdPkY,10788
4
+ fastllm/anthropic.py,sha256=N8gfoDftlH-WFVO3CED4OGTv5qDTDGBHzp4JevgOFss,19347
5
+ fastllm/chat.py,sha256=f92AOIpJipEi3GZPnTKinJvxMU2RLdu_qEKkAUjgtcU,25099
6
+ fastllm/codex.py,sha256=r6auZ8egia83aGkWcF4132AzW4SsBvKeDwg_CxyXj9E,5808
7
+ fastllm/gemini.py,sha256=56C3NJt8WLwx_XUWU-AElyfz_V5Zp2hIGmmkFhjFWn0,17527
8
+ fastllm/model_prices.json,sha256=pa0j96LZgklYii8howX443QbukUP1iFr3lBcucS-6Yo,2133018
9
+ fastllm/openai_chat.py,sha256=yGtUQbN8UBzMwDkBUALLWdlxYOcHWX78ZT2Ic8FCXWg,12489
10
+ fastllm/openai_responses.py,sha256=n0uzAULex61rXNWuzvxSBV9-qHpqSQfl-7KBNk_GO6w,15440
11
+ fastllm/responses.py,sha256=k5D3YS8zgvUBpk_qADPtvVD9CUOWl4iej_0qvi-GuVs,23166
12
+ fastllm/streaming.py,sha256=dQ7ZFc4JEtPanoKXizww91rmYXTODTisQJb1FlSfWMU,8765
13
+ fastllm/types.py,sha256=dWFeU3UGA1Z6y5Sn4lmFZfhJe2ROw7skOtg_1W9wgZE,19618
14
+ python_fastllm-0.0.60.dist-info/METADATA,sha256=IL1bXk4TbnCuK8kw9D7DhmIJsiDZa8gLlUFkou29iAU,9136
15
+ python_fastllm-0.0.60.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
16
+ python_fastllm-0.0.60.dist-info/entry_points.txt,sha256=WkWd04F3xVYFP5bsexaQe9Se6uelwE5ULoTtHWreO0s,133
17
+ python_fastllm-0.0.60.dist-info/top_level.txt,sha256=F8qodL7nEGUHGmzzqfhNKCTIr1i0D6cvudOnm3z7o0Y,8
18
+ python_fastllm-0.0.60.dist-info/RECORD,,
@@ -1,373 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: python-fastllm
3
- Version: 0.0.58
4
- Author-email: Kerem Turgutlu <keremturgutlu@gmail.com>
5
- License: Apache-2.0
6
- Project-URL: Repository, https://github.com/AnswerDotAI/fastllm
7
- Keywords: nbdev
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: Programming Language :: Python :: 3 :: Only
10
- Requires-Python: >=3.10
11
- Description-Content-Type: text/markdown
12
- Requires-Dist: fastcore>=2.1.18
13
- Requires-Dist: aidialog>=0.0.27
14
- Requires-Dist: fasttransport>=0.0.2
15
- Requires-Dist: pillow
16
-
17
- # fastllm
18
-
19
-
20
- <!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->
21
-
22
- ## Install
23
-
24
- Run `pip install python-fastllm` or clone from github and install locally.
25
-
26
- ## Setup
27
-
28
- ``` python
29
- from aidialog.msg_parts import Msg, Part, Text, Thinking, ToolUse, InputImage, mk_tool_res_msg, Completion
30
- from fastllm.acomplete import acomplete
31
- from fastllm.types import sample_img_url, sample_doms
32
- import asyncio, json
33
-
34
- # Helpers
35
- def user(text): return Msg(role='user', content=[Text(text)])
36
-
37
- async def stream(msgs, model, max_think=10, **kw):
38
- "Stream a response, printing each item's `formatted` (🧠 per thinking delta). Returns the final Completion."
39
- cnt = 0
40
- async for o in await acomplete(msgs, model, stream=True, **kw):
41
- if not isinstance(o, Part): continue
42
- if isinstance(o, Thinking):
43
- cnt += 1
44
- if cnt > max_think: continue
45
- print(o.formatted, end='', flush=True)
46
- print()
47
- return o
48
- ```
49
-
50
- ``` python
51
- mtok = 1024
52
- ```
53
-
54
- ## Chat — One Interface, Every Provider
55
-
56
- The same `acomplete` call works with Claude, GPT, Gemini, and Kimi. Just change the model name:
57
-
58
- ``` python
59
- models = [
60
- ('claude-sonnet-5', {}),
61
- ('gpt-4o-mini', {}),
62
- ('models/gemini-3-flash-preview', {}),
63
- ('accounts/fireworks/models/kimi-k2p5', dict(vendor_name='fireworks_ai'))
64
- ]
65
- for name, kw in models:
66
- r = await acomplete([user("Say 'hello' in French.")], model=name, max_tokens=mtok, **kw)
67
- print(f"{name:>30s} → {r.message.content[0].text.strip()}")
68
- ```
69
-
70
- claude-sonnet-4-20250514 → Bonjour!
71
- gpt-4o-mini → In French, "hello" is said as "bonjour."
72
- models/gemini-3-flash-preview → Bonjour.
73
- accounts/fireworks/models/kimi-k2p5 → The user is asking me to say "hello" in French. This is a very straightforward request. The common ways to say hello in French are:
74
-
75
- 1. "Bonjour" - the standard, formal way to say hello/good day
76
- 2. "Salut" - informal way to say hi/hello (also used for goodbye)
77
- 3. "Bonsoir" - good evening
78
- 4. "Coucou" - very informal, cute way to say hi
79
-
80
- Since the user just asked for "hello" without specifying context, "Bonjour" is the most appropriate and standard answer. I should provide the most common translation and perhaps mention the informal alternative for completeness.
81
-
82
- The response should be simple and direct.
83
-
84
- ## System Prompts
85
-
86
- Pass a system prompt to any provider — `fastllm` maps it to each API’s native mechanism (Anthropic `system`, OpenAI `instructions`, Gemini `system_instruction`):
87
-
88
- ``` python
89
- sys = "You are a pirate chef. Always respond in pirate speak and mention food."
90
-
91
- print("Claude: ", end='')
92
- r = await stream([user("What should I do today?")], model='claude-sonnet-5', system=sys, max_tokens=mtok)
93
-
94
- print("Gemini: ", end='')
95
- r = await stream([user("What should I do today?")], model='models/gemini-3-flash-preview', system=sys, max_tokens=mtok)
96
- ```
97
-
98
- Claude: Ahoy there, me hearty! *tips chef's hat with a feathered plume*
99
-
100
- Ye be askin' what to do on this fine day, eh? Well, shiver me spatulas, I've got some tasty suggestions for ye!
101
-
102
- First, ye should be raidin' yer galley (that be yer kitchen, landlubber!) and whip up some grub fit for a crew! Perhaps some hearty sea biscuits with honey, or a fine fish stew that'll warm yer bones like treasure warms the heart!
103
-
104
- Then, if the weather be fair, take yerself outside and feel that salty breeze on yer face while ye munch on some portable provisions - maybe some dried fruits and nuts, perfect for any seafarin' adventure!
105
-
106
- And if ye be feelin' ambitious, batten down the hatches and try cookin' somethin' new! A spicy jambalaya or some coconut rice that'll transport ye straight to the Caribbean seas!
107
-
108
- Remember, me matey - a pirate's day ain't complete without good food in yer belly and the spirit of adventure in yer heart! Now off with ye, and may yer meals be as bountiful as buried treasure!
109
-
110
- *waves wooden spoon like a cutlass*
111
-
112
- Arrr, what say ye? Does any of this grub sound temptin' to yer taste buds?
113
- Gemini: Ahoy there, ye scurvy bilge-rat! If ye be lookin' for a way to spend yer daylight hours, I’ve got just the plan for a hungry soul like yerself.
114
-
115
- First, ye should sharpen yer cutlass—not for fightin' off the Royal Navy, mind ye, but for slicin' through a thick slab of **salted pork** and some **ripe mangoes**! A dull blade is the mark of a lazy cook, and we'll have none of that on this vessel.
116
-
117
- Once yer steel is keen, head down to the shoreline and see if ye can't scavenge some **clams** or a **fat crustacean** from the tide pools. If ye find a crab the size of a cannonball, we'll toss it in the pot with some stolen spices and a splash of grog to make a **bisque** that’ll make yer toes curl!
118
-
119
- And if the wind be light, ye can spend the afternoon scrubbin' the barnacles off the hull—it builds an appetite for a massive bowl of **hardtack and lobscouse stew**.
120
-
121
- Now, quit yer lollygaggin' and get to the galley! There be **onions** that won't peel themselves, and I'll be hornswoggled if I let a single clove of **garlic** go to waste! Arrr!
122
-
123
- ## Tool Calling — Define Once, Use Anywhere
124
-
125
- Define tools in a single canonical format. `fastllm` translates to each provider’s native tool schema automatically. Here Claude requests the tool, receives the result, and answers:
126
-
127
- ``` python
128
- tools = [{"type": "function", "function": {
129
- "name": "get_weather",
130
- "description": "Get current weather for a city",
131
- "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
132
- }}]
133
-
134
- # Turn 1: Claude requests the tool
135
- msgs = [user("What's the weather in Paris?")]
136
- r1 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
137
- print("Tool calls:", r1.tool_calls)
138
- ```
139
-
140
- I'll check the current weather in Paris for you.
141
- - ⏳ `get_weather(city="Paris")` ⏳
142
-
143
- Tool calls: [ToolUse(raw={'caller': {'type': 'direct'}}, cache_control=None, id='toolu_01RMN1WM7vPBT3ovv5Ex6VzC', name='get_weather', arguments={'city': 'Paris'}, server=False, text=None)]
144
-
145
- ``` python
146
- # Provide the tool result, then continue the same conversation
147
- msgs += [r1.message, mk_tool_res_msg(r1.tool_calls, ['22°C, sunny with light clouds']), user("Should I bring a jacket?")]
148
- print("Claude: ", end='')
149
- r2 = await stream(msgs, model='claude-sonnet-5', tools=tools, max_tokens=mtok)
150
- ```
151
-
152
- GPT: With the temperature at 22°C and sunny with light clouds, a jacket isn't necessary for most people. However, if you tend to feel cold easily or if you plan to be out in the evening when it might cool down a bit, it could be a good idea to bring a light jacket.
153
-
154
- ## Tool Choice
155
-
156
- Control whether the model must use tools, can’t use tools, or decides on its own:
157
-
158
- ``` python
159
- # Force the model to call a tool (even for a greeting)
160
- r = await stream([user("Hello there!")], model='claude-sonnet-5', tools=tools, tool_choice='required', max_tokens=mtok)
161
- print("Forced:", r.tool_calls)
162
-
163
- # Prevent tool use (model must answer directly)
164
- print("\nNo tools: ", end='')
165
- r = await stream([user("What's the weather?")], model='claude-sonnet-5', tools=tools, tool_choice='none', max_tokens=mtok)
166
- ```
167
-
168
- - ⏳ `get_weather(city="<UNKNOWN>")` ⏳
169
-
170
- Forced: [ToolUse(raw={'caller': {'type': 'direct'}}, cache_control=None, id='toolu_01U7tAXjXAtwjp6xNSMbsyPU', name='get_weather', arguments={'city': '<UNKNOWN>'}, server=False, text=None)]
171
-
172
- No tools: I'd be happy to help you get the weather information! However, I need to know which city you'd like me to check the weather for. Could you please tell me the city name?
173
-
174
- ## Thinking / Extended Reasoning
175
-
176
- Enable model reasoning with `reasoning_effort`. The canonical values (`low`, `medium`, `high`) map to each provider’s native budget system. Thinking tokens stream as 🧠:
177
-
178
- ``` python
179
- # Claude with thinking
180
- print("Claude: ", end='')
181
- r = await stream([user("What is 127 × 849?")], model='claude-sonnet-4-6', reasoning_effort='low', max_tokens=8192)
182
- for p in r.message.content:
183
- if isinstance(p, Thinking): print(f"\n🧠 {p.text[:150]}...")
184
-
185
- # Kimi with thinking — same interface
186
- print("\nKimi: ", end='')
187
- r = await stream([user("What is 127 × 849?")], model='accounts/fireworks/models/kimi-k2p5', vendor_name='fireworks_ai', reasoning_effort='low', max_tokens=8192)
188
- for p in r.message.content:
189
- if isinstance(p, Thinking): print(f"\n🧠 {p.text[:150]}...")
190
- ```
191
-
192
- Claude: 🧠🧠🧠## Calculating 127 × 849
193
-
194
- **Breaking it down:**
195
- - 127 × 800 = 101,600
196
- - 127 × 40 = 5,080
197
- - 127 × 9 = 1,143
198
-
199
- **Adding the parts:**
200
- 101,600 + 5,080 + 1,143 = **107,823**
201
-
202
- 🧠 127 × 849:
203
- 127 × 800 = 101,600
204
- 127 × 49 = 127 × 50 - 127 = 6350 - 127 = 6223
205
- Total: 107,823...
206
-
207
- Kimi: 🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠**107,823**
208
-
209
- Here's the calculation:
210
- - 127 × 800 = 101,600
211
- - 127 × 40 = 5,080
212
- - 127 × 9 = 1,143
213
- - **Total: 101,600 + 5,080 + 1,143 = 107,823**
214
-
215
- 🧠 The user is asking for the product of 127 and 849. I need to calculate 127 × 849.
216
-
217
- Let me calculate this step by step.
218
-
219
- Method 1: Standard multiplicat...
220
-
221
- ## Web Search (Server Tools)
222
-
223
- OpenAI’s Responses API supports server-side web search. Server tool calls are normalized alongside regular tool calls:
224
-
225
- ``` python
226
- ws_tools = [{"type": "web_search_preview"}]
227
- print("GPT + web search: ", end='')
228
- r = await stream([user("What is the latest Python release?")], model='gpt-4o-mini', tools=ws_tools, max_tokens=512)
229
- print(f"\nServer tools used: {[tc.name for tc in r.tool_calls if tc.server]}")
230
- ```
231
-
232
- GPT + web search:
233
-
234
- ```json {.tool}
235
- {
236
- "id": "ws_0f48f4b49d3a4937006a2c04cb944481a3882056257ae43956",
237
- "name": "web_search",
238
- "args": {
239
- "type": "search",
240
- "queries": [
241
- "latest Python release October 2023"
242
- ],
243
- "query": "latest Python release October 2023"
244
- },
245
- "result": "Server tool call executed.",
246
- "server": true
247
- }
248
- ```
249
-
250
- As of June 12, 2026, the latest stable release of Python is version 3.14.5, which was released on May 10, 2026. ([test.python.org](https://test.python.org/downloads/latest?utm_source=openai))
251
-
252
- Python 3.14 introduced several significant features, including:
253
-
254
- - **PEP 779**: Official support for free-threaded Python, allowing threads to run more concurrently.
255
- - **PEP 649**: Deferred evaluation of annotations, improving the semantics of using annotations.
256
- - **PEP 750**: Template string literals (t-strings) for custom string processing, using the familiar syntax of f-strings.
257
- - **PEP 734**: Support for multiple interpreters in the standard library.
258
- - **PEP 784**: A new module `compression.zstd` providing support for the Zstandard compression algorithm.
259
-
260
- For a comprehensive list of changes and improvements in Python 3.14, you can refer to the official release notes. ([docs.python.org](https://docs.python.org/it/3.14/whatsnew/3.14.html?utm_source=openai))
261
-
262
- You can download the latest version of Python from the official Python website. ([python.org](https://www.python.org/downloads/?lang=python&utm_source=openai))
263
-
264
- Server tools used: ['web_search']
265
-
266
- ## Caching (Anthropic)
267
-
268
- Anthropic supports prompt caching via `cache_control`. Set it on a part’s `cache_control` — repeat calls with the same cached content save tokens:
269
-
270
- ``` python
271
- # Cache a long system prompt (must be >1024 tokens for Anthropic caching)
272
- long_ctx = "You are an expert on the solar system. " * 200
273
- system = Text(long_ctx, cache_control={'type': 'ephemeral'})
274
-
275
- print("Call 1: ", end='')
276
- r1 = await stream([user("What is Jupiter's mass?")], model='claude-sonnet-5', system=system, max_tokens=mtok)
277
- print(f"Usage: {r1.usage}")
278
-
279
- print("Call 2: ", end='')
280
- r2 = await stream([user("How about Saturn?")], model='claude-sonnet-5', system=system, max_tokens=mtok)
281
- print(f"Usage: {r2.usage}")
282
- ```
283
-
284
- Call 1: Jupiter's mass is approximately 1.898 × 10²⁷ kilograms (or 1,898,000,000,000,000,000,000,000,000 kg).
285
-
286
- To put this in perspective:
287
- - Jupiter is about 318 times more massive than Earth
288
- - It contains more than twice the mass of all other planets in our solar system combined
289
- - Jupiter's mass is about 1/1047th the mass of the Sun
290
-
291
- This enormous mass gives Jupiter its strong gravitational influence, which helps it act as a "cosmic vacuum cleaner" by capturing asteroids and comets that might otherwise threaten the inner planets.
292
- Usage: Usage(prompt_tokens=1814, completion_tokens=145, total_tokens=1959, cached_tokens=0, cache_creation_tokens=1802, reasoning_tokens=0, raw={'input_tokens': 12, 'cache_creation_input_tokens': 1802, 'cache_read_input_tokens': 0, 'output_tokens': 145})
293
- Call 2: Saturn is truly one of the most spectacular planets in our solar system! Here are some key facts about this gas giant:
294
-
295
- **Basic Characteristics:**
296
- - Sixth planet from the Sun
297
- - Second largest planet (after Jupiter)
298
- - Composed primarily of hydrogen and helium
299
- - Has the lowest density of any planet - it would actually float in water!
300
-
301
- **Famous Ring System:**
302
- - Most prominent and extensive ring system in the solar system
303
- - Made primarily of ice particles and rocky debris
304
- - Rings are organized into distinct sections (A, B, C rings are the main ones)
305
- - Ring particles range from tiny ice crystals to house-sized chunks
306
-
307
- **Moons:**
308
- - Has 146 confirmed moons (the most of any planet)
309
- - Titan is its largest moon - larger than Mercury and has a thick atmosphere
310
- - Enceladus has geysers of water ice erupting from its south pole
311
- - Many other fascinating moons like Iapetus, Mimas, and Dione
312
-
313
- **Physical Features:**
314
- - Takes about 29.5 Earth years to orbit the Sun
315
- - A day on Saturn is only about 10.7 hours
316
- - Has extreme winds reaching up to 1,800 km/h
317
- - Beautiful hexagonal storm at its north pole
318
-
319
- **Exploration:**
320
- - Visited by Pioneer 11, Voyager 1 & 2, and most notably the Cassini mission (2004-2017)
321
-
322
- Is there a particular aspect of Saturn you'd like to know more about?
323
- Usage: Usage(prompt_tokens=1812, completion_tokens=337, total_tokens=2149, cached_tokens=1802, cache_creation_tokens=0, reasoning_tokens=0, raw={'input_tokens': 10, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 1802, 'output_tokens': 337})
324
-
325
- ## Media Inputs
326
-
327
- Send images to any provider that supports them. The canonical `input_image` part works everywhere:
328
-
329
- ``` python
330
- img_msg = Msg(role='user', content=[
331
- InputImage(sample_img_url),
332
- Text("What do you see in this image?")
333
- ])
334
-
335
- for name, kw in [('claude-sonnet-5', {}), ('gpt-4o-mini', {}), ('models/gemini-3-flash-preview', {})]:
336
- print(f"{name:>30s}: ", end='')
337
- r = await stream([img_msg], model=name, max_tokens=mtok, **kw)
338
- ```
339
-
340
- claude-sonnet-4-20250514: I see a beautiful, serene landscape photograph taken from what appears to be a wooden dock or platform. The image shows:
341
-
342
- - A perfectly still lake that creates mirror-like reflections
343
- - Snow-capped mountains in the background under a partly cloudy sky
344
- - Dense forests of evergreen trees (likely pine or fir) lining the shoreline
345
- - The mountains, trees, and sky all reflected clearly in the calm water
346
- - Wooden planks in the foreground, suggesting the photo was taken from a dock or wooden viewing platform
347
- - The lighting appears soft and atmospheric, possibly during golden hour
348
- - The overall scene has a very peaceful, pristine wilderness quality typical of places like the Canadian Rockies, Alaska, or similar mountainous lake regions
349
-
350
- The composition creates a sense of depth and tranquility, with the wooden platform in the foreground leading the eye toward the expansive natural landscape beyond.
351
- gpt-4o-mini: The image features a serene landscape with a body of water reflecting surrounding trees and mountains. In the foreground, there is a wooden deck or platform, enhancing the picturesque view. The scene is calm and depicts natural beauty, characterized by lush greenery, a clear sky, and distant snow-capped mountains.
352
- models/gemini-3-flash-preview: This image depicts a serene and majestic landscape, viewed from the perspective of someone standing on a wooden deck.
353
-
354
- Here is a breakdown of what is visible:
355
-
356
- * **Foreground:** In the immediate foreground are weathered brown wooden planks of a deck or pier. The planks run diagonally, drawing the eye toward the center of the image.
357
- * **Midground (The Lake):** Beyond the deck is a large, still lake. The water is a deep blue and acts as a perfect mirror, reflecting the trees and mountains above with startling clarity.
358
- * **Shoreline:** Along the far edge of the lake is a dense line of lush green evergreen trees. A thin, bright yellow or golden strip of vegetation runs along the very edge of the water.
359
- * **Background (Mountains):** Towering over the entire scene are massive, rugged mountains. The peaks are covered in white snow or glaciers and appear in shades of light blue and grey due to a soft, atmospheric haze.
360
- * **Sky:** The sky is a pale, bright blue, almost white in some areas, with a few soft clouds visible in the upper right corner.
361
-
362
- The overall mood of the image is peaceful, cool-toned, and tranquil.
363
-
364
- `fastllm` supports four media part classes. Provider support varies:
365
-
366
- | Media part | Anthropic | OpenAI Responses | OpenAI Chat | Gemini |
367
- |--------------|-----------|------------------|-------------|--------|
368
- | `InputImage` | ✅ | ✅ | ✅ | ✅ |
369
- | `InputAudio` | ❌ | ❌ (coming soon) | ✅ | ✅ |
370
- | `InputVideo` | ❌ | ❌ | ❌ | ✅ |
371
- | `InputFile` | ✅ | ✅ | ✅ | ✅ |
372
-
373
- All media parts accept either a URL or a base64 data URL as their `text`. Unsupported combinations raise a clear `ValueError`.
@@ -1,18 +0,0 @@
1
- fastllm/__init__.py,sha256=uDoIIuBjxZKcbyRrdFaaGJlfJwWkr9dF7dNA3b04A9k,131
2
- fastllm/_modidx.py,sha256=qzUtLbjOche-GNbeNun3VRIQdcm5g4dUOCLJ6T9fG-w,46374
3
- fastllm/acomplete.py,sha256=CusLue35xiaswpmTZyLkJj6t4CVsNDeeY3Sk7hzVNoY,10777
4
- fastllm/anthropic.py,sha256=ktH-EuQMQnp8O2_6VmSrvvDCIe2QTeSR7jNpWfqf3L4,18884
5
- fastllm/chat.py,sha256=klji3SOYs5xyzrSFbNhy3uoQ2TSZ3a-8HTjvdRtaG5Q,25036
6
- fastllm/codex.py,sha256=r6auZ8egia83aGkWcF4132AzW4SsBvKeDwg_CxyXj9E,5808
7
- fastllm/gemini.py,sha256=vpY_f4Jqn344l9CANm6_c7Cmhjq2URXxWhdVv_LswSg,17615
8
- fastllm/model_prices.json,sha256=pa0j96LZgklYii8howX443QbukUP1iFr3lBcucS-6Yo,2133018
9
- fastllm/openai_chat.py,sha256=n5INfDCsAZIiKUImWeaX6SJ_OAJ_uMhw20HUkgFgdgM,12379
10
- fastllm/openai_responses.py,sha256=NrvzNCKkSQabbAMnxYCSPdROaT1u8Vp2eoRzbzAA-4M,15345
11
- fastllm/responses.py,sha256=9gh-UlhfswCcP0RCzLuPT5meWNh9mF_OIivzCzZU94I,22726
12
- fastllm/streaming.py,sha256=DrqNJkMhyntFKokY6GF6oW3CjELrk5XWmnxwcApJUyM,8770
13
- fastllm/types.py,sha256=OznlKQZUx2DrLE_O-sbIBsT6q6EBeVDlH9kXxhsGBSE,19445
14
- python_fastllm-0.0.58.dist-info/METADATA,sha256=scQa4XA1Oh28m6HO6knQtPmOwV_E0ppqFdXuMH8GTA8,18537
15
- python_fastllm-0.0.58.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
16
- python_fastllm-0.0.58.dist-info/entry_points.txt,sha256=WkWd04F3xVYFP5bsexaQe9Se6uelwE5ULoTtHWreO0s,133
17
- python_fastllm-0.0.58.dist-info/top_level.txt,sha256=F8qodL7nEGUHGmzzqfhNKCTIr1i0D6cvudOnm3z7o0Y,8
18
- python_fastllm-0.0.58.dist-info/RECORD,,