From af5d8007590fa535f784d50039e9766ed3ad809e Mon Sep 17 00:00:00 2001 From: Cong Vu Chi <129714106+congvc-dev@users.noreply.github.com> Date: Sat, 25 Apr 2026 01:56:16 +0700 Subject: [PATCH] fix(vision-bridge): force GPT-family image fallback (#1571) Integrated into release/v3.7.0 --- src/lib/guardrails/visionBridge.ts | 5 +++- src/shared/constants/visionBridgeDefaults.ts | 8 +++++ tests/unit/guardrails/visionBridge.test.ts | 31 ++++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/lib/guardrails/visionBridge.ts b/src/lib/guardrails/visionBridge.ts index 995ca0261f..460d342471 100644 --- a/src/lib/guardrails/visionBridge.ts +++ b/src/lib/guardrails/visionBridge.ts @@ -15,6 +15,7 @@ import { import { VISION_BRIDGE_DEFAULTS, getVisionBridgeConfig, + isVisionBridgeForcedModel, } from "@/shared/constants/visionBridgeDefaults"; export interface VisionBridgeDependencies { @@ -55,9 +56,11 @@ export class VisionBridgeGuardrail extends BaseGuardrail { return { block: false }; } + const forceVisionBridge = isVisionBridgeForcedModel(model); + // 4. Check if model supports vision const capabilities = getResolvedModelCapabilities(model); - if (capabilities.supportsVision === true) { + if (capabilities.supportsVision === true && !forceVisionBridge) { return { block: false }; } diff --git a/src/shared/constants/visionBridgeDefaults.ts b/src/shared/constants/visionBridgeDefaults.ts index 626eebbe1f..e4392af60b 100644 --- a/src/shared/constants/visionBridgeDefaults.ts +++ b/src/shared/constants/visionBridgeDefaults.ts @@ -2,6 +2,14 @@ * Vision Bridge default configuration values. */ +const NORMALIZED_GPT_MODEL_PATTERN = /^gpt-/i; + +export function isVisionBridgeForcedModel(model: string | null | undefined): boolean { + if (!model) return false; + const normalizedModel = model.includes("/") ? model.split("/").pop() || model : model; + return NORMALIZED_GPT_MODEL_PATTERN.test(normalizedModel); +} + export const VISION_BRIDGE_DEFAULTS = { enabled: true, model: "openai/gpt-4o-mini", diff --git a/tests/unit/guardrails/visionBridge.test.ts b/tests/unit/guardrails/visionBridge.test.ts index 1d11f63cae..d8b241ad4d 100644 --- a/tests/unit/guardrails/visionBridge.test.ts +++ b/tests/unit/guardrails/visionBridge.test.ts @@ -164,6 +164,37 @@ test("VB-S02: passthroughs for vision-capable model (gpt-4o)", async () => { } }); +test("VB-S02b: forces Vision Bridge for GPT-family models even when model capabilities advertise vision", async () => { + const guardrail = createGuardrail(); + + for (const model of ["gpt-5.4", "gpt-5.4-mini", "gpt-4o", "openai/gpt-4o-mini"]) { + mockVisionResponse = `Forced bridge description for ${model}`; + visionCallCount = 0; + + const payload = createPayload({ + model, + messages: [ + { + role: "user", + content: [ + { type: "text", text: "What is this?" }, + { + type: "image_url", + image_url: { url: "https://example.com/image.png" }, + }, + ], + }, + ], + }); + + const result = await guardrail.preCall(payload, createContext({ model })); + + assert.strictEqual(result.block, false, `expected passthrough=false for ${model}`); + assert.ok(result.modifiedPayload, `expected modified payload for ${model}`); + assert.strictEqual(visionCallCount, 1, `expected one forced bridge call for ${model}`); + } +}); + test("VB-S02: model capabilities returns supportsVision for known models", () => { const gpt4oCaps = getResolvedModelCapabilities("openai/gpt-4o"); // supportsVision may be true (if sync data exists) or null (if not synced)