mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-01 21:16:12 +08:00
feat(codex): route native ChatGPT sessions through proxy takeover
Allow the built-in Codex official provider to participate in takeover mode while preserving Codex's native OAuth or API-key credentials instead of persisting them into provider records. Project official routing into a dedicated TOML provider, normalize inline tables, clean stale managed placeholders, and fail closed when the live configuration cannot be transformed safely. Validate forwarded authorization, make official 401/403 responses non-retryable, avoid circuit-breaker pollution, and share the first-party ChatGPT endpoint across the Codex and Claude adapters.
This commit is contained in:
@@ -666,7 +666,7 @@ impl ProviderAdapter for ClaudeAdapter {
|
||||
fn extract_base_url(&self, provider: &Provider) -> Result<String, ProxyError> {
|
||||
// Codex OAuth: 强制使用 ChatGPT 后端 API 端点(忽略用户配置的 base_url)
|
||||
if self.is_codex_oauth(provider) {
|
||||
return Ok("https://chatgpt.com/backend-api/codex".to_string());
|
||||
return Ok(super::CHATGPT_CODEX_BASE_URL.to_string());
|
||||
}
|
||||
|
||||
// 1. 从 env 中获取
|
||||
@@ -778,9 +778,9 @@ impl ProviderAdapter for ClaudeAdapter {
|
||||
|
||||
fn build_url(&self, base_url: &str, endpoint: &str) -> String {
|
||||
// Codex OAuth: 所有请求统一走 /responses 端点
|
||||
if base_url == "https://chatgpt.com/backend-api/codex" {
|
||||
if base_url == super::CHATGPT_CODEX_BASE_URL {
|
||||
let _ = endpoint; // 忽略原始 endpoint
|
||||
return "https://chatgpt.com/backend-api/codex/responses".to_string();
|
||||
return format!("{}/responses", super::CHATGPT_CODEX_BASE_URL);
|
||||
}
|
||||
|
||||
// NOTE:
|
||||
|
||||
@@ -206,6 +206,14 @@ pub fn should_convert_codex_responses_to_anthropic(provider: &Provider, endpoint
|
||||
) && codex_provider_uses_anthropic(provider)
|
||||
}
|
||||
|
||||
/// The single built-in official Codex provider. Unlike managed Codex OAuth
|
||||
/// providers used by Claude, this route receives authentication from the
|
||||
/// calling Codex client (`requires_openai_auth = true`).
|
||||
pub fn is_codex_official_provider(provider: &Provider) -> bool {
|
||||
provider.id == crate::database::CODEX_OFFICIAL_PROVIDER_ID
|
||||
&& provider.category.as_deref() == Some("official")
|
||||
}
|
||||
|
||||
/// Resolve the model-catalog tool profile for a Codex provider using the SAME
|
||||
/// Anthropic detection as the proxy router ([`codex_provider_uses_anthropic`]), so the
|
||||
/// generated catalog never disagrees with the routed transform. A provider whose
|
||||
@@ -217,6 +225,9 @@ pub fn resolve_codex_catalog_tool_profile(
|
||||
provider: &Provider,
|
||||
) -> crate::codex_config::CodexCatalogToolProfile {
|
||||
use crate::codex_config::CodexCatalogToolProfile;
|
||||
if is_codex_official_provider(provider) {
|
||||
return CodexCatalogToolProfile::NativeResponses;
|
||||
}
|
||||
if codex_provider_uses_anthropic(provider) {
|
||||
return CodexCatalogToolProfile::Anthropic;
|
||||
}
|
||||
@@ -634,6 +645,10 @@ impl ProviderAdapter for CodexAdapter {
|
||||
}
|
||||
|
||||
fn extract_base_url(&self, provider: &Provider) -> Result<String, ProxyError> {
|
||||
if is_codex_official_provider(provider) {
|
||||
return Ok(super::CHATGPT_CODEX_BASE_URL.to_string());
|
||||
}
|
||||
|
||||
// 1. 尝试直接获取 base_url 字段
|
||||
if let Some(url) = provider
|
||||
.settings_config
|
||||
@@ -781,6 +796,30 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn official_provider_uses_fixed_chatgpt_backend_without_stored_key() {
|
||||
let mut provider = create_provider(json!({ "auth": {}, "config": "" }));
|
||||
provider.id = "codex-official".to_string();
|
||||
provider.category = Some("official".to_string());
|
||||
let adapter = CodexAdapter::new();
|
||||
|
||||
assert!(is_codex_official_provider(&provider));
|
||||
assert_eq!(
|
||||
adapter
|
||||
.extract_base_url(&provider)
|
||||
.expect("official base url"),
|
||||
"https://chatgpt.com/backend-api/codex"
|
||||
);
|
||||
assert!(adapter.extract_auth(&provider).is_none());
|
||||
assert_eq!(
|
||||
adapter.build_url(
|
||||
"https://chatgpt.com/backend-api/codex",
|
||||
"/responses/compact"
|
||||
),
|
||||
"https://chatgpt.com/backend-api/codex/responses/compact"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_cache_routing_auto_enables_known_upstreams_only() {
|
||||
let kimi = create_provider(json!({
|
||||
|
||||
@@ -41,6 +41,8 @@ use crate::app_config::AppType;
|
||||
use crate::provider::Provider;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const CHATGPT_CODEX_BASE_URL: &str = "https://chatgpt.com/backend-api/codex";
|
||||
|
||||
// 公开导出
|
||||
pub use adapter::ProviderAdapter;
|
||||
pub use auth::{AuthInfo, AuthStrategy};
|
||||
@@ -52,9 +54,9 @@ pub use claude::{
|
||||
pub use codex::CodexAdapter;
|
||||
pub use codex::{
|
||||
apply_codex_chat_upstream_model, apply_codex_upstream_model, codex_provider_upstream_model,
|
||||
inject_codex_chat_prompt_cache_key, resolve_codex_catalog_tool_profile,
|
||||
resolve_codex_chat_reasoning_config, should_convert_codex_responses_to_anthropic,
|
||||
should_convert_codex_responses_to_chat,
|
||||
inject_codex_chat_prompt_cache_key, is_codex_official_provider,
|
||||
resolve_codex_catalog_tool_profile, resolve_codex_chat_reasoning_config,
|
||||
should_convert_codex_responses_to_anthropic, should_convert_codex_responses_to_chat,
|
||||
};
|
||||
pub use gemini::GeminiAdapter;
|
||||
|
||||
@@ -110,7 +112,7 @@ impl ProviderType {
|
||||
}
|
||||
ProviderType::OpenRouter => "https://openrouter.ai/api",
|
||||
ProviderType::GitHubCopilot => "https://api.githubcopilot.com",
|
||||
ProviderType::CodexOAuth => "https://chatgpt.com/backend-api/codex",
|
||||
ProviderType::CodexOAuth => CHATGPT_CODEX_BASE_URL,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user