mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
Add Codex Chat-to-Responses bridge
- Add Codex provider API format selection and model mapping for Chat-only upstreams. - Convert Codex Responses requests to Chat Completions and rebuild Chat responses as Responses output. - Preserve reasoning_content across non-streaming, streaming, tool calls, and previous_response_id follow-ups. - Add a bounded Codex Chat history cache for restoring tool calls before tool outputs. - Cover Chat bridge, compact routing, streaming, and history recovery with focused tests.
This commit is contained in:
@@ -9,6 +9,7 @@ use super::{AuthInfo, AuthStrategy, ProviderAdapter};
|
||||
use crate::provider::Provider;
|
||||
use crate::proxy::error::ProxyError;
|
||||
use regex::Regex;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::sync::LazyLock;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
@@ -20,6 +21,13 @@ static CODEX_CLIENT_REGEX: LazyLock<Regex> =
|
||||
/// Codex 适配器
|
||||
pub struct CodexAdapter;
|
||||
|
||||
/// Local model written into Codex config.toml for Chat-only upstreams.
|
||||
///
|
||||
/// Codex itself must see a model it can load metadata for; the real provider
|
||||
/// model is restored inside the proxy immediately before Chat Completions
|
||||
/// conversion.
|
||||
pub const CODEX_CHAT_CLIENT_MODEL: &str = "gpt-5.4";
|
||||
|
||||
/// Whether this Codex provider's real upstream should be called through
|
||||
/// OpenAI Chat Completions, even if the local Codex client is talking to CC
|
||||
/// Switch through the Responses API.
|
||||
@@ -82,6 +90,39 @@ pub fn should_convert_codex_responses_to_chat(provider: &Provider, endpoint: &st
|
||||
) && codex_provider_uses_chat_completions(provider)
|
||||
}
|
||||
|
||||
/// Extract the real upstream model configured for a Codex provider.
|
||||
pub fn codex_provider_upstream_model(provider: &Provider) -> Option<String> {
|
||||
provider
|
||||
.settings_config
|
||||
.get("model")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|model| !model.is_empty())
|
||||
.map(ToString::to_string)
|
||||
.or_else(|| {
|
||||
provider
|
||||
.settings_config
|
||||
.get("config")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(extract_codex_model_from_toml)
|
||||
})
|
||||
}
|
||||
|
||||
/// For Codex Chat providers, replace the local Codex-safe model with the real
|
||||
/// upstream model before converting the request to Chat Completions.
|
||||
pub fn apply_codex_chat_upstream_model(
|
||||
provider: &Provider,
|
||||
body: &mut JsonValue,
|
||||
) -> Option<String> {
|
||||
if !codex_provider_uses_chat_completions(provider) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let upstream_model = codex_provider_upstream_model(provider)?;
|
||||
body["model"] = JsonValue::String(upstream_model.clone());
|
||||
Some(upstream_model)
|
||||
}
|
||||
|
||||
fn is_chat_wire_api(value: &str) -> bool {
|
||||
matches!(
|
||||
value.trim().to_ascii_lowercase().as_str(),
|
||||
@@ -120,6 +161,16 @@ fn extract_codex_wire_api_from_toml(config_text: &str) -> Option<String> {
|
||||
.map(ToString::to_string)
|
||||
}
|
||||
|
||||
fn extract_codex_model_from_toml(config_text: &str) -> Option<String> {
|
||||
let doc = config_text.parse::<TomlValue>().ok()?;
|
||||
|
||||
doc.get("model")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|model| !model.is_empty())
|
||||
.map(ToString::to_string)
|
||||
}
|
||||
|
||||
fn extract_codex_base_url_from_toml(config_text: &str) -> Option<String> {
|
||||
let doc = config_text.parse::<TomlValue>().ok()?;
|
||||
|
||||
@@ -481,4 +532,51 @@ wire_api = "chat"
|
||||
"/responses/compact?stream=true"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_codex_provider_uses_chat_completions_from_meta_api_format_for_responses() {
|
||||
let mut provider = create_provider(json!({
|
||||
"base_url": "https://api.deepseek.com/v1"
|
||||
}));
|
||||
provider.meta = Some(crate::provider::ProviderMeta {
|
||||
api_format: Some("openai_chat".to_string()),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
assert!(should_convert_codex_responses_to_chat(
|
||||
&provider,
|
||||
"/v1/responses"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_codex_chat_upstream_model_uses_provider_config_model() {
|
||||
let mut provider = create_provider(json!({
|
||||
"config": r#"
|
||||
model_provider = "deepseek"
|
||||
model = "deepseek-v4-flash"
|
||||
|
||||
[model_providers.deepseek]
|
||||
name = "DeepSeek"
|
||||
base_url = "https://api.deepseek.com/v1"
|
||||
wire_api = "responses"
|
||||
"#
|
||||
}));
|
||||
provider.meta = Some(crate::provider::ProviderMeta {
|
||||
api_format: Some("openai_chat".to_string()),
|
||||
..Default::default()
|
||||
});
|
||||
let mut body = json!({
|
||||
"model": CODEX_CHAT_CLIENT_MODEL,
|
||||
"input": "ping"
|
||||
});
|
||||
|
||||
let upstream_model = apply_codex_chat_upstream_model(&provider, &mut body);
|
||||
|
||||
assert_eq!(upstream_model.as_deref(), Some("deepseek-v4-flash"));
|
||||
assert_eq!(
|
||||
body.get("model").and_then(|v| v.as_str()),
|
||||
Some("deepseek-v4-flash")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user