From 12319817473f8c799663d1f66bb8969421037cfc Mon Sep 17 00:00:00 2001 From: qiufuyuTony <60765480+qiufuyu123@users.noreply.github.com> Date: Wed, 16 Sep 2026 18:21:39 +0000 Subject: [PATCH] fix(codex): align OAuth client identity for GPT-6 --- .../proxy/forwarder/tests/request_building.rs | 2 +- src-tauri/src/proxy/providers/claude.rs | 34 ++++++++++-- .../src/proxy/providers/codex_oauth_auth.rs | 6 ++ src-tauri/src/services/codex_oauth_models.rs | 55 ++++++++++++++++--- 4 files changed, 82 insertions(+), 15 deletions(-) diff --git a/src-tauri/src/proxy/forwarder/tests/request_building.rs b/src-tauri/src/proxy/forwarder/tests/request_building.rs index 4c05f154a..f941c4269 100644 --- a/src-tauri/src/proxy/forwarder/tests/request_building.rs +++ b/src-tauri/src/proxy/forwarder/tests/request_building.rs @@ -806,7 +806,7 @@ async fn codex_oauth_prepare_request_ignores_stale_full_url_and_injects_bound_ac Some("acc-bound") ); assert_eq!(header_value(&request, "originator"), Some("codex_cli_rs")); - assert_eq!(header_value(&request, "version"), Some("0.144.1")); + assert_eq!(header_value(&request, "version"), Some("0.153.4")); assert_eq!(header_value(&request, "anthropic-beta"), None); assert_eq!(header_value(&request, "anthropic-version"), None); } diff --git a/src-tauri/src/proxy/providers/claude.rs b/src-tauri/src/proxy/providers/claude.rs index 86605e393..f8cb4565e 100644 --- a/src-tauri/src/proxy/providers/claude.rs +++ b/src-tauri/src/proxy/providers/claude.rs @@ -4,8 +4,10 @@ use serde_json::{json, Value}; use crate::{provider::Provider, proxy::error::ProxyError}; use super::{ - gemini_shadow::GeminiShadowStore, transform_gemini::AnthropicToolSchemaHints, AuthInfo, - AuthStrategy, ProviderAdapter, ProviderType, + codex_oauth_auth::{CODEX_OAUTH_CLIENT_VERSION, CODEX_OAUTH_ORIGINATOR}, + gemini_shadow::GeminiShadowStore, + transform_gemini::AnthropicToolSchemaHints, + AuthInfo, AuthStrategy, ProviderAdapter, ProviderType, }; pub struct ClaudeAdapter; @@ -13,10 +15,6 @@ pub struct ClaudeAdapter; const ANTHROPIC_THINKING_PLACEHOLDER: &str = "tool call"; const ANTHROPIC_REDACTED_THINKING_PLACEHOLDER: &str = "[redacted thinking]"; const REASONING_VENDOR_HINTS: &[&str] = &["moonshot", "kimi", "deepseek", "mimo", "xiaomimimo"]; -// ChatGPT Codex selects model cohorts from this header pair. Keep both values -// aligned with a real Codex CLI release new enough for the newest preset model. -const CODEX_OAUTH_ORIGINATOR: &str = "codex_cli_rs"; -const CODEX_OAUTH_CLIENT_VERSION: &str = "0.144.1"; pub fn get_claude_api_format(provider: &Provider) -> &'static str { if let Some(meta) = provider.meta.as_ref() { @@ -912,6 +910,30 @@ mod tests { assert!(adapter.needs_transform(&provider)); } + #[test] + fn codex_oauth_generation_uses_gpt6_compatible_identity() { + let request = ClaudeAdapter::new() + .add_auth_headers( + reqwest::Client::new().get("https://example.com"), + &AuthInfo::new("test-token".into(), AuthStrategy::CodexOAuth), + ) + .build() + .unwrap(); + assert_eq!(request.headers()["authorization"], "Bearer test-token"); + assert_eq!(request.headers()["originator"], "codex_cli_rs"); + let version: Vec = request.headers()["version"] + .to_str() + .unwrap() + .split('.') + .map(|part| part.parse().unwrap()) + .collect(); + // Official rust-v0.153.4 catalog: gpt-6-astra requires 0.153.0. + assert!( + version.as_slice() >= [0, 153, 0].as_slice(), + "gpt-6-astra requires Codex >= 0.153.0; sent {version:?}" + ); + } + #[test] fn gemini_native_oauth_access_token_is_trimmed_and_classified() { let adapter = ClaudeAdapter::new(); diff --git a/src-tauri/src/proxy/providers/codex_oauth_auth.rs b/src-tauri/src/proxy/providers/codex_oauth_auth.rs index 3fe7c09aa..e0cc2974b 100644 --- a/src-tauri/src/proxy/providers/codex_oauth_auth.rs +++ b/src-tauri/src/proxy/providers/codex_oauth_auth.rs @@ -17,6 +17,12 @@ const DEVICE_CODE_DEFAULT_EXPIRES_IN: u64 = 900; const POLLING_SAFETY_MARGIN_SECS: u64 = 3; const CODEX_USER_AGENT: &str = "cc-switch-codex-oauth"; +// Shared by model discovery and generation: ChatGPT gates models by this +// client identity. gpt-6-astra requires >= 0.153.0 in the rust-v0.153.4 catalog. +// Bump together when a new model raises its minimal_client_version. +pub(crate) const CODEX_OAUTH_ORIGINATOR: &str = "codex_cli_rs"; +pub(crate) const CODEX_OAUTH_CLIENT_VERSION: &str = "0.153.4"; + #[derive(Debug, thiserror::Error)] pub enum CodexOAuthError { #[error("等待用户授权中")] diff --git a/src-tauri/src/services/codex_oauth_models.rs b/src-tauri/src/services/codex_oauth_models.rs index 04f788966..dddde20d7 100644 --- a/src-tauri/src/services/codex_oauth_models.rs +++ b/src-tauri/src/services/codex_oauth_models.rs @@ -3,6 +3,9 @@ //! ChatGPT Codex exposes models through `chatgpt.com/backend-api/codex/models`, //! which is not an OpenAI-compatible `/v1/models` endpoint. +use crate::proxy::providers::codex_oauth_auth::{ + CODEX_OAUTH_CLIENT_VERSION, CODEX_OAUTH_ORIGINATOR, +}; use crate::services::model_fetch::FetchedModel; use serde_json::Value; use std::time::Duration; @@ -10,20 +13,13 @@ use std::time::Duration; const CODEX_OAUTH_MODELS_URL: &str = "https://chatgpt.com/backend-api/codex/models"; const CODEX_OAUTH_FETCH_TIMEOUT_SECS: u64 = 15; const ERROR_BODY_MAX_CHARS: usize = 512; -const CODEX_OAUTH_CLIENT_VERSION: &str = env!("CARGO_PKG_VERSION"); pub async fn fetch_models_with_token( token: &str, account_id: &str, ) -> Result, String> { let client = crate::proxy::http_client::get(); - let response = client - .get(CODEX_OAUTH_MODELS_URL) - .query(&[("client_version", CODEX_OAUTH_CLIENT_VERSION)]) - .header("Authorization", format!("Bearer {token}")) - .header("originator", "cc-switch") - .header("chatgpt-account-id", account_id) - .timeout(Duration::from_secs(CODEX_OAUTH_FETCH_TIMEOUT_SECS)) + let response = build_models_request(&client, token, account_id) .send() .await .map_err(|e| format!("Request failed: {e}"))?; @@ -42,6 +38,21 @@ pub async fn fetch_models_with_token( Ok(parse_models(value)) } +fn build_models_request( + client: &reqwest::Client, + token: &str, + account_id: &str, +) -> reqwest::RequestBuilder { + client + .get(CODEX_OAUTH_MODELS_URL) + .query(&[("client_version", CODEX_OAUTH_CLIENT_VERSION)]) + .header("Authorization", format!("Bearer {token}")) + .header("originator", CODEX_OAUTH_ORIGINATOR) + .header("version", CODEX_OAUTH_CLIENT_VERSION) + .header("chatgpt-account-id", account_id) + .timeout(Duration::from_secs(CODEX_OAUTH_FETCH_TIMEOUT_SECS)) +} + fn parse_models(value: Value) -> Vec { let entries = value .get("data") @@ -131,6 +142,34 @@ mod tests { use super::*; use serde_json::json; + #[test] + fn codex_oauth_model_discovery_uses_gpt6_compatible_identity() { + let request = build_models_request(&reqwest::Client::new(), "test-token", "test-account") + .build() + .unwrap(); + assert_eq!(request.headers()["authorization"], "Bearer test-token"); + assert_eq!(request.headers()["chatgpt-account-id"], "test-account"); + assert_eq!(request.headers()["originator"], "codex_cli_rs"); + let version = request + .url() + .query_pairs() + .find(|(key, _)| key == "client_version") + .unwrap() + .1 + .into_owned(); + let parts: Vec = version + .split('.') + .map(|part| part.parse().unwrap()) + .collect(); + // Official rust-v0.153.4 catalog: gpt-6-astra requires 0.153.0. + assert!(parts.as_slice() >= [0, 153, 0].as_slice()); + assert_eq!(request.headers()["version"], version); + let models = parse_models(json!({"models": [{ + "slug": "gpt-6-astra", "minimal_client_version": "0.153.0" + }]})); + assert_eq!(models[0].id, "gpt-6-astra"); + } + #[test] fn parse_codex_oauth_models_accepts_openai_style_data() { let models = parse_models(json!({