From 3f8acbf83559c6807a31264ed5125327088ec861 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Fri, 17 Jul 2026 01:01:18 -0300 Subject: [PATCH 01/94] [needs-vps] fix(dashboard): add vision-capability toggle for custom OpenAI-compatible models (#7124) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(dashboard): add vision-capability toggle for custom OpenAI-compatible models (port from 9router#1904) detectVisionInput()/getCustomVisionCapabilityFields() already honoured an explicit supportsVision flag on a custom-model record, but there was no way to set it: the POST/PUT /api/provider-models Zod schema and updateCustomModel()/addCustomModel() silently dropped the field, and the 'Custom Models' add/edit UI had no checkbox at all. Self-hosted/local backends that don't self-report an image input modality (OpenRouter-style architecture.input_modalities) therefore had no way to be flagged vision-capable, so the vision tag never appeared and image inputs were rejected. Reported-by: nguyenphi37 (https://github.com/decolua/9router/issues/1904) * refactor(dashboard): extract providerCredentialText from providerPageHelpers to respect the file-size gate providerPageHelpers.ts is a frozen god-file (cap 1053, split(\n).length metric) and this PR's own +3 lines (the #1904 supportsVision field) pushed it to 1054, failing check:file-size. Extract the cohesive providerText utility + the 4 web-session-credential label/hint/title helpers into a new leaf module (providerCredentialText.ts), re-exported from providerPageHelpers.ts for backward compatibility so all existing import sites keep working unchanged. File now sits at 946 lines, well under the frozen cap. * refactor(db): extract tri-state override helper to keep the complexity ratchet at baseline The #1904 supportsVision override added a second copy of the "absent keeps / null clears / else coerce" block already used by preserveOpenAIDeveloperRole, pushing updateCustomModel to 84 lines and check:complexity to 2057 > 2056. The file-size failure was masking this one: the gate exits on its first red, so complexity never ran until providerPageHelpers was back under its cap. Fold both blocks into applyTriStateBooleanOverride(). Behavior is unchanged — updateCustomModel is back under max-lines-per-function and the global count returns to the 2056 baseline (cognitive-complexity stays at 890). --- .../fixes/1904-custom-model-vision-toggle.md | 1 + .../[id]/components/CustomModelsSection.tsx | 54 ++++++ .../providers/[id]/providerCredentialText.ts | 132 +++++++++++++++ .../providers/[id]/providerPageHelpers.ts | 155 +++--------------- src/app/api/provider-models/route.ts | 8 +- src/i18n/messages/ar.json | 4 +- src/i18n/messages/az.json | 4 +- src/i18n/messages/bg.json | 4 +- src/i18n/messages/bn.json | 4 +- src/i18n/messages/cs.json | 4 +- src/i18n/messages/da.json | 4 +- src/i18n/messages/de.json | 4 +- src/i18n/messages/en.json | 2 + src/i18n/messages/es.json | 4 +- src/i18n/messages/fa.json | 4 +- src/i18n/messages/fi.json | 4 +- src/i18n/messages/fr.json | 4 +- src/i18n/messages/gu.json | 4 +- src/i18n/messages/he.json | 4 +- src/i18n/messages/hi.json | 4 +- src/i18n/messages/hu.json | 4 +- src/i18n/messages/id.json | 4 +- src/i18n/messages/in.json | 4 +- src/i18n/messages/it.json | 4 +- src/i18n/messages/ja.json | 4 +- src/i18n/messages/ko.json | 4 +- src/i18n/messages/mr.json | 4 +- src/i18n/messages/ms.json | 4 +- src/i18n/messages/nl.json | 4 +- src/i18n/messages/no.json | 4 +- src/i18n/messages/phi.json | 4 +- src/i18n/messages/pl.json | 4 +- src/i18n/messages/pt-BR.json | 4 +- src/i18n/messages/pt.json | 4 +- src/i18n/messages/ro.json | 4 +- src/i18n/messages/ru.json | 4 +- src/i18n/messages/sk.json | 4 +- src/i18n/messages/sv.json | 4 +- src/i18n/messages/sw.json | 4 +- src/i18n/messages/ta.json | 4 +- src/i18n/messages/te.json | 4 +- src/i18n/messages/th.json | 4 +- src/i18n/messages/tr.json | 4 +- src/i18n/messages/uk-UA.json | 4 +- src/i18n/messages/ur.json | 4 +- src/i18n/messages/vi.json | 4 +- src/i18n/messages/zh-CN.json | 4 +- src/i18n/messages/zh-TW.json | 4 +- src/lib/db/models.ts | 36 +++- src/shared/validation/schemas/provider.ts | 6 + ...ovider-models-vision-override-1904.test.ts | 151 +++++++++++++++++ 51 files changed, 532 insertions(+), 181 deletions(-) create mode 100644 changelog.d/fixes/1904-custom-model-vision-toggle.md create mode 100644 src/app/(dashboard)/dashboard/providers/[id]/providerCredentialText.ts create mode 100644 tests/unit/provider-models-vision-override-1904.test.ts diff --git a/changelog.d/fixes/1904-custom-model-vision-toggle.md b/changelog.d/fixes/1904-custom-model-vision-toggle.md new file mode 100644 index 0000000000..85fd14aff1 --- /dev/null +++ b/changelog.d/fixes/1904-custom-model-vision-toggle.md @@ -0,0 +1 @@ +- **fix(dashboard):** the "Custom Models" add/edit form now has a "Vision capable" toggle so a custom OpenAI-compatible model can be manually flagged as vision-capable when the provider's discovery metadata doesn't report an image input modality (thanks @nguyenphi37) diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/CustomModelsSection.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/CustomModelsSection.tsx index 76b861739d..dd2e842159 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/CustomModelsSection.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/CustomModelsSection.tsx @@ -98,6 +98,11 @@ export default function CustomModelsSection({ // #4125: manual context-window override (Feature 5004 table) — free text so the // field can be left blank (no override) without fighting a number input's "0". const [editingContextWindowOverride, setEditingContextWindowOverride] = useState(""); + // #1904: manual vision-capability override — some self-hosted/local OpenAI-compatible + // backends don't self-report an image input modality, so the user needs a way to flag + // the model as vision-capable by hand (read back by getCustomVisionCapabilityFields()). + const [newSupportsVision, setNewSupportsVision] = useState(false); + const [editingSupportsVision, setEditingSupportsVision] = useState(false); const customMap = useMemo(() => buildCompatMap(customModels), [customModels]); const overrideMap = useMemo(() => buildCompatMap(modelCompatOverrides), [modelCompatOverrides]); @@ -135,6 +140,7 @@ export default function CustomModelsSection({ apiFormat: newApiFormat, supportedEndpoints: newEndpoints, ...(newTargetFormat ? { targetFormat: newTargetFormat } : {}), + ...(newSupportsVision ? { supportsVision: true } : {}), }), }); if (res.ok) { @@ -143,6 +149,7 @@ export default function CustomModelsSection({ setNewApiFormat("chat-completions"); setNewEndpoints(["chat"]); setNewTargetFormat(""); + setNewSupportsVision(false); await fetchCustomModels(); onModelsChanged?.(); } @@ -202,6 +209,7 @@ export default function CustomModelsSection({ setEditingContextWindowOverride( typeof model.contextWindowOverride === "number" ? String(model.contextWindowOverride) : "" ); + setEditingSupportsVision(model.supportsVision === true); }; const cancelEdit = () => { @@ -210,6 +218,7 @@ export default function CustomModelsSection({ setEditingEndpoints(["chat"]); setEditingTargetFormat(""); setEditingContextWindowOverride(""); + setEditingSupportsVision(false); setSavingModelId(null); }; @@ -268,6 +277,9 @@ export default function CustomModelsSection({ ...(editingTargetFormat ? { targetFormat: editingTargetFormat } : {}), // #4125: manual context-window override — number to set, null to clear. contextWindowOverride, + // #1904: manual vision-capability override — true/false to set, null to + // clear back to the id-based heuristic. + supportsVision: editingSupportsVision ? true : null, }), }); @@ -425,6 +437,23 @@ export default function CustomModelsSection({ ))} +
+   + +
@@ -482,6 +511,14 @@ export default function CustomModelsSection({ {`🪟 ${model.contextWindowOverride.toLocaleString()}`} )} + {model.supportsVision === true && ( + + {`👁️ ${t("visionCapableLabel")}`} + + )} {model.supportedEndpoints?.includes("embeddings") && ( {`📐 ${t("supportedEndpointEmbeddings")}`} @@ -578,6 +615,23 @@ export default function CustomModelsSection({ className="w-full px-2.5 py-2 text-xs border border-border rounded-lg bg-background text-text-main focus:outline-none focus:border-primary" /> +
+ + +
{t("supportedEndpointsLabel")} diff --git a/src/app/(dashboard)/dashboard/providers/[id]/providerCredentialText.ts b/src/app/(dashboard)/dashboard/providers/[id]/providerCredentialText.ts new file mode 100644 index 0000000000..6c2c20026d --- /dev/null +++ b/src/app/(dashboard)/dashboard/providers/[id]/providerCredentialText.ts @@ -0,0 +1,132 @@ +// Pure, shared helpers for provider credential copy (labels/hints/titles for +// the API-key and web-session-credential modals). Extracted out of +// providerPageHelpers.ts (Issue #3501 strangler-fig decomposition) — that leaf +// is frozen at its file-size cap, so this cohesive slice (message-translation +// utility + the 4 web-session-credential text builders) lives here instead and +// is re-exported from providerPageHelpers.ts for backward compatibility. Leaf +// module — imports only from @/shared, @/lib and colocated sibling modules. +import { type WebSessionCredentialRequirement } from "./webSessionCredentials"; + +export type ProviderMessageTranslator = (( + key: string, + values?: Record +) => string) & { + has?: (key: string) => boolean; +}; + +export function providerText( + t: ProviderMessageTranslator, + key: string, + fallback: string, + values?: Record +): string { + if (typeof t.has === "function" && t.has(key)) { + return t(key, values); + } + if (values) { + return Object.entries(values).reduce( + (acc, [name, value]) => acc.replaceAll(`{${name}}`, String(value)), + fallback + ); + } + return fallback; +} + +export function getWebSessionCredentialLabel( + t: ProviderMessageTranslator, + requirement: WebSessionCredentialRequirement, + optional: boolean +): string { + if (requirement.kind === "none") { + return providerText(t, "webNoAuthCredentialLabel", "No credential required"); + } + const baseLabel = + requirement.kind === "token" + ? providerText(t, "webTokenCredentialLabel", "Web session token") + : t("sessionCookieLabel"); + return optional ? `${baseLabel} (${t("optional").toLowerCase()})` : baseLabel; +} + +export function getWebSessionCredentialHint( + t: ProviderMessageTranslator, + requirement: WebSessionCredentialRequirement, + providerName: string, + editing: boolean +): string | undefined { + if (requirement.kind === "none") return undefined; + + const values = { provider: providerName, credential: requirement.credentialName }; + if (editing) { + return requirement.kind === "token" + ? providerText( + t, + "webTokenEditHint", + "Leave blank to keep the current web session token. Credential: {credential}.", + values + ) + : providerText( + t, + "webCookieEditHint", + "Leave blank to keep the current session cookie. Required cookie: {credential}.", + values + ); + } + + // #5465 — a provider-specific hint (e.g. t3.chat's step-by-step DevTools copy) + // replaces the generic one-line cookie/token template when that template is + // unclear for the provider (t3.chat needs a localStorage value AND the Cookie + // header, so "Required cookie: convex-session-id + Cookie header…" reads + // circular). The override key ships translated in every locale. + if (requirement.hintKey) { + return providerText( + t, + requirement.hintKey, + requirement.hintFallback ?? + "Open the provider's web session in DevTools, copy the required credential(s), and paste them in the fields below.", + values + ); + } + + return requirement.kind === "token" + ? providerText( + t, + "webTokenCredentialHint", + "Credential: {credential}. Paste the token value from your own signed-in {provider} web session, or a DevTools HAR export if the provider supports it.", + values + ) + : providerText( + t, + "webCookieCredentialHint", + "Required cookie: {credential}. Paste the Cookie header value from your own signed-in {provider} web session. Do not include the Cookie: prefix.", + values + ); +} + +export function getWebSessionCredentialCheckLabel( + t: ProviderMessageTranslator, + requirement: WebSessionCredentialRequirement +): string { + if (requirement.kind === "token") return providerText(t, "checkWebToken", "Check token"); + return providerText(t, "checkCookie", "Check cookie"); +} + +export function getAddCredentialModalTitle( + t: ProviderMessageTranslator, + providerName: string, + requirement: WebSessionCredentialRequirement | null +): string { + if (!requirement) return t("addProviderApiKeyTitle", { provider: providerName }); + if (requirement.kind === "none") { + return providerText(t, "addProviderConnectionTitle", "Add {provider} connection", { + provider: providerName, + }); + } + if (requirement.kind === "token") { + return providerText(t, "addProviderWebTokenTitle", "Add {provider} web token", { + provider: providerName, + }); + } + return providerText(t, "addProviderSessionCookieTitle", "Add {provider} session cookie", { + provider: providerName, + }); +} diff --git a/src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts b/src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts index 4345d11962..22a25dc59f 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts +++ b/src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts @@ -16,20 +16,32 @@ import { type CodexServiceTier, } from "@/lib/providers/requestDefaults"; import { type CodexGlobalServiceMode } from "@/lib/providers/codexFastTier"; -import { type WebSessionCredentialRequirement } from "./webSessionCredentials"; import { CC_COMPATIBLE_DEFAULT_CHAT_PATH } from "./providerDetailConstants"; +import { + type ProviderMessageTranslator, + providerText, + getWebSessionCredentialLabel, + getWebSessionCredentialHint, + getWebSessionCredentialCheckLabel, + getAddCredentialModalTitle, +} from "./providerCredentialText"; + +// Re-exported for backward compatibility — these used to be defined here +// (Issue #3501 strangler-fig home), but were extracted to providerCredentialText.ts +// once this leaf hit its frozen file-size cap (#1904 own growth). +export { + type ProviderMessageTranslator, + providerText, + getWebSessionCredentialLabel, + getWebSessionCredentialHint, + getWebSessionCredentialCheckLabel, + getAddCredentialModalTitle, +}; // --------------------------------------------------------------------------- // Types shared between page + modals // --------------------------------------------------------------------------- -export type ProviderMessageTranslator = (( - key: string, - values?: Record -) => string) & { - has?: (key: string) => boolean; -}; - export type LocalProviderMetadata = { name?: string; localDefault?: string; @@ -76,6 +88,9 @@ export type CompatModelRow = { compatByProtocol?: CompatByProtocolMap; /** #2905: per-model upstream wire-format override. */ targetFormat?: string; /** #4125: manual context-window override (tokens), when set. */ contextWindowOverride?: number; + /** #1904: manual vision-capability override for custom models whose upstream + * discovery metadata doesn't self-report an image input modality. */ + supportsVision?: boolean; }; export type CompatModelMap = Map; @@ -98,28 +113,6 @@ export function targetFormatBadgeI18nKey(value: string): string | null { return TARGET_FORMAT_BADGE_I18N_KEYS[value] ?? null; } -// --------------------------------------------------------------------------- -// Utility — message translation with fallback -// --------------------------------------------------------------------------- - -export function providerText( - t: ProviderMessageTranslator, - key: string, - fallback: string, - values?: Record -): string { - if (typeof t.has === "function" && t.has(key)) { - return t(key, values); - } - if (values) { - return Object.entries(values).reduce( - (acc, [name, value]) => acc.replaceAll(`{${name}}`, String(value)), - fallback - ); - } - return fallback; -} - /** #5442 — badge for add-credential validation; unsupported → neutral N/A (not red Invalid). */ export function validationBadgeProps(result: string): { variant: "success" | "error" | "info"; @@ -385,108 +378,10 @@ export function formatExcludedModelsInput(value: unknown): string { } // --------------------------------------------------------------------------- -// Web-session credential label / hint helpers (Phase 2b) +// Web-session credential label / hint helpers (Phase 2b) — moved to +// providerCredentialText.ts (#1904 own growth); re-exported above. // --------------------------------------------------------------------------- -export function getWebSessionCredentialLabel( - t: ProviderMessageTranslator, - requirement: WebSessionCredentialRequirement, - optional: boolean -): string { - if (requirement.kind === "none") { - return providerText(t, "webNoAuthCredentialLabel", "No credential required"); - } - const baseLabel = - requirement.kind === "token" - ? providerText(t, "webTokenCredentialLabel", "Web session token") - : t("sessionCookieLabel"); - return optional ? `${baseLabel} (${t("optional").toLowerCase()})` : baseLabel; -} - -export function getWebSessionCredentialHint( - t: ProviderMessageTranslator, - requirement: WebSessionCredentialRequirement, - providerName: string, - editing: boolean -): string | undefined { - if (requirement.kind === "none") return undefined; - - const values = { provider: providerName, credential: requirement.credentialName }; - if (editing) { - return requirement.kind === "token" - ? providerText( - t, - "webTokenEditHint", - "Leave blank to keep the current web session token. Credential: {credential}.", - values - ) - : providerText( - t, - "webCookieEditHint", - "Leave blank to keep the current session cookie. Required cookie: {credential}.", - values - ); - } - - // #5465 — a provider-specific hint (e.g. t3.chat's step-by-step DevTools copy) - // replaces the generic one-line cookie/token template when that template is - // unclear for the provider (t3.chat needs a localStorage value AND the Cookie - // header, so "Required cookie: convex-session-id + Cookie header…" reads - // circular). The override key ships translated in every locale. - if (requirement.hintKey) { - return providerText( - t, - requirement.hintKey, - requirement.hintFallback ?? - "Open the provider's web session in DevTools, copy the required credential(s), and paste them in the fields below.", - values - ); - } - - return requirement.kind === "token" - ? providerText( - t, - "webTokenCredentialHint", - "Credential: {credential}. Paste the token value from your own signed-in {provider} web session, or a DevTools HAR export if the provider supports it.", - values - ) - : providerText( - t, - "webCookieCredentialHint", - "Required cookie: {credential}. Paste the Cookie header value from your own signed-in {provider} web session. Do not include the Cookie: prefix.", - values - ); -} - -export function getWebSessionCredentialCheckLabel( - t: ProviderMessageTranslator, - requirement: WebSessionCredentialRequirement -): string { - if (requirement.kind === "token") return providerText(t, "checkWebToken", "Check token"); - return providerText(t, "checkCookie", "Check cookie"); -} - -export function getAddCredentialModalTitle( - t: ProviderMessageTranslator, - providerName: string, - requirement: WebSessionCredentialRequirement | null -): string { - if (!requirement) return t("addProviderApiKeyTitle", { provider: providerName }); - if (requirement.kind === "none") { - return providerText(t, "addProviderConnectionTitle", "Add {provider} connection", { - provider: providerName, - }); - } - if (requirement.kind === "token") { - return providerText(t, "addProviderWebTokenTitle", "Add {provider} web token", { - provider: providerName, - }); - } - return providerText(t, "addProviderSessionCookieTitle", "Add {provider} session cookie", { - provider: providerName, - }); -} - // --------------------------------------------------------------------------- // Upstream-headers helpers (Phase 2b) // --------------------------------------------------------------------------- diff --git a/src/app/api/provider-models/route.ts b/src/app/api/provider-models/route.ts index e606a124d0..0bb1ee765b 100644 --- a/src/app/api/provider-models/route.ts +++ b/src/app/api/provider-models/route.ts @@ -129,6 +129,8 @@ export async function POST(request) { // #1294: persist the per-model token limits set in the add-model form. max_input_tokens: maxInputTokens, max_output_tokens: maxOutputTokens, + // #1904: manual vision-capability override set in the add-model form. + supportsVision, } = validation.data; const model = await addCustomModel( @@ -142,7 +144,8 @@ export async function POST(request) { { ...(maxInputTokens != null ? { inputTokenLimit: maxInputTokens } : {}), ...(maxOutputTokens != null ? { outputTokenLimit: maxOutputTokens } : {}), - } + }, + typeof supportsVision === "boolean" ? supportsVision : undefined ); return Response.json({ model }); } catch (error) { @@ -194,6 +197,7 @@ export async function PUT(request) { upstreamHeaders, compatByProtocol, contextWindowOverride, + supportsVision, } = validation.data; const raw = rawBody as Record; @@ -206,6 +210,8 @@ export async function PUT(request) { if ("preserveOpenAIDeveloperRole" in raw) updates.preserveOpenAIDeveloperRole = preserveOpenAIDeveloperRole; if ("upstreamHeaders" in raw) updates.upstreamHeaders = upstreamHeaders; + // #1904: manual vision-capability override — null clears back to heuristic. + if ("supportsVision" in raw) updates.supportsVision = supportsVision; if ("compatByProtocol" in raw && compatByProtocol !== undefined) { updates.compatByProtocol = compatByProtocol; } diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index de049135af..4a283b1739 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -4427,7 +4427,9 @@ "contextWindowOverrideLabel": "__MISSING__:Context Window Override", "contextWindowOverridePlaceholder": "__MISSING__:e.g. 131072", "contextWindowOverrideHint": "__MISSING__:Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", - "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens" + "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "الإعدادات", diff --git a/src/i18n/messages/az.json b/src/i18n/messages/az.json index e0694b225d..6db0afc9b2 100644 --- a/src/i18n/messages/az.json +++ b/src/i18n/messages/az.json @@ -4427,7 +4427,9 @@ "contextWindowOverrideLabel": "__MISSING__:Context Window Override", "contextWindowOverridePlaceholder": "__MISSING__:e.g. 131072", "contextWindowOverrideHint": "__MISSING__:Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", - "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens" + "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Settings", diff --git a/src/i18n/messages/bg.json b/src/i18n/messages/bg.json index b6e9952e85..4bcacbc6a7 100644 --- a/src/i18n/messages/bg.json +++ b/src/i18n/messages/bg.json @@ -4427,7 +4427,9 @@ "contextWindowOverrideLabel": "__MISSING__:Context Window Override", "contextWindowOverridePlaceholder": "__MISSING__:e.g. 131072", "contextWindowOverrideHint": "__MISSING__:Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", - "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens" + "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Настройки", diff --git a/src/i18n/messages/bn.json b/src/i18n/messages/bn.json index 27a35531b4..7b50366906 100644 --- a/src/i18n/messages/bn.json +++ b/src/i18n/messages/bn.json @@ -4427,7 +4427,9 @@ "contextWindowOverrideLabel": "__MISSING__:Context Window Override", "contextWindowOverridePlaceholder": "__MISSING__:e.g. 131072", "contextWindowOverrideHint": "__MISSING__:Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", - "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens" + "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Settings", diff --git a/src/i18n/messages/cs.json b/src/i18n/messages/cs.json index 0f2db58994..dbd7bd93af 100644 --- a/src/i18n/messages/cs.json +++ b/src/i18n/messages/cs.json @@ -4427,7 +4427,9 @@ "contextWindowOverrideLabel": "__MISSING__:Context Window Override", "contextWindowOverridePlaceholder": "__MISSING__:e.g. 131072", "contextWindowOverrideHint": "__MISSING__:Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", - "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens" + "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Nastavení", diff --git a/src/i18n/messages/da.json b/src/i18n/messages/da.json index 3db278c319..06e5530671 100644 --- a/src/i18n/messages/da.json +++ b/src/i18n/messages/da.json @@ -4427,7 +4427,9 @@ "contextWindowOverrideLabel": "__MISSING__:Context Window Override", "contextWindowOverridePlaceholder": "__MISSING__:e.g. 131072", "contextWindowOverrideHint": "__MISSING__:Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", - "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens" + "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Indstillinger", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index f9b37487c5..b6b3c7d610 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -4432,7 +4432,9 @@ "contextWindowOverrideLabel": "__MISSING__:Context Window Override", "contextWindowOverridePlaceholder": "__MISSING__:e.g. 131072", "contextWindowOverrideHint": "__MISSING__:Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", - "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens" + "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Einstellungen", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 1e172158a6..49e452e56e 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -4326,6 +4326,8 @@ "contextWindowOverridePlaceholder": "e.g. 131072", "contextWindowOverrideHint": "Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", "contextWindowOverrideInvalid": "Context window override must be a positive whole number of tokens", + "visionCapableLabel": "Vision capable", + "visionCapableHint": "Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends).", "compatParamFiltersLabel": "Param Filters", "compatBlockedParamsHint": "Blocked params (stripped from requests)", "compatAllowedParamsHint": "Allowed params (re-added after deny)", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 84c20d31b9..695fc392f8 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -4427,7 +4427,9 @@ "contextWindowOverrideLabel": "__MISSING__:Context Window Override", "contextWindowOverridePlaceholder": "__MISSING__:e.g. 131072", "contextWindowOverrideHint": "__MISSING__:Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", - "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens" + "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Configuración", diff --git a/src/i18n/messages/fa.json b/src/i18n/messages/fa.json index 2e85c028be..b3a0a4a5f4 100644 --- a/src/i18n/messages/fa.json +++ b/src/i18n/messages/fa.json @@ -4427,7 +4427,9 @@ "contextWindowOverrideLabel": "__MISSING__:Context Window Override", "contextWindowOverridePlaceholder": "__MISSING__:e.g. 131072", "contextWindowOverrideHint": "__MISSING__:Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", - "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens" + "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Settings", diff --git a/src/i18n/messages/fi.json b/src/i18n/messages/fi.json index dff90e6c94..4d400432b6 100644 --- a/src/i18n/messages/fi.json +++ b/src/i18n/messages/fi.json @@ -4427,7 +4427,9 @@ "contextWindowOverrideLabel": "__MISSING__:Context Window Override", "contextWindowOverridePlaceholder": "__MISSING__:e.g. 131072", "contextWindowOverrideHint": "__MISSING__:Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", - "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens" + "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Asetukset", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 39188d982a..24b2fe0719 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -4427,7 +4427,9 @@ "contextWindowOverrideLabel": "__MISSING__:Context Window Override", "contextWindowOverridePlaceholder": "__MISSING__:e.g. 131072", "contextWindowOverrideHint": "__MISSING__:Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", - "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens" + "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Paramètres", diff --git a/src/i18n/messages/gu.json b/src/i18n/messages/gu.json index 4b79943447..c134d188ac 100644 --- a/src/i18n/messages/gu.json +++ b/src/i18n/messages/gu.json @@ -4427,7 +4427,9 @@ "contextWindowOverrideLabel": "__MISSING__:Context Window Override", "contextWindowOverridePlaceholder": "__MISSING__:e.g. 131072", "contextWindowOverrideHint": "__MISSING__:Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", - "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens" + "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Settings", diff --git a/src/i18n/messages/he.json b/src/i18n/messages/he.json index 5740d5cd97..ca05be2f50 100644 --- a/src/i18n/messages/he.json +++ b/src/i18n/messages/he.json @@ -4427,7 +4427,9 @@ "contextWindowOverrideLabel": "__MISSING__:Context Window Override", "contextWindowOverridePlaceholder": "__MISSING__:e.g. 131072", "contextWindowOverrideHint": "__MISSING__:Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", - "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens" + "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "הגדרות", diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index 11bee167d9..9747aa5692 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -4427,7 +4427,9 @@ "contextWindowOverrideLabel": "__MISSING__:Context Window Override", "contextWindowOverridePlaceholder": "__MISSING__:e.g. 131072", "contextWindowOverrideHint": "__MISSING__:Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", - "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens" + "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "सेटिंग्स", diff --git a/src/i18n/messages/hu.json b/src/i18n/messages/hu.json index 1dd27e618e..f78262d8dd 100644 --- a/src/i18n/messages/hu.json +++ b/src/i18n/messages/hu.json @@ -4427,7 +4427,9 @@ "contextWindowOverrideLabel": "__MISSING__:Context Window Override", "contextWindowOverridePlaceholder": "__MISSING__:e.g. 131072", "contextWindowOverrideHint": "__MISSING__:Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", - "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens" + "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Beállítások elemre", diff --git a/src/i18n/messages/id.json b/src/i18n/messages/id.json index 4f2c76c499..a391edda6b 100644 --- a/src/i18n/messages/id.json +++ b/src/i18n/messages/id.json @@ -4427,7 +4427,9 @@ "contextWindowOverrideLabel": "__MISSING__:Context Window Override", "contextWindowOverridePlaceholder": "__MISSING__:e.g. 131072", "contextWindowOverrideHint": "__MISSING__:Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", - "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens" + "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Pengaturan", diff --git a/src/i18n/messages/in.json b/src/i18n/messages/in.json index ace72ac9ca..6b9d722b85 100644 --- a/src/i18n/messages/in.json +++ b/src/i18n/messages/in.json @@ -4427,7 +4427,9 @@ "contextWindowOverrideLabel": "__MISSING__:Context Window Override", "contextWindowOverridePlaceholder": "__MISSING__:e.g. 131072", "contextWindowOverrideHint": "__MISSING__:Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", - "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens" + "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Settings", diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index 1e1f70cf43..0289b4753c 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -4854,7 +4854,9 @@ "compatibleDefaultModelHint": "Inserisci l'ID modello esattamente come lo aspetta il tuo endpoint compatibile. Questo modello verrà salvato come default della connessione.", "compatibleDefaultModelLabel": "Modello Predefinito", "iconUrlHint": "Opzionale. URL dell'immagine mostrata come icona di questo provider.", - "iconUrlLabel": "URL Icona" + "iconUrlLabel": "URL Icona", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Impostazioni", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index c052fed1d7..0e0790b90e 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -4820,7 +4820,9 @@ "kimiWebLabel": "__MISSING__:Kimi Web", "kimiWebDesc": "__MISSING__:Moonshot AI consumer chat via www.kimi.com (international, Connect-RPC API)", "doubaoWebLabel": "__MISSING__:Doubao Web", - "doubaoWebDesc": "__MISSING__:ByteDance AI chat via doubao.com" + "doubaoWebDesc": "__MISSING__:ByteDance AI chat via doubao.com", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "設定", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index e300c36890..8f4653aa6f 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -4820,7 +4820,9 @@ "kimiWebLabel": "__MISSING__:Kimi Web", "kimiWebDesc": "__MISSING__:Moonshot AI consumer chat via www.kimi.com (international, Connect-RPC API)", "doubaoWebLabel": "Dola Web", - "doubaoWebDesc": "dola.com을 통한 ByteDance AI 채팅" + "doubaoWebDesc": "dola.com을 통한 ByteDance AI 채팅", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "설정", diff --git a/src/i18n/messages/mr.json b/src/i18n/messages/mr.json index 1ad40bb3ea..9db23dc9ae 100644 --- a/src/i18n/messages/mr.json +++ b/src/i18n/messages/mr.json @@ -4820,7 +4820,9 @@ "kimiWebLabel": "__MISSING__:Kimi Web", "kimiWebDesc": "__MISSING__:Moonshot AI consumer chat via www.kimi.com (international, Connect-RPC API)", "doubaoWebLabel": "__MISSING__:Doubao Web", - "doubaoWebDesc": "__MISSING__:ByteDance AI chat via doubao.com" + "doubaoWebDesc": "__MISSING__:ByteDance AI chat via doubao.com", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Settings", diff --git a/src/i18n/messages/ms.json b/src/i18n/messages/ms.json index f01ceac27e..431c3f30f4 100644 --- a/src/i18n/messages/ms.json +++ b/src/i18n/messages/ms.json @@ -4820,7 +4820,9 @@ "kimiWebLabel": "__MISSING__:Kimi Web", "kimiWebDesc": "__MISSING__:Moonshot AI consumer chat via www.kimi.com (international, Connect-RPC API)", "doubaoWebLabel": "__MISSING__:Doubao Web", - "doubaoWebDesc": "__MISSING__:ByteDance AI chat via doubao.com" + "doubaoWebDesc": "__MISSING__:ByteDance AI chat via doubao.com", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "tetapan", diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json index 560df3cb73..59434cb7a3 100644 --- a/src/i18n/messages/nl.json +++ b/src/i18n/messages/nl.json @@ -4820,7 +4820,9 @@ "kimiWebLabel": "__MISSING__:Kimi Web", "kimiWebDesc": "__MISSING__:Moonshot AI consumer chat via www.kimi.com (international, Connect-RPC API)", "doubaoWebLabel": "__MISSING__:Doubao Web", - "doubaoWebDesc": "__MISSING__:ByteDance AI chat via doubao.com" + "doubaoWebDesc": "__MISSING__:ByteDance AI chat via doubao.com", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Instellingen", diff --git a/src/i18n/messages/no.json b/src/i18n/messages/no.json index 47b6584623..7565205350 100644 --- a/src/i18n/messages/no.json +++ b/src/i18n/messages/no.json @@ -4820,7 +4820,9 @@ "kimiWebLabel": "__MISSING__:Kimi Web", "kimiWebDesc": "__MISSING__:Moonshot AI consumer chat via www.kimi.com (international, Connect-RPC API)", "doubaoWebLabel": "__MISSING__:Doubao Web", - "doubaoWebDesc": "__MISSING__:ByteDance AI chat via doubao.com" + "doubaoWebDesc": "__MISSING__:ByteDance AI chat via doubao.com", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Innstillinger", diff --git a/src/i18n/messages/phi.json b/src/i18n/messages/phi.json index 7e6afb8e3d..7c95c0ea5f 100644 --- a/src/i18n/messages/phi.json +++ b/src/i18n/messages/phi.json @@ -4820,7 +4820,9 @@ "kimiWebLabel": "__MISSING__:Kimi Web", "kimiWebDesc": "__MISSING__:Moonshot AI consumer chat via www.kimi.com (international, Connect-RPC API)", "doubaoWebLabel": "__MISSING__:Doubao Web", - "doubaoWebDesc": "__MISSING__:ByteDance AI chat via doubao.com" + "doubaoWebDesc": "__MISSING__:ByteDance AI chat via doubao.com", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Mga setting", diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json index 6ea76ab160..582616a908 100644 --- a/src/i18n/messages/pl.json +++ b/src/i18n/messages/pl.json @@ -4820,7 +4820,9 @@ "kimiWebLabel": "__MISSING__:Kimi Web", "kimiWebDesc": "__MISSING__:Moonshot AI consumer chat via www.kimi.com (international, Connect-RPC API)", "doubaoWebLabel": "__MISSING__:Doubao Web", - "doubaoWebDesc": "__MISSING__:ByteDance AI chat via doubao.com" + "doubaoWebDesc": "__MISSING__:ByteDance AI chat via doubao.com", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Ustawienia", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index 758dd533a2..aeec076978 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -4891,7 +4891,9 @@ "overrideBaseUrlAdvanced": "Avançado: sobrescrever URL base", "overrideBaseUrlHint": "Avançado: aponta este provedor embutido para um endpoint personalizado. Deixe em branco para usar o padrão.", "bulkAddFormatHintCloudflare": "Uma chave por linha. Formato: nome|accountId|apiKey (ID de conta Cloudflare + token de API).", - "lmarenaWebCookieHint": "Abra arena.ai, faça login e depois copie o cabeçalho Cookie completo de uma requisição de rede. Inclua arena-auth-prod-v1.0 e arena-auth-prod-v1.1 (e outros fragmentos, se houver), preferencialmente com cf_clearance. Não cole apenas o cookie vazio arena-auth-prod-v1. Opcional: providerSpecificData.recaptchaV3Token se create-evaluation ainda retornar 403." + "lmarenaWebCookieHint": "Abra arena.ai, faça login e depois copie o cabeçalho Cookie completo de uma requisição de rede. Inclua arena-auth-prod-v1.0 e arena-auth-prod-v1.1 (e outros fragmentos, se houver), preferencialmente com cf_clearance. Não cole apenas o cookie vazio arena-auth-prod-v1. Opcional: providerSpecificData.recaptchaV3Token se create-evaluation ainda retornar 403.", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Configurações", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 0b01ff143f..7e4e961ae0 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -4820,7 +4820,9 @@ "kimiWebLabel": "__MISSING__:Kimi Web", "kimiWebDesc": "__MISSING__:Moonshot AI consumer chat via www.kimi.com (international, Connect-RPC API)", "doubaoWebLabel": "__MISSING__:Doubao Web", - "doubaoWebDesc": "__MISSING__:ByteDance AI chat via doubao.com" + "doubaoWebDesc": "__MISSING__:ByteDance AI chat via doubao.com", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Configurações", diff --git a/src/i18n/messages/ro.json b/src/i18n/messages/ro.json index 635ce8f982..7685d3d604 100644 --- a/src/i18n/messages/ro.json +++ b/src/i18n/messages/ro.json @@ -4820,7 +4820,9 @@ "kimiWebLabel": "__MISSING__:Kimi Web", "kimiWebDesc": "__MISSING__:Moonshot AI consumer chat via www.kimi.com (international, Connect-RPC API)", "doubaoWebLabel": "__MISSING__:Doubao Web", - "doubaoWebDesc": "__MISSING__:ByteDance AI chat via doubao.com" + "doubaoWebDesc": "__MISSING__:ByteDance AI chat via doubao.com", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Setări", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index a3d025301a..4b7369a7d5 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -4820,7 +4820,9 @@ "kimiWebLabel": "Kimi Web", "kimiWebDesc": "Чат Moonshot AI через www.kimi.com (международная версия, Connect-RPC API)", "doubaoWebLabel": "Doubao Web", - "doubaoWebDesc": "Чат AI ByteDance через doubao.com" + "doubaoWebDesc": "Чат AI ByteDance через doubao.com", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Настройки", diff --git a/src/i18n/messages/sk.json b/src/i18n/messages/sk.json index e65ced31e9..e6fc0eab2c 100644 --- a/src/i18n/messages/sk.json +++ b/src/i18n/messages/sk.json @@ -4820,7 +4820,9 @@ "kimiWebLabel": "__MISSING__:Kimi Web", "kimiWebDesc": "__MISSING__:Moonshot AI consumer chat via www.kimi.com (international, Connect-RPC API)", "doubaoWebLabel": "__MISSING__:Doubao Web", - "doubaoWebDesc": "__MISSING__:ByteDance AI chat via doubao.com" + "doubaoWebDesc": "__MISSING__:ByteDance AI chat via doubao.com", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Nastavenia", diff --git a/src/i18n/messages/sv.json b/src/i18n/messages/sv.json index 2a22581414..b5941283c7 100644 --- a/src/i18n/messages/sv.json +++ b/src/i18n/messages/sv.json @@ -4820,7 +4820,9 @@ "kimiWebLabel": "__MISSING__:Kimi Web", "kimiWebDesc": "__MISSING__:Moonshot AI consumer chat via www.kimi.com (international, Connect-RPC API)", "doubaoWebLabel": "__MISSING__:Doubao Web", - "doubaoWebDesc": "__MISSING__:ByteDance AI chat via doubao.com" + "doubaoWebDesc": "__MISSING__:ByteDance AI chat via doubao.com", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Inställningar", diff --git a/src/i18n/messages/sw.json b/src/i18n/messages/sw.json index 8d6c40741e..86e6111d11 100644 --- a/src/i18n/messages/sw.json +++ b/src/i18n/messages/sw.json @@ -4427,7 +4427,9 @@ "contextWindowOverrideLabel": "__MISSING__:Context Window Override", "contextWindowOverridePlaceholder": "__MISSING__:e.g. 131072", "contextWindowOverrideHint": "__MISSING__:Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", - "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens" + "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Settings", diff --git a/src/i18n/messages/ta.json b/src/i18n/messages/ta.json index 8fa80330a8..8220caf5f5 100644 --- a/src/i18n/messages/ta.json +++ b/src/i18n/messages/ta.json @@ -4427,7 +4427,9 @@ "contextWindowOverrideLabel": "__MISSING__:Context Window Override", "contextWindowOverridePlaceholder": "__MISSING__:e.g. 131072", "contextWindowOverrideHint": "__MISSING__:Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", - "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens" + "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Settings", diff --git a/src/i18n/messages/te.json b/src/i18n/messages/te.json index 2147b8325c..694ff45368 100644 --- a/src/i18n/messages/te.json +++ b/src/i18n/messages/te.json @@ -4427,7 +4427,9 @@ "contextWindowOverrideLabel": "__MISSING__:Context Window Override", "contextWindowOverridePlaceholder": "__MISSING__:e.g. 131072", "contextWindowOverrideHint": "__MISSING__:Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", - "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens" + "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Settings", diff --git a/src/i18n/messages/th.json b/src/i18n/messages/th.json index c7e7cec0e8..f779475a96 100644 --- a/src/i18n/messages/th.json +++ b/src/i18n/messages/th.json @@ -4427,7 +4427,9 @@ "contextWindowOverrideLabel": "__MISSING__:Context Window Override", "contextWindowOverridePlaceholder": "__MISSING__:e.g. 131072", "contextWindowOverrideHint": "__MISSING__:Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", - "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens" + "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "การตั้งค่า", diff --git a/src/i18n/messages/tr.json b/src/i18n/messages/tr.json index 13dc5b2267..8273558927 100644 --- a/src/i18n/messages/tr.json +++ b/src/i18n/messages/tr.json @@ -4427,7 +4427,9 @@ "contextWindowOverrideLabel": "__MISSING__:Context Window Override", "contextWindowOverridePlaceholder": "__MISSING__:e.g. 131072", "contextWindowOverrideHint": "__MISSING__:Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", - "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens" + "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Ayarlar", diff --git a/src/i18n/messages/uk-UA.json b/src/i18n/messages/uk-UA.json index 3e3310ff63..4058e80478 100644 --- a/src/i18n/messages/uk-UA.json +++ b/src/i18n/messages/uk-UA.json @@ -4427,7 +4427,9 @@ "contextWindowOverrideLabel": "__MISSING__:Context Window Override", "contextWindowOverridePlaceholder": "__MISSING__:e.g. 131072", "contextWindowOverrideHint": "__MISSING__:Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", - "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens" + "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Налаштування", diff --git a/src/i18n/messages/ur.json b/src/i18n/messages/ur.json index 23bb2cd677..98d69a30a1 100644 --- a/src/i18n/messages/ur.json +++ b/src/i18n/messages/ur.json @@ -4427,7 +4427,9 @@ "contextWindowOverrideLabel": "__MISSING__:Context Window Override", "contextWindowOverridePlaceholder": "__MISSING__:e.g. 131072", "contextWindowOverrideHint": "__MISSING__:Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", - "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens" + "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Settings", diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index f67c186783..0e7e6e4cfe 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -4427,7 +4427,9 @@ "contextWindowOverrideLabel": "__MISSING__:Context Window Override", "contextWindowOverridePlaceholder": "__MISSING__:e.g. 131072", "contextWindowOverrideHint": "__MISSING__:Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", - "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens" + "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "Cài đặt", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 5c5a3bbd13..184e947d10 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -4729,7 +4729,9 @@ "contextWindowOverrideLabel": "__MISSING__:Context Window Override", "contextWindowOverridePlaceholder": "__MISSING__:e.g. 131072", "contextWindowOverrideHint": "__MISSING__:Manually set this model's real context window (tokens) when the provider misreports it. Wins over auto-detected/catalog values and prevents combo routing from dropping the model.", - "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens" + "contextWindowOverrideInvalid": "__MISSING__:Context window override must be a positive whole number of tokens", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "设置", diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index b879139a9f..15b8e82247 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -4855,7 +4855,9 @@ "doubaoWebDesc": "通過 dola.com 訪問字節跳動 AI 聊天", "overrideBaseUrlAdvanced": "__MISSING__:Advanced: override base URL", "overrideBaseUrlHint": "__MISSING__:Advanced: point this built-in provider at a custom endpoint. Leave blank to use the default.", - "bulkAddFormatHintCloudflare": "__MISSING__:One key per line. Format: name|accountId|apiKey (Cloudflare account ID + API token)." + "bulkAddFormatHintCloudflare": "__MISSING__:One key per line. Format: name|accountId|apiKey (Cloudflare account ID + API token).", + "visionCapableLabel": "__MISSING__:Vision capable", + "visionCapableHint": "__MISSING__:Manually flag this model as vision-capable when the provider's discovery metadata doesn't report an image input modality (common for self-hosted/local backends)." }, "settings": { "title": "設定", diff --git a/src/lib/db/models.ts b/src/lib/db/models.ts index 986844a5d0..02aebf9428 100644 --- a/src/lib/db/models.ts +++ b/src/lib/db/models.ts @@ -97,7 +97,10 @@ export async function addCustomModel( targetFormat?: string, // #1294: optional per-model token limits supplied from the "add custom model" // form. Persisted under the same keys the /v1/models catalog reads back. - tokenLimits: { inputTokenLimit?: number; outputTokenLimit?: number } = {} + tokenLimits: { inputTokenLimit?: number; outputTokenLimit?: number } = {}, + // #1904: optional manual vision-capability override for the "add custom model" + // form — read back by getCustomVisionCapabilityFields() in the /v1/models catalog. + supportsVision?: boolean ) { const db = getDbInstance(); const row = db @@ -122,6 +125,7 @@ export async function addCustomModel( ...(tokenLimits.outputTokenLimit != null ? { outputTokenLimit: tokenLimits.outputTokenLimit } : {}), + ...(typeof supportsVision === "boolean" ? { supportsVision } : {}), }; models.push(model); db.prepare( @@ -588,6 +592,25 @@ export async function pruneStaleSyncedAvailableModelsForProvider( return Number(result.changes || 0); } +/** + * Apply a tri-state boolean override from `updates` onto `next`: + * field absent → keep whatever `next` already carries; explicit `null` → clear + * the override (callers fall back to their heuristic); anything else → persist + * the coerced boolean. + */ +function applyTriStateBooleanOverride( + next: JsonRecord, + updates: Record, + field: string +): void { + if (!Object.prototype.hasOwnProperty.call(updates, field)) return; + if (updates[field] === null) { + delete next[field]; + return; + } + next[field] = Boolean(updates[field]); +} + export async function updateCustomModel( providerId: string, modelId: string, @@ -637,13 +660,10 @@ export async function updateCustomModel( : {}), ...(updates.isHidden !== undefined ? { isHidden: Boolean(updates.isHidden) } : {}), }; - if (Object.prototype.hasOwnProperty.call(updates, "preserveOpenAIDeveloperRole")) { - if (updates.preserveOpenAIDeveloperRole === null) { - delete next.preserveOpenAIDeveloperRole; - } else { - next.preserveOpenAIDeveloperRole = Boolean(updates.preserveOpenAIDeveloperRole); - } - } + applyTriStateBooleanOverride(next, updates, "preserveOpenAIDeveloperRole"); + // #1904: manual vision-capability override — `null` clears back to the + // id-based heuristic in getCustomVisionCapabilityFields(). + applyTriStateBooleanOverride(next, updates, "supportsVision"); if (updates.compatByProtocol !== undefined) { if (mergedCompat && compatByProtocolHasEntries(mergedCompat)) { next.compatByProtocol = mergedCompat; diff --git a/src/shared/validation/schemas/provider.ts b/src/shared/validation/schemas/provider.ts index c45bd4fb2d..718f6e5164 100644 --- a/src/shared/validation/schemas/provider.ts +++ b/src/shared/validation/schemas/provider.ts @@ -214,6 +214,12 @@ export const providerModelMutationSchema = z.object({ // — fixes the "provider misreports context length" combo-drop case. `null` clears // a previously set override. contextWindowOverride: z.number().int().positive().nullable().optional(), + // #1904: manual vision-capability override for custom OpenAI-compatible models whose + // upstream discovery metadata does not self-report an image input modality (many + // self-hosted/local backends). Mirrors the auto-discovery `supportsVision` field so + // the same flag flows through `getCustomVisionCapabilityFields()` in the /v1/models + // catalog. `null` clears a manual override back to the id-based heuristic. + supportsVision: z.boolean().nullable().optional(), normalizeToolCallId: z.boolean().optional(), preserveOpenAIDeveloperRole: z.boolean().nullable().optional(), upstreamHeaders: upstreamHeadersRecordSchema.nullable().optional(), diff --git a/tests/unit/provider-models-vision-override-1904.test.ts b/tests/unit/provider-models-vision-override-1904.test.ts new file mode 100644 index 0000000000..508c6a1399 --- /dev/null +++ b/tests/unit/provider-models-vision-override-1904.test.ts @@ -0,0 +1,151 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +// #1904: manual vision-capability override for custom OpenAI-compatible models. +// +// detectVisionInput()/getCustomVisionCapabilityFields() already honour an explicit +// `supportsVision` flag when it is present on a custom-model record, but there was no +// way to *set* that flag from the dashboard's "Custom Models" add/edit form and no +// persistence path in the POST/PUT /api/provider-models handlers — so a user whose +// self-hosted backend doesn't self-report an image input modality (e.g. OpenRouter-style +// `architecture.input_modalities`) had no way to manually flag the model as +// vision-capable, exactly the report in the linked issue (Qwen-based custom vision +// model not showing the vision tag). +// +// This test proves the API round trip end-to-end: POST/PUT persist supportsVision on +// the custom-model row, GET surfaces it back, and getCustomVisionCapabilityFields() +// (what the /v1/models catalog calls) honours the explicit override. + +const TEST_DATA_DIR = fs.mkdtempSync( + path.join(os.tmpdir(), "omniroute-provider-model-vision-override-1904-") +); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const modelsDb = await import("../../src/lib/db/models.ts"); +const providerModelsRoute = await import("../../src/app/api/provider-models/route.ts"); +const catalogVision = await import("../../src/app/api/v1/models/catalogVision.ts"); + +async function resetStorage() { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +} + +test.beforeEach(async () => { + await resetStorage(); +}); + +test.after(async () => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +function buildRequest(method: string, body: unknown) { + return new Request("http://localhost/api/provider-models", { + method, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); +} + +test("POST with supportsVision:true persists the flag on the custom model row", async () => { + const postRes = await providerModelsRoute.POST( + buildRequest("POST", { + provider: "openai-compatible-demo", + modelId: "qwen-vl-custom", + modelName: "Qwen VL Custom", + apiFormat: "chat-completions", + supportedEndpoints: ["chat"], + supportsVision: true, + }) + ); + const postBody = (await postRes.json()) as { model?: { supportsVision?: boolean } }; + assert.equal(postRes.status, 200); + assert.equal(postBody.model?.supportsVision, true); + + const models = await modelsDb.getCustomModels("openai-compatible-demo"); + const row = (models as Array<{ id?: string; supportsVision?: boolean }>).find( + (m) => m.id === "qwen-vl-custom" + ); + assert.ok(row, "model row should exist"); + assert.equal(row!.supportsVision, true); +}); + +test("PUT with supportsVision:true persists a manual override and PUT null clears it", async () => { + await modelsDb.addCustomModel( + "openai-compatible-demo", + "custom-local-model", + "Custom Local Model" + ); + + const putRes = await providerModelsRoute.PUT( + buildRequest("PUT", { + provider: "openai-compatible-demo", + modelId: "custom-local-model", + supportsVision: true, + }) + ); + const putBody = (await putRes.json()) as { model?: { supportsVision?: boolean } }; + assert.equal(putRes.status, 200); + assert.equal(putBody.model?.supportsVision, true); + + const getRes = await providerModelsRoute.GET( + new Request("http://localhost/api/provider-models?provider=openai-compatible-demo") + ); + const getBody = (await getRes.json()) as { + models: Array<{ id?: string; supportsVision?: boolean }>; + }; + const row = getBody.models.find((m) => m.id === "custom-local-model"); + assert.ok(row, "model row should be present"); + assert.equal(row!.supportsVision, true); + + // Clearing back to the id-based heuristic. + const clearRes = await providerModelsRoute.PUT( + buildRequest("PUT", { + provider: "openai-compatible-demo", + modelId: "custom-local-model", + supportsVision: null, + }) + ); + const clearBody = (await clearRes.json()) as { model?: { supportsVision?: boolean } }; + assert.equal(clearRes.status, 200); + assert.equal(clearBody.model?.supportsVision, undefined); +}); + +test("getCustomVisionCapabilityFields honours an explicit supportsVision:true override", () => { + const fields = catalogVision.getCustomVisionCapabilityFields( + { supportsVision: true }, + "openai-compatible-demo/qwen-not-heuristic-matched" + ); + assert.ok(fields, "explicit override should produce vision capability fields"); + assert.deepEqual(fields!.capabilities, { vision: true }); +}); + +test("getCustomVisionCapabilityFields honours an explicit supportsVision:false override even for a vision-like id", () => { + const fields = catalogVision.getCustomVisionCapabilityFields( + { supportsVision: false }, + "openai-compatible-demo/gpt-4-vision-preview" + ); + assert.equal(fields, null); +}); + +test("without an explicit flag, the UI has no field wired to persist supportsVision by default", async () => { + // Before this fix there was no request-shape carrying supportsVision at all; a plain + // add-model POST (matching the pre-fix form payload) must not silently mark a model + // vision-capable — the flag stays absent unless the user explicitly opts in. + const postRes = await providerModelsRoute.POST( + buildRequest("POST", { + provider: "openai-compatible-demo", + modelId: "plain-model", + apiFormat: "chat-completions", + supportedEndpoints: ["chat"], + }) + ); + const postBody = (await postRes.json()) as { model?: { supportsVision?: boolean } }; + assert.equal(postRes.status, 200); + assert.equal(postBody.model?.supportsVision, undefined); +}); From 88507a6edc4a07211ad3bcb2562b5038e4246292 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Fri, 17 Jul 2026 06:01:29 +0200 Subject: [PATCH 02/94] [needs-vps] fix(dashboard): align onboarding tier content (#7125) * fix(dashboard): align onboarding welcome feature cards vertically * fix(dashboard): align onboarding tier content * chore: scope onboarding PR to UI fix * i18n(pt-BR): add onboarding.tier.flowCaption + afterSetup keys The two new tier keys added to en.json were missing from pt-BR.json, tripping the i18n-pt-br no-drift test (#6695). Add their pt-BR translations. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> --- .../fixes/7125-onboarding-tiers-layout.md | 1 + .../onboarding/components/TierFlowDiagram.tsx | 10 ++++----- .../(dashboard)/dashboard/onboarding/page.tsx | 21 ++++++++++++------- .../dashboard/onboarding/steps/TierTour.tsx | 8 ++----- src/i18n/messages/en.json | 2 ++ src/i18n/messages/pt-BR.json | 4 +++- 6 files changed, 27 insertions(+), 19 deletions(-) create mode 100644 changelog.d/fixes/7125-onboarding-tiers-layout.md diff --git a/changelog.d/fixes/7125-onboarding-tiers-layout.md b/changelog.d/fixes/7125-onboarding-tiers-layout.md new file mode 100644 index 0000000000..8bb9f718eb --- /dev/null +++ b/changelog.d/fixes/7125-onboarding-tiers-layout.md @@ -0,0 +1 @@ +- **fix(dashboard):** align onboarding tier descriptions and localize the tier step header and flow copy ([#7125](https://github.com/diegosouzapw/OmniRoute/pull/7125)) — thanks @Wibias diff --git a/src/app/(dashboard)/dashboard/onboarding/components/TierFlowDiagram.tsx b/src/app/(dashboard)/dashboard/onboarding/components/TierFlowDiagram.tsx index d2cc88ad13..ded42fdf0d 100644 --- a/src/app/(dashboard)/dashboard/onboarding/components/TierFlowDiagram.tsx +++ b/src/app/(dashboard)/dashboard/onboarding/components/TierFlowDiagram.tsx @@ -5,7 +5,8 @@ import { useTranslations } from "next-intl"; import Image from "next/image"; export function TierFlowDiagram() { - const t = useTranslations("onboarding"); + const t = useTranslations("onboarding.tier"); + const tOnboarding = useTranslations("onboarding"); const { resolvedTheme } = useTheme(); const src = resolvedTheme === "dark" ? "/images/tier-flow-dark.svg" : "/images/tier-flow-light.svg"; @@ -14,15 +15,14 @@ export function TierFlowDiagram() {
{t("tierFlowDiagramAlt")} -

- Requests flow through your subscription quotas first, then pay-per-token cheap providers, - then free-tier providers — automatic, zero-config. +

+ {t("flowCaption")}

); diff --git a/src/app/(dashboard)/dashboard/onboarding/page.tsx b/src/app/(dashboard)/dashboard/onboarding/page.tsx index a1cbf3544e..de8b44e3cf 100644 --- a/src/app/(dashboard)/dashboard/onboarding/page.tsx +++ b/src/app/(dashboard)/dashboard/onboarding/page.tsx @@ -274,7 +274,12 @@ export default function OnboardingWizard() { > {currentStep.icon}
-

{currentStep.title}

+

{currentStep.title}

+ {currentStep.id === "tiers" && ( +

+ {t("tier.subtitle")} +

+ )}
{/* Step Content */} @@ -283,7 +288,7 @@ export default function OnboardingWizard() { {currentStep.id === "welcome" && (

{t("welcomeDesc")}

-
+
{[ { icon: "swap_horiz", label: t("multiProvider") }, { icon: "monitoring", label: t("usageTracking") }, @@ -291,12 +296,14 @@ export default function OnboardingWizard() { ].map((f) => (
- - {f.icon} - - {f.label} +
+ + {f.icon} + + {f.label} +
))}
diff --git a/src/app/(dashboard)/dashboard/onboarding/steps/TierTour.tsx b/src/app/(dashboard)/dashboard/onboarding/steps/TierTour.tsx index 02f0d3d286..c09276df3d 100644 --- a/src/app/(dashboard)/dashboard/onboarding/steps/TierTour.tsx +++ b/src/app/(dashboard)/dashboard/onboarding/steps/TierTour.tsx @@ -19,7 +19,7 @@ function TierCard({ number, colorClass, label, description, examples }: TierCard {number} {label}
-

{description}

+

{description}

+ ); + })}
diff --git a/src/app/api/settings/background-degradation/route.ts b/src/app/api/settings/background-degradation/route.ts index fc7f526add..87744edd98 100644 --- a/src/app/api/settings/background-degradation/route.ts +++ b/src/app/api/settings/background-degradation/route.ts @@ -4,10 +4,28 @@ import { setBackgroundDegradationConfig, resetStats, } from "@omniroute/open-sse/services/backgroundTaskDetector.ts"; -import { updateSettings } from "@/lib/db/settings"; +import { getSettings, updateSettings } from "@/lib/db/settings"; import { jsonObjectSchema, resetStatsActionSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; +import { isPaidModelTarget } from "@/shared/utils/freeModels"; + +/** + * #6540: is any degradation "to" target a paid-only model while hidePaidModels is on? + * Only the "to" side is checked — "from" is a detection trigger key, not an invocation + * target, so a paid "from" is never blocked. Fails open on "unknown" (aliases/combo + * names), mirroring the settings/combo-defaults routes. + */ +async function hasBlockedPaidTarget( + degradationMap: Record | undefined +): Promise { + if (!degradationMap || typeof degradationMap !== "object") return false; + const currentSettings: any = await getSettings(); + if (currentSettings?.hidePaidModels !== true) return false; + return Object.values(degradationMap).some( + (to) => typeof to === "string" && isPaidModelTarget(to) === "paid" + ); +} /** * GET /api/settings/background-degradation @@ -52,7 +70,20 @@ export async function PUT(request: Request) { if (isValidationFailure(validation)) { return NextResponse.json({ error: validation.error }, { status: 400 }); } - const config = validation.data; + const config = validation.data as { degradationMap?: Record }; + + if (await hasBlockedPaidTarget(config.degradationMap)) { + return NextResponse.json( + { + error: { + code: "PAID_MODEL_TARGET_BLOCKED", + message: + "This field cannot target a paid-only model while 'Hide paid models' is enabled.", + }, + }, + { status: 400 } + ); + } setBackgroundDegradationConfig(config); diff --git a/src/app/api/settings/combo-defaults/route.ts b/src/app/api/settings/combo-defaults/route.ts index 9e6ca358e4..643be54c0e 100644 --- a/src/app/api/settings/combo-defaults/route.ts +++ b/src/app/api/settings/combo-defaults/route.ts @@ -3,6 +3,7 @@ import { getSettings, updateSettings } from "@/lib/localDb"; import { updateComboDefaultsSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; +import { isPaidModelTarget } from "@/shared/utils/freeModels"; const LEGACY_COMBO_RESILIENCE_KEYS = new Set([ "timeoutMs", @@ -96,6 +97,30 @@ export async function PATCH(request: Request) { } const body = validation.data; + // #6540: reject a paid-only handoffModel target when hidePaidModels is on. + // Fails open on "unknown" (aliases/combo names) — mirrors the settings + // route's PAID_MODEL_TARGET_BLOCKED check. + if ( + typeof body.comboDefaults?.handoffModel === "string" && + body.comboDefaults.handoffModel.trim() !== "" + ) { + const currentSettings: any = await getSettings(); + if (currentSettings?.hidePaidModels === true) { + if (isPaidModelTarget(body.comboDefaults.handoffModel) === "paid") { + return NextResponse.json( + { + error: { + code: "PAID_MODEL_TARGET_BLOCKED", + message: + "This field cannot target a paid-only model while 'Hide paid models' is enabled.", + }, + }, + { status: 400 } + ); + } + } + } + const updates: Record = {}; if (body.comboDefaults) { diff --git a/src/app/api/settings/route.ts b/src/app/api/settings/route.ts index 00bc0cf398..d4bada962e 100644 --- a/src/app/api/settings/route.ts +++ b/src/app/api/settings/route.ts @@ -20,6 +20,7 @@ import { verifyManagementPassword, } from "@/lib/auth/managementPassword"; import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; +import { isPaidModelTarget } from "@/shared/utils/freeModels"; import { getAuditRequestContext, logAuditEvent } from "@/lib/compliance"; import { isDashboardSessionAuthenticated } from "@/shared/utils/apiAuth"; import { isCliTokenAuthValid } from "@/lib/middleware/cliTokenAuth"; @@ -296,6 +297,30 @@ export async function PATCH(request: Request) { } } + // #6540: reject a paid-only webSearchRouteModel target when hidePaidModels + // is on. Business-rule check (needs an async DB read), so it runs after + // Zod shape validation rather than as a Zod .refine(). Fails open on + // "unknown" (aliases/combo names) — only a positively-identified paid + // catalog entry is blocked. + if (typeof body.webSearchRouteModel === "string" && body.webSearchRouteModel.trim() !== "") { + const currentSettings = await getSettings(); + if ((currentSettings as Record)?.hidePaidModels === true) { + if (isPaidModelTarget(body.webSearchRouteModel) === "paid") { + emitSettingsFailureAudit(request, actor, "PAID_MODEL_TARGET_BLOCKED", attemptedKeys); + return NextResponse.json( + { + error: { + code: "PAID_MODEL_TARGET_BLOCKED", + message: + "This field cannot target a paid-only model while 'Hide paid models' is enabled.", + }, + }, + { status: 400 } + ); + } + } + } + // Password rotation: hash the new value AFTER the gate has accepted the // currentPassword (or the cold-boot exception fired). The gate already // included `newPassword` in SECURITY_IMPACTING_KEYS, so no separate diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index c18e6f206a..e71d54f17a 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -5645,7 +5645,8 @@ "echoRequestedModelDesc": "When enabled, the response `model` field echoes the alias or combo name the client requested instead of the upstream model name. Fixes strict clients (e.g. Claude Desktop) that reject a response whose model does not match the request.", "webSearchRouteTitle": "Web search routing", "webSearchRouteDesc": "When a request includes a native web_search tool, route the whole request to this model instead of the default — useful for providers that don't implement Anthropic's web_search server tool. Leave blank to disable.", - "webSearchRoutePlaceholder": "e.g. openrouter,anthropic/claude-3.5-sonnet", + "webSearchRoutePlaceholder": "Search or select a model…", + "paidModelPatternWarning": "This pattern only matches paid models — enable paid models or adjust the pattern.", "clearLkgpCache": "Clear LKGP Cache", "lkgpCacheCleared": "LKGP cache cleared successfully", "lkgpCacheClearFailed": "Failed to clear LKGP cache", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index 2f63d1d6af..9d9f755549 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -5608,6 +5608,7 @@ "webSearchRouteTitle": "__MISSING__:Web search routing", "webSearchRouteDesc": "__MISSING__:When a request includes a native web_search tool, route the whole request to this model instead of the default — useful for providers that don't implement Anthropic's web_search server tool. Leave blank to disable.", "webSearchRoutePlaceholder": "__MISSING__:e.g. openrouter,anthropic/claude-3.5-sonnet", + "paidModelPatternWarning": "Este padrão corresponde apenas a modelos pagos — habilite modelos pagos ou ajuste o padrão.", "clearLkgpCache": "Clear LKGP Cache", "lkgpCacheCleared": "LKGP cache cleared successfully", "lkgpCacheClearFailed": "Failed to clear LKGP cache", diff --git a/src/lib/db/modelComboMappings.ts b/src/lib/db/modelComboMappings.ts index 1d11c2fcff..ecbdb5291f 100644 --- a/src/lib/db/modelComboMappings.ts +++ b/src/lib/db/modelComboMappings.ts @@ -9,6 +9,7 @@ import { v4 as uuidv4 } from "uuid"; import { getDbInstance } from "./core"; +import { globToRegex } from "@/shared/utils/globPattern"; // ────────────────────────────────────────────────────────── // Types @@ -38,23 +39,6 @@ interface MappingRow { updated_at: string; } -// ────────────────────────────────────────────────────────── -// Glob → RegExp conversion -// ────────────────────────────────────────────────────────── - -/** - * Convert a simple glob pattern to a RegExp. - * Supports `*` (any characters) and `?` (single character). - * Case-insensitive matching. - */ -function globToRegex(pattern: string): RegExp { - const escaped = pattern - .replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape regex specials - .replace(/\*/g, ".*") // * → .* - .replace(/\?/g, "."); // ? → . - return new RegExp(`^${escaped}$`, "i"); -} - // ────────────────────────────────────────────────────────── // Row mapping // ────────────────────────────────────────────────────────── diff --git a/src/shared/components/ModelRoutingSection.tsx b/src/shared/components/ModelRoutingSection.tsx index f8f597247a..af29a9045f 100644 --- a/src/shared/components/ModelRoutingSection.tsx +++ b/src/shared/components/ModelRoutingSection.tsx @@ -3,6 +3,7 @@ import { useState, useEffect } from "react"; import { useTranslations } from "next-intl"; import Card from "./Card"; +import { matchesOnlyPaidModels } from "@/shared/utils/freeModels"; export interface ModelMapping { id: string; @@ -26,6 +27,7 @@ export default function ModelRoutingSection({ combos: externalCombos }: { combos const [loading, setLoading] = useState(true); const [adding, setAdding] = useState(false); const [editingId, setEditingId] = useState(null); + const [hidePaidModels, setHidePaidModels] = useState(false); const combos = externalCombos || internalCombos; // Form state @@ -58,6 +60,21 @@ export default function ModelRoutingSection({ combos: externalCombos }: { combos }; }, []); + // #6540: read hidePaidModels once so the pattern field can warn (fail-open) + // when it resolves only to paid model families. + useEffect(() => { + let cancelled = false; + fetch("/api/settings") + .then((res) => (res.ok ? res.json() : null)) + .then((data) => { + if (!cancelled && data) setHidePaidModels(data.hidePaidModels === true); + }) + .catch(() => {}); + return () => { + cancelled = true; + }; + }, []); + useEffect(() => { if (externalCombos !== undefined) return; let cancelled = false; @@ -141,6 +158,11 @@ export default function ModelRoutingSection({ combos: externalCombos }: { combos } catch {} }; + // #6540: fail-open heuristic — only warn/block when the pattern resolves + // to at least one model AND every match is paid. A pattern matching a + // mix of free and paid models (or nothing recognizable) is left alone. + const patternIsPaidOnly = hidePaidModels && matchesOnlyPaidModels(pattern); + return (
@@ -183,6 +205,12 @@ export default function ModelRoutingSection({ combos: externalCombos }: { combos bg-white dark:bg-black/20 focus:outline-none focus:ring-1 focus:ring-primary" />

{t("patternHint")}

+ {patternIsPaidOnly && ( +

+ {t("paidModelPatternWarning") || + "This pattern only matches paid models — enable paid models or adjust the pattern."} +

+ )}