python-fastllm 0.0.45__py3-none-any.whl → 0.0.47__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.45"
5
+ __version__ = "0.0.47"
fastllm/_modidx.py CHANGED
@@ -237,6 +237,10 @@ d = { 'settings': { 'branch': 'main',
237
237
  'fastllm/responses.py'),
238
238
  'fastllm.responses.AsyncResponses.prepare': ( 'responses.html#asyncresponses.prepare',
239
239
  'fastllm/responses.py'),
240
+ 'fastllm.responses.AsyncResponses.server_results': ( 'responses.html#asyncresponses.server_results',
241
+ 'fastllm/responses.py'),
242
+ 'fastllm.responses.AsyncResponses.server_turn': ( 'responses.html#asyncresponses.server_turn',
243
+ 'fastllm/responses.py'),
240
244
  'fastllm.responses.AsyncResponses.state': ( 'responses.html#asyncresponses.state',
241
245
  'fastllm/responses.py'),
242
246
  'fastllm.responses.ResponseState': ('responses.html#responsestate', 'fastllm/responses.py'),
@@ -316,10 +320,13 @@ d = { 'settings': { 'branch': 'main',
316
320
  'fastllm.types.Completion.cost': ('types.html#completion.cost', 'fastllm/types.py'),
317
321
  'fastllm.types.Completion.formatted': ('types.html#completion.formatted', 'fastllm/types.py'),
318
322
  'fastllm.types.Usage': ('types.html#usage', 'fastllm/types.py'),
323
+ 'fastllm.types.Usage.__add__': ('types.html#usage.__add__', 'fastllm/types.py'),
319
324
  'fastllm.types.Usage.__eq__': ('types.html#usage.__eq__', 'fastllm/types.py'),
320
325
  'fastllm.types.Usage.__init__': ('types.html#usage.__init__', 'fastllm/types.py'),
321
326
  'fastllm.types._rm_ctx_tiers': ('types.html#_rm_ctx_tiers', 'fastllm/types.py'),
322
327
  'fastllm.types.approx_pricing': ('types.html#approx_pricing', 'fastllm/types.py'),
328
+ 'fastllm.types.effort_code': ('types.html#effort_code', 'fastllm/types.py'),
329
+ 'fastllm.types.effort_levels': ('types.html#effort_levels', 'fastllm/types.py'),
323
330
  'fastllm.types.fn_schema': ('types.html#fn_schema', 'fastllm/types.py'),
324
331
  'fastllm.types.get_api_key': ('types.html#get_api_key', 'fastllm/types.py'),
325
332
  'fastllm.types.get_model_info': ('types.html#get_model_info', 'fastllm/types.py'),
@@ -334,6 +341,7 @@ d = { 'settings': { 'branch': 'main',
334
341
  'fastllm.types.provider_req': ('types.html#provider_req', 'fastllm/types.py'),
335
342
  'fastllm.types.register_model_info': ('types.html#register_model_info', 'fastllm/types.py'),
336
343
  'fastllm.types.resize_b64': ('types.html#resize_b64', 'fastllm/types.py'),
344
+ 'fastllm.types.resolve_effort': ('types.html#resolve_effort', 'fastllm/types.py'),
337
345
  'fastllm.types.tier_rate': ('types.html#tier_rate', 'fastllm/types.py'),
338
346
  'fastllm.types.unwrap_typed': ('types.html#unwrap_typed', 'fastllm/types.py'),
339
347
  'fastllm.types.wrap_typed': ('types.html#wrap_typed', 'fastllm/types.py')}}}
fastllm/chat.py CHANGED
@@ -225,21 +225,15 @@ class AsyncChat:
225
225
  def _usrtools(tcs): return L(tcs).filter(lambda o: not o.server) if tcs else None
226
226
 
227
227
  # %% ../nbs/07_chat.ipynb #19b87f53
228
- _effort_names = dict(none='n', low='l', medium='m', high='h', xhigh='x', max='x')
229
-
230
228
  def _think_kw(model, think, vendor_name):
231
- think = _effort_names.get(think, think)
232
- if not think: return {}
233
- if 'opus-4-7' in model:
234
- e = 'xhigh' if think=='h' else effort.get(think)
235
- eff = dict(thinking={"type":"adaptive", "display":"summarized"}, output_config={"effort":e})
236
- return dict(reasoning_effort=eff)
237
- try: xhigh = get_model_info(model, vendor_name).get('supports_xhigh_reasoning_effort')
238
- except: xhigh = False
239
- eff = effort.get(think) if think!='x' else 'xhigh' if xhigh else 'high'
229
+ if think is None or think == '': return {}
230
+ eff = resolve_effort(effort_levels(get_model_info(model, vendor_name)), think)
231
+ if eff is None: return {}
232
+ if 'opus-4-7' in model: return dict(reasoning_effort=dict(thinking={"type":"adaptive", "display":"summarized"}, output_config={"effort":eff}))
240
233
  if vendor_name == 'codex': return dict(reasoning_effort={'effort':eff, 'summary':'auto'})
241
234
  return dict(reasoning_effort=eff)
242
235
 
236
+
243
237
  # %% ../nbs/07_chat.ipynb #06e898fd
244
238
  @patch
245
239
  def _prep_call(self:AsyncChat, search, max_tokens, kwargs, stream=False, think=None):
@@ -256,7 +250,7 @@ def _prep_call(self:AsyncChat, search, max_tokens, kwargs, stream=False, think=N
256
250
  if self.api_key: kwargs['api_key'] = self.api_key
257
251
  if self.base_url: kwargs['base_url'] = self.base_url
258
252
  if self.endpoint: kwargs['endpoint'] = self.endpoint
259
- if self.extra_headers: kwargs['xtra_headers'] = self.extra_headers
253
+ if self.extra_headers: kwargs['xtra_hdrs'] = self.extra_headers
260
254
  kwargs.update(_think_kw(self.model, think, self.vendor_name))
261
255
  return max_tokens
262
256
 
@@ -186,6 +186,7 @@ def denorm_tool_choice(v):
186
186
  def denorm_reasoning(v):
187
187
  "Map canonical reasoning_effort to OpenAI Responses reasoning param."
188
188
  if v is None: return None
189
+ if isinstance(v, int): return {'level': v} # a 0-3 level, for a Responses server that resolves it per model
189
190
  return {'effort': v} if isinstance(v, str) else v
190
191
 
191
192
  # %% ../nbs/02_oai_responses.ipynb #e11c15c0
fastllm/responses.py CHANGED
@@ -15,13 +15,14 @@ __all__ = ['ResponsesError', 'response_input', 'normalize_call_ids', 'response_o
15
15
  'ResponseState', 'ResponseTurn', 'AsyncResponses', 'response_event', 'pending_response']
16
16
 
17
17
  # %% ../nbs/06a_responses.ipynb #e16fc24e
18
- import asyncio, json, secrets, time
18
+ import asyncio, copy, json, secrets, time
19
19
  from fastcore.utils import *
20
+ from fastcore.funccall import call_func_async, get_schema
20
21
  from fasttransport.errors import APIError
21
22
 
22
23
  from .acomplete import acomplete
23
24
  from .streaming import Status
24
- from aidialog.msg_parts import Msg, Text, ToolUse, ToolResult, InputImage, InputFile, Completion
25
+ from aidialog.msg_parts import Msg, Text, ToolUse, ToolResult, InputImage, InputFile, Completion, tool_text
25
26
 
26
27
  # %% ../nbs/06a_responses.ipynb #f904c16f
27
28
  class ResponsesError(Exception):
@@ -69,7 +70,9 @@ def response_input(inp):
69
70
  if isinstance(args, str):
70
71
  try: args = json.loads(args)
71
72
  except json.JSONDecodeError: pass
72
- msgs.append(Msg('assistant', [ToolUse(id=item.get('call_id'), name=item.get('name'), arguments=args)]))
73
+ call = ToolUse(id=item.get('call_id'), name=item.get('name'), arguments=args)
74
+ if msgs and msgs[-1].role == 'assistant': msgs[-1].content.append(call)
75
+ else: msgs.append(Msg('assistant', [call]))
73
76
  elif typ == 'function_call_output':
74
77
  out = item.get('output', '')
75
78
  pending_tools.append(ToolResult(id=item.get('call_id'), name=item.get('name', ''),
@@ -145,15 +148,18 @@ class ResponseState(BasicRepr):
145
148
  # %% ../nbs/06a_responses.ipynb #c9ca3744
146
149
  class ResponseTurn(BasicRepr):
147
150
  "A prepared Responses request for one provider call."
148
- def __init__(self, id, created_at, body, history, provider_messages, system='', previous=None): store_attr()
151
+ def __init__(self, id, created_at, body, history, provider_messages, system='', previous=None, server_tools=None):
152
+ store_attr()
153
+ if server_tools is None: self.server_tools = {}
149
154
 
150
155
  @property
151
156
  def provider_previous_id(self): return self.previous.provider_response_id if self.previous else None
152
157
 
158
+
153
159
  # %% ../nbs/06a_responses.ipynb #a648297f
154
160
  class AsyncResponses:
155
161
  "Run individual Responses turns over FastLLM."
156
- def prepare(self, body, previous=None):
162
+ def prepare(self, body, previous=None, server_tools=None):
157
163
  if not isinstance(body, dict): raise ResponsesError('Request body must be an object')
158
164
  model = body.get('model')
159
165
  if not model: raise ResponsesError('model is required', param='model')
@@ -167,27 +173,57 @@ class AsyncResponses:
167
173
  if isinstance(p, ToolResult) and not p.name: p.name = names.get(p.id, '')
168
174
  instructions = '\n\n'.join(filter(None, [body.get('instructions', ''), input_system]))
169
175
  provider_messages = new_msgs if previous and previous.provider_response_id else list(history)
170
- return ResponseTurn(_new_id('resp'), int(time.time()), body, history, provider_messages, instructions, previous)
176
+ return ResponseTurn(_new_id('resp'), int(time.time()), body, history, provider_messages, instructions, previous,
177
+ {f.__name__: f for f in server_tools or []})
178
+
171
179
 
172
180
  # %% ../nbs/06a_responses.ipynb #373f7d5e
173
181
  @patch
174
182
  async def call(self:AsyncResponses, turn):
175
- "Start the provider stream for a prepared turn."
183
+ "Start the provider stream for a prepared turn, having received its first chunk"
176
184
  body = turn.body
177
185
  reasoning = body.get('reasoning') or {}
178
- kwargs = dict(previous_response_id=turn.provider_previous_id, system=turn.system or None, tools=body.get('tools'))
186
+ tools = list(body.get('tools') or []) + [dict(type='function', **get_schema(f, pname='parameters')) for f in turn.server_tools.values()]
187
+ kwargs = dict(previous_response_id=turn.provider_previous_id, system=turn.system or None, tools=tools or None)
179
188
  kwargs.update(tool_choice=body.get('tool_choice'), parallel_tool_calls=body.get('parallel_tool_calls', True),
180
189
  reasoning_effort=reasoning.get('effort'), max_tokens=body.get('max_output_tokens'), temperature=body.get('temperature'),
181
190
  cache_idxs=body.get('cache_idxs'), ttl=body.get('ttl'), web_search_options=body.get('web_search_options'))
182
- return await acomplete(turn.provider_messages, model=body['model'], stream=True, **kwargs)
191
+ stream = await acomplete(turn.provider_messages, model=body['model'], stream=True, **kwargs)
192
+ first = await anext(stream)
193
+ async def _rest():
194
+ yield first
195
+ async for o in stream: yield o
196
+ return _rest()
197
+
183
198
 
184
199
  # %% ../nbs/06a_responses.ipynb #4f424285
185
200
  @patch
186
- def state(self:AsyncResponses, turn, comp):
187
- "Build the next continuation state."
201
+ def state(self:AsyncResponses, turn, comp, results=()):
202
+ "Build the next continuation state; `results` are server tool results answering `comp`'s calls."
188
203
  normalize_call_ids(comp)
189
- return ResponseState(turn.id, turn.body['model'], (*turn.history, comp.message),
190
- comp.response_id, comp.response_id_reusable)
204
+ history = (*turn.history, comp.message, *([Msg('tool', list(results))] if results else []))
205
+ return ResponseState(turn.id, turn.body['model'], history, comp.response_id, comp.response_id_reusable)
206
+
207
+
208
+ # %% ../nbs/06a_responses.ipynb #7615b274
209
+ @patch
210
+ async def server_results(self:AsyncResponses, turn, comp):
211
+ "Run the server tool calls in `comp`, returning their results"
212
+ normalize_call_ids(comp)
213
+ res = []
214
+ for tc in comp.tool_calls:
215
+ if tc.name not in turn.server_tools: continue
216
+ out = await call_func_async(tc.name, tc.arguments, ns=turn.server_tools, raise_on_err=False)
217
+ res.append(ToolResult(id=tc.id, name=tc.name, arguments=tc.arguments, text=tool_text(out)))
218
+ return res
219
+
220
+ @patch
221
+ def server_turn(self:AsyncResponses, turn, comp, results):
222
+ "The follow-up provider turn answering server tool `results`, within the same public response"
223
+ outputs = [dict(type='function_call_output', call_id=r.id, output=r.text) for r in results]
224
+ nxt = self.prepare(turn.body | dict(input=outputs), self.state(turn, comp), turn.server_tools.values())
225
+ nxt.id = turn.id
226
+ return nxt
191
227
 
192
228
  # %% ../nbs/06a_responses.ipynb #25502099
193
229
  def response_event(event_type, sequence_number, **kwargs):
@@ -278,22 +314,31 @@ def done_events(self:_ResponseStream, comp):
278
314
 
279
315
  # %% ../nbs/06a_responses.ipynb #e10c928b
280
316
  @patch
281
- async def events(self:AsyncResponses, turn, finalize=None):
282
- state = _ResponseStream(turn)
317
+ async def events(self:AsyncResponses, turn, finalize=None, stream=None):
318
+ state,first = _ResponseStream(turn),turn
283
319
  yield state.event('response.created', response=pending_response(turn))
284
320
  try:
285
- stream = await self.call(turn)
286
- async for part in stream:
287
- if isinstance(part, Status): continue
288
- if isinstance(part, Completion): comp = part
289
- else:
290
- for event in state.part_events(part): yield event
321
+ texts,usage = [],None
322
+ while True:
323
+ async for part in (stream or await self.call(turn)):
324
+ if isinstance(part, Completion): comp = part
325
+ elif isinstance(part, Status) or (isinstance(part, ToolUse) and part.name in turn.server_tools): continue
326
+ else:
327
+ for event in state.part_events(part): yield event
328
+ usage,stream = (comp.usage if usage is None else usage + comp.usage),None
329
+ results = await self.server_results(turn, comp)
330
+ if not results or len(results) < len(comp.tool_calls): break
331
+ texts += [p for p in comp.message.content if isinstance(p, Text)]
332
+ turn = self.server_turn(turn, comp, results)
333
+ next_state = self.state(turn, comp, results)
334
+ comp = copy.copy(comp)
335
+ comp.message,comp.usage = Msg('assistant', texts + comp.message.content),usage
291
336
  for event in state.done_events(comp): yield event
292
- next_state = self.state(turn, comp)
293
- response = response_object(turn.id, turn.body, comp, turn.previous.id if turn.previous else None, turn.created_at)
337
+ response = response_object(first.id, first.body, comp, first.previous.id if first.previous else None, first.created_at)
294
338
  if finalize: await finalize(next_state, comp)
295
339
  name = 'response.incomplete' if response['status'] == 'incomplete' else 'response.completed'
296
340
  yield state.event(name, response=response)
297
341
  except asyncio.CancelledError: raise
298
342
  except ResponsesError as exc: yield state.event('error', code=exc.code, message=str(exc), param=exc.param)
299
343
  except APIError as exc: yield state.event('error', code=exc.code or 'provider_error', message=str(exc), param=None)
344
+
fastllm/types.py CHANGED
@@ -3,12 +3,13 @@
3
3
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_types.ipynb.
4
4
 
5
5
  # %% auto #0
6
- __all__ = ['FinishReason', 'api_registry', 'model_prices_url', 'haik45', 'sonn45', 'sonn46', 'sonn', 'sonn5', 'opus46', 'opus48',
7
- 'opus', 'opus5', 'fable', 'fable5', 'gpt54', 'gpt54m', 'gpt55', 'codex54', 'codex54m', 'codex55',
8
- 'codex53spark', 'model_info_registry', 'modern_llm', 'deepseek_v4_common', 'mimo_v25_common',
9
- 'codex_pricing', 'sol', 'terra', 'luna', 'gpt56s', 'Usage', 'APIRegistry', 'mk_completion', 'fn_schema',
10
- 'payload_kwargs', 'provider_req', 'get_api_key', 'wrap_typed', 'unwrap_typed', 'resize_b64',
11
- 'model_prices_meta', 'infer_api_name', 'get_model_meta', 'register_model_info', 'get_model_info',
6
+ __all__ = ['FinishReason', 'api_registry', 'model_prices_url', 'sample_img_url', 'haik45', 'sonn45', 'sonn46', 'sonn', 'sonn5',
7
+ 'opus46', 'opus48', 'opus', 'opus5', 'fable', 'fable5', 'fable51', 'gpt54', 'gpt54m', 'gpt55', 'codex54',
8
+ 'codex54m', 'codex55', 'codex53spark', 'model_info_registry', 'modern_llm', 'effort_codes',
9
+ 'deepseek_v4_common', 'deepseek_v4_flash_prices', 'mimo_v25_common', 'codex_pricing', 'sol', 'terra', 'luna',
10
+ 'gpt56s', 'Usage', 'APIRegistry', 'mk_completion', 'fn_schema', 'payload_kwargs', 'provider_req',
11
+ 'get_api_key', 'wrap_typed', 'unwrap_typed', 'resize_b64', 'model_prices_meta', 'infer_api_name',
12
+ 'get_model_meta', 'register_model_info', 'get_model_info', 'effort_code', 'effort_levels', 'resolve_effort',
12
13
  'get_model_pricing', 'approx_pricing', 'is_deepseek_peak_hour', 'price_tier', 'tier_rate']
13
14
 
14
15
  # %% ../nbs/00_types.ipynb #b4d047fd
@@ -32,6 +33,12 @@ class Usage(BasicRepr):
32
33
  def __eq__(self, o): return type(o) is type(self) and self.__dict__ == o.__dict__
33
34
 
34
35
 
36
+ # %% ../nbs/00_types.ipynb #bae1c130
37
+ @patch
38
+ def __add__(self:Usage, o):
39
+ fields = 'prompt_tokens completion_tokens total_tokens cached_tokens cache_creation_tokens reasoning_tokens'.split()
40
+ return Usage(**{k: getattr(self, k)+getattr(o, k) for k in fields}, raw=o.raw)
41
+
35
42
  # %% ../nbs/00_types.ipynb #4901e693
36
43
  @patch(as_prop=True)
37
44
  def formatted(self:Completion): return ''
@@ -160,6 +167,9 @@ def model_prices_meta():
160
167
  if not p.exists(): p.write_text(httpx2.get(model_prices_url, follow_redirects=True).text)
161
168
  return loads(p.read_text())
162
169
 
170
+ # %% ../nbs/00_types.ipynb #019882a3
171
+ sample_img_url = "https://img.magnific.com/free-photo/mountain-range-body-water_53876-139760.jpg?semt=ais_hybrid&w=740&q=80"
172
+
163
173
  # %% ../nbs/00_types.ipynb #68e488d8
164
174
  def infer_api_name(model):
165
175
  "Infer api_name from model"
@@ -186,6 +196,7 @@ opus46 = "claude-opus-4-6"
186
196
  opus48 = "claude-opus-4-8"
187
197
  opus = opus5 = "claude-opus-5"
188
198
  fable = fable5 = 'claude-fable-5'
199
+ fable51 = 'claude-fable-5-1'
189
200
  gpt54 = "gpt-5.4"
190
201
  gpt54m = "gpt-5.4-mini"
191
202
  gpt55 = "gpt-5.5"
@@ -216,56 +227,55 @@ modern_llm = dict(supports_function_calling=True, supports_tool_choice=True, sup
216
227
  supports_reasoning=True, supports_response_schema=True, supports_system_messages=True)
217
228
 
218
229
  # %% ../nbs/00_types.ipynb #8261dcd0
219
- register_model_info('accounts/fireworks/models/qwen3p7-plus', vendor_name='fireworks_ai', **modern_llm, supports_vision=True,
220
- max_tokens=1000000, max_input_tokens=1000000, max_output_tokens=65536,
221
- input_cost_per_token=0.4e-6, cache_read_input_token_cost=0.08e-6, output_cost_per_token=1.6e-6)
222
-
223
- register_model_info('accounts/fireworks/models/qwen3p6-plus', vendor_name='fireworks_ai', **modern_llm, supports_vision=True,
224
- max_tokens=1000000, max_input_tokens=1000000, max_output_tokens=65536,
225
- input_cost_per_token=0.5e-6, cache_read_input_token_cost=0.1e-6, output_cost_per_token=3.0e-6)
226
-
227
- register_model_info('accounts/fireworks/models/glm-5p2', vendor_name='fireworks_ai', **modern_llm, supports_vision=False,
228
- max_tokens=1000000, max_input_tokens=1000000, max_output_tokens=65536,
229
- input_cost_per_token=1.4e-6, cache_read_input_token_cost=0.14e-6, output_cost_per_token=4.4e-6)
230
-
231
- register_model_info('accounts/fireworks/routers/glm-5p2-fast', vendor_name='fireworks_ai', **modern_llm, supports_vision=False,
232
- max_tokens=1000000, max_input_tokens=1000000, max_output_tokens=65536,
233
- input_cost_per_token=2.8e-6, cache_read_input_token_cost=0.28e-6, output_cost_per_token=8.8e-6)
234
-
235
- register_model_info('gemini-3.5-flash', vendor_name='gemini', base='gemini-3-flash-preview',
236
- input_cost_per_token=1.5e-6, output_cost_per_token=9e-6, output_cost_per_reasoning_token=9e-6, cache_read_input_token_cost=1.5e-7)
237
-
238
- register_model_info('gpt-5.4', vendor_name='openai', base='gpt-5.4', supports_web_search=True, mode=None)
239
- # Upstream metadata says 1,050,000 input tokens for gpt-5.4-mini, but OpenAI documents a 272k limit
240
- register_model_info('gpt-5.4-mini', vendor_name='openai', base='gpt-5.4-mini', supports_web_search=True, mode=None, max_input_tokens=272_000)
241
-
242
- for model in ('kimi-k2.5', 'kimi-k2.6' , 'kimi-k3'):
243
- register_model_info(model, vendor_name='moonshot', base=f'moonshot/{model}', base_vendor_name=None,
244
- supports_reasoning=True, supports_vision=True)
245
-
246
- register_model_info('gemini-3.1-flash-lite', vendor_name='gemini', base='gemini-3.1-flash-lite-preview')
247
- register_model_info('models/gemini-3.1-flash-lite', vendor_name='gemini', base='gemini-3.1-flash-lite-preview')
248
-
249
- register_model_info('accounts/fireworks/models/kimi-k2p5', vendor_name='fireworks_ai', base='accounts/fireworks/models/kimi-k2p5',
250
- supports_reasoning=True, supports_vision=True,
251
- input_cost_per_token=0.60e-6, cache_read_input_token_cost=0.10e-6, output_cost_per_token=3.0e-6)
252
- register_model_info('accounts/fireworks/models/kimi-k2p6', vendor_name='fireworks_ai', base='accounts/fireworks/models/kimi-k2p5',
253
- supports_reasoning=True, supports_vision=True,
254
- input_cost_per_token=0.95e-6, cache_read_input_token_cost=0.16e-6, output_cost_per_token=4.0e-6)
255
- register_model_info('kimi-k2.7-code', vendor_name='moonshot', base='kimi-k2.6',
256
- input_cost_per_token=0.95e-6, cache_read_input_token_cost=0.19e-6, output_cost_per_token=4.0e-6)
257
- register_model_info('kimi-k3', vendor_name='moonshot', base='kimi-k2.6', supports_reasoning=True, supports_vision=True,
258
- max_input_tokens=1_000_000, input_cost_per_token=3e-6, cache_read_input_token_cost=0.30e-6, output_cost_per_token=15.0e-6)
230
+ register_model_info('gpt-5.4', vendor_name='openai', base='gpt-5.4', supports_web_search=True)
231
+
232
+ register_model_info('accounts/fireworks/models/deepseek-v4-flash-vision-exp', vendor_name='fireworks_ai',
233
+ base='accounts/fireworks/models/deepseek-v4-flash-0731', supports_vision=True)
234
+ register_model_info('glm-5.3-flash', vendor_name='zai', base='glm-5.3-flash', reasoning_effort_levels=['low', 'high', 'max'])
235
+ register_model_info('accounts/fireworks/models/glm-5p3-flash', vendor_name='fireworks_ai', base='glm-5.3-flash', base_vendor_name='zai')
236
+
237
+ # %% ../nbs/00_types.ipynb #b6279e07
238
+ effort_codes = dict(n='none', l='low', m='medium', h='high', x='xhigh')
239
+
240
+ def effort_code(v):
241
+ "The effort code (`n l m h x`) for an effort name, code, or level 0-3"
242
+ if isinstance(v, int): return 'lmhx'[v]
243
+ return {**{v:k for k,v in effort_codes.items()}, 'max':'x'}.get(v, v)
244
+
245
+ def effort_levels(info):
246
+ "Ordered reasoning effort levels a model accepts, lowest first, from its metadata `info`; empty when it does not reason"
247
+ if not info.get('supports_reasoning'): return []
248
+ if lv := info.get('reasoning_effort_levels'): return list(lv)
249
+ lv = ['low', 'medium', 'high']
250
+ if info.get('supports_none_reasoning_effort'): lv.insert(0, 'none')
251
+ if info.get('supports_xhigh_reasoning_effort'): lv.append('xhigh')
252
+ if info.get('supports_max_reasoning_effort'): lv.append('max')
253
+ return lv
254
+
255
+ def resolve_effort(levels, effort):
256
+ "The level among ordered `levels` that `effort` (a code `n l m h x`, a name, or a level 0-3) selects; None when `n` has no `none` level"
257
+ code = effort_code(effort)
258
+ lv = [o for o in levels if o != 'none']
259
+ if code not in effort_codes: return effort
260
+ if not lv: return effort if isinstance(effort, int) else effort_codes[code]
261
+ if code == 'n': return 'none' if 'none' in levels else None
262
+ if code == 'x': return lv[-1]
263
+ return lv[min('lmh'.index(code), len(lv)-1)]
259
264
 
260
- # %% ../nbs/00_types.ipynb #948d55d0
261
- deepseek_v4_common = dict(**modern_llm, max_input_tokens=1048576, max_output_tokens=393216, max_tokens=393216)
262
265
 
263
- register_model_info('deepseek-v4-flash', vendor_name='deepseek', base='deepseek/deepseek-v3.2', **deepseek_v4_common,
264
- input_cost_per_token=1.4e-07, input_cost_per_token_cache_hit=2.8e-09,
265
- output_cost_per_token=2.8e-07, cache_read_input_token_cost=1.4e-07/10)
266
- register_model_info('deepseek-v4-pro', vendor_name='deepseek', base='deepseek/deepseek-v3.2', **deepseek_v4_common,
267
- input_cost_per_token=4.35e-07, input_cost_per_token_cache_hit=3.625e-09,
268
- output_cost_per_token=8.7e-07, cache_read_input_token_cost=4.35e-07/10)
266
+ # %% ../nbs/00_types.ipynb #948d55d0
267
+ # The price map carries DeepSeek's peak rates, and `Completion.cost` doubles at peak, so register the off-peak rates
268
+ deepseek_v4_common = dict(supports_native_structured_output=True)
269
+ deepseek_v4_flash_prices = dict(input_cost_per_token=0.22e-6, input_cost_per_token_cache_hit=0.007e-6,
270
+ cache_read_input_token_cost=0.007e-6, output_cost_per_token=0.66e-6)
271
+
272
+ register_model_info('deepseek-v4-flash', vendor_name='deepseek', base='deepseek-v4-flash',
273
+ **deepseek_v4_common, **deepseek_v4_flash_prices)
274
+ register_model_info('deepseek-v4-flash-vision-exp', vendor_name='deepseek', base='deepseek-v4-flash-vision-exp',
275
+ **deepseek_v4_common, **deepseek_v4_flash_prices, supports_image_input=True)
276
+ register_model_info('deepseek-v4-pro', vendor_name='deepseek', base='deepseek-v4-pro', **deepseek_v4_common,
277
+ input_cost_per_token=0.66e-6, input_cost_per_token_cache_hit=0.022e-6,
278
+ cache_read_input_token_cost=0.022e-6, output_cost_per_token=1.98e-6)
269
279
 
270
280
  mimo_v25_common = dict(**modern_llm, supports_web_search=True, max_input_tokens=1048576, max_output_tokens=131072, max_tokens=131072)
271
281
 
@@ -286,12 +296,6 @@ register_model_info('muse-spark-1.1', vendor_name='meta_ai', **modern_llm, max_i
286
296
  supports_vision=True, supports_image_input=True, supports_video_input=True, supports_pdf_input=True, supports_web_search=True,
287
297
  search_context_cost_per_query=0.0025, input_cost_per_token=1.25e-6, output_cost_per_token=4.25e-6, cache_read_input_token_cost=0.15e-6)
288
298
 
289
- # %% ../nbs/00_types.ipynb #5a2b3430
290
- # z.ai list pricing (a 50% discount applies until 2026-09-09)
291
- register_model_info('glm-5.3-flash', vendor_name='zai', base='glm-5.1', **modern_llm, supports_vision=True, supports_image_input=True,
292
- max_input_tokens=1000000, max_output_tokens=128000, max_tokens=128000, input_cost_per_token=1.5e-07, cache_read_input_token_cost=3e-08,
293
- output_cost_per_token=5e-07)
294
-
295
299
  # %% ../nbs/00_types.ipynb #2c23d11e
296
300
  codex_pricing = dict(input_cost_per_token = 0.10/1_000_000, output_cost_per_token = 0.50/1_000_000,
297
301
  cache_creation_input_token_cost = 0.10/1_000_000, cache_read_input_token_cost = 0.10/1_000_000)
@@ -310,12 +314,13 @@ register_model_info(codex53spark, 'codex', **codex_pricing,
310
314
  max_tokens=128000, max_input_tokens=128000, max_output_tokens=128000)
311
315
 
312
316
  # %% ../nbs/00_types.ipynb #5d3e4720
313
- for model in (haik45, sonn45, sonn46, sonn5, opus46, opus48, opus5, fable5):
317
+ # Fable 5.1 isn't in the price map yet; it prices and behaves like Fable 5
318
+ register_model_info(fable51, 'anthropic', base=fable5)
319
+ for model in (haik45, sonn45, sonn46, sonn5, opus46, opus48, opus5, fable5, fable51):
314
320
  register_model_info(model, 'claude_code', base=model, base_vendor_name='anthropic', **codex_pricing)
315
321
 
316
322
  # %% ../nbs/00_types.ipynb #bb0c4c2a
317
323
  sol,terra,luna = gpt56s = 'gpt-5.6-sol gpt-5.6-terra gpt-5.6-luna'.split()
318
- for model in ['gpt-5.6']+gpt56s: register_model_info(model, 'openai', base=model, base_vendor_name='openai')
319
324
  # Codex serves only the suffixed names; bare `gpt-5.6` is rejected with a ChatGPT account.
320
325
  # Its window is smaller than the API's: 371,331 input tokens is accepted and 371,981 is not, on all three.
321
326
  for model in gpt56s:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fastllm
3
- Version: 0.0.45
3
+ Version: 0.0.47
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
@@ -0,0 +1,17 @@
1
+ fastllm/__init__.py,sha256=AsmunnRJMjLeGqFnegMHf36qWFuc3ZMLADqDdrpYD2I,131
2
+ fastllm/_modidx.py,sha256=OQDybSzrNmP5deiYtA14HV3rJsoRTR_0NPU5019rVAI,41130
3
+ fastllm/acomplete.py,sha256=N4JnvuJwVCPc6iqbPuclJmcKFATmRytkUy9D3HwBPpY,10363
4
+ fastllm/anthropic.py,sha256=udojlLuritp90JQlH-dGHcWRpzgnO1mZYFYtbt2F5IA,17015
5
+ fastllm/chat.py,sha256=W8PPpZ95Az7wzLgQ15u3HMroWBVXDoaUz871f0x7cEk,24842
6
+ fastllm/codex.py,sha256=HZchfrGUgdf8ayhtOFbIRmh9YmIqfQBwqviAEeir4Uo,161
7
+ fastllm/gemini.py,sha256=DveWJNd78Pf2omC_t46iOzT84a-Lo3kWxchCKR3WsCA,15135
8
+ fastllm/openai_chat.py,sha256=f11U-iaxGbBq0-Rjz17GhA1Vame5n9kMqsk1gkfDtIQ,11633
9
+ fastllm/openai_responses.py,sha256=MUIBZY447tJ4Lc6saBPrVolXe8cdQdMK58YOtaJUHYk,14482
10
+ fastllm/responses.py,sha256=zBzND8Sl_aE_Um1pvNKwzTtqcc6z0uxpvNjdavO_Pbs,18593
11
+ fastllm/streaming.py,sha256=8qVtWeZ7NlP-9rup_4Ho0ol2dx3-XuGcIMXpZ1GYdEo,9079
12
+ fastllm/types.py,sha256=3qXyiS431AlSSn8GyGCNDflZxMWPeIWirp8TIyhIu_A,19146
13
+ python_fastllm-0.0.47.dist-info/METADATA,sha256=KuhdYwKV_gWivBjwBpjNR794DOJlfRPFSkNGOW51omE,21143
14
+ python_fastllm-0.0.47.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
15
+ python_fastllm-0.0.47.dist-info/entry_points.txt,sha256=_cVRbpVkvyxdZfR7UCXa0lIXZtTSsZQWoBPhDwMqrHs,80
16
+ python_fastllm-0.0.47.dist-info/top_level.txt,sha256=F8qodL7nEGUHGmzzqfhNKCTIr1i0D6cvudOnm3z7o0Y,8
17
+ python_fastllm-0.0.47.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- fastllm/__init__.py,sha256=1FIai9vCI4Wk-TozHDezQYiz9cdNhuIl94Olnuvc0eo,131
2
- fastllm/_modidx.py,sha256=zvw4lGfv-nxMOrYfsRvX3rXyOnFBOTAOW4fDpIdtzdo,40195
3
- fastllm/acomplete.py,sha256=N4JnvuJwVCPc6iqbPuclJmcKFATmRytkUy9D3HwBPpY,10363
4
- fastllm/anthropic.py,sha256=udojlLuritp90JQlH-dGHcWRpzgnO1mZYFYtbt2F5IA,17015
5
- fastllm/chat.py,sha256=JBeSSN4_VWIbBMWvXMribaMeawGHiS34vpk2FA0KDvE,25113
6
- fastllm/codex.py,sha256=HZchfrGUgdf8ayhtOFbIRmh9YmIqfQBwqviAEeir4Uo,161
7
- fastllm/gemini.py,sha256=DveWJNd78Pf2omC_t46iOzT84a-Lo3kWxchCKR3WsCA,15135
8
- fastllm/openai_chat.py,sha256=f11U-iaxGbBq0-Rjz17GhA1Vame5n9kMqsk1gkfDtIQ,11633
9
- fastllm/openai_responses.py,sha256=yqwB4kdpWTMXfJfhT58CrJmpbkVWdgiHiZDLtrbtxqo,14369
10
- fastllm/responses.py,sha256=71SOGFa77YoWJ_9K4ZvmEh_2zIGVzq030krL3Ql_W9k,16237
11
- fastllm/streaming.py,sha256=8qVtWeZ7NlP-9rup_4Ho0ol2dx3-XuGcIMXpZ1GYdEo,9079
12
- fastllm/types.py,sha256=vLga_hWzsWKUKVHvUp577hHnShff_y2WQsAp0jnZsuE,19892
13
- python_fastllm-0.0.45.dist-info/METADATA,sha256=qHfFt2AW0BWX__LrbhD6OQRgG6PlmMcZrYIYojR2VP8,21143
14
- python_fastllm-0.0.45.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
15
- python_fastllm-0.0.45.dist-info/entry_points.txt,sha256=_cVRbpVkvyxdZfR7UCXa0lIXZtTSsZQWoBPhDwMqrHs,80
16
- python_fastllm-0.0.45.dist-info/top_level.txt,sha256=F8qodL7nEGUHGmzzqfhNKCTIr1i0D6cvudOnm3z7o0Y,8
17
- python_fastllm-0.0.45.dist-info/RECORD,,