mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
feat(codex): support native Responses direct-connect with generated model catalog
Codex providers can now run in two modes per provider: - Proxy-Chat (route takeover): apiFormat=openai_chat, existing Responses<->Chat conversion. Unchanged. - Native-Responses (direct): apiFormat=openai_responses, no proxy. cc-switch generates ~/.codex/cc-switch-model-catalog.json so Codex shows the custom models and tools work without the freeform apply_patch (type=custom) tool that native gateways like MiMo reject; editing falls back to shell_command. Catalog generation is keyed on apiFormat (CodexCatalogToolProfile), decoupled from the takeover toggle, so native providers persist a catalog without enabling local route mapping. Codex's catalog parser requires base_instructions on every entry; the native template carries a neutral default and per-vendor official text overrides it (MiMo, MiniMax). Synthesized catalogs for Qwen/Doubao/LongCat use the neutral default. Existing native providers must be re-saved once to regenerate a valid catalog (no DB migration).
This commit is contained in:
+299
-24
@@ -15,6 +15,33 @@ pub const CC_SWITCH_CODEX_MODEL_PROVIDER_ID: &str = "custom";
|
||||
pub const CC_SWITCH_CODEX_MODEL_CATALOG_FILENAME: &str = "cc-switch-model-catalog.json";
|
||||
const CODEX_MODEL_CATALOG_TEMPLATE_SLUG: &str = "gpt-5.5";
|
||||
|
||||
/// Which Codex tool surface the generated model catalog should target.
|
||||
///
|
||||
/// - `ProxyChat`: cc-switch's proxy takes over and converts Responses<->Chat,
|
||||
/// so the catalog keeps Codex's default tool set (incl. the freeform
|
||||
/// `apply_patch` custom tool, which the proxy rewrites to a function tool).
|
||||
/// - `NativeResponses`: Codex talks directly to a provider's native
|
||||
/// `/responses` endpoint (no proxy). Such gateways (e.g. Xiaomi MiMo,
|
||||
/// MiniMax) reject `type=="custom"` tools, so the catalog must suppress the
|
||||
/// freeform `apply_patch` and rely on `shell_type="shell_command"` for edits.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CodexCatalogToolProfile {
|
||||
ProxyChat,
|
||||
NativeResponses,
|
||||
}
|
||||
|
||||
impl CodexCatalogToolProfile {
|
||||
/// Pick the catalog tool profile from a provider's `apiFormat` meta value.
|
||||
/// Native (direct) Responses providers must suppress the custom apply_patch
|
||||
/// tool; everything else keeps the proxy-chat behavior.
|
||||
pub fn from_api_format(api_format: Option<&str>) -> Self {
|
||||
match api_format {
|
||||
Some("openai_responses") => CodexCatalogToolProfile::NativeResponses,
|
||||
_ => CodexCatalogToolProfile::ProxyChat,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reserved built-in provider IDs from OpenAI Codex's config/model-provider
|
||||
/// catalog. Keep in sync with Codex `RESERVED_MODEL_PROVIDER_IDS` and legacy
|
||||
/// removed provider aliases.
|
||||
@@ -313,27 +340,63 @@ fn extract_codex_top_level_u64(config_text: &str, field: &str) -> Option<u64> {
|
||||
|
||||
fn codex_catalog_model_entry(
|
||||
template: &Value,
|
||||
model: &str,
|
||||
display_name: &str,
|
||||
context_window: u64,
|
||||
spec: &CodexCatalogModelSpec,
|
||||
priority: usize,
|
||||
profile: CodexCatalogToolProfile,
|
||||
) -> Value {
|
||||
let mut entry = template.clone();
|
||||
let Some(entry_obj) = entry.as_object_mut() else {
|
||||
return json!({});
|
||||
};
|
||||
|
||||
entry_obj.insert("slug".to_string(), json!(model));
|
||||
entry_obj.insert("display_name".to_string(), json!(display_name));
|
||||
entry_obj.insert("description".to_string(), json!(display_name));
|
||||
entry_obj.insert("context_window".to_string(), json!(context_window));
|
||||
entry_obj.insert("max_context_window".to_string(), json!(context_window));
|
||||
entry_obj.insert("slug".to_string(), json!(spec.model));
|
||||
entry_obj.insert("display_name".to_string(), json!(spec.display_name));
|
||||
entry_obj.insert("description".to_string(), json!(spec.display_name));
|
||||
entry_obj.insert("context_window".to_string(), json!(spec.context_window));
|
||||
entry_obj.insert("max_context_window".to_string(), json!(spec.context_window));
|
||||
entry_obj.insert("priority".to_string(), json!(1000 + priority));
|
||||
entry_obj.insert("additional_speed_tiers".to_string(), json!([]));
|
||||
entry_obj.insert("service_tiers".to_string(), json!([]));
|
||||
entry_obj.insert("availability_nux".to_string(), Value::Null);
|
||||
entry_obj.insert("upgrade".to_string(), Value::Null);
|
||||
|
||||
if profile == CodexCatalogToolProfile::NativeResponses {
|
||||
// Native `/responses` gateways reject Codex's freeform `apply_patch`
|
||||
// (type=="custom") tool. Strip any key that would make Codex emit a
|
||||
// custom/freeform tool, and rely on shell_type="shell_command" for
|
||||
// edits. Defensive even though the native template is already clean
|
||||
// (guards against template drift / an accidental gpt-5.5 clone).
|
||||
//
|
||||
// NOTE: `base_instructions` is NOT stripped — Codex's catalog parser
|
||||
// treats it as a REQUIRED field and refuses to load the file without
|
||||
// it ("missing field `base_instructions`"). The template carries a
|
||||
// neutral identity default; per-vendor official text overrides below.
|
||||
for key in [
|
||||
"apply_patch_tool_type",
|
||||
"web_search_tool_type",
|
||||
"tools",
|
||||
"model_messages",
|
||||
] {
|
||||
entry_obj.remove(key);
|
||||
}
|
||||
entry_obj.insert("shell_type".to_string(), json!("shell_command"));
|
||||
|
||||
if let Some(base_instructions) = spec
|
||||
.base_instructions
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
{
|
||||
entry_obj.insert("base_instructions".to_string(), json!(base_instructions));
|
||||
}
|
||||
if let Some(parallel) = spec.supports_parallel_tool_calls {
|
||||
entry_obj.insert("supports_parallel_tool_calls".to_string(), json!(parallel));
|
||||
}
|
||||
if let Some(modalities) = &spec.input_modalities {
|
||||
entry_obj.insert("input_modalities".to_string(), json!(modalities));
|
||||
}
|
||||
}
|
||||
|
||||
entry
|
||||
}
|
||||
|
||||
@@ -342,6 +405,18 @@ struct CodexCatalogModelSpec {
|
||||
model: String,
|
||||
display_name: String,
|
||||
context_window: u64,
|
||||
/// Per-row override for the native template's `supports_parallel_tool_calls`
|
||||
/// (e.g. MiniMax=true, MiMo=false). Only consulted for `NativeResponses`.
|
||||
supports_parallel_tool_calls: Option<bool>,
|
||||
/// Per-row override for the native template's `input_modalities`
|
||||
/// (e.g. `["text","image"]`). Only consulted for `NativeResponses`.
|
||||
input_modalities: Option<Vec<String>>,
|
||||
/// Per-row override for the native template's `base_instructions` (the
|
||||
/// model identity / system preamble). Carries each vendor's OFFICIAL value
|
||||
/// (e.g. MiMo "developed by Xiaomi", MiniMax "based on MiniMax-M3"); falls
|
||||
/// back to the template default when absent. Only consulted for
|
||||
/// `NativeResponses`.
|
||||
base_instructions: Option<String>,
|
||||
}
|
||||
|
||||
fn codex_catalog_model_specs(settings: &Value, config_text: &str) -> Vec<CodexCatalogModelSpec> {
|
||||
@@ -386,10 +461,38 @@ fn codex_catalog_model_specs(settings: &Value, config_text: &str) -> Vec<CodexCa
|
||||
)
|
||||
.unwrap_or(default_context_window);
|
||||
|
||||
let supports_parallel_tool_calls = model_config
|
||||
.get("supportsParallelToolCalls")
|
||||
.or_else(|| model_config.get("supports_parallel_tool_calls"))
|
||||
.and_then(|value| value.as_bool());
|
||||
let input_modalities = model_config
|
||||
.get("inputModalities")
|
||||
.or_else(|| model_config.get("input_modalities"))
|
||||
.and_then(|value| value.as_array())
|
||||
.map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|item| item.as_str())
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.filter(|items| !items.is_empty());
|
||||
|
||||
let base_instructions = model_config
|
||||
.get("baseInstructions")
|
||||
.or_else(|| model_config.get("base_instructions"))
|
||||
.and_then(|value| value.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|text| !text.is_empty())
|
||||
.map(str::to_string);
|
||||
|
||||
specs.push(CodexCatalogModelSpec {
|
||||
model: model.to_string(),
|
||||
display_name: display_name.to_string(),
|
||||
context_window,
|
||||
supports_parallel_tool_calls,
|
||||
input_modalities,
|
||||
base_instructions,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -628,6 +731,18 @@ fn load_codex_model_template_static() -> Option<Value> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Bundled clean template for native `/responses` providers. Unlike the
|
||||
/// gpt-5.5 template it carries NO freeform `apply_patch` / `web_search` tool
|
||||
/// declarations and no GPT-5 base_instructions, so Codex never emits a
|
||||
/// `type=="custom"` tool that native gateways (MiMo/MiniMax/…) reject. Edits
|
||||
/// flow through `shell_type="shell_command"` instead. We deliberately do NOT
|
||||
/// fall back to `models_cache.json` here (that would reintroduce gpt-5.5's
|
||||
/// freeform apply_patch).
|
||||
fn load_codex_native_responses_template() -> Value {
|
||||
let text = include_str!("resources/codex_native_responses_template.json");
|
||||
serde_json::from_str(text).expect("bundled codex native responses template must be valid JSON")
|
||||
}
|
||||
|
||||
fn load_codex_model_catalog_template() -> Result<Value, AppError> {
|
||||
// ① models_cache.json (created by Codex when it connects to OpenAI)
|
||||
if let Some(template) = load_codex_model_template_from_cache()? {
|
||||
@@ -647,19 +762,15 @@ fn load_codex_model_catalog_template() -> Result<Value, AppError> {
|
||||
)))
|
||||
}
|
||||
|
||||
fn codex_model_catalog_from_specs(specs: &[CodexCatalogModelSpec], template: &Value) -> Value {
|
||||
fn codex_model_catalog_from_specs(
|
||||
specs: &[CodexCatalogModelSpec],
|
||||
template: &Value,
|
||||
profile: CodexCatalogToolProfile,
|
||||
) -> Value {
|
||||
let entries: Vec<Value> = specs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, spec)| {
|
||||
codex_catalog_model_entry(
|
||||
template,
|
||||
&spec.model,
|
||||
&spec.display_name,
|
||||
spec.context_window,
|
||||
index,
|
||||
)
|
||||
})
|
||||
.map(|(index, spec)| codex_catalog_model_entry(template, spec, index, profile))
|
||||
.collect();
|
||||
|
||||
json!({ "models": entries })
|
||||
@@ -668,14 +779,23 @@ fn codex_model_catalog_from_specs(specs: &[CodexCatalogModelSpec], template: &Va
|
||||
fn codex_model_catalog_from_settings(
|
||||
settings: &Value,
|
||||
config_text: &str,
|
||||
profile: CodexCatalogToolProfile,
|
||||
) -> Result<Option<Value>, AppError> {
|
||||
let specs = codex_catalog_model_specs(settings, config_text);
|
||||
if specs.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let template = load_codex_model_catalog_template()?;
|
||||
Ok(Some(codex_model_catalog_from_specs(&specs, &template)))
|
||||
// Native providers use the bundled clean template (no freeform apply_patch,
|
||||
// no cache dependency); proxy-chat providers keep cloning Codex's gpt-5.5
|
||||
// entry so the proxy can rewrite custom<->function tools as before.
|
||||
let template = match profile {
|
||||
CodexCatalogToolProfile::NativeResponses => load_codex_native_responses_template(),
|
||||
CodexCatalogToolProfile::ProxyChat => load_codex_model_catalog_template()?,
|
||||
};
|
||||
Ok(Some(codex_model_catalog_from_specs(
|
||||
&specs, &template, profile,
|
||||
)))
|
||||
}
|
||||
|
||||
fn set_codex_model_catalog_json_field(
|
||||
@@ -713,10 +833,11 @@ fn set_codex_model_catalog_json_field(
|
||||
pub fn prepare_codex_config_text_with_model_catalog(
|
||||
settings: &Value,
|
||||
config_text: &str,
|
||||
profile: CodexCatalogToolProfile,
|
||||
) -> Result<String, AppError> {
|
||||
let catalog_path = get_codex_model_catalog_path();
|
||||
|
||||
if let Some(catalog) = codex_model_catalog_from_settings(settings, config_text)? {
|
||||
if let Some(catalog) = codex_model_catalog_from_settings(settings, config_text, profile)? {
|
||||
let config_text = set_codex_model_catalog_json_field(config_text, Some(&catalog_path))?;
|
||||
write_json_file(&catalog_path, &catalog)?;
|
||||
Ok(config_text)
|
||||
@@ -839,6 +960,26 @@ fn build_simplified_catalog_from_texts(config_text: &str, catalog_text: &str) ->
|
||||
obj.insert("contextWindow".to_string(), json!(context_window));
|
||||
}
|
||||
|
||||
// Preserve native-profile per-row overrides so a DB-SSOT-missing
|
||||
// fallback round-trip doesn't silently drop them (they are ignored by
|
||||
// the ProxyChat profile, so carrying them is harmless).
|
||||
if let Some(parallel) = entry
|
||||
.get("supports_parallel_tool_calls")
|
||||
.and_then(|v| v.as_bool())
|
||||
{
|
||||
obj.insert("supportsParallelToolCalls".to_string(), json!(parallel));
|
||||
}
|
||||
if let Some(modalities) = entry.get("input_modalities").and_then(|v| v.as_array()) {
|
||||
let mods: Vec<String> = modalities
|
||||
.iter()
|
||||
.filter_map(|m| m.as_str())
|
||||
.map(str::to_string)
|
||||
.collect();
|
||||
if !mods.is_empty() {
|
||||
obj.insert("inputModalities".to_string(), json!(mods));
|
||||
}
|
||||
}
|
||||
|
||||
entries.push(Value::Object(obj));
|
||||
}
|
||||
|
||||
@@ -876,9 +1017,10 @@ fn build_simplified_catalog_from_texts(config_text: &str, catalog_text: &str) ->
|
||||
pub fn prepare_codex_live_config_text_with_optional_catalog(
|
||||
settings: &Value,
|
||||
config_text: &str,
|
||||
profile: CodexCatalogToolProfile,
|
||||
) -> Result<String, AppError> {
|
||||
if settings.get("modelCatalog").is_some() {
|
||||
prepare_codex_config_text_with_model_catalog(settings, config_text)
|
||||
prepare_codex_config_text_with_model_catalog(settings, config_text, profile)
|
||||
} else {
|
||||
Ok(config_text.to_string())
|
||||
}
|
||||
@@ -889,9 +1031,10 @@ pub fn write_codex_provider_live_with_catalog(
|
||||
category: Option<&str>,
|
||||
auth: &Value,
|
||||
config_text: Option<&str>,
|
||||
profile: CodexCatalogToolProfile,
|
||||
) -> Result<(), AppError> {
|
||||
let prepared_config = config_text
|
||||
.map(|text| prepare_codex_config_text_with_model_catalog(settings, text))
|
||||
.map(|text| prepare_codex_config_text_with_model_catalog(settings, text, profile))
|
||||
.transpose()?;
|
||||
|
||||
write_codex_live_for_provider(category, auth, prepared_config.as_deref())
|
||||
@@ -2070,7 +2213,8 @@ base_url = "https://production.api/v1"
|
||||
}
|
||||
});
|
||||
let specs = codex_catalog_model_specs(&settings, r#"model_context_window = 128000"#);
|
||||
let catalog = codex_model_catalog_from_specs(&specs, &template);
|
||||
let catalog =
|
||||
codex_model_catalog_from_specs(&specs, &template, CodexCatalogToolProfile::ProxyChat);
|
||||
let models = catalog
|
||||
.get("models")
|
||||
.and_then(|value| value.as_array())
|
||||
@@ -2121,6 +2265,137 @@ base_url = "https://production.api/v1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_responses_profile_suppresses_apply_patch_and_keeps_shell() {
|
||||
// Native (direct) /responses providers must NOT emit a freeform
|
||||
// apply_patch (type=="custom") tool — gateways like MiMo reject it.
|
||||
// The native profile uses the bundled clean template and relies on
|
||||
// shell_type="shell_command" for edits, plus per-row overrides.
|
||||
let settings = json!({
|
||||
"modelCatalog": {
|
||||
"models": [
|
||||
{
|
||||
"model": "MiniMax-M3",
|
||||
"displayName": "MiniMax-M3",
|
||||
"contextWindow": 1_000_000,
|
||||
"supportsParallelToolCalls": true,
|
||||
"inputModalities": ["text", "image"],
|
||||
"baseInstructions": "You are Codex, a coding agent based on MiniMax-M3."
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
let catalog = codex_model_catalog_from_settings(
|
||||
&settings,
|
||||
"",
|
||||
CodexCatalogToolProfile::NativeResponses,
|
||||
)
|
||||
.expect("native catalog generation should not error")
|
||||
.expect("non-empty modelCatalog must yield a catalog");
|
||||
|
||||
let entry = &catalog["models"][0];
|
||||
assert_eq!(
|
||||
entry.get("slug").and_then(|v| v.as_str()),
|
||||
Some("MiniMax-M3")
|
||||
);
|
||||
assert_eq!(
|
||||
entry.get("shell_type").and_then(|v| v.as_str()),
|
||||
Some("shell_command"),
|
||||
"native entries edit via shell, not the custom apply_patch tool"
|
||||
);
|
||||
assert!(
|
||||
entry.get("apply_patch_tool_type").is_none(),
|
||||
"native entries must NOT declare a freeform apply_patch tool"
|
||||
);
|
||||
// `base_instructions` is REQUIRED by Codex's catalog parser, so it must
|
||||
// be present — and the per-row official override must win over the
|
||||
// template default.
|
||||
assert_eq!(
|
||||
entry.get("base_instructions").and_then(|v| v.as_str()),
|
||||
Some("You are Codex, a coding agent based on MiniMax-M3."),
|
||||
"per-row baseInstructions override must apply (and field must exist)"
|
||||
);
|
||||
assert!(
|
||||
entry.get("model_messages").is_none(),
|
||||
"native entries must not carry the gpt-5.5 model_messages persona text"
|
||||
);
|
||||
assert_eq!(
|
||||
entry.get("supports_parallel_tool_calls"),
|
||||
Some(&json!(true)),
|
||||
"per-row supportsParallelToolCalls override must apply"
|
||||
);
|
||||
assert_eq!(
|
||||
entry.get("input_modalities"),
|
||||
Some(&json!(["text", "image"])),
|
||||
"per-row inputModalities override must apply"
|
||||
);
|
||||
assert_eq!(
|
||||
entry.get("context_window").and_then(|v| v.as_u64()),
|
||||
Some(1_000_000)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_responses_catalog_always_carries_base_instructions() {
|
||||
// Regression guard for the "missing field `base_instructions`" parse
|
||||
// error: Codex refuses to load a model catalog whose entries lack
|
||||
// base_instructions. Synthesized presets carry no per-row override, so
|
||||
// the entry MUST inherit the template's neutral default rather than
|
||||
// dropping the field entirely.
|
||||
let settings = json!({
|
||||
"modelCatalog": { "models": [{ "model": "qwen3-coder-plus" }] }
|
||||
});
|
||||
|
||||
let catalog = codex_model_catalog_from_settings(
|
||||
&settings,
|
||||
"",
|
||||
CodexCatalogToolProfile::NativeResponses,
|
||||
)
|
||||
.expect("native catalog generation should not error")
|
||||
.expect("non-empty modelCatalog must yield a catalog");
|
||||
|
||||
let base = catalog["models"][0]
|
||||
.get("base_instructions")
|
||||
.and_then(|v| v.as_str());
|
||||
assert!(
|
||||
base.is_some_and(|s| !s.trim().is_empty()),
|
||||
"every native entry must carry a non-empty base_instructions (Codex requires it)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_chat_profile_still_keeps_apply_patch() {
|
||||
// Regression guard for Mode A: the proxy-chat profile must keep the
|
||||
// freeform apply_patch tool (the proxy rewrites custom<->function).
|
||||
let template = load_codex_native_responses_template();
|
||||
let specs = vec![CodexCatalogModelSpec {
|
||||
model: "x".to_string(),
|
||||
display_name: "x".to_string(),
|
||||
context_window: 128_000,
|
||||
supports_parallel_tool_calls: None,
|
||||
input_modalities: None,
|
||||
base_instructions: None,
|
||||
}];
|
||||
// Using a gpt-5.5-shaped template under ProxyChat must NOT strip
|
||||
// apply_patch_tool_type. (The native template lacks it, so synthesize
|
||||
// one with the field present to prove ProxyChat leaves it intact.)
|
||||
let mut proxy_template = template.clone();
|
||||
proxy_template["apply_patch_tool_type"] = json!("freeform");
|
||||
let catalog = codex_model_catalog_from_specs(
|
||||
&specs,
|
||||
&proxy_template,
|
||||
CodexCatalogToolProfile::ProxyChat,
|
||||
);
|
||||
assert_eq!(
|
||||
catalog["models"][0]
|
||||
.get("apply_patch_tool_type")
|
||||
.and_then(|v| v.as_str()),
|
||||
Some("freeform"),
|
||||
"ProxyChat must preserve apply_patch_tool_type (no native stripping)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_catalog_json_field_writes_relative_filename() {
|
||||
let input = r#"model_provider = "any"
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"slug": "native-responses-template",
|
||||
"display_name": "native-responses-template",
|
||||
"description": "native-responses-template",
|
||||
"base_instructions": "You are Codex, a coding agent. You and the user share the same workspace and collaborate to achieve the user's goals.",
|
||||
"default_reasoning_level": "high",
|
||||
"supported_reasoning_levels": [
|
||||
{
|
||||
"effort": "none",
|
||||
"description": "Disable Thinking"
|
||||
},
|
||||
{
|
||||
"effort": "high",
|
||||
"description": "Enabled Thinking"
|
||||
}
|
||||
],
|
||||
"shell_type": "shell_command",
|
||||
"visibility": "list",
|
||||
"supported_in_api": true,
|
||||
"priority": 0,
|
||||
"supports_reasoning_summaries": true,
|
||||
"default_reasoning_summary": "none",
|
||||
"support_verbosity": false,
|
||||
"truncation_policy": {
|
||||
"mode": "bytes",
|
||||
"limit": 10000
|
||||
},
|
||||
"supports_parallel_tool_calls": false,
|
||||
"supports_image_detail_original": false,
|
||||
"context_window": 262144,
|
||||
"max_context_window": 262144,
|
||||
"effective_context_window_percent": 95,
|
||||
"experimental_supported_tools": [],
|
||||
"input_modalities": [
|
||||
"text"
|
||||
],
|
||||
"supports_search_tool": false
|
||||
}
|
||||
@@ -159,11 +159,16 @@ impl ConfigService {
|
||||
}
|
||||
let cfg_text = settings.get("config").and_then(Value::as_str);
|
||||
|
||||
let profile = crate::codex_config::CodexCatalogToolProfile::from_api_format(
|
||||
provider.meta.as_ref().and_then(|m| m.api_format.as_deref()),
|
||||
);
|
||||
|
||||
crate::codex_config::write_codex_provider_live_with_catalog(
|
||||
&provider.settings_config,
|
||||
provider.category.as_deref(),
|
||||
auth,
|
||||
cfg_text,
|
||||
profile,
|
||||
)?;
|
||||
// 注意:MCP 同步在 v3.7.0 中已通过 McpService 进行,不再在此调用
|
||||
// sync_enabled_to_codex 使用旧的 config.mcp.codex 结构,在新架构中为空
|
||||
|
||||
@@ -760,11 +760,19 @@ pub(crate) fn write_live_snapshot(app_type: &AppType, provider: &Provider) -> Re
|
||||
.ok_or_else(|| AppError::Config("Codex 供应商配置缺少 'auth' 字段".to_string()))?;
|
||||
let config_str = obj.get("config").and_then(|v| v.as_str());
|
||||
|
||||
// Native (direct) Responses providers must suppress Codex's freeform
|
||||
// apply_patch custom tool via the generated catalog; chat/proxy
|
||||
// providers keep the default tool set. Keyed on provider.meta.apiFormat.
|
||||
let profile = crate::codex_config::CodexCatalogToolProfile::from_api_format(
|
||||
provider.meta.as_ref().and_then(|m| m.api_format.as_deref()),
|
||||
);
|
||||
|
||||
crate::codex_config::write_codex_provider_live_with_catalog(
|
||||
&provider.settings_config,
|
||||
provider.category.as_deref(),
|
||||
auth,
|
||||
config_str,
|
||||
profile,
|
||||
)?;
|
||||
}
|
||||
AppType::Gemini => {
|
||||
|
||||
@@ -2145,12 +2145,16 @@ impl ProxyService {
|
||||
.get("auth")
|
||||
.ok_or_else(|| "Codex 供应商缺少 auth 配置".to_string())?;
|
||||
let config_str = effective_settings.get("config").and_then(|v| v.as_str());
|
||||
let profile = crate::codex_config::CodexCatalogToolProfile::from_api_format(
|
||||
provider.meta.as_ref().and_then(|m| m.api_format.as_deref()),
|
||||
);
|
||||
|
||||
crate::codex_config::write_codex_provider_live_with_catalog(
|
||||
&effective_settings,
|
||||
provider.category.as_deref(),
|
||||
auth,
|
||||
config_str,
|
||||
profile,
|
||||
)
|
||||
.map_err(|e| format!("写入 Codex 配置失败: {e}"))?;
|
||||
}
|
||||
@@ -2410,12 +2414,16 @@ impl ProxyService {
|
||||
.get("auth")
|
||||
.ok_or_else(|| "Codex 配置缺少 auth 字段".to_string())?;
|
||||
let config_str = config.get("config").and_then(|v| v.as_str());
|
||||
let profile = crate::codex_config::CodexCatalogToolProfile::from_api_format(
|
||||
provider.meta.as_ref().and_then(|m| m.api_format.as_deref()),
|
||||
);
|
||||
|
||||
crate::codex_config::write_codex_provider_live_with_catalog(
|
||||
config,
|
||||
provider.category.as_deref(),
|
||||
auth,
|
||||
config_str,
|
||||
profile,
|
||||
)
|
||||
.map_err(|e| format!("写入 Codex 配置失败: {e}"))
|
||||
}
|
||||
@@ -2435,9 +2443,12 @@ impl ProxyService {
|
||||
.filter(|auth| Self::codex_auth_has_proxy_placeholder(auth))
|
||||
{
|
||||
let config_str = config.get("config").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let profile = crate::codex_config::CodexCatalogToolProfile::from_api_format(
|
||||
provider.and_then(|p| p.meta.as_ref()?.api_format.as_deref()),
|
||||
);
|
||||
let prepared_config =
|
||||
crate::codex_config::prepare_codex_live_config_text_with_optional_catalog(
|
||||
config, config_str,
|
||||
config, config_str, profile,
|
||||
)
|
||||
.map_err(|e| format!("写入 Codex 配置失败: {e}"))?;
|
||||
let live_config =
|
||||
@@ -2471,10 +2482,18 @@ impl ProxyService {
|
||||
// living in the config's `experimental_bearer_token`). Computing it up here
|
||||
// keeps every config-writing branch — write-auth, delete-auth, no-auth —
|
||||
// consistent instead of letting the empty-auth path skip projection.
|
||||
// Verbatim restore has no Provider in hand (we only have the stored
|
||||
// backup config), so the catalog tool profile can't be recovered here.
|
||||
// Default to ProxyChat: a restored native-direct backup keeps its inline
|
||||
// modelCatalog but would not get apply_patch re-stripped until the next
|
||||
// provider switch rewrites it via write_live_snapshot. Acceptable known
|
||||
// limitation (restore-of-deleted-provider-backup only).
|
||||
let prepared_cfg = config_str
|
||||
.map(|cfg| {
|
||||
crate::codex_config::prepare_codex_live_config_text_with_optional_catalog(
|
||||
config, cfg,
|
||||
config,
|
||||
cfg,
|
||||
crate::codex_config::CodexCatalogToolProfile::ProxyChat,
|
||||
)
|
||||
})
|
||||
.transpose()
|
||||
|
||||
Reference in New Issue
Block a user