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({ ))} +
- Requests flow through your subscription quotas first, then pay-per-token cheap providers, - then free-tier providers — automatic, zero-config. +
+ {t("flowCaption")}
+ {t("tier.subtitle")} +
+ )}{t("welcomeDesc")}
-{description}
+{description}
{t("subtitle")}
-{t("patternHint")}
+ {patternIsPaidOnly && ( ++ {t("paidModelPatternWarning") || + "This pattern only matches paid models — enable paid models or adjust the pattern."} +
+ )}