Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src-tauri/src/modules/codex_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15348,16 +15348,28 @@ 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");

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);
Expand Down
46 changes: 46 additions & 0 deletions src-tauri/src/modules/codex_local_access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
92 changes: 54 additions & 38 deletions src/components/codex/CodexModelProviderManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ import {
incrementCodexPlanFilterCount,
} from "../../utils/codexAccountOverview";
import {
canConfigureCodexProviderVision,
resolveCodexProviderCapabilityProfile,
type CodexProviderEnableModePreference,
type CodexProviderWireApi,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -2355,6 +2357,7 @@ export function CodexModelProviderManager({
apiVisionRoutingModel: savedProvider.visionRoutingModel,
});
if (updatedAccountCount > 0) {
linkedAccountSnapshotUpdated = true;
await emitAccountsChanged({
platformId: "codex",
reason: "provider-snapshot-sync",
Expand All @@ -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", "模型供应商已保存"),
});
Expand Down Expand Up @@ -3436,6 +3440,10 @@ export function CodexModelProviderManager({
},
[formatUsageMoney, t],
);
const showProviderVisionSettings = canConfigureCodexProviderVision({
presetId: selectedPresetId,
wireApi: form.wireApi,
});

return (
<div className="codex-provider-manager-page">
Expand Down Expand Up @@ -5110,8 +5118,8 @@ export function CodexModelProviderManager({
</label>
</div>
)}
{form.wireApi === "chat_completions" && (
<>
<>
{form.wireApi === "chat_completions" && (
<div className="form-group">
<label>
{t("codex.modelProviders.fields.modelCatalog", "模型目录")}
Expand Down Expand Up @@ -5140,6 +5148,8 @@ export function CodexModelProviderManager({
disabled={saving}
/>
</div>
)}
{showProviderVisionSettings && (
<div className="form-group">
<label>
{t(
Expand All @@ -5158,7 +5168,7 @@ export function CodexModelProviderManager({
<span className="provider-vision-toggle-desc">
{t(
"codex.modelProviders.vision.providerDefaultHint",
"关闭时,只有下方列出的模型会允许图片输入;其他模型会在本地网关直接提示不支持。",
"关闭时,只有下方列出的模型会接收图片;其他模型会省略图片并继续处理文本。",
)}
</span>
</span>
Expand All @@ -5175,6 +5185,8 @@ export function CodexModelProviderManager({
</span>
</label>
</div>
)}
{showProviderVisionSettings && (
<div className="form-group">
<label>
{t(
Expand All @@ -5192,44 +5204,48 @@ export function CodexModelProviderManager({
placeholder={"qwen-vl-plus\ngpt-4o"}
disabled={saving}
/>
<p className="api-provider-hint">
{t(
"codex.modelProviders.vision.modelsHint",
"每行一个模型名。适合同一供应商里只有部分视觉模型支持粘贴图片的情况。",
)}
</p>
</div>
)}
{showProviderVisionSettings && (
<div className="form-group">
<label>
{t(
"codex.modelProviders.fields.visionRoutingModel",
"图片请求默认模型",
)}
</label>
<input
className="form-input"
value={form.visionRoutingModel}
onChange={(event) =>
mutateForm({ visionRoutingModel: event.target.value })
}
placeholder={"mimo-v2.5"}
disabled={saving}
/>
<p className="api-provider-hint">
{t(
"codex.modelProviders.vision.routingModelHint",
"当前模型不支持图片时,带图片请求会改用该模型;留空时若没有唯一视觉模型,则省略图片并继续处理文本。",
)}
</p>
</div>
)}
{form.wireApi === "chat_completions" && (
<p className="api-provider-hint">
{t(
"codex.modelProviders.vision.modelsHint",
"每行一个模型名。适合同一供应商里只有部分视觉模型支持粘贴图片的情况。",
)}
</p>
</div>
<div className="form-group">
<label>
{t(
"codex.modelProviders.fields.visionRoutingModel",
"图片请求默认模型",
)}
</label>
<input
className="form-input"
value={form.visionRoutingModel}
onChange={(event) =>
mutateForm({ visionRoutingModel: event.target.value })
}
placeholder={"mimo-v2.5"}
disabled={saving}
/>
<p className="api-provider-hint">
{t(
"codex.modelProviders.vision.routingModelHint",
"当前模型不支持图片时,带图片的请求会改用该模型;留空则直接提示不支持。",
)}
</p>
</div>
<p className="api-provider-hint">
{t(
"codex.modelProviders.gatewayHint",
"codex.modelProviders.gatewayHint",
"第三方供应商启动时会使用本地网关隔离实例并完成协议转换;OpenAI 官方供应商保持直连。",
)}
</p>
</>
)}
)}
</>
<div className="form-group">
<label>
{t("codex.modelProviders.fields.website", "官网(可选)")}
Expand Down
7 changes: 4 additions & 3 deletions src/locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -3073,7 +3073,8 @@
"fetchCredentialsRequired": "أدخل API Key وBase URL أولًا.",
"fetchEmpty": "لم يُرجع المزوّد أي نماذج. تم الاحتفاظ بالقائمة الحالية.",
"fetchFailed": "تعذر جلب نماذج المزوّد: {{error}}",
"syncRequiresModels": "اجلب نموذجًا واحدًا على الأقل أو أدخله قبل المزامنة مع Codex."
"syncRequiresModels": "اجلب نموذجًا واحدًا على الأقل أو أدخله قبل المزامنة مع Codex.",
"restartHint": "تم تحديث إعدادات المزوّد. أعد تشغيل Codex أو خدمة API إذا كانت قيد التشغيل."
},
"actions": {
"addAndSwitch": "إضافة وتبديل"
Expand Down Expand Up @@ -3404,9 +3405,9 @@
},
"vision": {
"providerDefault": "يدعم هذا المزوّد إدخال الصور افتراضيًا",
"providerDefaultHint": "عند الإيقاف، يسمح فقط للنماذج المذكورة أدناه باستقبال الصور؛ وترفض البوابة المحلية باقي النماذج.",
"providerDefaultHint": "عند الإيقاف، تستقبل الصورَ النماذج المذكورة أدناه فقط؛ أما البقية فتتجاهل الصور وتواصل معالجة النص.",
"modelsHint": "اكتب نموذجًا واحدًا في كل سطر. مناسب عندما تدعم بعض نماذج المزوّد الصور فقط.",
"routingModelHint": "عندما لا يقرأ النموذج الحالي الصور، يستخدم الطلب هذا النموذج؛ إذا تُرك فارغًا يُستخدم نموذج الصور الوحيد تلقائيًا.",
"routingModelHint": "عندما لا يقرأ النموذج الحالي الصور، يستخدم الطلب هذا النموذج؛ وإذا تُرك فارغًا يُستخدم نموذج الصور الوحيد تلقائيًا، وإلا تُحذف الصور وتستمر معالجة النص.",
"allModels": "صور",
"partialModels": "صور جزئية",
"textOnly": "نص فقط"
Expand Down
7 changes: 4 additions & 3 deletions src/locales/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -3026,7 +3026,8 @@
"fetchCredentialsRequired": "Nejprve zadejte API Key a Base URL.",
"fetchEmpty": "Poskytovatel nevrátil žádné modely. Aktuální seznam zůstal zachován.",
"fetchFailed": "Načtení modelů poskytovatele se nezdařilo: {{error}}",
"syncRequiresModels": "Před synchronizací do Codexu načtěte nebo zadejte alespoň jeden model."
"syncRequiresModels": "Před synchronizací do Codexu načtěte nebo zadejte alespoň jeden model.",
"restartHint": "Nastavení poskytovatele bylo aktualizováno. Pokud běží Codex nebo služba API, restartujte je."
},
"actions": {
"addAndSwitch": "Přidat a přepnout"
Expand Down Expand Up @@ -3357,9 +3358,9 @@
},
"vision": {
"providerDefault": "Tento poskytovatel ve výchozím stavu podporuje obrázky",
"providerDefaultHint": "Když je vypnuto, obrázky přijmou jen níže uvedené modely; ostatní místní brána odmítne.",
"providerDefaultHint": "Když je vypnuto, obrázky přijmou jen níže uvedené modely; ostatní obrázky vynechají a pokračují zpracováním textu.",
"modelsHint": "Jeden model na řádek. Hodí se, když obrázky podporuje jen část modelů poskytovatele.",
"routingModelHint": "Když aktuální model nečte obrázky, požadavek použije tento model; prázdné pole automaticky použije jediný vizuální model.",
"routingModelHint": "Když aktuální model nečte obrázky, požadavek použije tento model; prázdné pole automaticky použije jediný vizuální model, jinak se obrázky vynechají a text pokračuje.",
"allModels": "Obrázky",
"partialModels": "Část obrázků",
"textOnly": "Jen text"
Expand Down
7 changes: 4 additions & 3 deletions src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -3026,7 +3026,8 @@
"fetchCredentialsRequired": "Zuerst API Key und Base URL eingeben.",
"fetchEmpty": "Der Anbieter hat keine Modelle zurückgegeben. Die aktuelle Liste wurde beibehalten.",
"fetchFailed": "Anbietermodelle konnten nicht abgerufen werden: {{error}}",
"syncRequiresModels": "Vor der Synchronisierung mit Codex mindestens ein Modell abrufen oder eingeben."
"syncRequiresModels": "Vor der Synchronisierung mit Codex mindestens ein Modell abrufen oder eingeben.",
"restartHint": "Die Anbietereinstellungen wurden aktualisiert. Starten Sie Codex oder den API-Dienst neu, falls er ausgeführt wird."
},
"actions": {
"addAndSwitch": "Hinzufügen und wechseln"
Expand Down Expand Up @@ -3357,9 +3358,9 @@
},
"vision": {
"providerDefault": "Dieser Anbieter unterstützt Bildeingaben standardmäßig",
"providerDefaultHint": "Wenn deaktiviert, dürfen nur die unten gelisteten Modelle Bilder erhalten; andere weist das lokale Gateway ab.",
"providerDefaultHint": "Wenn deaktiviert, erhalten nur die unten gelisteten Modelle Bilder; andere lassen Bilder aus und verarbeiten den Text weiter.",
"modelsHint": "Ein Modell pro Zeile. Nützlich, wenn nur einige Modelle dieses Anbieters eingefügte Bilder unterstützen.",
"routingModelHint": "Wenn das aktuelle Modell keine Bilder liest, nutzt die Anfrage dieses Modell; leer nutzt automatisch das einzige Vision-Modell.",
"routingModelHint": "Wenn das aktuelle Modell keine Bilder liest, nutzt die Anfrage dieses Modell; leer nutzt automatisch das einzige Vision-Modell, andernfalls werden Bilder ausgelassen und Text weiterverarbeitet.",
"allModels": "Bilder",
"partialModels": "Teilweise Bilder",
"textOnly": "Nur Text"
Expand Down
7 changes: 4 additions & 3 deletions src/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -2672,7 +2672,8 @@
"fetchCredentialsRequired": "Enter the API Key and Base URL first.",
"fetchEmpty": "The provider returned no models. The current list was kept.",
"fetchFailed": "Failed to fetch provider models: {{error}}",
"syncRequiresModels": "Fetch or enter at least one model before syncing to Codex."
"syncRequiresModels": "Fetch or enter at least one model before syncing to Codex.",
"restartHint": "Provider settings updated. Restart Codex or the API service if it is running."
},
"actions": {
"addAndSwitch": "Add and Switch"
Expand Down Expand Up @@ -3023,9 +3024,9 @@
},
"vision": {
"providerDefault": "This provider supports image input by default",
"providerDefaultHint": "When off, only the models listed below can receive image input; other models are rejected by the local gateway.",
"providerDefaultHint": "When off, only the models listed below receive image input; other models omit images and continue with text.",
"modelsHint": "One model per line. Use this when only some models in the provider support pasted images.",
"routingModelHint": "When the current model cannot read images, image requests use this model; blank uses the only vision model automatically.",
"routingModelHint": "When the current model cannot read images, image requests use this model; blank uses the only vision model automatically, otherwise images are omitted and text continues.",
"allModels": "Images",
"partialModels": "Some Images",
"textOnly": "Text Only"
Expand Down
7 changes: 4 additions & 3 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2672,7 +2672,8 @@
"fetchCredentialsRequired": "Enter the API Key and Base URL first.",
"fetchEmpty": "The provider returned no models. The current list was kept.",
"fetchFailed": "Failed to fetch provider models: {{error}}",
"syncRequiresModels": "Fetch or enter at least one model before syncing to Codex."
"syncRequiresModels": "Fetch or enter at least one model before syncing to Codex.",
"restartHint": "Provider settings updated. Restart Codex or the API service if it is running."
},
"actions": {
"addAndSwitch": "Add and Switch"
Expand Down Expand Up @@ -3023,9 +3024,9 @@
},
"vision": {
"providerDefault": "This provider supports image input by default",
"providerDefaultHint": "When off, only the models listed below can receive image input; other models are rejected by the local gateway.",
"providerDefaultHint": "When off, only the models listed below receive image input; other models omit images and continue with text.",
"modelsHint": "One model per line. Use this when only some models in the provider support pasted images.",
"routingModelHint": "When the current model cannot read images, image requests use this model; blank uses the only vision model automatically.",
"routingModelHint": "When the current model cannot read images, image requests use this model; blank uses the only vision model automatically, otherwise images are omitted and text continues.",
"allModels": "Images",
"partialModels": "Some Images",
"textOnly": "Text Only"
Expand Down
Loading