tsugite-codex-cli 0.23.2__tar.gz → 0.24.0__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.
- {tsugite_codex_cli-0.23.2 → tsugite_codex_cli-0.24.0}/PKG-INFO +2 -2
- {tsugite_codex_cli-0.23.2 → tsugite_codex_cli-0.24.0}/pyproject.toml +5 -2
- {tsugite_codex_cli-0.23.2 → tsugite_codex_cli-0.24.0}/tsugite_codex_cli/auth.py +2 -2
- {tsugite_codex_cli-0.23.2 → tsugite_codex_cli-0.24.0}/tsugite_codex_cli/provider.py +29 -28
- tsugite_codex_cli-0.24.0/tsugite_codex_cli/usage.py +81 -0
- {tsugite_codex_cli-0.23.2 → tsugite_codex_cli-0.24.0}/.gitignore +0 -0
- {tsugite_codex_cli-0.23.2 → tsugite_codex_cli-0.24.0}/tsugite_codex_cli/__init__.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tsugite-codex-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.24.0
|
|
4
4
|
Summary: Tsugite plugin: ChatGPT subscription auth via Codex CLI token store
|
|
5
5
|
Author: Justyn Shull
|
|
6
6
|
Requires-Python: >=3.11
|
|
7
|
-
Requires-Dist: tsugite-cli==0.
|
|
7
|
+
Requires-Dist: tsugite-cli==0.24.0
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tsugite-codex-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.24.0"
|
|
4
4
|
description = "Tsugite plugin: ChatGPT subscription auth via Codex CLI token store"
|
|
5
5
|
authors = [{ name = "Justyn Shull" }]
|
|
6
6
|
requires-python = ">=3.11"
|
|
7
7
|
dependencies = [
|
|
8
|
-
"tsugite-cli==0.
|
|
8
|
+
"tsugite-cli==0.24.0",
|
|
9
9
|
]
|
|
10
10
|
|
|
11
11
|
[project.entry-points."tsugite.providers"]
|
|
12
12
|
codex_cli = "tsugite_codex_cli:create_provider"
|
|
13
13
|
|
|
14
|
+
[project.entry-points."tsugite.usage_providers"]
|
|
15
|
+
codex_cli = "tsugite_codex_cli.usage"
|
|
16
|
+
|
|
14
17
|
[build-system]
|
|
15
18
|
requires = ["hatchling"]
|
|
16
19
|
build-backend = "hatchling.build"
|
|
@@ -64,9 +64,9 @@ class CodexAuthStore:
|
|
|
64
64
|
|
|
65
65
|
async def get_access_token(self) -> tuple[str, str]:
|
|
66
66
|
"""Return (access_token, account_id), refreshing if near expiry."""
|
|
67
|
-
return await asyncio.to_thread(self.
|
|
67
|
+
return await asyncio.to_thread(self.sync_get_or_refresh)
|
|
68
68
|
|
|
69
|
-
def
|
|
69
|
+
def sync_get_or_refresh(self) -> tuple[str, str]:
|
|
70
70
|
if not self._auth_path.exists():
|
|
71
71
|
raise CodexAuthError(f"No Codex auth file at {self._auth_path}. Run `codex login` first.")
|
|
72
72
|
|
|
@@ -18,15 +18,9 @@ from tsugite_codex_cli.auth import CodexAuthError, CodexAuthStore
|
|
|
18
18
|
logger = logging.getLogger(__name__)
|
|
19
19
|
|
|
20
20
|
API_BASE = "https://chatgpt.com/backend-api/codex"
|
|
21
|
-
_REASONING_LEVELS = ["low", "medium", "high"]
|
|
22
|
-
# gpt-5.5 added xhigh; older codex models don't accept it.
|
|
23
|
-
_REASONING_LEVELS_V5_5 = ["low", "medium", "high", "xhigh"]
|
|
24
|
-
# gpt-5.6 added max; its sol/terra tiers and gpt-6-astra also accept ultra (delegating).
|
|
25
|
-
_REASONING_LEVELS_V5_6 = [*_REASONING_LEVELS_V5_5, "max"]
|
|
26
|
-
_REASONING_LEVELS_V5_6_AGENTIC = [*_REASONING_LEVELS_V5_6, "ultra"]
|
|
27
21
|
|
|
28
22
|
|
|
29
|
-
def _codex_model_info(max_input_tokens: int, effort_levels: list[str]
|
|
23
|
+
def _codex_model_info(max_input_tokens: int, effort_levels: list[str]) -> ModelInfo:
|
|
30
24
|
# Subscription billing means per-token cost is not surfaced here.
|
|
31
25
|
return ModelInfo(
|
|
32
26
|
max_input_tokens=max_input_tokens,
|
|
@@ -39,18 +33,22 @@ def _codex_model_info(max_input_tokens: int, effort_levels: list[str] = _REASONI
|
|
|
39
33
|
)
|
|
40
34
|
|
|
41
35
|
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
36
|
+
# --- BEGIN GENERATED MODELS (codex_cli) --- #
|
|
37
|
+
# Auto-generated by scripts/update_model_registry.py, do not edit manually
|
|
38
|
+
# fmt: off
|
|
45
39
|
_CODEX_CLI_MODELS: dict[str, ModelInfo] = {
|
|
46
|
-
"codex_cli/gpt-5.
|
|
47
|
-
"codex_cli/gpt-5.6-
|
|
48
|
-
"codex_cli/gpt-5.6-
|
|
49
|
-
"codex_cli/gpt-5.
|
|
50
|
-
"codex_cli/gpt-6-astra": _codex_model_info(272_000,
|
|
40
|
+
"codex_cli/gpt-5.5": _codex_model_info(272_000, ["low", "medium", "high", "xhigh"]),
|
|
41
|
+
"codex_cli/gpt-5.6-luna": _codex_model_info(272_000, ["low", "medium", "high", "xhigh", "max"]),
|
|
42
|
+
"codex_cli/gpt-5.6-sol": _codex_model_info(272_000, ["low", "medium", "high", "xhigh", "max", "ultra"]),
|
|
43
|
+
"codex_cli/gpt-5.6-terra": _codex_model_info(272_000, ["low", "medium", "high", "xhigh", "max", "ultra"]),
|
|
44
|
+
"codex_cli/gpt-6-astra": _codex_model_info(272_000, ["low", "medium", "high", "xhigh", "max", "ultra"]),
|
|
45
|
+
"codex_cli/gpt-6-luna": _codex_model_info(272_000, ["low", "medium", "high", "xhigh", "max"]),
|
|
46
|
+
"codex_cli/gpt-6-sol": _codex_model_info(272_000, ["low", "medium", "high", "xhigh", "max", "ultra"]),
|
|
51
47
|
}
|
|
48
|
+
# fmt: on
|
|
49
|
+
# --- END GENERATED MODELS (codex_cli) --- #
|
|
52
50
|
|
|
53
|
-
# Used when /models is unreachable
|
|
51
|
+
# Used when /models is unreachable.
|
|
54
52
|
_FALLBACK_MODELS = [k.split("/", 1)[1] for k in _CODEX_CLI_MODELS]
|
|
55
53
|
|
|
56
54
|
|
|
@@ -335,22 +333,25 @@ class CodexCliProvider:
|
|
|
335
333
|
# on parallel sessions. The client is released when the process exits.
|
|
336
334
|
pass
|
|
337
335
|
|
|
336
|
+
async def fetch_models(self) -> list[dict]:
|
|
337
|
+
access_token, account_id = await self._auth.get_access_token()
|
|
338
|
+
client = self._get_client()
|
|
339
|
+
resp = await client.get(
|
|
340
|
+
f"{API_BASE}/models?client_version=1.0.0",
|
|
341
|
+
headers=self._build_headers(access_token, account_id),
|
|
342
|
+
)
|
|
343
|
+
resp.raise_for_status()
|
|
344
|
+
# The Codex backend answers {"models":[{"slug":...}]}, not the OpenAI
|
|
345
|
+
# {"data":[{"id":...}]} shape. `visibility: hide` marks special-purpose
|
|
346
|
+
# entries (auto-review, watermark variants) the CLI keeps out of its
|
|
347
|
+
# own picker, so they stay out of ours too.
|
|
348
|
+
return [m for m in resp.json()["models"] if isinstance(m, dict) and m.get("visibility") != "hide"]
|
|
349
|
+
|
|
338
350
|
async def list_models(self) -> list[str]:
|
|
339
351
|
# Auth lookup is inside the try: model-picker UIs should always get a
|
|
340
352
|
# usable list even before `codex login` has been run.
|
|
341
353
|
try:
|
|
342
|
-
|
|
343
|
-
client = self._get_client()
|
|
344
|
-
resp = await client.get(
|
|
345
|
-
f"{API_BASE}/models?client_version=1.0.0",
|
|
346
|
-
headers=self._build_headers(access_token, account_id),
|
|
347
|
-
)
|
|
348
|
-
resp.raise_for_status()
|
|
349
|
-
# The Codex backend answers {"models":[{"slug":...}]}, not the OpenAI
|
|
350
|
-
# {"data":[{"id":...}]} shape. `visibility: hide` marks special-purpose
|
|
351
|
-
# entries (auto-review, watermark variants) the CLI keeps out of its
|
|
352
|
-
# own picker, so they stay out of ours too.
|
|
353
|
-
return [m["slug"] for m in resp.json()["models"] if isinstance(m, dict) and m.get("visibility") != "hide"]
|
|
354
|
+
return [m["slug"] for m in await self.fetch_models()]
|
|
354
355
|
except (httpx.HTTPError, KeyError, ValueError, CodexAuthError):
|
|
355
356
|
return list(_FALLBACK_MODELS)
|
|
356
357
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""ChatGPT subscription limits for Codex as a tsugite quota source.
|
|
2
|
+
|
|
3
|
+
Read from ``rate_limit`` in the ChatGPT usage endpoint, with the Codex CLI's
|
|
4
|
+
OAuth token. Each window is labeled by its duration, so a window length
|
|
5
|
+
tsugite has never seen still shows up.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import time
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
from tsugite.usage.quota import QuotaReport, QuotaWindow, register_quota_source
|
|
17
|
+
from tsugite.user_agent import set_user_agent_header
|
|
18
|
+
from tsugite_codex_cli.auth import CodexAuthStore
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
PROVIDER = "codex_cli"
|
|
23
|
+
LABEL = "Codex"
|
|
24
|
+
USAGE_URL = "https://chatgpt.com/backend-api/wham/usage"
|
|
25
|
+
LIVE_TTL_S = 60
|
|
26
|
+
|
|
27
|
+
_last_live: tuple[float, dict] | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _live_rate_limit() -> dict:
|
|
31
|
+
global _last_live
|
|
32
|
+
if _last_live and time.monotonic() - _last_live[0] < LIVE_TTL_S:
|
|
33
|
+
return _last_live[1]
|
|
34
|
+
access_token, account_id = CodexAuthStore().sync_get_or_refresh()
|
|
35
|
+
headers = {"Authorization": f"Bearer {access_token}", "ChatGPT-Account-ID": account_id}
|
|
36
|
+
set_user_agent_header(headers)
|
|
37
|
+
response = httpx.get(USAGE_URL, headers=headers, timeout=10)
|
|
38
|
+
response.raise_for_status()
|
|
39
|
+
rate_limit = response.json()["rate_limit"]
|
|
40
|
+
_last_live = (time.monotonic(), rate_limit)
|
|
41
|
+
return rate_limit
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _label(seconds: int) -> str:
|
|
45
|
+
if seconds == 7 * 24 * 3600:
|
|
46
|
+
return "week"
|
|
47
|
+
if seconds % 3600:
|
|
48
|
+
return f"{seconds // 60}m"
|
|
49
|
+
return f"{seconds // 3600}h"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _error(message: str) -> QuotaReport:
|
|
53
|
+
return QuotaReport(provider=PROVIDER, label=LABEL, error=message)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def fetch() -> QuotaReport:
|
|
57
|
+
try:
|
|
58
|
+
rate_limit = _live_rate_limit()
|
|
59
|
+
except Exception as e:
|
|
60
|
+
# A failed token refresh can quote the auth server's response body.
|
|
61
|
+
logger.warning("Codex usage unavailable: %s", type(e).__name__)
|
|
62
|
+
return _error("Codex's usage limits could not be fetched")
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
windows = tuple(
|
|
66
|
+
QuotaWindow(
|
|
67
|
+
key=key,
|
|
68
|
+
label=_label(window["limit_window_seconds"]),
|
|
69
|
+
used_pct=float(window["used_percent"]),
|
|
70
|
+
resets_at=datetime.fromtimestamp(window["reset_at"], timezone.utc).isoformat(),
|
|
71
|
+
)
|
|
72
|
+
for key in ("primary", "secondary")
|
|
73
|
+
if (window := rate_limit.get(f"{key}_window"))
|
|
74
|
+
)
|
|
75
|
+
except Exception:
|
|
76
|
+
return _error("Codex's usage limits are not in a shape tsugite understands")
|
|
77
|
+
|
|
78
|
+
return QuotaReport(provider=PROVIDER, label=LABEL, windows=windows)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
register_quota_source(PROVIDER, LABEL, fetch)
|
|
File without changes
|
|
File without changes
|