feat: preserve user-selected catalog model in Codex Chat requests

When Codex client sends a model from the catalog (e.g., user selected via /model),
preserve that choice instead of always replacing with config.toml's default model.

- Check if request model exists in modelCatalog before falling back to config model
- Remove CODEX_CHAT_CLIENT_MODEL constant (no longer needed)
- Add test coverage for catalog model preservation
This commit is contained in:
Jason
2026-05-20 10:55:20 +08:00
parent 9b9578206b
commit 2a4651a21e
2 changed files with 69 additions and 12 deletions
+68 -10
View File
@@ -10,6 +10,7 @@ use crate::provider::Provider;
use crate::proxy::error::ProxyError;
use regex::Regex;
use serde_json::Value as JsonValue;
use std::collections::HashSet;
use std::sync::LazyLock;
use toml::Value as TomlValue;
@@ -21,13 +22,6 @@ 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.
@@ -108,8 +102,26 @@ pub fn codex_provider_upstream_model(provider: &Provider) -> Option<String> {
})
}
/// For Codex Chat providers, replace the local Codex-safe model with the real
/// upstream model before converting the request to Chat Completions.
fn codex_provider_catalog_model_ids(provider: &Provider) -> HashSet<String> {
provider
.settings_config
.get("modelCatalog")
.and_then(|catalog| catalog.get("models"))
.and_then(|models| models.as_array())
.map(|models| {
models
.iter()
.filter_map(|model| model.get("model").and_then(|value| value.as_str()))
.map(str::trim)
.filter(|model| !model.is_empty())
.map(ToString::to_string)
.collect()
})
.unwrap_or_default()
}
/// For Codex Chat providers, ensure the request uses the configured upstream
/// model before converting the request to Chat Completions.
pub fn apply_codex_chat_upstream_model(
provider: &Provider,
body: &mut JsonValue,
@@ -118,6 +130,18 @@ pub fn apply_codex_chat_upstream_model(
return None;
}
let catalog_model_ids = codex_provider_catalog_model_ids(provider);
if let Some(request_model) = body
.get("model")
.and_then(|value| value.as_str())
.map(str::trim)
.filter(|model| !model.is_empty())
{
if catalog_model_ids.contains(request_model) {
return Some(request_model.to_string());
}
}
let upstream_model = codex_provider_upstream_model(provider)?;
body["model"] = JsonValue::String(upstream_model.clone());
Some(upstream_model)
@@ -567,7 +591,7 @@ wire_api = "responses"
..Default::default()
});
let mut body = json!({
"model": CODEX_CHAT_CLIENT_MODEL,
"model": "placeholder-client-model",
"input": "ping"
});
@@ -579,4 +603,38 @@ wire_api = "responses"
Some("deepseek-v4-flash")
);
}
#[test]
fn test_apply_codex_chat_upstream_model_preserves_catalog_model_selection() {
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"
"#,
"modelCatalog": {
"models": [
{ "model": "deepseek-v4-flash" },
{ "model": "kimi-k2" }
]
}
}));
provider.meta = Some(crate::provider::ProviderMeta {
api_format: Some("openai_chat".to_string()),
..Default::default()
});
let mut body = json!({
"model": "kimi-k2",
"input": "ping"
});
let upstream_model = apply_codex_chat_upstream_model(&provider, &mut body);
assert_eq!(upstream_model.as_deref(), Some("kimi-k2"));
assert_eq!(body.get("model").and_then(|v| v.as_str()), Some("kimi-k2"));
}
}
+1 -2
View File
@@ -47,8 +47,7 @@ pub use claude::{
pub use codex::CodexAdapter;
pub use codex::{
apply_codex_chat_upstream_model, codex_provider_upstream_model,
codex_provider_uses_chat_completions, should_convert_codex_responses_to_chat,
CODEX_CHAT_CLIENT_MODEL,
should_convert_codex_responses_to_chat,
};
pub use gemini::GeminiAdapter;