python-fastllm 0.0.46__py3-none-any.whl → 0.0.48__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 +1 -1
- fastllm/_modidx.py +24 -1
- fastllm/acomplete.py +5 -2
- fastllm/chat.py +5 -11
- fastllm/codex.py +112 -1
- fastllm/openai_responses.py +1 -0
- fastllm/responses.py +69 -23
- fastllm/types.py +53 -55
- {python_fastllm-0.0.46.dist-info → python_fastllm-0.0.48.dist-info}/METADATA +1 -1
- python_fastllm-0.0.48.dist-info/RECORD +17 -0
- {python_fastllm-0.0.46.dist-info → python_fastllm-0.0.48.dist-info}/entry_points.txt +3 -0
- python_fastllm-0.0.46.dist-info/RECORD +0 -17
- {python_fastllm-0.0.46.dist-info → python_fastllm-0.0.48.dist-info}/WHEEL +0 -0
- {python_fastllm-0.0.46.dist-info → python_fastllm-0.0.48.dist-info}/top_level.txt +0 -0
fastllm/__init__.py
CHANGED
fastllm/_modidx.py
CHANGED
|
@@ -120,7 +120,22 @@ d = { 'settings': { 'branch': 'main',
|
|
|
120
120
|
'fastllm.chat.stop_reason': ('chat.html#stop_reason', 'fastllm/chat.py'),
|
|
121
121
|
'fastllm.chat.structured': ('chat.html#structured', 'fastllm/chat.py'),
|
|
122
122
|
'fastllm.chat.think_sep': ('chat.html#think_sep', 'fastllm/chat.py')},
|
|
123
|
-
'fastllm.codex': {
|
|
123
|
+
'fastllm.codex': { 'fastllm.codex._merge_tokens': ('codex.html#_merge_tokens', 'fastllm/codex.py'),
|
|
124
|
+
'fastllm.codex._post': ('codex.html#_post', 'fastllm/codex.py'),
|
|
125
|
+
'fastllm.codex._token_grant': ('codex.html#_token_grant', 'fastllm/codex.py'),
|
|
126
|
+
'fastllm.codex.codex_auth': ('codex.html#codex_auth', 'fastllm/codex.py'),
|
|
127
|
+
'fastllm.codex.codex_auth_path': ('codex.html#codex_auth_path', 'fastllm/codex.py'),
|
|
128
|
+
'fastllm.codex.codex_device_code': ('codex.html#codex_device_code', 'fastllm/codex.py'),
|
|
129
|
+
'fastllm.codex.codex_device_tokens': ('codex.html#codex_device_tokens', 'fastllm/codex.py'),
|
|
130
|
+
'fastllm.codex.codex_info': ('codex.html#codex_info', 'fastllm/codex.py'),
|
|
131
|
+
'fastllm.codex.codex_login': ('codex.html#codex_login', 'fastllm/codex.py'),
|
|
132
|
+
'fastllm.codex.codex_refresh': ('codex.html#codex_refresh', 'fastllm/codex.py'),
|
|
133
|
+
'fastllm.codex.codex_token': ('codex.html#codex_token', 'fastllm/codex.py'),
|
|
134
|
+
'fastllm.codex.codex_tokens': ('codex.html#codex_tokens', 'fastllm/codex.py'),
|
|
135
|
+
'fastllm.codex.jwt_claims': ('codex.html#jwt_claims', 'fastllm/codex.py'),
|
|
136
|
+
'fastllm.codex.main': ('codex.html#main', 'fastllm/codex.py'),
|
|
137
|
+
'fastllm.codex.mk_jwt': ('codex.html#mk_jwt', 'fastllm/codex.py'),
|
|
138
|
+
'fastllm.codex.token_expired': ('codex.html#token_expired', 'fastllm/codex.py')},
|
|
124
139
|
'fastllm.gemini': { 'fastllm.gemini._gem_filter_sch': ('gemini.html#_gem_filter_sch', 'fastllm/gemini.py'),
|
|
125
140
|
'fastllm.gemini._gem_part_type': ('gemini.html#_gem_part_type', 'fastllm/gemini.py'),
|
|
126
141
|
'fastllm.gemini.acollect_stream': ('gemini.html#acollect_stream', 'fastllm/gemini.py'),
|
|
@@ -237,6 +252,10 @@ d = { 'settings': { 'branch': 'main',
|
|
|
237
252
|
'fastllm/responses.py'),
|
|
238
253
|
'fastllm.responses.AsyncResponses.prepare': ( 'responses.html#asyncresponses.prepare',
|
|
239
254
|
'fastllm/responses.py'),
|
|
255
|
+
'fastllm.responses.AsyncResponses.server_results': ( 'responses.html#asyncresponses.server_results',
|
|
256
|
+
'fastllm/responses.py'),
|
|
257
|
+
'fastllm.responses.AsyncResponses.server_turn': ( 'responses.html#asyncresponses.server_turn',
|
|
258
|
+
'fastllm/responses.py'),
|
|
240
259
|
'fastllm.responses.AsyncResponses.state': ( 'responses.html#asyncresponses.state',
|
|
241
260
|
'fastllm/responses.py'),
|
|
242
261
|
'fastllm.responses.ResponseState': ('responses.html#responsestate', 'fastllm/responses.py'),
|
|
@@ -316,10 +335,13 @@ d = { 'settings': { 'branch': 'main',
|
|
|
316
335
|
'fastllm.types.Completion.cost': ('types.html#completion.cost', 'fastllm/types.py'),
|
|
317
336
|
'fastllm.types.Completion.formatted': ('types.html#completion.formatted', 'fastllm/types.py'),
|
|
318
337
|
'fastllm.types.Usage': ('types.html#usage', 'fastllm/types.py'),
|
|
338
|
+
'fastllm.types.Usage.__add__': ('types.html#usage.__add__', 'fastllm/types.py'),
|
|
319
339
|
'fastllm.types.Usage.__eq__': ('types.html#usage.__eq__', 'fastllm/types.py'),
|
|
320
340
|
'fastllm.types.Usage.__init__': ('types.html#usage.__init__', 'fastllm/types.py'),
|
|
321
341
|
'fastllm.types._rm_ctx_tiers': ('types.html#_rm_ctx_tiers', 'fastllm/types.py'),
|
|
322
342
|
'fastllm.types.approx_pricing': ('types.html#approx_pricing', 'fastllm/types.py'),
|
|
343
|
+
'fastllm.types.effort_code': ('types.html#effort_code', 'fastllm/types.py'),
|
|
344
|
+
'fastllm.types.effort_levels': ('types.html#effort_levels', 'fastllm/types.py'),
|
|
323
345
|
'fastllm.types.fn_schema': ('types.html#fn_schema', 'fastllm/types.py'),
|
|
324
346
|
'fastllm.types.get_api_key': ('types.html#get_api_key', 'fastllm/types.py'),
|
|
325
347
|
'fastllm.types.get_model_info': ('types.html#get_model_info', 'fastllm/types.py'),
|
|
@@ -334,6 +356,7 @@ d = { 'settings': { 'branch': 'main',
|
|
|
334
356
|
'fastllm.types.provider_req': ('types.html#provider_req', 'fastllm/types.py'),
|
|
335
357
|
'fastllm.types.register_model_info': ('types.html#register_model_info', 'fastllm/types.py'),
|
|
336
358
|
'fastllm.types.resize_b64': ('types.html#resize_b64', 'fastllm/types.py'),
|
|
359
|
+
'fastllm.types.resolve_effort': ('types.html#resolve_effort', 'fastllm/types.py'),
|
|
337
360
|
'fastllm.types.tier_rate': ('types.html#tier_rate', 'fastllm/types.py'),
|
|
338
361
|
'fastllm.types.unwrap_typed': ('types.html#unwrap_typed', 'fastllm/types.py'),
|
|
339
362
|
'fastllm.types.wrap_typed': ('types.html#wrap_typed', 'fastllm/types.py')}}}
|
fastllm/acomplete.py
CHANGED
|
@@ -13,6 +13,7 @@ from fasttransport.core import AsyncHttpCli
|
|
|
13
13
|
from fasttransport.errors import APIError
|
|
14
14
|
|
|
15
15
|
from .types import *
|
|
16
|
+
from .codex import codex_auth_path
|
|
16
17
|
from aidialog.msg_parts import (Part, PartType, Msg, Text, Thinking, ToolUse, ToolResult, InputImage, InputAudio, InputVideo,
|
|
17
18
|
InputFile, mk_tool_res_msg, Completion)
|
|
18
19
|
from .streaming import *
|
|
@@ -22,7 +23,7 @@ from .anthropic import *
|
|
|
22
23
|
from .gemini import *
|
|
23
24
|
|
|
24
25
|
# %% ../nbs/06_acomplete.ipynb #32ee2546
|
|
25
|
-
_codex_path =
|
|
26
|
+
_codex_path = codex_auth_path()
|
|
26
27
|
_codex_json = _codex_path, ('tokens','access_token')
|
|
27
28
|
vendor_mapping = {
|
|
28
29
|
"openai": ('openai', 'https://api.openai.com/v1', 'OPENAI_API_KEY'),
|
|
@@ -162,7 +163,9 @@ async def acomplete(msgs, model, api_name=None, vendor_name=None, api_key=None,
|
|
|
162
163
|
if v in vendor_mapping: vendor_name, model = v, m
|
|
163
164
|
elif v: api_name, model = v, m # a registered transport api (e.g. claude_code): not an HTTP vendor
|
|
164
165
|
own = api_name in api_registry and not hasattr(api_registry[api_name], 'endpoint') if api_name else False
|
|
165
|
-
if own:
|
|
166
|
+
if own:
|
|
167
|
+
vendor_name = ifnone(vendor_name, api_name) # an own-transport api: no HTTP client to build
|
|
168
|
+
if api_key: kwargs['api_key'] = api_key # its `mk_payload` applies a per-user credential itself
|
|
166
169
|
else: 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)
|
|
167
170
|
api = api_registry[api_name]
|
|
168
171
|
if previous_response_id is not None:
|
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
|
|
232
|
-
|
|
233
|
-
if
|
|
234
|
-
|
|
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):
|
fastllm/codex.py
CHANGED
|
@@ -1,7 +1,118 @@
|
|
|
1
|
+
"""ChatGPT subscription tokens for the Codex backend"""
|
|
2
|
+
|
|
1
3
|
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/05_codex.ipynb.
|
|
2
4
|
|
|
3
5
|
# %% auto #0
|
|
4
|
-
__all__ = [
|
|
6
|
+
__all__ = ['SKEW_SECS', 'jwt_claims', 'mk_jwt', 'token_expired', 'codex_tokens', 'codex_info', 'codex_refresh', 'codex_token',
|
|
7
|
+
'codex_auth_path', 'codex_auth', 'codex_device_code', 'codex_device_tokens', 'codex_login', 'main']
|
|
5
8
|
|
|
6
9
|
# %% ../nbs/05_codex.ipynb #a1d088d2
|
|
10
|
+
import asyncio, base64, json, sys, time, httpx2
|
|
11
|
+
from fastcore.aio import wait_until
|
|
7
12
|
from fastcore.utils import *
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# %% ../nbs/05_codex.ipynb #aa7f9d4f
|
|
16
|
+
_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann'
|
|
17
|
+
_AUTH = 'https://auth.openai.com'
|
|
18
|
+
SKEW_SECS = 60
|
|
19
|
+
|
|
20
|
+
# %% ../nbs/05_codex.ipynb #2008472d
|
|
21
|
+
def jwt_claims(token):
|
|
22
|
+
"The payload of JWT `token`, decoded without verification"
|
|
23
|
+
payload = token.split('.')[1]
|
|
24
|
+
return json.loads(base64.urlsafe_b64decode(payload + '='*(-len(payload)%4)))
|
|
25
|
+
|
|
26
|
+
# %% ../nbs/05_codex.ipynb #fdba8e96
|
|
27
|
+
def mk_jwt(**claims):
|
|
28
|
+
"An unsigned JWT carrying `claims`, shaped like an access token; for examples and tests"
|
|
29
|
+
enc = lambda o: base64.urlsafe_b64encode(json.dumps(o).encode()).decode().rstrip('=')
|
|
30
|
+
return f"{enc(dict(alg='RS256', typ='JWT'))}.{enc(claims)}.sig"
|
|
31
|
+
|
|
32
|
+
# %% ../nbs/05_codex.ipynb #6b6a339d
|
|
33
|
+
def token_expired(token, skew=SKEW_SECS):
|
|
34
|
+
"Is JWT `token` expired, or within `skew` seconds of expiring?"
|
|
35
|
+
try: exp = jwt_claims(token)['exp']
|
|
36
|
+
except (ValueError, KeyError, IndexError): return True
|
|
37
|
+
return time.time() >= exp - skew
|
|
38
|
+
|
|
39
|
+
# %% ../nbs/05_codex.ipynb #099399d5
|
|
40
|
+
def codex_tokens(auth):
|
|
41
|
+
"The `tokens` block of an `auth.json` dict (or an already-unwrapped block), checked for the keys a refresh needs"
|
|
42
|
+
if not isinstance(auth, dict): raise ValueError('Codex auth must be a JSON object')
|
|
43
|
+
tokens = auth.get('tokens', auth)
|
|
44
|
+
missing = [k for k in ('access_token', 'refresh_token') if not tokens.get(k)]
|
|
45
|
+
if missing: raise ValueError(f"Codex auth is missing {', '.join(missing)}")
|
|
46
|
+
return tokens
|
|
47
|
+
|
|
48
|
+
# %% ../nbs/05_codex.ipynb #46864051
|
|
49
|
+
def codex_info(tokens):
|
|
50
|
+
"Plan type and expiry time of the access token in `tokens`"
|
|
51
|
+
claims = jwt_claims(tokens['access_token'])
|
|
52
|
+
return dict(plan=claims.get('https://api.openai.com/auth', {}).get('chatgpt_plan_type'), expires=claims.get('exp'))
|
|
53
|
+
|
|
54
|
+
# %% ../nbs/05_codex.ipynb #28410636
|
|
55
|
+
async def _post(path, form=False, **body):
|
|
56
|
+
"POST `body` to the OpenAI auth server at `path`, as a form or as JSON, returning the JSON reply"
|
|
57
|
+
async with httpx2.AsyncClient() as cli: r = await cli.post(f'{_AUTH}{path}', **(dict(data=body) if form else dict(json=body)))
|
|
58
|
+
r.raise_for_status()
|
|
59
|
+
return r.json()
|
|
60
|
+
|
|
61
|
+
def _merge_tokens(tokens, new): return {**tokens, **{k: new[k] for k in ('access_token', 'id_token', 'refresh_token') if k in new}}
|
|
62
|
+
|
|
63
|
+
async def _token_grant(tokens, form=False, **grant):
|
|
64
|
+
"The token block after `grant` at the token endpoint, merged over `tokens`; the exchange of a sign-in code is form-encoded, a refresh JSON"
|
|
65
|
+
return _merge_tokens(tokens, await _post('/oauth/token', form=form, client_id=_CLIENT_ID, **grant))
|
|
66
|
+
|
|
67
|
+
# %% ../nbs/05_codex.ipynb #48efd465
|
|
68
|
+
async def codex_refresh(tokens):
|
|
69
|
+
"Exchange the refresh token in `tokens` for a new token block"
|
|
70
|
+
return await _token_grant(tokens, grant_type='refresh_token', refresh_token=tokens['refresh_token'], scope='openid profile email')
|
|
71
|
+
|
|
72
|
+
# %% ../nbs/05_codex.ipynb #be590818
|
|
73
|
+
async def codex_token(tokens, force_refresh=False):
|
|
74
|
+
"A valid access token from `tokens`, and the refreshed block to store when a refresh happened (else `None`)"
|
|
75
|
+
if not force_refresh and not token_expired(tokens['access_token']): return tokens['access_token'], None
|
|
76
|
+
tokens = await codex_refresh(tokens)
|
|
77
|
+
return tokens['access_token'], tokens
|
|
78
|
+
|
|
79
|
+
# %% ../nbs/05_codex.ipynb #ea6c1e6a
|
|
80
|
+
def codex_auth_path(): return Path(os.environ.get('CODEX_AUTH_PATH', '~/.codex/auth.json')).expanduser()
|
|
81
|
+
|
|
82
|
+
async def codex_auth(force_refresh=False):
|
|
83
|
+
"A valid access token from the local Codex login, refreshing the file when needed"
|
|
84
|
+
path = codex_auth_path()
|
|
85
|
+
data = json.loads(path.read_text())
|
|
86
|
+
token,new = await codex_token(codex_tokens(data), force_refresh)
|
|
87
|
+
if new: path.write_text(json.dumps({**data, 'tokens':new}, indent=2))
|
|
88
|
+
return token
|
|
89
|
+
|
|
90
|
+
# %% ../nbs/05_codex.ipynb #216c81ae
|
|
91
|
+
_DEVICE = '/api/accounts/deviceauth'
|
|
92
|
+
async def codex_device_code():
|
|
93
|
+
"Start a device sign-in: the page and code to show the user, and what the poll needs"
|
|
94
|
+
r = await _post(f'{_DEVICE}/usercode', client_id=_CLIENT_ID)
|
|
95
|
+
return dict(url=f'{_AUTH}/codex/device', code=r['user_code'], device_auth_id=r['device_auth_id'], interval=int(r.get('interval') or 5))
|
|
96
|
+
|
|
97
|
+
# %% ../nbs/05_codex.ipynb #9ada6ae8
|
|
98
|
+
async def codex_device_tokens(dev, timeout=15*60):
|
|
99
|
+
"Wait for the user to enter the code from `dev`, then exchange the grant for a token block"
|
|
100
|
+
async def _poll():
|
|
101
|
+
try: return await _post(f'{_DEVICE}/token', device_auth_id=dev['device_auth_id'], user_code=dev['code'])
|
|
102
|
+
except httpx2.HTTPStatusError as e:
|
|
103
|
+
if e.response.status_code in (403, 404): return None
|
|
104
|
+
raise
|
|
105
|
+
grant = await wait_until(_poll, 'the sign-in to complete', timeout, sleep=dev['interval'])
|
|
106
|
+
return await _token_grant({}, form=True, grant_type='authorization_code', code=grant['authorization_code'], redirect_uri=f'{_AUTH}/deviceauth/callback',
|
|
107
|
+
code_verifier=grant['code_verifier'])
|
|
108
|
+
|
|
109
|
+
# %% ../nbs/05_codex.ipynb #0ca00e36
|
|
110
|
+
async def codex_login():
|
|
111
|
+
"Sign in to ChatGPT with a device code, returning the token block"
|
|
112
|
+
dev = await codex_device_code()
|
|
113
|
+
print(f"Open {dev['url']} and enter the code {dev['code']}", file=sys.stderr)
|
|
114
|
+
return await codex_device_tokens(dev)
|
|
115
|
+
|
|
116
|
+
def main():
|
|
117
|
+
"`fastllm-auth`: sign in to ChatGPT and print the token block"
|
|
118
|
+
print(json.dumps(asyncio.run(codex_login())))
|
fastllm/openai_responses.py
CHANGED
|
@@ -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
|
-
|
|
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,19 @@ 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):
|
|
151
|
+
def __init__(self, id, created_at, body, history, provider_messages, system='', previous=None, server_tools=None, call_kwargs=None):
|
|
152
|
+
store_attr()
|
|
153
|
+
if server_tools is None: self.server_tools = {}
|
|
154
|
+
if call_kwargs is None: self.call_kwargs = {}
|
|
149
155
|
|
|
150
156
|
@property
|
|
151
157
|
def provider_previous_id(self): return self.previous.provider_response_id if self.previous else None
|
|
152
158
|
|
|
159
|
+
|
|
153
160
|
# %% ../nbs/06a_responses.ipynb #a648297f
|
|
154
161
|
class AsyncResponses:
|
|
155
162
|
"Run individual Responses turns over FastLLM."
|
|
156
|
-
def prepare(self, body, previous=None):
|
|
163
|
+
def prepare(self, body, previous=None, server_tools=None, **kwargs):
|
|
157
164
|
if not isinstance(body, dict): raise ResponsesError('Request body must be an object')
|
|
158
165
|
model = body.get('model')
|
|
159
166
|
if not model: raise ResponsesError('model is required', param='model')
|
|
@@ -167,27 +174,57 @@ class AsyncResponses:
|
|
|
167
174
|
if isinstance(p, ToolResult) and not p.name: p.name = names.get(p.id, '')
|
|
168
175
|
instructions = '\n\n'.join(filter(None, [body.get('instructions', ''), input_system]))
|
|
169
176
|
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
|
|
177
|
+
return ResponseTurn(_new_id('resp'), int(time.time()), body, history, provider_messages, instructions, previous,
|
|
178
|
+
{f.__name__: f for f in server_tools or []}, kwargs)
|
|
179
|
+
|
|
171
180
|
|
|
172
181
|
# %% ../nbs/06a_responses.ipynb #373f7d5e
|
|
173
182
|
@patch
|
|
174
183
|
async def call(self:AsyncResponses, turn):
|
|
175
|
-
"Start the provider stream for a prepared turn
|
|
184
|
+
"Start the provider stream for a prepared turn, having received its first chunk; the turn's `call_kwargs` reach `acomplete`"
|
|
176
185
|
body = turn.body
|
|
177
186
|
reasoning = body.get('reasoning') or {}
|
|
178
|
-
|
|
187
|
+
tools = list(body.get('tools') or []) + [dict(type='function', **get_schema(f, pname='parameters')) for f in turn.server_tools.values()]
|
|
188
|
+
kwargs = dict(previous_response_id=turn.provider_previous_id, system=turn.system or None, tools=tools or None)
|
|
179
189
|
kwargs.update(tool_choice=body.get('tool_choice'), parallel_tool_calls=body.get('parallel_tool_calls', True),
|
|
180
190
|
reasoning_effort=reasoning.get('effort'), max_tokens=body.get('max_output_tokens'), temperature=body.get('temperature'),
|
|
181
191
|
cache_idxs=body.get('cache_idxs'), ttl=body.get('ttl'), web_search_options=body.get('web_search_options'))
|
|
182
|
-
|
|
192
|
+
stream = await acomplete(turn.provider_messages, model=body['model'], stream=True, **kwargs, **turn.call_kwargs)
|
|
193
|
+
first = await anext(stream)
|
|
194
|
+
async def _rest():
|
|
195
|
+
yield first
|
|
196
|
+
async for o in stream: yield o
|
|
197
|
+
return _rest()
|
|
198
|
+
|
|
183
199
|
|
|
184
200
|
# %% ../nbs/06a_responses.ipynb #4f424285
|
|
185
201
|
@patch
|
|
186
|
-
def state(self:AsyncResponses, turn, comp):
|
|
187
|
-
"Build the next continuation state."
|
|
202
|
+
def state(self:AsyncResponses, turn, comp, results=()):
|
|
203
|
+
"Build the next continuation state; `results` are server tool results answering `comp`'s calls."
|
|
188
204
|
normalize_call_ids(comp)
|
|
189
|
-
|
|
190
|
-
|
|
205
|
+
history = (*turn.history, comp.message, *([Msg('tool', list(results))] if results else []))
|
|
206
|
+
return ResponseState(turn.id, turn.body['model'], history, comp.response_id, comp.response_id_reusable)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# %% ../nbs/06a_responses.ipynb #7615b274
|
|
210
|
+
@patch
|
|
211
|
+
async def server_results(self:AsyncResponses, turn, comp):
|
|
212
|
+
"Run the server tool calls in `comp`, returning their results"
|
|
213
|
+
normalize_call_ids(comp)
|
|
214
|
+
res = []
|
|
215
|
+
for tc in comp.tool_calls:
|
|
216
|
+
if tc.name not in turn.server_tools: continue
|
|
217
|
+
out = await call_func_async(tc.name, tc.arguments, ns=turn.server_tools, raise_on_err=False)
|
|
218
|
+
res.append(ToolResult(id=tc.id, name=tc.name, arguments=tc.arguments, text=tool_text(out)))
|
|
219
|
+
return res
|
|
220
|
+
|
|
221
|
+
@patch
|
|
222
|
+
def server_turn(self:AsyncResponses, turn, comp, results):
|
|
223
|
+
"The follow-up provider turn answering server tool `results`, within the same public response"
|
|
224
|
+
outputs = [dict(type='function_call_output', call_id=r.id, output=r.text) for r in results]
|
|
225
|
+
nxt = self.prepare(turn.body | dict(input=outputs), self.state(turn, comp), turn.server_tools.values(), **turn.call_kwargs)
|
|
226
|
+
nxt.id = turn.id
|
|
227
|
+
return nxt
|
|
191
228
|
|
|
192
229
|
# %% ../nbs/06a_responses.ipynb #25502099
|
|
193
230
|
def response_event(event_type, sequence_number, **kwargs):
|
|
@@ -278,22 +315,31 @@ def done_events(self:_ResponseStream, comp):
|
|
|
278
315
|
|
|
279
316
|
# %% ../nbs/06a_responses.ipynb #e10c928b
|
|
280
317
|
@patch
|
|
281
|
-
async def events(self:AsyncResponses, turn, finalize=None):
|
|
282
|
-
state = _ResponseStream(turn)
|
|
318
|
+
async def events(self:AsyncResponses, turn, finalize=None, stream=None):
|
|
319
|
+
state,first = _ResponseStream(turn),turn
|
|
283
320
|
yield state.event('response.created', response=pending_response(turn))
|
|
284
321
|
try:
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
322
|
+
texts,usage = [],None
|
|
323
|
+
while True:
|
|
324
|
+
async for part in (stream or await self.call(turn)):
|
|
325
|
+
if isinstance(part, Completion): comp = part
|
|
326
|
+
elif isinstance(part, Status) or (isinstance(part, ToolUse) and part.name in turn.server_tools): continue
|
|
327
|
+
else:
|
|
328
|
+
for event in state.part_events(part): yield event
|
|
329
|
+
usage,stream = (comp.usage if usage is None else usage + comp.usage),None
|
|
330
|
+
results = await self.server_results(turn, comp)
|
|
331
|
+
if not results or len(results) < len(comp.tool_calls): break
|
|
332
|
+
texts += [p for p in comp.message.content if isinstance(p, Text)]
|
|
333
|
+
turn = self.server_turn(turn, comp, results)
|
|
334
|
+
next_state = self.state(turn, comp, results)
|
|
335
|
+
comp = copy.copy(comp)
|
|
336
|
+
comp.message,comp.usage = Msg('assistant', texts + comp.message.content),usage
|
|
291
337
|
for event in state.done_events(comp): yield event
|
|
292
|
-
|
|
293
|
-
response = response_object(turn.id, turn.body, comp, turn.previous.id if turn.previous else None, turn.created_at)
|
|
338
|
+
response = response_object(first.id, first.body, comp, first.previous.id if first.previous else None, first.created_at)
|
|
294
339
|
if finalize: await finalize(next_state, comp)
|
|
295
340
|
name = 'response.incomplete' if response['status'] == 'incomplete' else 'response.completed'
|
|
296
341
|
yield state.event(name, response=response)
|
|
297
342
|
except asyncio.CancelledError: raise
|
|
298
343
|
except ResponsesError as exc: yield state.event('error', code=exc.code, message=str(exc), param=exc.param)
|
|
299
344
|
except APIError as exc: yield state.event('error', code=exc.code or 'provider_error', message=str(exc), param=None)
|
|
345
|
+
|
fastllm/types.py
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
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',
|
|
7
|
-
'opus', 'opus5', 'fable', 'fable5', 'fable51', 'gpt54', 'gpt54m', 'gpt55', 'codex54',
|
|
8
|
-
'
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
'
|
|
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',
|
|
13
|
+
'get_model_pricing', 'approx_pricing', 'is_deepseek_peak_hour', 'price_tier', 'tier_rate']
|
|
13
14
|
|
|
14
15
|
# %% ../nbs/00_types.ipynb #b4d047fd
|
|
15
16
|
import httpx2, base64, io
|
|
@@ -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"
|
|
@@ -217,50 +227,45 @@ modern_llm = dict(supports_function_calling=True, supports_tool_choice=True, sup
|
|
|
217
227
|
supports_reasoning=True, supports_response_schema=True, supports_system_messages=True)
|
|
218
228
|
|
|
219
229
|
# %% ../nbs/00_types.ipynb #8261dcd0
|
|
220
|
-
register_model_info('
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
register_model_info('
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
input_cost_per_token=0.95e-6, cache_read_input_token_cost=0.16e-6, output_cost_per_token=4.0e-6)
|
|
256
|
-
register_model_info('kimi-k2.7-code', vendor_name='moonshot', base='kimi-k2.6',
|
|
257
|
-
input_cost_per_token=0.95e-6, cache_read_input_token_cost=0.19e-6, output_cost_per_token=4.0e-6)
|
|
258
|
-
register_model_info('kimi-k3', vendor_name='moonshot', base='kimi-k2.6', supports_reasoning=True, supports_vision=True,
|
|
259
|
-
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)]
|
|
264
|
+
|
|
260
265
|
|
|
261
266
|
# %% ../nbs/00_types.ipynb #948d55d0
|
|
262
267
|
# The price map carries DeepSeek's peak rates, and `Completion.cost` doubles at peak, so register the off-peak rates
|
|
263
|
-
deepseek_v4_common = dict(supports_native_structured_output=True
|
|
268
|
+
deepseek_v4_common = dict(supports_native_structured_output=True)
|
|
264
269
|
deepseek_v4_flash_prices = dict(input_cost_per_token=0.22e-6, input_cost_per_token_cache_hit=0.007e-6,
|
|
265
270
|
cache_read_input_token_cost=0.007e-6, output_cost_per_token=0.66e-6)
|
|
266
271
|
|
|
@@ -291,12 +296,6 @@ register_model_info('muse-spark-1.1', vendor_name='meta_ai', **modern_llm, max_i
|
|
|
291
296
|
supports_vision=True, supports_image_input=True, supports_video_input=True, supports_pdf_input=True, supports_web_search=True,
|
|
292
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)
|
|
293
298
|
|
|
294
|
-
# %% ../nbs/00_types.ipynb #5a2b3430
|
|
295
|
-
# z.ai list pricing (a 50% discount applies until 2026-09-09)
|
|
296
|
-
register_model_info('glm-5.3-flash', vendor_name='zai', base='glm-5.1', **modern_llm, supports_vision=True, supports_image_input=True,
|
|
297
|
-
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,
|
|
298
|
-
output_cost_per_token=5e-07)
|
|
299
|
-
|
|
300
299
|
# %% ../nbs/00_types.ipynb #2c23d11e
|
|
301
300
|
codex_pricing = dict(input_cost_per_token = 0.10/1_000_000, output_cost_per_token = 0.50/1_000_000,
|
|
302
301
|
cache_creation_input_token_cost = 0.10/1_000_000, cache_read_input_token_cost = 0.10/1_000_000)
|
|
@@ -322,7 +321,6 @@ for model in (haik45, sonn45, sonn46, sonn5, opus46, opus48, opus5, fable5, fabl
|
|
|
322
321
|
|
|
323
322
|
# %% ../nbs/00_types.ipynb #bb0c4c2a
|
|
324
323
|
sol,terra,luna = gpt56s = 'gpt-5.6-sol gpt-5.6-terra gpt-5.6-luna'.split()
|
|
325
|
-
for model in ['gpt-5.6']+gpt56s: register_model_info(model, 'openai', base=model, base_vendor_name='openai')
|
|
326
324
|
# Codex serves only the suffixed names; bare `gpt-5.6` is rejected with a ChatGPT account.
|
|
327
325
|
# Its window is smaller than the API's: 371,331 input tokens is accepted and 371,981 is not, on all three.
|
|
328
326
|
for model in gpt56s:
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
fastllm/__init__.py,sha256=ioW91VJRYfSiEMjolhjzoNX1vPYfkbDKdrHXp79B9cQ,131
|
|
2
|
+
fastllm/_modidx.py,sha256=jF1SdJd1qbfI_ksSmsrjKUgRHGrANOpxyqTn_hcZ2LY,42836
|
|
3
|
+
fastllm/acomplete.py,sha256=Fb1GWkU6F7-bllbEEp7TPOzqmoWp309i5sJqYJbU6PM,10478
|
|
4
|
+
fastllm/anthropic.py,sha256=udojlLuritp90JQlH-dGHcWRpzgnO1mZYFYtbt2F5IA,17015
|
|
5
|
+
fastllm/chat.py,sha256=W8PPpZ95Az7wzLgQ15u3HMroWBVXDoaUz871f0x7cEk,24842
|
|
6
|
+
fastllm/codex.py,sha256=r6auZ8egia83aGkWcF4132AzW4SsBvKeDwg_CxyXj9E,5808
|
|
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=jcbKUwbEIsy7ngNcHUtv2bfYeDgqSysK_qWM-0LC9Mg,18767
|
|
11
|
+
fastllm/streaming.py,sha256=8qVtWeZ7NlP-9rup_4Ho0ol2dx3-XuGcIMXpZ1GYdEo,9079
|
|
12
|
+
fastllm/types.py,sha256=3qXyiS431AlSSn8GyGCNDflZxMWPeIWirp8TIyhIu_A,19146
|
|
13
|
+
python_fastllm-0.0.48.dist-info/METADATA,sha256=meHtTkX0PRsmljM1a72zXLGwlFHm1IKkjroJKnAmc1g,21143
|
|
14
|
+
python_fastllm-0.0.48.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
15
|
+
python_fastllm-0.0.48.dist-info/entry_points.txt,sha256=WkWd04F3xVYFP5bsexaQe9Se6uelwE5ULoTtHWreO0s,133
|
|
16
|
+
python_fastllm-0.0.48.dist-info/top_level.txt,sha256=F8qodL7nEGUHGmzzqfhNKCTIr1i0D6cvudOnm3z7o0Y,8
|
|
17
|
+
python_fastllm-0.0.48.dist-info/RECORD,,
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
fastllm/__init__.py,sha256=vURwCxFsBCHTqkPwJTZzHL2iXy0aGMjAinI8E6iqZnI,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=zEMynkgqsShsfPwk2cMEl67QUBo0VVNG0n79VCvqvzw,25110
|
|
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=H7xgkBFVwA-iyoqsQ68SIVADNRIwEQ5XpjQskiZ8I8w,20433
|
|
13
|
-
python_fastllm-0.0.46.dist-info/METADATA,sha256=qK-cBjqv3-yAsrDhBXQGXr9yeJZGmhParYfRYMI29zM,21143
|
|
14
|
-
python_fastllm-0.0.46.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
15
|
-
python_fastllm-0.0.46.dist-info/entry_points.txt,sha256=_cVRbpVkvyxdZfR7UCXa0lIXZtTSsZQWoBPhDwMqrHs,80
|
|
16
|
-
python_fastllm-0.0.46.dist-info/top_level.txt,sha256=F8qodL7nEGUHGmzzqfhNKCTIr1i0D6cvudOnm3z7o0Y,8
|
|
17
|
-
python_fastllm-0.0.46.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|