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.
- {python_fastllm-0.0.33 → python_fastllm-0.0.35}/PKG-INFO +2 -2
- python_fastllm-0.0.35/fastllm/__init__.py +1 -0
- {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/_modidx.py +8 -7
- {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/acomplete.py +22 -7
- {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/chat.py +6 -39
- {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/gemini.py +6 -9
- {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/openai_responses.py +4 -3
- {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/streaming.py +1 -1
- {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/types.py +42 -10
- {python_fastllm-0.0.33 → python_fastllm-0.0.35}/pyproject.toml +1 -1
- {python_fastllm-0.0.33 → python_fastllm-0.0.35}/python_fastllm.egg-info/PKG-INFO +2 -2
- {python_fastllm-0.0.33 → python_fastllm-0.0.35}/python_fastllm.egg-info/requires.txt +1 -1
- python_fastllm-0.0.33/fastllm/__init__.py +0 -1
- {python_fastllm-0.0.33 → python_fastllm-0.0.35}/README.md +0 -0
- {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/anthropic.py +0 -0
- {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/codex.py +0 -0
- {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/openai_chat.py +0 -0
- {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/specs/anthropic.json +0 -0
- {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/specs/anthropic.yml +0 -0
- {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/specs/gemini.json +0 -0
- {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/specs/openai.with-code-samples.json +0 -0
- {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/specs/openai.with-code-samples.yml +0 -0
- {python_fastllm-0.0.33 → python_fastllm-0.0.35}/fastllm/specs/spec_manifest.json +0 -0
- {python_fastllm-0.0.33 → python_fastllm-0.0.35}/python_fastllm.egg-info/SOURCES.txt +0 -0
- {python_fastllm-0.0.33 → python_fastllm-0.0.35}/python_fastllm.egg-info/dependency_links.txt +0 -0
- {python_fastllm-0.0.33 → python_fastllm-0.0.35}/python_fastllm.egg-info/entry_points.txt +0 -0
- {python_fastllm-0.0.33 → python_fastllm-0.0.35}/python_fastllm.egg-info/top_level.txt +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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', '
|
|
8
|
-
'
|
|
9
|
-
'
|
|
10
|
-
'
|
|
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,
|
|
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 =
|
|
277
|
-
in_rate = m
|
|
278
|
-
out_rate = m
|
|
279
|
-
cache_rate = m
|
|
280
|
-
|
|
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
|
-
|
|
255
|
-
cost =
|
|
256
|
-
cost +=
|
|
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
|
|
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
|
-
|
|
79
|
-
def register(self, name, finalize_usage=noop, **kwargs): self
|
|
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
|
|
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
|
-
|
|
219
|
-
|
|
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'
|
|
289
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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 +0,0 @@
|
|
|
1
|
-
__version__ = "0.0.33"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_fastllm-0.0.33 → python_fastllm-0.0.35}/python_fastllm.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|