mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-29 09:37:37 +08:00
fix(codex): infer image capability for generated catalogs and resync takeover live on save
Mapped GPT models were rejected by Codex clients with "model does not support image inputs". Two root causes: - Catalog entries for native-Responses/Anthropic providers cloned a template whose input_modalities defaulted to ["text"], so every mapped model was advertised text-only. model_catalog_json replaces Codex's built-in model table wholesale, and both the TUI and the IDE extension block images pre-send when the current model is found without "image". - Editing the current Codex provider during proxy takeover only refreshed the DB backup, so removing the mapping left a stale model_catalog_json pointer (and its text-only catalog file) active in live config. Changes: - New shared model_capabilities module: explicit row declaration first, then a confirmed text-only registry (exact tail match only — prefix matching removed, variants enumerated so future -vl/-vision models fail open), everything else unknown. - Catalog generation writes input_modalities from that inference for all tool profiles: unknown models fail open to ["text","image"]; only confirmed text-only models are advertised as ["text"], giving users a clear client-side prompt instead of silent image stripping. - Live catalog reverse-import collapses modalities that match current inference, so registry corrections are not frozen into hidden row overrides and the rectifier's heuristic opt-out keeps working. - Saving the current Codex provider while takeover owns live now re-projects the live config (mirrors the hot-switch path), so mapping edits and removals take effect immediately. - Media rectifier delegates to the shared module; its preflight toggle is documented (4 locales) as proxy-request-only, never affecting catalog capability declarations.
This commit is contained in:
@@ -1108,6 +1108,135 @@ command = "legacy-cmd"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn update_current_codex_provider_refreshes_and_clears_catalog_during_takeover() {
|
||||
let _home = TempHome::new();
|
||||
crate::settings::reload_settings().expect("reload settings");
|
||||
|
||||
let db = Arc::new(Database::memory().expect("init db"));
|
||||
let state = AppState::new(db.clone());
|
||||
|
||||
let mut original = Provider::with_id(
|
||||
"p1".into(),
|
||||
"Codex A".into(),
|
||||
json!({
|
||||
"auth": { "OPENAI_API_KEY": "token-a" },
|
||||
"config": r#"model_provider = "custom"
|
||||
model = "old-model"
|
||||
|
||||
[model_providers.custom]
|
||||
name = "Codex A"
|
||||
base_url = "https://api.a.example/v1"
|
||||
wire_api = "responses"
|
||||
requires_openai_auth = true
|
||||
"#,
|
||||
"modelCatalog": {
|
||||
"models": [{ "model": "old-model" }]
|
||||
}
|
||||
}),
|
||||
None,
|
||||
);
|
||||
original.meta = Some(ProviderMeta {
|
||||
api_format: Some("openai_responses".into()),
|
||||
..Default::default()
|
||||
});
|
||||
db.save_provider("codex", &original).expect("save provider");
|
||||
db.set_current_provider("codex", "p1")
|
||||
.expect("set current provider");
|
||||
crate::settings::set_current_provider(&AppType::Codex, Some("p1"))
|
||||
.expect("set local current provider");
|
||||
|
||||
db.update_proxy_config(ProxyConfig {
|
||||
live_takeover_active: true,
|
||||
listen_port: 0,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.expect("update proxy config");
|
||||
{
|
||||
let mut config = db
|
||||
.get_proxy_config_for_app("codex")
|
||||
.await
|
||||
.expect("get app proxy config");
|
||||
config.enabled = true;
|
||||
db.update_proxy_config_for_app(config)
|
||||
.await
|
||||
.expect("enable Codex proxy config");
|
||||
}
|
||||
db.save_live_backup(
|
||||
"codex",
|
||||
&serde_json::to_string(&original.settings_config).expect("serialize backup"),
|
||||
)
|
||||
.await
|
||||
.expect("seed live backup");
|
||||
|
||||
state
|
||||
.proxy_service
|
||||
.start()
|
||||
.await
|
||||
.expect("start proxy service");
|
||||
state
|
||||
.proxy_service
|
||||
.sync_codex_live_from_provider_while_proxy_active(&original)
|
||||
.await
|
||||
.expect("seed taken-over Codex live config");
|
||||
assert!(
|
||||
state
|
||||
.proxy_service
|
||||
.detect_takeover_in_live_config_for_app(&AppType::Codex),
|
||||
"seeded Codex live config should be recognized as takeover-owned"
|
||||
);
|
||||
|
||||
let mut updated = original.clone();
|
||||
updated.settings_config["config"] = json!(
|
||||
r#"model_provider = "custom"
|
||||
model = "gpt-5.4"
|
||||
|
||||
[model_providers.custom]
|
||||
name = "Codex A"
|
||||
base_url = "https://api.updated.example/v1"
|
||||
wire_api = "responses"
|
||||
requires_openai_auth = true
|
||||
"#
|
||||
);
|
||||
updated.settings_config["modelCatalog"] = json!({
|
||||
"models": [{ "model": "gpt-5.4", "displayName": "GPT 5.4" }]
|
||||
});
|
||||
|
||||
ProviderService::update(&state, AppType::Codex, None, updated.clone())
|
||||
.expect("update current Codex provider mapping");
|
||||
|
||||
let catalog_path = crate::codex_config::get_codex_model_catalog_path();
|
||||
let catalog: Value = read_json_file(&catalog_path).expect("read generated catalog");
|
||||
assert_eq!(catalog["models"][0]["slug"], "gpt-5.4");
|
||||
assert_eq!(
|
||||
catalog["models"][0]["input_modalities"],
|
||||
json!(["text", "image"]),
|
||||
"unknown/GPT models must fail open to image input"
|
||||
);
|
||||
let live_config = fs::read_to_string(crate::codex_config::get_codex_config_path())
|
||||
.expect("read Codex config.toml");
|
||||
assert!(live_config.contains("model_catalog_json"));
|
||||
|
||||
updated.settings_config["modelCatalog"] = json!({ "models": [] });
|
||||
ProviderService::update(&state, AppType::Codex, None, updated)
|
||||
.expect("remove current Codex provider mapping");
|
||||
|
||||
let live_config = fs::read_to_string(crate::codex_config::get_codex_config_path())
|
||||
.expect("read Codex config.toml after mapping removal");
|
||||
assert!(
|
||||
!live_config.contains("model_catalog_json"),
|
||||
"removing mappings during takeover must clear the stale catalog pointer"
|
||||
);
|
||||
|
||||
state
|
||||
.proxy_service
|
||||
.stop()
|
||||
.await
|
||||
.expect("stop proxy service");
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", windows))]
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
@@ -2191,15 +2320,28 @@ impl ProviderService {
|
||||
.map_err(|e| AppError::Message(format!("更新 Live 备份失败: {e}")))?;
|
||||
}
|
||||
|
||||
if matches!(app_type, AppType::Claude)
|
||||
&& futures::executor::block_on(state.proxy_service.is_running())
|
||||
{
|
||||
futures::executor::block_on(
|
||||
state
|
||||
.proxy_service
|
||||
.sync_claude_live_from_provider_while_proxy_active(&provider),
|
||||
)
|
||||
.map_err(|e| AppError::Message(format!("同步 Claude Live 配置失败: {e}")))?;
|
||||
if futures::executor::block_on(state.proxy_service.is_running()) {
|
||||
if matches!(app_type, AppType::Claude) {
|
||||
futures::executor::block_on(
|
||||
state
|
||||
.proxy_service
|
||||
.sync_claude_live_from_provider_while_proxy_active(&provider),
|
||||
)
|
||||
.map_err(|e| {
|
||||
AppError::Message(format!("同步 Claude Live 配置失败: {e}"))
|
||||
})?;
|
||||
} else if live_taken_over && matches!(app_type, AppType::Codex) {
|
||||
// Codex model mappings are projected into a generated
|
||||
// model_catalog_json file. Refresh takeover-owned Live
|
||||
// immediately so adding/removing mappings cannot leave
|
||||
// the previous catalog pointer and capabilities active.
|
||||
futures::executor::block_on(
|
||||
state
|
||||
.proxy_service
|
||||
.sync_codex_live_from_provider_while_proxy_active(&provider),
|
||||
)
|
||||
.map_err(|e| AppError::Message(format!("同步 Codex Live 配置失败: {e}")))?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
write_live_with_common_config(state.db.as_ref(), &app_type, &provider)?;
|
||||
|
||||
Reference in New Issue
Block a user