feat(providers): add ChatGPT/Codex subscription OAuth (PKCE + account-id, hardcoded catalog)

29th platform `openai-codex` (uses_oauth, Responses wire). PKCE-localhost login at
auth.openai.com (client app_EMoamEEZ73f0CkXaXp7hrann, redirect localhost:1455/auth/
callback, form token exchange, fresh-random state) reusing the claude-pro-max flow;
OAuthFlow::PkceLocalhost gained a redirect_path and OAuthConfig an authorize_extra
(empty elsewhere, so claude/xai/copilot authorize URLs stay byte-identical).

Codex-specific: the access token is a JWT carrying chatgpt_account_id, which becomes
the `chatgpt-account-id` inference header. It is derived STATELESSLY from whichever
bearer rides each request (so a rotated token needs no persisted field), and BOTH
login and refresh fail fast when the claim is absent — gated on the explicit
OAuthConfig.requires_chatgpt_account_id fact, never inferred from the token-body
encoding (a plain form endpoint is the OAuth norm and must not inherit this).

Inference rides the existing Responses wire at chatgpt.com/backend-api/codex →
/responses, with codex headers (chatgpt-account-id, originator, OpenAI-Beta
responses=experimental, codex UA) gated on SamplerConfig.openai_codex so API-key
`openai` stays byte-identical; store:false was already the global Responses default.

Catalog is HARDCODED (no live endpoint exists for this backend; read from the
official Codex CLI's model cache): gpt-5.6-sol/terra/luna + gpt-5.5, ctx 272000,
each with its real reasoning levels (low..ultra — ReasoningEffort gained Ultra).
Excluded: gpt-5.3-codex-spark (supported_in_api=false), gpt-5.4/-mini and
codex-auto-review (hidden) — they would list but fail at inference. The fetch
short-circuits before any HTTP; Kigi never shells out to the codex CLI or reads
~/.codex.

Security review fixes: redact any `account-id` header from request logs (it was
reaching debug logs), strict 3-segment JWT check (fail closed), refresh no longer
fails open on a missing claim. Inherits leak-safe pooled routing (scope
oauth/openai-codex) — never the Kimi token. Full gate green (234 suites, 0 warnings).
This commit is contained in:
2026-07-22 08:28:03 -04:00
parent 8179438278
commit 422e241e13
27 changed files with 1294 additions and 106 deletions
@@ -153,6 +153,46 @@ mod tests {
.expect("github-copilot carries an OAuthConfig")
}
fn codex_oauth() -> &'static kigi_models::OAuthConfig {
kigi_models::PlatformId::OpenaiCodex
.oauth()
.expect("openai-codex carries an OAuthConfig")
}
/// An `openai-codex/<model>` turn resolves to the process-global pooled
/// openai-codex manager (its OWN `oauth/openai-codex` scope), NEVER the
/// primary Kimi manager — the same leak-safe routing as the other OAuth
/// platforms, and a DISTINCT pool entry from each. Fail-fast: even with a
/// Kimi primary, a codex turn never yields the Kimi bearer.
#[tokio::test]
async fn openai_codex_model_resolves_to_its_own_manager_not_kimi() {
let (_kd, kimi) = primary_with_token("kimi-tok");
let home = tempfile::tempdir().unwrap();
let resolved = manager_for_model(home.path(), "openai-codex/gpt-5.5", Some(&kimi))
.expect("openai-codex model resolves to its pooled manager");
assert!(
!Arc::ptr_eq(&resolved, &kimi),
"openai-codex must NOT resolve to the Kimi manager"
);
assert!(
Arc::ptr_eq(&resolved, &global_manager_for(home.path(), codex_oauth())),
"openai-codex must resolve to its OWN process-global pooled manager"
);
assert!(
!Arc::ptr_eq(&resolved, &global_manager_for(home.path(), copilot_oauth())),
"openai-codex and github-copilot must not share a pooled manager"
);
assert!(
!Arc::ptr_eq(&resolved, &global_manager_for(home.path(), claude_oauth())),
"openai-codex and claude-pro-max must not share a pooled manager"
);
assert_ne!(
session_key_for_model(home.path(), "openai-codex/gpt-5.5", Some(&kimi)),
Some("kimi-tok".to_string()),
"an openai-codex model must never receive the primary Kimi token"
);
}
/// A `github-copilot/<model>` turn resolves to the process-global pooled
/// github-copilot manager (its OWN `oauth/github-copilot` scope), NEVER the
/// primary Kimi manager — the same leak-safe routing as xai-grok /