From 83332a08d3119536155f06d145477c0f79064784 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Wed, 19 Aug 2026 12:54:05 -0300 Subject: [PATCH] fix(dashboard): filter Modality Bridge Vision model picker by supportsVision (#10703) (#10763) Co-authored-by: Markus Hartung --- ...703-modality-bridge-vision-model-filter.md | 1 + .../ModalityBridgeVisionTab.tsx | 5 +- ...ty-bridge-vision-tab-filter-10703.test.tsx | 96 +++++++++++++++++++ 3 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 changelog.d/fixes/10703-modality-bridge-vision-model-filter.md create mode 100644 tests/unit/ui/modality-bridge-vision-tab-filter-10703.test.tsx diff --git a/changelog.d/fixes/10703-modality-bridge-vision-model-filter.md b/changelog.d/fixes/10703-modality-bridge-vision-model-filter.md new file mode 100644 index 0000000000..1cefad69a5 --- /dev/null +++ b/changelog.d/fixes/10703-modality-bridge-vision-model-filter.md @@ -0,0 +1 @@ +- fix(dashboard): filter the Modality Bridge Vision model picker to vision-capable models, matching the sibling Video/Audio tabs (#10703) diff --git a/src/app/(dashboard)/dashboard/settings/components/modalityBridge/ModalityBridgeVisionTab.tsx b/src/app/(dashboard)/dashboard/settings/components/modalityBridge/ModalityBridgeVisionTab.tsx index 21f856c53b..f94702e059 100644 --- a/src/app/(dashboard)/dashboard/settings/components/modalityBridge/ModalityBridgeVisionTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/modalityBridge/ModalityBridgeVisionTab.tsx @@ -1,9 +1,10 @@ "use client"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useTranslations } from "next-intl"; import { Card, ModelSelectField, Toggle } from "@/shared/components"; +import type { ApiModel } from "@/shared/components/ModelSelectField"; import { MODALITY_BRIDGE_DEFAULTS, resolveVisionBridgeRuntimeSettings, @@ -55,6 +56,7 @@ function clampNumber(raw: string, min: number, max: number, fallback: number): n export default function ModalityBridgeVisionTab() { const t = useTranslations("settings"); const [settings, setSettings] = useState(null); + const isVisionModel = useCallback((model: ApiModel) => model.supportsVision === true, []); useEffect(() => { let cancelled = false; @@ -174,6 +176,7 @@ export default function ModalityBridgeVisionTab() { allowEmpty onChange={(value) => void update({ modalityBridgeVisionModel: value })} className="text-sm" + modelFilter={isVisionModel} /> ({ + useTranslations: () => (key: string) => key, +})); + +type MountedRoot = { root: Root; el: HTMLDivElement }; +const roots: MountedRoot[] = []; + +async function waitFor(predicate: () => boolean, label: string): Promise { + const startedAt = Date.now(); + while (!predicate()) { + if (Date.now() - startedAt > 2000) throw new Error(`Timed out waiting for: ${label}`); + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 10)); + }); + } +} + +describe("ModalityBridgeVisionTab — model filtering (#10703)", () => { + let fetchMock: ReturnType; + + beforeEach(() => { + (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input); + if (url.includes("/api/models")) { + return new Response( + JSON.stringify({ + models: [ + { provider: "openai", model: "gpt-4o-mini", supportsVision: true }, + { provider: "cmd", model: "gpt-5.3-codex", supportsVision: false }, + ], + }), + { status: 200 } + ); + } + if (url.includes("/api/modality-bridge/stats")) { + return new Response( + JSON.stringify({ + vision: { bridged: 0, cacheHits: 0, failures: 0, lastUsedAt: null }, + audio: { bridged: 0, cacheHits: 0, failures: 0, lastUsedAt: null }, + }), + { status: 200 } + ); + } + if (url.includes("/api/settings")) { + if (init?.method === "PATCH") return new Response("{}", { status: 200 }); + return new Response(JSON.stringify({}), { status: 200 }); + } + return new Response("{}", { status: 200 }); + }); + vi.stubGlobal("fetch", fetchMock); + }); + + afterEach(() => { + for (const { root, el } of roots.splice(0)) { + act(() => root.unmount()); + el.remove(); + } + vi.unstubAllGlobals(); + }); + + async function render(): Promise { + const el = document.createElement("div"); + document.body.appendChild(el); + const root = createRoot(el); + await act(async () => { + root.render(); + }); + roots.push({ root, el }); + await waitFor( + () => el.querySelector('[data-testid="modality-bridge-mode"]') !== null, + "vision settings to load" + ); + return el; + } + + it("only lists vision-capable models in the Vision model select", async () => { + const el = await render(); + const modelLabel = Array.from(el.querySelectorAll("label")).find((label) => + label.textContent?.includes("modalityBridgeVisionModel") + ); + const modelSelect = modelLabel?.parentElement?.querySelector("select") ?? null; + expect(modelSelect).toBeTruthy(); + await waitFor(() => (modelSelect?.options.length ?? 0) > 1, "model options to load"); + const optionValues = Array.from(modelSelect?.options ?? []).map((o) => o.value); + expect(optionValues).toContain("openai/gpt-4o-mini"); + expect(optionValues).not.toContain("cmd/gpt-5.3-codex"); + }); +});