python-fastllm 0.0.33__tar.gz → 0.0.35__tar.gz

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.
Files changed (28) hide show
  1. {python_fastllm-0.0.33 → python_fastllm-0.0.35}/PKG-INFO +2 -2
  2. python_fastllm-0.0.35/fastllm/__init__.py +1 -0
  3. {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/_modidx.py +8 -7
  4. {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/acomplete.py +22 -7
  5. {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/chat.py +6 -39
  6. {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/gemini.py +6 -9
  7. {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/openai_responses.py +4 -3
  8. {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/streaming.py +1 -1
  9. {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/types.py +42 -10
  10. {python_fastllm-0.0.33 → python_fastllm-0.0.35}/pyproject.toml +1 -1
  11. {python_fastllm-0.0.33 → python_fastllm-0.0.35}/python_fastllm.egg-info/PKG-INFO +2 -2
  12. {python_fastllm-0.0.33 → python_fastllm-0.0.35}/python_fastllm.egg-info/requires.txt +1 -1
  13. python_fastllm-0.0.33/fastllm/__init__.py +0 -1
  14. {python_fastllm-0.0.33 → python_fastllm-0.0.35}/README.md +0 -0
  15. {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/anthropic.py +0 -0
  16. {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/codex.py +0 -0
  17. {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/openai_chat.py +0 -0
  18. {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/specs/anthropic.json +0 -0
  19. {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/specs/anthropic.yml +0 -0
  20. {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/specs/gemini.json +0 -0
  21. {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/specs/openai.with-code-samples.json +0 -0
  22. {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/specs/openai.with-code-samples.yml +0 -0
  23. {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/specs/spec_manifest.json +0 -0
  24. {python_fastllm-0.0.33 → python_fastllm-0.0.35}/python_fastllm.egg-info/SOURCES.txt +0 -0
  25. {python_fastllm-0.0.33 → python_fastllm-0.0.35}/python_fastllm.egg-info/dependency_links.txt +0 -0
  26. {python_fastllm-0.0.33 → python_fastllm-0.0.35}/python_fastllm.egg-info/entry_points.txt +0 -0
  27. {python_fastllm-0.0.33 → python_fastllm-0.0.35}/python_fastllm.egg-info/top_level.txt +0 -0
  28. {python_fastllm-0.0.33 → python_fastllm-0.0.35}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fastllm
3
- Version: 0.0.33
3
+ Version: 0.0.35
4
4
  Author-email: Kerem Turgutlu <keremturgutlu@gmail.com>
5
5
  License: Apache-2.0
6
6
  Project-URL: Repository, https://github.com/AnswerDotAI/fastllm
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
10
10
  Requires-Python: >=3.10
11
11
  Description-Content-Type: text/markdown
12
12
  Requires-Dist: fastcore>=2.1.11
13
- Requires-Dist: aidialog>=0.0.3
13
+ Requires-Dist: aidialog>=0.0.5
14
14
  Requires-Dist: fastspec>=0.0.11
15
15
  Requires-Dist: toolslm
16
16
  Requires-Dist: pillow
@@ -0,0 +1 @@
1
+ __version__ = "0.0.35"
@@ -10,6 +10,7 @@ d = { 'settings': { 'branch': 'main',
10
10
  'fastllm.acomplete._classify_error': ('acomplete.html#_classify_error', 'fastllm/acomplete.py'),
11
11
  'fastllm.acomplete._classify_error_stream': ( 'acomplete.html#_classify_error_stream',
12
12
  'fastllm/acomplete.py'),
13
+ 'fastllm.acomplete._codex_mtime_policy': ('acomplete.html#_codex_mtime_policy', 'fastllm/acomplete.py'),
13
14
  'fastllm.acomplete._debug_print': ('acomplete.html#_debug_print', 'fastllm/acomplete.py'),
14
15
  'fastllm.acomplete._is_ctx_exceeded': ('acomplete.html#_is_ctx_exceeded', 'fastllm/acomplete.py'),
15
16
  'fastllm.acomplete._raise_if_done': ('acomplete.html#_raise_if_done', 'fastllm/acomplete.py'),
@@ -94,7 +95,6 @@ d = { 'settings': { 'branch': 'main',
94
95
  'fastllm.chat.ToolReminderCallback.__init__': ('chat.html#toolremindercallback.__init__', 'fastllm/chat.py'),
95
96
  'fastllm.chat.ToolReminderCallback.after_msgs': ( 'chat.html#toolremindercallback.after_msgs',
96
97
  'fastllm/chat.py'),
97
- 'fastllm.chat.ToolResponse': ('chat.html#toolresponse', 'fastllm/chat.py'),
98
98
  'fastllm.chat.UsageStats': ('chat.html#usagestats', 'fastllm/chat.py'),
99
99
  'fastllm.chat.UsageStats.__add__': ('chat.html#usagestats.__add__', 'fastllm/chat.py'),
100
100
  'fastllm.chat.UsageStats.__init__': ('chat.html#usagestats.__init__', 'fastllm/chat.py'),
@@ -106,7 +106,6 @@ d = { 'settings': { 'branch': 'main',
106
106
  'fastllm.chat._add_cache_control': ('chat.html#_add_cache_control', 'fastllm/chat.py'),
107
107
  'fastllm.chat._alite_call_func': ('chat.html#_alite_call_func', 'fastllm/chat.py'),
108
108
  'fastllm.chat._apply_cache_idxs': ('chat.html#_apply_cache_idxs', 'fastllm/chat.py'),
109
- 'fastllm.chat._bytes2content': ('chat.html#_bytes2content', 'fastllm/chat.py'),
110
109
  'fastllm.chat._call_func': ('chat.html#_call_func', 'fastllm/chat.py'),
111
110
  'fastllm.chat._handle_stop_reason': ('chat.html#_handle_stop_reason', 'fastllm/chat.py'),
112
111
  'fastllm.chat._has_cache': ('chat.html#_has_cache', 'fastllm/chat.py'),
@@ -114,14 +113,11 @@ d = { 'settings': { 'branch': 'main',
114
113
  'fastllm.chat._has_stop': ('chat.html#_has_stop', 'fastllm/chat.py'),
115
114
  'fastllm.chat._inject_tool_reminder': ('chat.html#_inject_tool_reminder', 'fastllm/chat.py'),
116
115
  'fastllm.chat._lite_call_func': ('chat.html#_lite_call_func', 'fastllm/chat.py'),
117
- 'fastllm.chat._mime2part_type': ('chat.html#_mime2part_type', 'fastllm/chat.py'),
118
- 'fastllm.chat._mk_content': ('chat.html#_mk_content', 'fastllm/chat.py'),
119
116
  'fastllm.chat._mk_prefill': ('chat.html#_mk_prefill', 'fastllm/chat.py'),
120
117
  'fastllm.chat._mk_tool_result': ('chat.html#_mk_tool_result', 'fastllm/chat.py'),
121
118
  'fastllm.chat._tc_summary': ('chat.html#_tc_summary', 'fastllm/chat.py'),
122
119
  'fastllm.chat._think_kw': ('chat.html#_think_kw', 'fastllm/chat.py'),
123
120
  'fastllm.chat._trunc_param': ('chat.html#_trunc_param', 'fastllm/chat.py'),
124
- 'fastllm.chat._url2content': ('chat.html#_url2content', 'fastllm/chat.py'),
125
121
  'fastllm.chat._usrtools': ('chat.html#_usrtools', 'fastllm/chat.py'),
126
122
  'fastllm.chat.add_warning': ('chat.html#add_warning', 'fastllm/chat.py'),
127
123
  'fastllm.chat.adisplay_stream': ('chat.html#adisplay_stream', 'fastllm/chat.py'),
@@ -253,12 +249,15 @@ d = { 'settings': { 'branch': 'main',
253
249
  'fastllm.streaming.norm_and_yield': ('streaming.html#norm_and_yield', 'fastllm/streaming.py'),
254
250
  'fastllm.streaming.stop_and_trim': ('streaming.html#stop_and_trim', 'fastllm/streaming.py')},
255
251
  'fastllm.types': { 'fastllm.types.APIRegistry': ('types.html#apiregistry', 'fastllm/types.py'),
256
- 'fastllm.types.APIRegistry.__init__': ('types.html#apiregistry.__init__', 'fastllm/types.py'),
252
+ 'fastllm.types.APIRegistry.__contains__': ('types.html#apiregistry.__contains__', 'fastllm/types.py'),
253
+ 'fastllm.types.APIRegistry.__missing__': ('types.html#apiregistry.__missing__', 'fastllm/types.py'),
254
+ 'fastllm.types.APIRegistry._load': ('types.html#apiregistry._load', 'fastllm/types.py'),
257
255
  'fastllm.types.APIRegistry.register': ('types.html#apiregistry.register', 'fastllm/types.py'),
258
256
  'fastllm.types.Completion': ('types.html#completion', 'fastllm/types.py'),
259
257
  'fastllm.types.Completion._repr_markdown_': ('types.html#completion._repr_markdown_', 'fastllm/types.py'),
260
258
  'fastllm.types.Completion.cost': ('types.html#completion.cost', 'fastllm/types.py'),
261
259
  'fastllm.types.Usage': ('types.html#usage', 'fastllm/types.py'),
260
+ 'fastllm.types._rm_ctx_tiers': ('types.html#_rm_ctx_tiers', 'fastllm/types.py'),
262
261
  'fastllm.types.approx_pricing': ('types.html#approx_pricing', 'fastllm/types.py'),
263
262
  'fastllm.types.fn_schema': ('types.html#fn_schema', 'fastllm/types.py'),
264
263
  'fastllm.types.get_api_key': ('types.html#get_api_key', 'fastllm/types.py'),
@@ -270,5 +269,7 @@ d = { 'settings': { 'branch': 'main',
270
269
  'fastllm.types.mk_completion': ('types.html#mk_completion', 'fastllm/types.py'),
271
270
  'fastllm.types.model_prices_meta': ('types.html#model_prices_meta', 'fastllm/types.py'),
272
271
  'fastllm.types.payload_kwargs': ('types.html#payload_kwargs', 'fastllm/types.py'),
272
+ 'fastllm.types.price_tier': ('types.html#price_tier', 'fastllm/types.py'),
273
273
  'fastllm.types.register_model_info': ('types.html#register_model_info', 'fastllm/types.py'),
274
- 'fastllm.types.resize_b64': ('types.html#resize_b64', 'fastllm/types.py')}}}
274
+ 'fastllm.types.resize_b64': ('types.html#resize_b64', 'fastllm/types.py'),
275
+ 'fastllm.types.tier_rate': ('types.html#tier_rate', 'fastllm/types.py')}}}
@@ -54,14 +54,29 @@ api2spec = {'openai':oai_spec, 'openai_chat':oai_spec, 'anthropic':ant_spec, 'ge
54
54
 
55
55
  # %% ../nbs/06_acomplete.ipynb #e3ed40fb
56
56
  def split_vendor(model):
57
- "Split a `'vendor/model'` path into `(vendor_name, model)`: the prefix must name a known vendor or a registered api, else `(None, model)`."
57
+ "Split a `'vendor/model'` path into `(vendor_name, model)`: the prefix must name a known vendor or a registered api (installed `fastllm.apis` plugins load on demand), else `(None, model)`."
58
58
  if model and '/' in model:
59
59
  v, m = model.split('/', 1)
60
- if v in vendor_mapping or v in api_registry.apis: return v, m
60
+ if v in vendor_mapping or v in api_registry: return v, m
61
61
  return None, model
62
62
 
63
+ # %% ../nbs/06_acomplete.ipynb #977a115b
64
+ def _codex_mtime_policy():
65
+ "`flexicache` policy: expire codex clients when the codex auth file changes"
66
+ def policy(state, args, kwargs):
67
+ v = kwargs.get('vendor_name') or (args[1] if len(args)>1 else None)
68
+ if not v:
69
+ m = kwargs.get('model') or (args[0] if args else None)
70
+ v = split_vendor(m)[0] if m else None
71
+ if v != 'codex': return None
72
+ try: mt = Path(_codex_path).expanduser().stat().st_mtime
73
+ except OSError: mt = None
74
+ return mt if state != mt else None
75
+ policy.needs_args = True
76
+ return policy
77
+
63
78
  # %% ../nbs/06_acomplete.ipynb #79075d95
64
- @flexicache()
79
+ @flexicache(_codex_mtime_policy())
65
80
  def mk_client(model=None, vendor_name=None, api_name=None, api_key=None, base_url=None, xtra_hdrs=None,
66
81
  timeout=httpx.Timeout(connect=30, read=300, write=30, pool=10)):
67
82
  err_msg = f"please pass a valid one vendor: {', '.join(list(vendor_mapping))} or pass `api_name`,`base_url` and `api_key`"
@@ -82,7 +97,7 @@ def mk_client(model=None, vendor_name=None, api_name=None, api_key=None, base_ur
82
97
  elif base_url and api_key: vendor_name, api_name = ifnone(vendor_name, 'custom'), ifnone(api_name, 'openai_chat')
83
98
  elif (api_name:=infer_api_name(model)): base_url, vendor_name = None, api_name
84
99
  else: raise ValueError(f"Model {model} can't be auto resolved, {err_msg}")
85
- api = api_registry.apis[api_name]
100
+ api = api_registry[api_name]
86
101
  spec, hdrs = api2spec[api_name], api.get_hdrs(api_key)
87
102
  cli = OpenAPIClient(spec, headers=merge(hdrs, ifnone(xtra_hdrs, {})), timeout=timeout)
88
103
  if base_url is not None:
@@ -163,13 +178,13 @@ async def acomplete(msgs, model, api_name=None, vendor_name=None, api_key=None,
163
178
  elif v: api_name, model = v, m # a registered transport api (e.g. claude_code): not an HTTP vendor
164
179
  if api_name == 'claude_code':
165
180
  if not stream: raise NotImplementedError("claude_code backend supports stream=True only for now")
166
- api = api_registry.apis[api_name]
181
+ api = api_registry[api_name]
167
182
  payload = api.mk_payload(msgs, model, stream=stream, **kwargs)
168
183
  async def _mk_gen():
169
184
  async for o in api.acollect_stream(payload, model=model, vendor_name='claude_code', stop_callables=stop_callables): yield o
170
185
  return _retry_stream(_mk_gen, retries, retry_delay)
171
- cli, api_name, vendor_name = mk_client(model, vendor_name, api_name, api_key, base_url, xtra_hdrs)
172
- api = api_registry.apis[api_name]
186
+ cli, api_name, vendor_name = mk_client(model=model, vendor_name=vendor_name, api_name=api_name, api_key=api_key, base_url=base_url, xtra_hdrs=xtra_hdrs)
187
+ api = api_registry[api_name]
173
188
  payload = api.mk_payload(msgs, model, stream=stream, **kwargs)
174
189
  payload = merge(payload, ifnone(xtra_body, {}))
175
190
  if vendor_name == 'codex':
@@ -4,10 +4,10 @@
4
4
 
5
5
  # %% auto #0
6
6
  __all__ = ['effort', 'remove_cache_ckpts', 'contents', 'stop_reason', 'mk_msg', 'FenceToolStop', 'extract_fence_call', 'mk_msgs',
7
- 'cite_footnote', 'postproc', 'lite_mk_func', 'ToolResponse', 'structured', 'search_count', 'UsageStats',
8
- 'AsyncChat', 'astream_with_complete', 'ChatCallback', 'DeepseekMsgsCallback', 'DeepseekPrefillCallback',
9
- 'add_warning', 'StopReasonCallback', 'run_fence_tool', 'FenceToolCallback', 'ToolReminderCallback',
10
- 'stop_sequences', 'StopSequencesCallback', 'StreamFormatter', 'AsyncStreamFormatter', 'adisplay_stream']
7
+ 'cite_footnote', 'postproc', 'lite_mk_func', 'structured', 'search_count', 'UsageStats', 'AsyncChat',
8
+ 'astream_with_complete', 'ChatCallback', 'DeepseekMsgsCallback', 'DeepseekPrefillCallback', 'add_warning',
9
+ 'StopReasonCallback', 'run_fence_tool', 'FenceToolCallback', 'ToolReminderCallback', 'stop_sequences',
10
+ 'StopSequencesCallback', 'StreamFormatter', 'AsyncStreamFormatter', 'adisplay_stream']
11
11
 
12
12
  # %% ../nbs/07_chat.ipynb #d5a3bc1f
13
13
  from typing import Optional,Callable
@@ -15,35 +15,13 @@ from toolslm.funccall import mk_ns, call_func, call_func_async, get_schema
15
15
  from fastcore.utils import *
16
16
  from fastcore.meta import delegates
17
17
  from dataclasses import dataclass
18
- import base64
19
18
 
20
19
  from .types import *
21
- from fastcore.xtras import detect_mime
22
20
  from aidialog.msg_parts import (Msg, Part, PartType, ToolCall, mk_tool_res_msg, mk_tr_details,
23
- tool_info, usage_info, think_start, think_end, StopResponse, display_list, MediaUrl, url_mime, fmt2hist,
24
- _mk_result_fence, _split_fence_msgs, _fence_back, _trunc_str)
21
+ tool_info, usage_info, think_start, think_end, StopResponse, ToolResponse, display_list, fmt2hist,
22
+ _mk_content, _mk_result_fence, _split_fence_msgs, _fence_back, _trunc_str)
25
23
  from .acomplete import *
26
24
 
27
- # %% ../nbs/07_chat.ipynb #4a37bae5
28
- def _mime2part_type(mime):
29
- "Map MIME string to canonical PartType"
30
- if mime.startswith('image/'): return PartType.input_image
31
- if mime.startswith('audio/'): return PartType.input_audio
32
- if mime.startswith('video/'): return PartType.input_video
33
- return PartType.input_file
34
-
35
- def _bytes2content(data):
36
- "Convert bytes to fastllm canonical content"
37
- mtype = detect_mime(data)
38
- if not mtype: raise ValueError(f'Data must be a supported file type, got {data[:10]}')
39
- encoded = base64.b64encode(data).decode("utf-8")
40
- return Part(type=_mime2part_type(mtype), text=f'data:{mtype};base64,{encoded}')
41
-
42
- def _url2content(o):
43
- "Convert MediaUrl to fastllm canonical content"
44
- mime = o.mime or url_mime(o.url)
45
- return Part(type=_mime2part_type(mime), text=o.url, data=dict(mime=mime))
46
-
47
25
  # %% ../nbs/07_chat.ipynb #43a55cda
48
26
  def _add_cache_control(
49
27
  msg, # LiteLLM formatted msg
@@ -67,12 +45,6 @@ def remove_cache_ckpts(msg):
67
45
  if part.data: part.data.pop('cache_control', None)
68
46
  return msg
69
47
 
70
- def _mk_content(o):
71
- if isinstance(o, str): return Part(type=PartType.text, text=o)
72
- elif isinstance(o, bytes): return _bytes2content(o)
73
- elif isinstance(o, MediaUrl): return _url2content(o)
74
- return o
75
-
76
48
 
77
49
  # %% ../nbs/07_chat.ipynb #48c78e48
78
50
  def contents(c):
@@ -169,11 +141,6 @@ def lite_mk_func(f):
169
141
  if isinstance(f, dict): return f
170
142
  return {'type':'function', 'function':get_schema(f, pname='parameters')}
171
143
 
172
- # %% ../nbs/07_chat.ipynb #3e0afa31
173
- @dataclass
174
- class ToolResponse:
175
- content: list[str,str]
176
-
177
144
  # %% ../nbs/07_chat.ipynb #bba6fd58
178
145
  def _mk_tool_result(res):
179
146
  "Unwrap `ToolResponse`, and format tool result message"
@@ -273,21 +273,18 @@ def get_hdrs(api_key=None):
273
273
  def cost(usage, m):
274
274
  raw = usage.raw
275
275
  prompt_tot = raw.get('promptTokenCount', 0)
276
- tier = '_above_200k_tokens' if prompt_tot > 200_000 else ''
277
- in_rate = m.get(f'input_cost_per_token{tier}') or m.input_cost_per_token
278
- out_rate = m.get(f'output_cost_per_token{tier}') or m.output_cost_per_token
279
- cache_rate = m.get(f'cache_read_input_token_cost{tier}')or m.get('cache_read_input_token_cost', 0)
280
- audio_rate = m.get('input_cost_per_audio_token') # None if not priced separately
281
-
276
+ tier = price_tier(m, prompt_tot)
277
+ in_rate = tier_rate(m, 'input_cost_per_token', tier)
278
+ out_rate = tier_rate(m, 'output_cost_per_token', tier)
279
+ cache_rate = tier_rate(m, 'cache_read_input_token_cost', tier)
280
+ # Models with no `input_cost_per_audio_token` (e.g. Gemini 3 Pro) bill audio at the standard input rate, so it stays in `in_txt`
281
+ audio_rate = m.get('input_cost_per_audio_token')
282
282
  cached = raw.get('cachedContentTokenCount', 0)
283
- # Gemini 3 Pro supports bills audio at the standard input rate (no separate input_cost_per_audio_token key in the metadata)
284
283
  audio = sum(d['tokenCount'] for d in raw.get('promptTokensDetails', []) if d.get('modality')=='AUDIO') if audio_rate else 0
285
284
  in_txt = prompt_tot - cached - audio
286
-
287
285
  thoughts = raw.get('thoughtsTokenCount', 0) or 0
288
286
  cands = raw.get('candidatesTokenCount', 0) or 0
289
287
  reason_rate = m.get('output_cost_per_reasoning_token') or out_rate
290
-
291
288
  cost = in_txt * in_rate + cands * out_rate
292
289
  cost += cached * cache_rate
293
290
  cost += audio * (audio_rate or 0)
@@ -251,9 +251,10 @@ def get_hdrs(api_key=None):
251
251
  def cost(usage, m):
252
252
  raw = usage.raw
253
253
  cached = raw.get('input_tokens_details', {}).get('cached_tokens', 0)
254
- in_txt = raw['input_tokens'] - cached
255
- cost = in_txt * m.input_cost_per_token + raw['output_tokens'] * m.output_cost_per_token
256
- cost += cached * m.get('cache_read_input_token_cost', 0)
254
+ tier = price_tier(m, raw['input_tokens'])
255
+ cost = (raw['input_tokens'] - cached) * tier_rate(m, 'input_cost_per_token', tier)
256
+ cost += raw['output_tokens'] * tier_rate(m, 'output_cost_per_token', tier)
257
+ cost += cached * tier_rate(m, 'cache_read_input_token_cost', tier)
257
258
  return cost
258
259
 
259
260
  # %% ../nbs/02_oai_responses.ipynb #07114b55
@@ -150,7 +150,7 @@ async def mk_acollect_stream(it, index_fn, model=None, api_name=None, vendor_nam
150
150
  deltas.append(d)
151
151
  part_accum.finalize()
152
152
  tcs = part_accum.tool_calls
153
- if api_name: usg = api_registry.apis[api_name].finalize_usage(usg, part_accum.parts)
153
+ if api_name: usg = api_registry[api_name].finalize_usage(usg, part_accum.parts)
154
154
  if stop: fin = FinishReason.stop
155
155
  fin = FinishReason.tool_calls if fin==FinishReason.stop and any(~L(tcs).attrgot('server')) else fin # recheck tool calls post collation
156
156
  # tool calls and non-anthropic citations are yielded at the end
@@ -9,10 +9,11 @@ __all__ = ['FinishReason', 'api_registry', 'model_prices_url', 'haik45', 'sonn45
9
9
  'codex_pricing', 'sol', 'terra', 'luna', 'gpt56s', 'Usage', 'Completion', 'APIRegistry', 'mk_completion',
10
10
  'fn_schema', 'payload_kwargs', 'get_api_key', 'resize_b64', 'model_prices_meta', 'infer_api_name',
11
11
  'get_model_meta', 'register_model_info', 'get_model_info', 'get_model_pricing', 'approx_pricing',
12
- 'is_deepseek_peak_hour']
12
+ 'is_deepseek_peak_hour', 'price_tier', 'tier_rate']
13
13
 
14
14
  # %% ../nbs/00_types.ipynb #b4d047fd
15
15
  import httpx, base64, io
16
+ from importlib.metadata import entry_points
16
17
  from dataclasses import dataclass, field
17
18
  from datetime import datetime, timezone
18
19
  from fastcore.net import urljson
@@ -74,9 +75,18 @@ def _repr_markdown_(self: Completion):
74
75
  FinishReason = str_enum('finish_reason', 'stop', 'tool_calls', 'length', 'content_filter')
75
76
 
76
77
  # %% ../nbs/00_types.ipynb #fc681c52
77
- class APIRegistry:
78
- def __init__(self): self.apis = {}
79
- def register(self, name, finalize_usage=noop, **kwargs): self.apis[name] = SimpleNamespace(finalize_usage=finalize_usage, **kwargs)
78
+ class APIRegistry(dict):
79
+ _eps = None
80
+ def register(self, name, finalize_usage=noop, **kwargs): self[name] = SimpleNamespace(finalize_usage=finalize_usage, **kwargs)
81
+ def _load(self, name):
82
+ "Import the plugin providing api `name` from the `fastllm.apis` entry-point group; its import registers the api."
83
+ if self._eps is None: self._eps = {o.name:o for o in entry_points(group='fastllm.apis')}
84
+ if name in self._eps: self._eps[name].load()
85
+ return dict.__contains__(self, name)
86
+ def __contains__(self, name): return dict.__contains__(self, name) or self._load(name)
87
+ def __missing__(self, name):
88
+ if self._load(name): return self[name]
89
+ raise KeyError(name)
80
90
 
81
91
  api_registry = APIRegistry()
82
92
 
@@ -85,7 +95,7 @@ api_registry = APIRegistry()
85
95
  def mk_completion(resp, model, api_name, vendor_name):
86
96
  "Normalize an api response into Completion."
87
97
  resp = obj2dict(resp)
88
- api = api_registry.apis[api_name]
98
+ api = api_registry[api_name]
89
99
  tcs = api.norm_tool_calls(resp)
90
100
  parts = api.norm_parts(resp)
91
101
  usg = api.finalize_usage(api.norm_usage(resp), parts)
@@ -215,8 +225,9 @@ register_model_info('gemini-3.5-flash', vendor_name='gemini', base='gemini-3-fla
215
225
  input_cost_per_token=1.5e-6, output_cost_per_token=9e-6,
216
226
  output_cost_per_reasoning_token=9e-6, cache_read_input_token_cost=1.5e-7)
217
227
 
218
- for model in ('gpt-5.4', 'gpt-5.4-mini'):
219
- register_model_info(model, vendor_name='openai', base=model, supports_web_search=True, mode=None, max_input_tokens=250000)
228
+ register_model_info('gpt-5.4', vendor_name='openai', base='gpt-5.4', supports_web_search=True, mode=None)
229
+ # Upstream metadata says 1,050,000 input tokens for gpt-5.4-mini, but OpenAI documents a 272k limit
230
+ 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)
220
231
 
221
232
  for model in ('kimi-k2.5', 'kimi-k2.6'):
222
233
  register_model_info(model, vendor_name='moonshot', base=f'moonshot/{model}', base_vendor_name=None,
@@ -271,8 +282,14 @@ codex_pricing = dict(
271
282
  input_cost_per_token = 0.10/1_000_000, output_cost_per_token = 0.50/1_000_000,
272
283
  cache_creation_input_token_cost = 0.10/1_000_000, cache_read_input_token_cost = 0.10/1_000_000)
273
284
 
285
+ def _rm_ctx_tiers(vendor_name, model):
286
+ "Drop upstream context-tier rates, which would otherwise override flat subscription pricing"
287
+ info = model_info_registry[vendor_name, model]
288
+ for k in [k for k in info if re.search(r'_above_\d+k_tokens', k)]: del info[k]
289
+
274
290
  for model in (codex54, codex54m, codex55):
275
291
  register_model_info(model, 'codex', base=model, base_vendor_name='chatgpt', supports_web_search=True, max_input_tokens=256000, **codex_pricing)
292
+ _rm_ctx_tiers('codex', model)
276
293
 
277
294
  register_model_info(codex53spark, 'codex', **codex_pricing,
278
295
  supports_vision=False, supports_image_input=False, supports_web_search=True, supports_reasoning=True, supports_function_calling=True,
@@ -285,8 +302,12 @@ for model in (haik45, sonn45, sonn46, sonn5, opus46, opus48, opus5, fable5):
285
302
  # %% ../nbs/00_types.ipynb #bb0c4c2a
286
303
  sol,terra,luna = gpt56s = 'gpt-5.6-sol gpt-5.6-terra gpt-5.6-luna'.split()
287
304
  for model in ['gpt-5.6']+gpt56s:
288
- register_model_info(model, 'openai', base=model, base_vendor_name='openai', max_input_tokens=272_000)
289
- register_model_info(model, 'codex', base=model, base_vendor_name='openai', max_input_tokens=272_000, **codex_pricing)
305
+ register_model_info(model, 'openai', base=model, base_vendor_name='openai')
306
+ # Codex serves only the suffixed names; bare `gpt-5.6` is rejected with a ChatGPT account.
307
+ # Its window is smaller than the API's: 371,331 input tokens is accepted and 371,981 is not, on all three.
308
+ for model in gpt56s:
309
+ register_model_info(model, 'codex', base=model, base_vendor_name='openai', max_input_tokens=371_000, **codex_pricing)
310
+ _rm_ctx_tiers('codex', model)
290
311
 
291
312
  # %% ../nbs/00_types.ipynb #24cc47ec
292
313
  def get_model_pricing(mn, vendor_name, million=True):
@@ -310,11 +331,22 @@ def is_deepseek_peak_hour(dt=None):
310
331
  h = dt.hour + dt.minute/60
311
332
  return 1 <= h < 4 or 6 <= h < 10
312
333
 
334
+ # %% ../nbs/00_types.ipynb #4c701619
335
+ def price_tier(meta, n):
336
+ "Suffix of the pricing tier applying to `n` prompt tokens, or '' for base rates."
337
+ o = first(o for k in meta if (o:=re.match(r'input_cost_per_token(_above_(\d+)k_tokens)$', k)))
338
+ return o[1] if o and n>int(o[2])*1000 else ''
339
+
340
+ # %% ../nbs/00_types.ipynb #956d2495
341
+ def tier_rate(meta, key, tier):
342
+ "Rate for `key` at `tier`, falling back to the base key."
343
+ return meta.get(f'{key}{tier}') or meta[key]
344
+
313
345
  # %% ../nbs/00_types.ipynb #8bfca02d
314
346
  @patch(as_prop=True)
315
347
  def cost(self:Completion):
316
348
  meta = dict2obj(get_model_info(self.model, self.vendor_name))
317
- api = api_registry.apis[self.api_name]
349
+ api = api_registry[self.api_name]
318
350
  if not hasattr(api, 'cost'): raise NotImplementedError(f"API: {self.api_name} doesn't have a registered `cost` function in ns")
319
351
  res = api.cost(self.usage, meta)
320
352
  return res*2 if self.vendor_name=='deepseek' and self.model.startswith('deepseek-v4') and is_deepseek_peak_hour() else res
@@ -15,7 +15,7 @@ classifiers = [
15
15
  "Programming Language :: Python :: 3",
16
16
  "Programming Language :: Python :: 3 :: Only",
17
17
  ]
18
- dependencies = ['fastcore>=2.1.11', 'aidialog>=0.0.3', 'fastspec>=0.0.11', 'toolslm', 'pillow']
18
+ dependencies = ['fastcore>=2.1.11', 'aidialog>=0.0.5', 'fastspec>=0.0.11', 'toolslm', 'pillow']
19
19
 
20
20
  [project.urls]
21
21
  Repository = "https://github.com/AnswerDotAI/fastllm"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fastllm
3
- Version: 0.0.33
3
+ Version: 0.0.35
4
4
  Author-email: Kerem Turgutlu <keremturgutlu@gmail.com>
5
5
  License: Apache-2.0
6
6
  Project-URL: Repository, https://github.com/AnswerDotAI/fastllm
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
10
10
  Requires-Python: >=3.10
11
11
  Description-Content-Type: text/markdown
12
12
  Requires-Dist: fastcore>=2.1.11
13
- Requires-Dist: aidialog>=0.0.3
13
+ Requires-Dist: aidialog>=0.0.5
14
14
  Requires-Dist: fastspec>=0.0.11
15
15
  Requires-Dist: toolslm
16
16
  Requires-Dist: pillow
@@ -1,5 +1,5 @@
1
1
  fastcore>=2.1.11
2
- aidialog>=0.0.3
2
+ aidialog>=0.0.5
3
3
  fastspec>=0.0.11
4
4
  toolslm
5
5
  pillow
@@ -1 +0,0 @@
1
- __version__ = "0.0.33"