fix: ai local auth

This commit is contained in:
null 2026-05-21 19:50:19 -05:00
parent 66bbdd7398
commit ac29c79ff2
3 changed files with 71 additions and 17 deletions

View File

@ -645,6 +645,9 @@ _CODEX_SUBSCRIPTION_URL = "https://chatgpt.com/backend-api/wham/usage"
# Path to the Claude Code OAuth credentials file, mounted read-only from the host.
_CLAUDE_CREDENTIALS_PATH = os.environ.get("CLAUDE_CREDENTIALS_PATH", "")
# Path to the Codex CLI auth file, mounted read-only from the host.
_CODEX_CREDENTIALS_PATH = os.environ.get("CODEX_CREDENTIALS_PATH", "")
_claude_oauth_cache: tuple[float, str] | None = None # (expires_at_ms, access_token)
@ -688,6 +691,55 @@ def _read_claude_local_oauth_token() -> str | None:
return access_token
def _read_codex_local_token() -> str | None:
"""Read the Codex CLI JWT access token from the host auth file (~/.codex/auth.json).
The Codex CLI stores a long-lived JWT (typically ~9 days) issued by OpenAI's
OAuth flow. This token works as a Bearer credential for the ChatGPT backend
API, including the wham/usage subscription endpoint.
Returns the access_token string, or None if the file is absent, unreadable,
or the token has expired. The caller should fall back to the manually
configured session_key when this returns None.
"""
path = _CODEX_CREDENTIALS_PATH
if not path:
home = os.path.expanduser("~")
path = os.path.join(home, ".codex", "auth.json")
try:
with open(path) as fh:
data = _json_module.load(fh)
except (FileNotFoundError, PermissionError, ValueError, OSError):
return None
tokens = data.get("tokens")
if not isinstance(tokens, dict):
return None
access_token = tokens.get("access_token")
if not isinstance(access_token, str) or not access_token:
return None
# Decode the JWT payload (no signature verification needed here — we just
# want to avoid using a token we know is expired before making a network call).
parts = access_token.split(".")
if len(parts) >= 2:
import base64 as _base64
try:
pad = parts[1] + "=" * (4 - len(parts[1]) % 4)
payload = _json_module.loads(_base64.urlsafe_b64decode(pad))
exp = payload.get("exp")
if isinstance(exp, (int, float)) and exp <= _time_module.time():
logger.debug("provider_usage.codex_oauth.token_expired exp=%s", exp)
return None
except Exception:
pass # Can't decode JWT — proceed optimistically
return access_token
_ANTHROPIC_WINDOW_LABELS: dict[str, str] = {
"five_hour": "Current session",
"seven_day": "All models",
@ -810,7 +862,7 @@ async def _fetch_codex_subscription(session_key: str) -> tuple[list[Subscription
reset_dt = datetime.fromtimestamp(reset_ts, tz=timezone.utc).replace(tzinfo=None)
windows.append(SubscriptionWindow(
key="primary",
label=f"{window_hours}h",
label=f"Current session ({window_hours}h)",
pct_used=round(float(primary["used_percent"]), 1),
reset_at=reset_dt,
))
@ -819,7 +871,7 @@ async def _fetch_codex_subscription(session_key: str) -> tuple[list[Subscription
if isinstance(secondary, dict) and secondary.get("used_percent") is not None:
sec_seconds = secondary.get("limit_window_seconds") or 86400
sec_hours = round(sec_seconds / 3600)
sec_label = "Week" if sec_hours >= 168 else f"{sec_hours}h"
sec_label = "All models (weekly)" if sec_hours >= 168 else f"All models ({sec_hours}h)"
reset_ts = secondary.get("reset_at")
reset_dt = None
if isinstance(reset_ts, (int, float)):
@ -942,9 +994,11 @@ async def fetch_provider_usage(
provider=provider, account_key=account_key,
checked_at=utcnow(), reachable=True,
)
# Overlay subscription windows when session_key available
if session_key and result.reachable is not False:
sub_windows, plan_label = await _fetch_codex_subscription(session_key)
# Overlay subscription windows — auto-detected Codex CLI token takes precedence
local_codex = _read_codex_local_token()
effective_codex_key = local_codex or session_key
if effective_codex_key and result.reachable is not False:
sub_windows, plan_label = await _fetch_codex_subscription(effective_codex_key)
if sub_windows:
result.subscription_windows = sub_windows
result.subscription_plan = plan_label

View File

@ -44,11 +44,13 @@ services:
LOCAL_AUTH_TOKEN: ${LOCAL_AUTH_TOKEN}
BASE_URL: ${BASE_URL:-http://localhost:8000}
RQ_REDIS_URL: redis://redis:6379/0
# Claude Code credentials — read-only mount lets the backend auto-detect
# the local OAuth token for subscription usage without manual session keys.
# Claude Code credentials — read-only mount for subscription usage auto-detection.
CLAUDE_CREDENTIALS_PATH: /run/secrets/claude_credentials
# Codex CLI credentials — read-only mount for ChatGPT subscription usage auto-detection.
CODEX_CREDENTIALS_PATH: /run/secrets/codex_credentials
volumes:
- ${CLAUDE_CREDENTIALS_FILE:-/home/kaspa/.claude/.credentials.json}:/run/secrets/claude_credentials:ro
- ${CODEX_CREDENTIALS_FILE:-/home/kaspa/.codex/auth.json}:/run/secrets/codex_credentials:ro
depends_on:
db:
condition: service_healthy

View File

@ -114,17 +114,15 @@ const PROVIDERS: ProviderConfig[] = [
"It is only shown once, so store it in a password manager",
],
showSessionKey: true,
sessionKeyLabel: "ChatGPT subscription token (optional — for plan usage %)",
sessionKeyPlaceholder: "eyJhbGc… (Bearer token from chatgpt.com)",
sessionKeyLabel: "Subscription token (optional — auto-detected via Codex CLI)",
sessionKeyPlaceholder: "eyJhbGc… (leave blank to auto-detect)",
autoDetectedNote:
"Subscription usage (current 5h window %, weekly %) is auto-detected from your local Codex CLI login at ~/.codex/auth.json. You only need to fill this in if Pipeline is running on a different machine from the Codex CLI, or the auto-detected token is not working.",
sessionKeyHelp: [
"Sign in to chatgpt.com in Chrome or Firefox",
"Open DevTools: press F12 (Windows/Linux) or Cmd+Option+I (Mac)",
"Click the Network tab, then refresh the page (F5)",
"In the filter box, type backend-api and click any request",
'In the right panel, choose "Headers" → "Request Headers"',
'Find the "Authorization" header — its value starts with "Bearer eyJhbG…"',
'Copy just the token part after the word "Bearer " (do not include "Bearer " itself)',
"Paste it here. Tokens expire when you log out of ChatGPT",
"Install the Codex CLI: npm install -g @openai/codex",
"Run: codex login",
"This stores a JWT at ~/.codex/auth.json — Pipeline reads it automatically",
"If you prefer to paste a token manually: sign in to chatgpt.com, open DevTools (F12), Network tab, filter to backend-api, click any request, find the Authorization header, copy the token after \"Bearer \"",
],
},
{