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:
@@ -6894,7 +6894,7 @@ pub(crate) mod tests {
|
||||
#[test]
|
||||
fn pending_menu_items_lists_interactive_methods_plus_quit() {
|
||||
let items = pending_menu_items(&fresh_user_auth_methods(), None);
|
||||
assert_eq!(items.len(), 29, "28 login rows + Quit, got {items:?}");
|
||||
assert_eq!(items.len(), 30, "29 login rows + Quit, got {items:?}");
|
||||
assert!(
|
||||
matches!(&items[0], PendingMenuItem::Login { label } if label == "Kimi Code (OAuth)"),
|
||||
"row 0 must be the OAuth login, got {:?}",
|
||||
@@ -6915,22 +6915,27 @@ pub(crate) mod tests {
|
||||
"row 3 must be the github-copilot OAuth login (after claude-pro-max), got {:?}",
|
||||
items[3]
|
||||
);
|
||||
assert!(
|
||||
matches!(&items[4], PendingMenuItem::Login { label } if label == "ChatGPT Plus/Pro (Codex) (OAuth)"),
|
||||
"row 4 must be the openai-codex OAuth login (after github-copilot), got {:?}",
|
||||
items[4]
|
||||
);
|
||||
assert_eq!(
|
||||
items[4],
|
||||
items[5],
|
||||
PendingMenuItem::ApiKey {
|
||||
target: PlatformLogin(kigi_shell::models::PlatformId::MoonshotCn),
|
||||
label: "Moonshot Open Platform (API key \u{b7} moonshot.cn)".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
items[5],
|
||||
items[6],
|
||||
PendingMenuItem::ApiKey {
|
||||
target: PlatformLogin(kigi_shell::models::PlatformId::MoonshotAi),
|
||||
label: "Moonshot Open Platform (API key \u{b7} moonshot.ai)".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
items[6],
|
||||
items[7],
|
||||
PendingMenuItem::ApiKey {
|
||||
target: PlatformLogin(kigi_shell::models::PlatformId::OpenAi),
|
||||
label: "OpenAI (API key)".into(),
|
||||
@@ -6938,153 +6943,153 @@ pub(crate) mod tests {
|
||||
"new registry rows must appear in the picker with zero TUI changes"
|
||||
);
|
||||
assert_eq!(
|
||||
items[7],
|
||||
items[8],
|
||||
PendingMenuItem::ApiKey {
|
||||
target: PlatformLogin(kigi_shell::models::PlatformId::Anthropic),
|
||||
label: "Anthropic (API key)".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
items[8],
|
||||
items[9],
|
||||
PendingMenuItem::ApiKey {
|
||||
target: PlatformLogin(kigi_shell::models::PlatformId::DeepSeek),
|
||||
label: "DeepSeek (API key)".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
items[9],
|
||||
items[10],
|
||||
PendingMenuItem::ApiKey {
|
||||
target: PlatformLogin(kigi_shell::models::PlatformId::Groq),
|
||||
label: "Groq (API key)".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
items[10],
|
||||
items[11],
|
||||
PendingMenuItem::ApiKey {
|
||||
target: PlatformLogin(kigi_shell::models::PlatformId::Mistral),
|
||||
label: "Mistral (API key)".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
items[11],
|
||||
items[12],
|
||||
PendingMenuItem::ApiKey {
|
||||
target: PlatformLogin(kigi_shell::models::PlatformId::Fireworks),
|
||||
label: "Fireworks AI (API key)".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
items[12],
|
||||
items[13],
|
||||
PendingMenuItem::ApiKey {
|
||||
target: PlatformLogin(kigi_shell::models::PlatformId::Google),
|
||||
label: "Google Gemini (API key)".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
items[13],
|
||||
items[14],
|
||||
PendingMenuItem::ApiKey {
|
||||
target: PlatformLogin(kigi_shell::models::PlatformId::OpenRouter),
|
||||
label: "OpenRouter (API key)".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
items[14],
|
||||
items[15],
|
||||
PendingMenuItem::ApiKey {
|
||||
target: PlatformLogin(kigi_shell::models::PlatformId::Together),
|
||||
label: "Together AI (API key)".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
items[15],
|
||||
items[16],
|
||||
PendingMenuItem::ApiKey {
|
||||
target: PlatformLogin(kigi_shell::models::PlatformId::Cerebras),
|
||||
label: "Cerebras (API key)".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
items[16],
|
||||
items[17],
|
||||
PendingMenuItem::ApiKey {
|
||||
target: PlatformLogin(kigi_shell::models::PlatformId::Nvidia),
|
||||
label: "NVIDIA NIM (API key)".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
items[17],
|
||||
items[18],
|
||||
PendingMenuItem::ApiKey {
|
||||
target: PlatformLogin(kigi_shell::models::PlatformId::Vercel),
|
||||
label: "Vercel AI Gateway (API key)".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
items[18],
|
||||
items[19],
|
||||
PendingMenuItem::ApiKey {
|
||||
target: PlatformLogin(kigi_shell::models::PlatformId::Xai),
|
||||
label: "xAI (Grok) (API key)".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
items[19],
|
||||
items[20],
|
||||
PendingMenuItem::ApiKey {
|
||||
target: PlatformLogin(kigi_shell::models::PlatformId::QwenTokenPlan),
|
||||
label: "Qwen Token Plan (API key)".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
items[20],
|
||||
items[21],
|
||||
PendingMenuItem::ApiKey {
|
||||
target: PlatformLogin(kigi_shell::models::PlatformId::QwenTokenPlanCn),
|
||||
label: "Qwen Token Plan China (API key)".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
items[21],
|
||||
items[22],
|
||||
PendingMenuItem::ApiKey {
|
||||
target: PlatformLogin(kigi_shell::models::PlatformId::KimiCoding),
|
||||
label: "Kimi For Coding (API key)".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
items[22],
|
||||
items[23],
|
||||
PendingMenuItem::ApiKey {
|
||||
target: PlatformLogin(kigi_shell::models::PlatformId::Zai),
|
||||
label: "Z.AI (API key)".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
items[23],
|
||||
items[24],
|
||||
PendingMenuItem::ApiKey {
|
||||
target: PlatformLogin(kigi_shell::models::PlatformId::ZaiCodingCn),
|
||||
label: "Z.AI Coding China (API key)".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
items[24],
|
||||
items[25],
|
||||
PendingMenuItem::ApiKey {
|
||||
target: PlatformLogin(kigi_shell::models::PlatformId::Xiaomi),
|
||||
label: "Xiaomi MiMo (API key)".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
items[25],
|
||||
items[26],
|
||||
PendingMenuItem::ApiKey {
|
||||
target: PlatformLogin(kigi_shell::models::PlatformId::XiaomiTokenPlanCn),
|
||||
label: "Xiaomi Token Plan China (API key)".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
items[26],
|
||||
items[27],
|
||||
PendingMenuItem::ApiKey {
|
||||
target: PlatformLogin(kigi_shell::models::PlatformId::Minimax),
|
||||
label: "MiniMax (API key)".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
items[27],
|
||||
items[28],
|
||||
PendingMenuItem::ApiKey {
|
||||
target: PlatformLogin(kigi_shell::models::PlatformId::MinimaxCn),
|
||||
label: "MiniMax China (API key)".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(items[28], PendingMenuItem::Quit);
|
||||
assert_eq!(items[29], PendingMenuItem::Quit);
|
||||
// The non-interactive methods must never appear as rows.
|
||||
let byok = kigi_shell::agent::auth_method::build_auth_methods(
|
||||
kigi_shell::agent::auth_method::AuthMethodsBuildInputs {
|
||||
@@ -7095,8 +7100,8 @@ pub(crate) mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
pending_menu_items(&byok.methods, None).len(),
|
||||
29,
|
||||
"xai.api_key / cached_token must not add rows (28 login rows + Quit)"
|
||||
30,
|
||||
"xai.api_key / cached_token must not add rows (29 login rows + Quit)"
|
||||
);
|
||||
}
|
||||
/// Startup lands on the picker only when there is a real choice: the
|
||||
@@ -7118,8 +7123,10 @@ pub(crate) mod tests {
|
||||
app.auth_state = AuthState::Pending { error: None };
|
||||
app.welcome_prompt_focused = false;
|
||||
// Interactive OAuth logins come first: row 0 (kimi-code), row 1
|
||||
// (xai-grok), row 2 (claude-pro-max), row 3 (github-copilot); the first
|
||||
// API-key row (moonshot-cn) is now row 4, so five Downs land on it.
|
||||
// (xai-grok), row 2 (claude-pro-max), row 3 (github-copilot), row 4
|
||||
// (openai-codex); the first API-key row (moonshot-cn) is now row 5, so
|
||||
// six Downs land on it.
|
||||
app.handle_input(&key_event(KeyCode::Down, KeyModifiers::NONE));
|
||||
app.handle_input(&key_event(KeyCode::Down, KeyModifiers::NONE));
|
||||
app.handle_input(&key_event(KeyCode::Down, KeyModifiers::NONE));
|
||||
app.handle_input(&key_event(KeyCode::Down, KeyModifiers::NONE));
|
||||
|
||||
@@ -25,6 +25,7 @@ pub(crate) fn effort_description(level: ReasoningEffort) -> &'static str {
|
||||
ReasoningEffort::High => "Heavy reasoning",
|
||||
ReasoningEffort::Xhigh => "Extra-heavy reasoning",
|
||||
ReasoningEffort::Max => "Maximum reasoning",
|
||||
ReasoningEffort::Ultra => "Ultra reasoning",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2118,6 +2118,10 @@ mod tests {
|
||||
text.contains("GitHub Copilot (subscription) (OAuth)"),
|
||||
"the github-copilot interactive OAuth login row must render: {text}"
|
||||
);
|
||||
assert!(
|
||||
text.contains("ChatGPT Plus/Pro (Codex) (OAuth)"),
|
||||
"the openai-codex interactive OAuth login row must render: {text}"
|
||||
);
|
||||
assert!(
|
||||
text.contains("Moonshot Open Platform (API key \u{b7} moonshot.cn)"),
|
||||
"{text}"
|
||||
|
||||
Reference in New Issue
Block a user