From cc00c2abfa066158365c9422362cfb3be8a1497c Mon Sep 17 00:00:00 2001 From: Herry Wang Date: Fri, 21 Aug 2026 10:07:45 +0800 Subject: [PATCH] fix(codex): expose vision settings for Responses providers --- src-tauri/src/modules/codex_account.rs | 18 +++- src-tauri/src/modules/codex_local_access.rs | 46 ++++++++++ .../codex/CodexModelProviderManager.tsx | 92 +++++++++++-------- src/locales/ar.json | 7 +- src/locales/cs.json | 7 +- src/locales/de.json | 7 +- src/locales/en-US.json | 7 +- src/locales/en.json | 7 +- src/locales/es.json | 7 +- src/locales/fr.json | 7 +- src/locales/id.json | 7 +- src/locales/it.json | 7 +- src/locales/ja.json | 7 +- src/locales/ko.json | 7 +- src/locales/pl.json | 7 +- src/locales/pt-br.json | 7 +- src/locales/ru.json | 7 +- src/locales/tr.json | 7 +- src/locales/vi.json | 7 +- src/locales/zh-CN.json | 7 +- src/locales/zh-tw.json | 7 +- src/utils/codexProviderGateway.ts | 12 +++ src/utils/codexProviderPresets.test.ts | 43 ++++++++- 23 files changed, 241 insertions(+), 96 deletions(-) diff --git a/src-tauri/src/modules/codex_account.rs b/src-tauri/src/modules/codex_account.rs index 661f818d9..08beb7ba1 100644 --- a/src-tauri/src/modules/codex_account.rs +++ b/src-tauri/src/modules/codex_account.rs @@ -15348,9 +15348,12 @@ supports_websockets = false vec!["gpt-5".to_string()], Some("responses".to_string()), true, - false, - Default::default(), - None, + true, + std::collections::HashMap::from([ + ("gpt-5".to_string(), true), + ("gpt-5-mini".to_string(), false), + ]), + Some("gpt-5".to_string()), None, ) .expect("sync provider snapshot"); @@ -15358,6 +15361,15 @@ supports_websockets = false assert_eq!(updated, 1); let saved = load_account(&account.id).expect("load updated account"); assert!(saved.api_supports_websockets); + assert!(saved.api_supports_vision); + assert_eq!( + saved.api_model_vision_support, + std::collections::HashMap::from([ + ("gpt-5".to_string(), true), + ("gpt-5-mini".to_string(), false), + ]) + ); + assert_eq!(saved.api_vision_routing_model.as_deref(), Some("gpt-5")); assert_eq!(saved.api_wire_api.as_deref(), Some("responses")); assert_eq!(saved.api_model_catalog, vec!["gpt-5".to_string()]); assert_eq!(saved.last_used, 123); diff --git a/src-tauri/src/modules/codex_local_access.rs b/src-tauri/src/modules/codex_local_access.rs index 72ab053dc..ded9cbdde 100644 --- a/src-tauri/src/modules/codex_local_access.rs +++ b/src-tauri/src/modules/codex_local_access.rs @@ -34461,6 +34461,52 @@ data: {"error":{"code":"server_error","type":"upstream","message":"stream aborte assert!(is_provider_gateway_eligible_account(&account)); } + #[test] + fn provider_gateway_preserves_responses_vision_capabilities() { + let mut account = CodexAccount::new_api_key( + "api-vision".to_string(), + "vision@example.com".to_string(), + "sk-test".to_string(), + CodexApiProviderMode::Custom, + Some("https://relay.example/v1".to_string()), + Some("relay".to_string()), + Some("Relay".to_string()), + vec!["text-model".to_string(), "vision-model".to_string()], + ); + account.api_wire_api = Some("responses".to_string()); + account.api_sync_model_catalog_to_codex = true; + account.api_model_vision_support = HashMap::from([ + ("text-model".to_string(), false), + ("vision-model".to_string(), true), + ]); + account.api_vision_routing_model = Some("vision-model".to_string()); + + assert!(account_requires_provider_gateway(&account)); + + let gateway = + super::provider_gateway_for_account(&account).expect("build provider gateway"); + let manifest_gateway = serde_json::to_value(&gateway).expect("serialize provider gateway"); + + assert_eq!(gateway.wire_api.as_deref(), Some("responses")); + assert!(!gateway.supports_vision); + assert_eq!( + gateway + .model_capabilities + .get("vision-model") + .map(|capability| capability.supports_vision), + Some(true) + ); + assert_eq!( + gateway.vision_routing_model.as_deref(), + Some("vision-model") + ); + assert_eq!( + manifest_gateway["modelCapabilities"]["vision-model"]["supportsVision"], + true + ); + assert_eq!(manifest_gateway["visionRoutingModel"], "vision-model"); + } + fn model_provider_chat_test_request( wire_api: &str, ) -> CodexModelProviderGatewayChatTestRequest { diff --git a/src/components/codex/CodexModelProviderManager.tsx b/src/components/codex/CodexModelProviderManager.tsx index 60eb1eca8..98fcf078f 100644 --- a/src/components/codex/CodexModelProviderManager.tsx +++ b/src/components/codex/CodexModelProviderManager.tsx @@ -151,6 +151,7 @@ import { incrementCodexPlanFilterCount, } from "../../utils/codexAccountOverview"; import { + canConfigureCodexProviderVision, resolveCodexProviderCapabilityProfile, type CodexProviderEnableModePreference, type CodexProviderWireApi, @@ -2249,6 +2250,7 @@ export function CodexModelProviderManager({ setSaving(true); try { let savedProvider: CodexModelProvider | null = null; + let linkedAccountSnapshotUpdated = false; if (!form.providerId) { savedProvider = await createCodexModelProvider({ name, @@ -2355,6 +2357,7 @@ export function CodexModelProviderManager({ apiVisionRoutingModel: savedProvider.visionRoutingModel, }); if (updatedAccountCount > 0) { + linkedAccountSnapshotUpdated = true; await emitAccountsChanged({ platformId: "codex", reason: "provider-snapshot-sync", @@ -2369,10 +2372,11 @@ export function CodexModelProviderManager({ setNotice({ tone: "success", text: - Object.keys(parsedWindows.windows).length > 0 + Object.keys(parsedWindows.windows).length > 0 || + linkedAccountSnapshotUpdated ? `${t("codex.modelProviders.saveSuccess", "模型供应商已保存")} ${t( "codex.api.modelCatalog.restartHint", - "模型目录已更新。若 Codex 正在运行,请重启后生效。", + "供应商配置已更新。若 Codex 或 API 服务正在运行,请重启后生效。", )}` : t("codex.modelProviders.saveSuccess", "模型供应商已保存"), }); @@ -3436,6 +3440,10 @@ export function CodexModelProviderManager({ }, [formatUsageMoney, t], ); + const showProviderVisionSettings = canConfigureCodexProviderVision({ + presetId: selectedPresetId, + wireApi: form.wireApi, + }); return (
@@ -5110,8 +5118,8 @@ export function CodexModelProviderManager({
)} - {form.wireApi === "chat_completions" && ( - <> + <> + {form.wireApi === "chat_completions" && (
+ )} + {showProviderVisionSettings && (
+ )} + {showProviderVisionSettings && (
+ )} + {showProviderVisionSettings && ( +
+ + + mutateForm({ visionRoutingModel: event.target.value }) + } + placeholder={"mimo-v2.5"} + disabled={saving} + /> +

+ {t( + "codex.modelProviders.vision.routingModelHint", + "当前模型不支持图片时,带图片请求会改用该模型;留空时若没有唯一视觉模型,则省略图片并继续处理文本。", + )} +

+
+ )} + {form.wireApi === "chat_completions" && (

{t( - "codex.modelProviders.vision.modelsHint", - "每行一个模型名。适合同一供应商里只有部分视觉模型支持粘贴图片的情况。", - )} -

- -
- - - mutateForm({ visionRoutingModel: event.target.value }) - } - placeholder={"mimo-v2.5"} - disabled={saving} - /> -

- {t( - "codex.modelProviders.vision.routingModelHint", - "当前模型不支持图片时,带图片的请求会改用该模型;留空则直接提示不支持。", - )} -

-
-

- {t( - "codex.modelProviders.gatewayHint", + "codex.modelProviders.gatewayHint", "第三方供应商启动时会使用本地网关隔离实例并完成协议转换;OpenAI 官方供应商保持直连。", )}

- - )} + )} +