fix: ai local auth
This commit is contained in:
parent
66bbdd7398
commit
ac29c79ff2
|
|
@ -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.
|
# Path to the Claude Code OAuth credentials file, mounted read-only from the host.
|
||||||
_CLAUDE_CREDENTIALS_PATH = os.environ.get("CLAUDE_CREDENTIALS_PATH", "")
|
_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)
|
_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
|
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] = {
|
_ANTHROPIC_WINDOW_LABELS: dict[str, str] = {
|
||||||
"five_hour": "Current session",
|
"five_hour": "Current session",
|
||||||
"seven_day": "All models",
|
"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)
|
reset_dt = datetime.fromtimestamp(reset_ts, tz=timezone.utc).replace(tzinfo=None)
|
||||||
windows.append(SubscriptionWindow(
|
windows.append(SubscriptionWindow(
|
||||||
key="primary",
|
key="primary",
|
||||||
label=f"{window_hours}h",
|
label=f"Current session ({window_hours}h)",
|
||||||
pct_used=round(float(primary["used_percent"]), 1),
|
pct_used=round(float(primary["used_percent"]), 1),
|
||||||
reset_at=reset_dt,
|
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:
|
if isinstance(secondary, dict) and secondary.get("used_percent") is not None:
|
||||||
sec_seconds = secondary.get("limit_window_seconds") or 86400
|
sec_seconds = secondary.get("limit_window_seconds") or 86400
|
||||||
sec_hours = round(sec_seconds / 3600)
|
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_ts = secondary.get("reset_at")
|
||||||
reset_dt = None
|
reset_dt = None
|
||||||
if isinstance(reset_ts, (int, float)):
|
if isinstance(reset_ts, (int, float)):
|
||||||
|
|
@ -942,9 +994,11 @@ async def fetch_provider_usage(
|
||||||
provider=provider, account_key=account_key,
|
provider=provider, account_key=account_key,
|
||||||
checked_at=utcnow(), reachable=True,
|
checked_at=utcnow(), reachable=True,
|
||||||
)
|
)
|
||||||
# Overlay subscription windows when session_key available
|
# Overlay subscription windows — auto-detected Codex CLI token takes precedence
|
||||||
if session_key and result.reachable is not False:
|
local_codex = _read_codex_local_token()
|
||||||
sub_windows, plan_label = await _fetch_codex_subscription(session_key)
|
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:
|
if sub_windows:
|
||||||
result.subscription_windows = sub_windows
|
result.subscription_windows = sub_windows
|
||||||
result.subscription_plan = plan_label
|
result.subscription_plan = plan_label
|
||||||
|
|
|
||||||
|
|
@ -44,11 +44,13 @@ services:
|
||||||
LOCAL_AUTH_TOKEN: ${LOCAL_AUTH_TOKEN}
|
LOCAL_AUTH_TOKEN: ${LOCAL_AUTH_TOKEN}
|
||||||
BASE_URL: ${BASE_URL:-http://localhost:8000}
|
BASE_URL: ${BASE_URL:-http://localhost:8000}
|
||||||
RQ_REDIS_URL: redis://redis:6379/0
|
RQ_REDIS_URL: redis://redis:6379/0
|
||||||
# Claude Code credentials — read-only mount lets the backend auto-detect
|
# Claude Code credentials — read-only mount for subscription usage auto-detection.
|
||||||
# the local OAuth token for subscription usage without manual session keys.
|
|
||||||
CLAUDE_CREDENTIALS_PATH: /run/secrets/claude_credentials
|
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:
|
volumes:
|
||||||
- ${CLAUDE_CREDENTIALS_FILE:-/home/kaspa/.claude/.credentials.json}:/run/secrets/claude_credentials:ro
|
- ${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:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
|
||||||
|
|
@ -114,17 +114,15 @@ const PROVIDERS: ProviderConfig[] = [
|
||||||
"It is only shown once, so store it in a password manager",
|
"It is only shown once, so store it in a password manager",
|
||||||
],
|
],
|
||||||
showSessionKey: true,
|
showSessionKey: true,
|
||||||
sessionKeyLabel: "ChatGPT subscription token (optional — for plan usage %)",
|
sessionKeyLabel: "Subscription token (optional — auto-detected via Codex CLI)",
|
||||||
sessionKeyPlaceholder: "eyJhbGc… (Bearer token from chatgpt.com)",
|
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: [
|
sessionKeyHelp: [
|
||||||
"Sign in to chatgpt.com in Chrome or Firefox",
|
"Install the Codex CLI: npm install -g @openai/codex",
|
||||||
"Open DevTools: press F12 (Windows/Linux) or Cmd+Option+I (Mac)",
|
"Run: codex login",
|
||||||
"Click the Network tab, then refresh the page (F5)",
|
"This stores a JWT at ~/.codex/auth.json — Pipeline reads it automatically",
|
||||||
"In the filter box, type backend-api and click any request",
|
"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 \"",
|
||||||
'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",
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue