python-fastllm 0.0.47__tar.gz → 0.0.48__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.48}/PKG-INFO +1 -1
- {python_fastllm-0.0.47 → python_fastllm-0.0.48}/fastllm/__init__.py +1 -1
- {python_fastllm-0.0.47 → python_fastllm-0.0.48}/fastllm/_modidx.py +16 -1
- {python_fastllm-0.0.47 → python_fastllm-0.0.48}/fastllm/acomplete.py +5 -2
- python_fastllm-0.0.48/fastllm/codex.py +118 -0
- {python_fastllm-0.0.47 → python_fastllm-0.0.48}/fastllm/responses.py +7 -6
- {python_fastllm-0.0.47 → python_fastllm-0.0.48}/pyproject.toml +3 -0
- {python_fastllm-0.0.47 → python_fastllm-0.0.48}/python_fastllm.egg-info/PKG-INFO +1 -1
- {python_fastllm-0.0.47 → python_fastllm-0.0.48}/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.48}/README.md +0 -0
- {python_fastllm-0.0.47 → python_fastllm-0.0.48}/fastllm/anthropic.py +0 -0
- {python_fastllm-0.0.47 → python_fastllm-0.0.48}/fastllm/chat.py +0 -0
- {python_fastllm-0.0.47 → python_fastllm-0.0.48}/fastllm/gemini.py +0 -0
- {python_fastllm-0.0.47 → python_fastllm-0.0.48}/fastllm/openai_chat.py +0 -0
- {python_fastllm-0.0.47 → python_fastllm-0.0.48}/fastllm/openai_responses.py +0 -0
- {python_fastllm-0.0.47 → python_fastllm-0.0.48}/fastllm/streaming.py +0 -0
- {python_fastllm-0.0.47 → python_fastllm-0.0.48}/fastllm/types.py +0 -0
- {python_fastllm-0.0.47 → python_fastllm-0.0.48}/python_fastllm.egg-info/SOURCES.txt +0 -0
- {python_fastllm-0.0.47 → python_fastllm-0.0.48}/python_fastllm.egg-info/dependency_links.txt +0 -0
- {python_fastllm-0.0.47 → python_fastllm-0.0.48}/python_fastllm.egg-info/requires.txt +0 -0
- {python_fastllm-0.0.47 → python_fastllm-0.0.48}/python_fastllm.egg-info/top_level.txt +0 -0
- {python_fastllm-0.0.47 → python_fastllm-0.0.48}/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'),
|
|
@@ -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:
|
|
@@ -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())))
|
|
@@ -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
|
|
|
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.47 → python_fastllm-0.0.48}/python_fastllm.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|