python-fastllm 0.0.47__tar.gz → 0.0.49__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.47 → python_fastllm-0.0.49}/PKG-INFO +1 -1
- {python_fastllm-0.0.47 → python_fastllm-0.0.49}/fastllm/__init__.py +1 -1
- {python_fastllm-0.0.47 → python_fastllm-0.0.49}/fastllm/_modidx.py +17 -2
- {python_fastllm-0.0.47 → python_fastllm-0.0.49}/fastllm/acomplete.py +15 -11
- {python_fastllm-0.0.47 → python_fastllm-0.0.49}/fastllm/anthropic.py +6 -2
- {python_fastllm-0.0.47 → python_fastllm-0.0.49}/fastllm/chat.py +3 -1
- python_fastllm-0.0.49/fastllm/codex.py +118 -0
- {python_fastllm-0.0.47 → python_fastllm-0.0.49}/fastllm/gemini.py +4 -1
- {python_fastllm-0.0.47 → python_fastllm-0.0.49}/fastllm/openai_chat.py +4 -1
- {python_fastllm-0.0.47 → python_fastllm-0.0.49}/fastllm/openai_responses.py +4 -1
- {python_fastllm-0.0.47 → python_fastllm-0.0.49}/fastllm/responses.py +7 -6
- {python_fastllm-0.0.47 → python_fastllm-0.0.49}/fastllm/types.py +6 -5
- {python_fastllm-0.0.47 → python_fastllm-0.0.49}/pyproject.toml +3 -0
- {python_fastllm-0.0.47 → python_fastllm-0.0.49}/python_fastllm.egg-info/PKG-INFO +1 -1
- {python_fastllm-0.0.47 → python_fastllm-0.0.49}/python_fastllm.egg-info/entry_points.txt +3 -0
- python_fastllm-0.0.47/fastllm/codex.py +0 -7
- {python_fastllm-0.0.47 → python_fastllm-0.0.49}/README.md +0 -0
- {python_fastllm-0.0.47 → python_fastllm-0.0.49}/fastllm/streaming.py +0 -0
- {python_fastllm-0.0.47 → python_fastllm-0.0.49}/python_fastllm.egg-info/SOURCES.txt +0 -0
- {python_fastllm-0.0.47 → python_fastllm-0.0.49}/python_fastllm.egg-info/dependency_links.txt +0 -0
- {python_fastllm-0.0.47 → python_fastllm-0.0.49}/python_fastllm.egg-info/requires.txt +0 -0
- {python_fastllm-0.0.47 → python_fastllm-0.0.49}/python_fastllm.egg-info/top_level.txt +0 -0
- {python_fastllm-0.0.47 → python_fastllm-0.0.49}/setup.cfg +0 -0
|
@@ -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'),
|
|
@@ -323,7 +338,7 @@ d = { 'settings': { 'branch': 'main',
|
|
|
323
338
|
'fastllm.types.Usage.__add__': ('types.html#usage.__add__', 'fastllm/types.py'),
|
|
324
339
|
'fastllm.types.Usage.__eq__': ('types.html#usage.__eq__', 'fastllm/types.py'),
|
|
325
340
|
'fastllm.types.Usage.__init__': ('types.html#usage.__init__', 'fastllm/types.py'),
|
|
326
|
-
'fastllm.types.
|
|
341
|
+
'fastllm.types._flat_rates': ('types.html#_flat_rates', 'fastllm/types.py'),
|
|
327
342
|
'fastllm.types.approx_pricing': ('types.html#approx_pricing', 'fastllm/types.py'),
|
|
328
343
|
'fastllm.types.effort_code': ('types.html#effort_code', 'fastllm/types.py'),
|
|
329
344
|
'fastllm.types.effort_levels': ('types.html#effort_levels', 'fastllm/types.py'),
|
|
@@ -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'),
|
|
@@ -66,10 +67,10 @@ def _codex_mtime_policy():
|
|
|
66
67
|
|
|
67
68
|
# %% ../nbs/06_acomplete.ipynb #79075d95
|
|
68
69
|
@flexicache(_codex_mtime_policy())
|
|
69
|
-
def mk_client(model=None, vendor_name=None, api_name=None, api_key=None, base_url=None, xtra_hdrs=None,
|
|
70
|
+
def mk_client(model=None, vendor_name=None, api_name=None, api_key=None, oauth_token=None, base_url=None, xtra_hdrs=None,
|
|
70
71
|
timeout=httpx2.Timeout(connect=30, read=300, write=30, pool=10)):
|
|
71
72
|
err_msg = f"please pass a valid one vendor: {', '.join(list(vendor_mapping))} or pass `api_name`,`base_url` and `api_key`"
|
|
72
|
-
if not vendor_name and not api_name and not (base_url and api_key):
|
|
73
|
+
if not vendor_name and not api_name and not (base_url and (api_key or oauth_token)):
|
|
73
74
|
v, m = split_vendor(model)
|
|
74
75
|
if v in vendor_mapping: vendor_name, model = v, m
|
|
75
76
|
if vendor_name:
|
|
@@ -77,17 +78,17 @@ def mk_client(model=None, vendor_name=None, api_name=None, api_key=None, base_ur
|
|
|
77
78
|
try:
|
|
78
79
|
api_name, base_url, env_api_nm, *auth_json = vendor_mapping[vendor_name]
|
|
79
80
|
base_url = override_base_url or base_url
|
|
80
|
-
if auth_json and not api_key and not os.getenv(env_api_nm):
|
|
81
|
+
if auth_json and not api_key and not oauth_token and not os.getenv(env_api_nm):
|
|
81
82
|
fn,keys = auth_json[0] # pyright: ignore[reportAssignmentType]
|
|
82
83
|
auth_fn = Path(fn).expanduser()
|
|
83
84
|
if auth_fn.exists(): api_key = nested_idx(json.loads(auth_fn.read_text()), *keys)
|
|
84
|
-
api_key = get_api_key(api_key, env_api_nm)
|
|
85
|
+
if not oauth_token: api_key = get_api_key(api_key, env_api_nm)
|
|
85
86
|
except KeyError: raise ValueError(f"Unknown vendor '{vendor_name}', {err_msg}")
|
|
86
|
-
elif base_url and api_key: vendor_name, api_name = ifnone(vendor_name, 'custom'), ifnone(api_name, 'openai_chat')
|
|
87
|
+
elif base_url and (api_key or oauth_token): vendor_name, api_name = ifnone(vendor_name, 'custom'), ifnone(api_name, 'openai_chat')
|
|
87
88
|
elif (api_name:=infer_api_name(model)): base_url, vendor_name = vendor_mapping[api_name][1], api_name
|
|
88
89
|
else: raise ValueError(f"Model {model} can't be auto resolved, {err_msg}")
|
|
89
90
|
api = api_registry[api_name]
|
|
90
|
-
hdrs = merge(api.get_hdrs(api_key), ifnone(xtra_hdrs, {}))
|
|
91
|
+
hdrs = merge(api.get_hdrs(api_key, oauth_token), ifnone(xtra_hdrs, {}))
|
|
91
92
|
cli = AsyncHttpCli(base_url, base_headers=hdrs, timeout=timeout)
|
|
92
93
|
return cli, api_name, vendor_name
|
|
93
94
|
|
|
@@ -154,16 +155,19 @@ async def _retry_stream(mk_gen, retries=2, retry_delay=0.5):
|
|
|
154
155
|
|
|
155
156
|
# %% ../nbs/06_acomplete.ipynb #2379ec94
|
|
156
157
|
@delegates(payload_kwargs)
|
|
157
|
-
async def acomplete(msgs, model, api_name=None, vendor_name=None, api_key=None, base_url=None, endpoint=None, xtra_body=None, xtra_hdrs=None,
|
|
158
|
+
async def acomplete(msgs, model, api_name=None, vendor_name=None, api_key=None, oauth_token=None, base_url=None, endpoint=None, xtra_body=None, xtra_hdrs=None,
|
|
158
159
|
stream=False, previous_response_id=None, stop_callables=None, retries=2, retry_delay=0.5, **kwargs):
|
|
159
160
|
"Unified completion across different APIs."
|
|
160
|
-
if
|
|
161
|
+
if api_key and oauth_token: raise ValueError('Pass `api_key` or `oauth_token`, not both: a request has one identity')
|
|
162
|
+
if not vendor_name and not api_name and not (base_url and (api_key or oauth_token)):
|
|
161
163
|
v, m = split_vendor(model)
|
|
162
164
|
if v in vendor_mapping: vendor_name, model = v, m
|
|
163
165
|
elif v: api_name, model = v, m # a registered transport api (e.g. claude_code): not an HTTP vendor
|
|
164
166
|
own = api_name in api_registry and not hasattr(api_registry[api_name], 'endpoint') if api_name else False
|
|
165
|
-
if own:
|
|
166
|
-
|
|
167
|
+
if own:
|
|
168
|
+
vendor_name = ifnone(vendor_name, api_name) # an own-transport api: no HTTP client to build
|
|
169
|
+
if oauth_token: kwargs['oauth_token'] = oauth_token # its `mk_payload` applies the user's token itself
|
|
170
|
+
else: cli, api_name, vendor_name = mk_client(model=model, vendor_name=vendor_name, api_name=api_name, api_key=api_key, oauth_token=oauth_token, base_url=base_url, xtra_hdrs=xtra_hdrs)
|
|
167
171
|
api = api_registry[api_name]
|
|
168
172
|
if previous_response_id is not None:
|
|
169
173
|
if not getattr(api, 'supports_previous_response_id', False):
|
|
@@ -322,8 +322,12 @@ def mk_payload(msgs, model, **kwargs):
|
|
|
322
322
|
return payload
|
|
323
323
|
|
|
324
324
|
# %% ../nbs/04_anthropic.ipynb #60d52c1f
|
|
325
|
-
def get_hdrs(api_key=None):
|
|
326
|
-
|
|
325
|
+
def get_hdrs(api_key=None, oauth_token=None):
|
|
326
|
+
"An OAuth token travels as a bearer under Anthropic's OAuth beta; an API key as `x-api-key`"
|
|
327
|
+
hdrs = {"anthropic-version": "2023-06-01"}
|
|
328
|
+
if oauth_token: return hdrs | {"Authorization": f"Bearer {oauth_token}", "anthropic-beta": "oauth-2025-04-20"}
|
|
329
|
+
return hdrs | {"x-api-key": get_api_key(api_key, 'ANTHROPIC_API_KEY')}
|
|
330
|
+
|
|
327
331
|
|
|
328
332
|
# %% ../nbs/04_anthropic.ipynb #0d03643a
|
|
329
333
|
def cost(usage, m):
|
|
@@ -168,6 +168,7 @@ class AsyncChat:
|
|
|
168
168
|
api_name=None, # API to use, one of ApiName: openai (responses), openai_chat, anthropic, gemini
|
|
169
169
|
vendor_name=None, # Vendor name, one of vendor_mapping which resolves api_base/api_key automatically
|
|
170
170
|
api_key=None, # API key when model can't be resolved or vendor_name is not known or codex
|
|
171
|
+
oauth_token=None, # A subscription's OAuth token (Codex, Claude Code) in place of an API key
|
|
171
172
|
base_url=None, # API base url when model can't be resolved or vendor_name is not known
|
|
172
173
|
endpoint=None, # Override the transport's request path, for a server mounting Responses at a custom location
|
|
173
174
|
extra_headers=None, # Extra HTTP headers for custom providers
|
|
@@ -178,7 +179,7 @@ class AsyncChat:
|
|
|
178
179
|
default_cbs=True # Whether to include default callbacks
|
|
179
180
|
):
|
|
180
181
|
"LiteLLM chat client."
|
|
181
|
-
if not vendor_name and not api_name and not (base_url and api_key):
|
|
182
|
+
if not vendor_name and not api_name and not (base_url and (api_key or oauth_token)):
|
|
182
183
|
v, model = split_vendor(model)
|
|
183
184
|
if v in vendor_mapping: vendor_name = v
|
|
184
185
|
elif v: api_name = v # a registered transport api (e.g. claude_code)
|
|
@@ -248,6 +249,7 @@ def _prep_call(self:AsyncChat, search, max_tokens, kwargs, stream=False, think=N
|
|
|
248
249
|
if self.api_name: kwargs['api_name'] = self.api_name
|
|
249
250
|
if self.vendor_name: kwargs['vendor_name'] = self.vendor_name
|
|
250
251
|
if self.api_key: kwargs['api_key'] = self.api_key
|
|
252
|
+
if self.oauth_token: kwargs['oauth_token'] = self.oauth_token
|
|
251
253
|
if self.base_url: kwargs['base_url'] = self.base_url
|
|
252
254
|
if self.endpoint: kwargs['endpoint'] = self.endpoint
|
|
253
255
|
if self.extra_headers: kwargs['xtra_hdrs'] = self.extra_headers
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""ChatGPT subscription tokens for the Codex backend"""
|
|
2
|
+
|
|
3
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/05_codex.ipynb.
|
|
4
|
+
|
|
5
|
+
# %% auto #0
|
|
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']
|
|
8
|
+
|
|
9
|
+
# %% ../nbs/05_codex.ipynb #a1d088d2
|
|
10
|
+
import asyncio, base64, json, sys, time, httpx2
|
|
11
|
+
from fastcore.aio import wait_until
|
|
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())))
|
|
@@ -267,9 +267,12 @@ def mk_payload(msgs, model, **kwargs):
|
|
|
267
267
|
return payload
|
|
268
268
|
|
|
269
269
|
# %% ../nbs/05_gemini.ipynb #60d52c1f
|
|
270
|
-
def get_hdrs(api_key=None):
|
|
270
|
+
def get_hdrs(api_key=None, oauth_token=None):
|
|
271
|
+
"Bearer auth for an OAuth token; otherwise the API key header"
|
|
272
|
+
if oauth_token: return {"Authorization": f"Bearer {oauth_token}"}
|
|
271
273
|
return {"x-goog-api-key": get_api_key(api_key, 'GEMINI_API_KEY')}
|
|
272
274
|
|
|
275
|
+
|
|
273
276
|
# %% ../nbs/05_gemini.ipynb #4ee3891f
|
|
274
277
|
def cost(usage, m):
|
|
275
278
|
raw = usage.raw
|
|
@@ -198,7 +198,10 @@ def mk_payload(msgs, model, **kwargs):
|
|
|
198
198
|
return payload
|
|
199
199
|
|
|
200
200
|
# %% ../nbs/03_oai_chat.ipynb #16e813d2
|
|
201
|
-
def get_hdrs(api_key=None
|
|
201
|
+
def get_hdrs(api_key=None, oauth_token=None):
|
|
202
|
+
"Bearer auth: an OAuth access token, or else an API key from the argument or the environment"
|
|
203
|
+
return {"Authorization": f"Bearer {oauth_token or get_api_key(api_key, 'OPENAI_API_KEY')}"}
|
|
204
|
+
|
|
202
205
|
|
|
203
206
|
# %% ../nbs/03_oai_chat.ipynb #f89e2bf6
|
|
204
207
|
def cost(usage, m):
|
|
@@ -256,7 +256,10 @@ def mk_payload(msgs, model, **kwargs):
|
|
|
256
256
|
return payload
|
|
257
257
|
|
|
258
258
|
# %% ../nbs/02_oai_responses.ipynb #529fd4bc
|
|
259
|
-
def get_hdrs(api_key=None
|
|
259
|
+
def get_hdrs(api_key=None, oauth_token=None):
|
|
260
|
+
"Bearer auth: an OAuth access token, or else an API key from the argument or the environment"
|
|
261
|
+
return {"Authorization": f"Bearer {oauth_token or get_api_key(api_key, 'OPENAI_API_KEY')}"}
|
|
262
|
+
|
|
260
263
|
|
|
261
264
|
# %% ../nbs/02_oai_responses.ipynb #a907ffa8
|
|
262
265
|
def cost(usage, m):
|
|
@@ -148,9 +148,10 @@ class ResponseState(BasicRepr):
|
|
|
148
148
|
# %% ../nbs/06a_responses.ipynb #c9ca3744
|
|
149
149
|
class ResponseTurn(BasicRepr):
|
|
150
150
|
"A prepared Responses request for one provider call."
|
|
151
|
-
def __init__(self, id, created_at, body, history, provider_messages, system='', previous=None, server_tools=None):
|
|
151
|
+
def __init__(self, id, created_at, body, history, provider_messages, system='', previous=None, server_tools=None, call_kwargs=None):
|
|
152
152
|
store_attr()
|
|
153
153
|
if server_tools is None: self.server_tools = {}
|
|
154
|
+
if call_kwargs is None: self.call_kwargs = {}
|
|
154
155
|
|
|
155
156
|
@property
|
|
156
157
|
def provider_previous_id(self): return self.previous.provider_response_id if self.previous else None
|
|
@@ -159,7 +160,7 @@ class ResponseTurn(BasicRepr):
|
|
|
159
160
|
# %% ../nbs/06a_responses.ipynb #a648297f
|
|
160
161
|
class AsyncResponses:
|
|
161
162
|
"Run individual Responses turns over FastLLM."
|
|
162
|
-
def prepare(self, body, previous=None, server_tools=None):
|
|
163
|
+
def prepare(self, body, previous=None, server_tools=None, **kwargs):
|
|
163
164
|
if not isinstance(body, dict): raise ResponsesError('Request body must be an object')
|
|
164
165
|
model = body.get('model')
|
|
165
166
|
if not model: raise ResponsesError('model is required', param='model')
|
|
@@ -174,13 +175,13 @@ class AsyncResponses:
|
|
|
174
175
|
instructions = '\n\n'.join(filter(None, [body.get('instructions', ''), input_system]))
|
|
175
176
|
provider_messages = new_msgs if previous and previous.provider_response_id else list(history)
|
|
176
177
|
return ResponseTurn(_new_id('resp'), int(time.time()), body, history, provider_messages, instructions, previous,
|
|
177
|
-
{f.__name__: f for f in server_tools or []})
|
|
178
|
+
{f.__name__: f for f in server_tools or []}, kwargs)
|
|
178
179
|
|
|
179
180
|
|
|
180
181
|
# %% ../nbs/06a_responses.ipynb #373f7d5e
|
|
181
182
|
@patch
|
|
182
183
|
async def call(self:AsyncResponses, turn):
|
|
183
|
-
"Start the provider stream for a prepared turn, having received its first chunk"
|
|
184
|
+
"Start the provider stream for a prepared turn, having received its first chunk; the turn's `call_kwargs` reach `acomplete`"
|
|
184
185
|
body = turn.body
|
|
185
186
|
reasoning = body.get('reasoning') or {}
|
|
186
187
|
tools = list(body.get('tools') or []) + [dict(type='function', **get_schema(f, pname='parameters')) for f in turn.server_tools.values()]
|
|
@@ -188,7 +189,7 @@ async def call(self:AsyncResponses, turn):
|
|
|
188
189
|
kwargs.update(tool_choice=body.get('tool_choice'), parallel_tool_calls=body.get('parallel_tool_calls', True),
|
|
189
190
|
reasoning_effort=reasoning.get('effort'), max_tokens=body.get('max_output_tokens'), temperature=body.get('temperature'),
|
|
190
191
|
cache_idxs=body.get('cache_idxs'), ttl=body.get('ttl'), web_search_options=body.get('web_search_options'))
|
|
191
|
-
stream = await acomplete(turn.provider_messages, model=body['model'], stream=True, **kwargs)
|
|
192
|
+
stream = await acomplete(turn.provider_messages, model=body['model'], stream=True, **kwargs, **turn.call_kwargs)
|
|
192
193
|
first = await anext(stream)
|
|
193
194
|
async def _rest():
|
|
194
195
|
yield first
|
|
@@ -221,7 +222,7 @@ async def server_results(self:AsyncResponses, turn, comp):
|
|
|
221
222
|
def server_turn(self:AsyncResponses, turn, comp, results):
|
|
222
223
|
"The follow-up provider turn answering server tool `results`, within the same public response"
|
|
223
224
|
outputs = [dict(type='function_call_output', call_id=r.id, output=r.text) for r in results]
|
|
224
|
-
nxt = self.prepare(turn.body | dict(input=outputs), self.state(turn, comp), turn.server_tools.values())
|
|
225
|
+
nxt = self.prepare(turn.body | dict(input=outputs), self.state(turn, comp), turn.server_tools.values(), **turn.call_kwargs)
|
|
225
226
|
nxt.id = turn.id
|
|
226
227
|
return nxt
|
|
227
228
|
|
|
@@ -300,14 +300,14 @@ register_model_info('muse-spark-1.1', vendor_name='meta_ai', **modern_llm, max_i
|
|
|
300
300
|
codex_pricing = dict(input_cost_per_token = 0.10/1_000_000, output_cost_per_token = 0.50/1_000_000,
|
|
301
301
|
cache_creation_input_token_cost = 0.10/1_000_000, cache_read_input_token_cost = 0.10/1_000_000)
|
|
302
302
|
|
|
303
|
-
def
|
|
304
|
-
"
|
|
303
|
+
def _flat_rates(vendor_name, model):
|
|
304
|
+
"Keep only the flat subscription rates: drop the upstream context, cache-TTL and service-tier variants that would otherwise apply"
|
|
305
305
|
info = model_info_registry[vendor_name, model]
|
|
306
|
-
for k in [k for k in info if
|
|
306
|
+
for k in [k for k in info if 'cost' in k and k not in codex_pricing and k != 'search_context_cost_per_query']: del info[k]
|
|
307
307
|
|
|
308
308
|
for model in (codex54, codex54m, codex55):
|
|
309
309
|
register_model_info(model, 'codex', base=model, base_vendor_name='chatgpt', supports_web_search=True, max_input_tokens=256000, **codex_pricing)
|
|
310
|
-
|
|
310
|
+
_flat_rates('codex', model)
|
|
311
311
|
|
|
312
312
|
register_model_info(codex53spark, 'codex', **codex_pricing,
|
|
313
313
|
supports_vision=False, supports_image_input=False, supports_web_search=True, supports_reasoning=True, supports_function_calling=True,
|
|
@@ -318,6 +318,7 @@ register_model_info(codex53spark, 'codex', **codex_pricing,
|
|
|
318
318
|
register_model_info(fable51, 'anthropic', base=fable5)
|
|
319
319
|
for model in (haik45, sonn45, sonn46, sonn5, opus46, opus48, opus5, fable5, fable51):
|
|
320
320
|
register_model_info(model, 'claude_code', base=model, base_vendor_name='anthropic', **codex_pricing)
|
|
321
|
+
_flat_rates('claude_code', model)
|
|
321
322
|
|
|
322
323
|
# %% ../nbs/00_types.ipynb #bb0c4c2a
|
|
323
324
|
sol,terra,luna = gpt56s = 'gpt-5.6-sol gpt-5.6-terra gpt-5.6-luna'.split()
|
|
@@ -325,7 +326,7 @@ sol,terra,luna = gpt56s = 'gpt-5.6-sol gpt-5.6-terra gpt-5.6-luna'.split()
|
|
|
325
326
|
# Its window is smaller than the API's: 371,331 input tokens is accepted and 371,981 is not, on all three.
|
|
326
327
|
for model in gpt56s:
|
|
327
328
|
register_model_info(model, 'codex', base=model, base_vendor_name='openai', max_input_tokens=371_000, **codex_pricing)
|
|
328
|
-
|
|
329
|
+
_flat_rates('codex', model)
|
|
329
330
|
|
|
330
331
|
# %% ../nbs/00_types.ipynb #24cc47ec
|
|
331
332
|
def get_model_pricing(mn, vendor_name, million=True):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_fastllm-0.0.47 → python_fastllm-0.0.49}/python_fastllm.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|