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 官方供应商保持直连。",
)}
- >
- )}
+ )}
+ >