From 6dd82b77de27c679ea3a71a4e5ef418b06010d80 Mon Sep 17 00:00:00 2001 From: "Bob.Hou" Date: Tue, 1 Sep 2026 11:08:04 -0400 Subject: [PATCH] fix(guardrails): pass providerId to getResolvedModelCapabilities in checkComboVision (#12112) (#12169) * fix(guardrails): pass providerId to getResolvedModelCapabilities in checkComboVision (#12112) Signed-off-by: Minxi Hou * chore(quality): register combo-vision providerId test in the stryker tap set --------- Signed-off-by: Minxi Hou Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- open-sse/services/combo/comboStructure.ts | 15 +++- src/lib/guardrails/visionBridge.ts | 11 ++- stryker.conf.json | 1 + .../combo-vision-provider-id-12112.test.ts | 69 +++++++++++++++++++ 4 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 tests/unit/guardrails/combo-vision-provider-id-12112.test.ts diff --git a/open-sse/services/combo/comboStructure.ts b/open-sse/services/combo/comboStructure.ts index e1a18fc194..8d7adc068a 100644 --- a/open-sse/services/combo/comboStructure.ts +++ b/open-sse/services/combo/comboStructure.ts @@ -529,7 +529,10 @@ function hasKnownCompatibleContextLimit( requirements: RequestCompatibilityRequirements ): boolean { if (requirements.requiredContextTokens <= 0) return false; - const capabilities = getResolvedModelCapabilities(target.modelStr); + const capabilities = getResolvedModelCapabilities({ + provider: target.providerId || target.provider || null, + model: target.modelStr, + }); return evaluateContextLimit(capabilities, requirements, target.modelStr) === true; } @@ -546,7 +549,10 @@ export function isVisionIncompatibleTarget( requirements: RequestCompatibilityRequirements ): boolean { if (!requirements.requiresVision) return false; - const capabilities = getResolvedModelCapabilities(target.modelStr); + const capabilities = getResolvedModelCapabilities({ + provider: target.providerId || target.provider || null, + model: target.modelStr, + }); return capabilities.supportsVision !== true; } @@ -571,7 +577,10 @@ function getTargetCompatibilityFailures( target: ResolvedComboTarget, requirements: RequestCompatibilityRequirements ): string[] { - const capabilities = getResolvedModelCapabilities(target.modelStr); + const capabilities = getResolvedModelCapabilities({ + provider: target.providerId || target.provider || null, + model: target.modelStr, + }); const failures: string[] = []; if ( diff --git a/src/lib/guardrails/visionBridge.ts b/src/lib/guardrails/visionBridge.ts index c24729a3c9..cdf4028cc0 100644 --- a/src/lib/guardrails/visionBridge.ts +++ b/src/lib/guardrails/visionBridge.ts @@ -89,7 +89,16 @@ export async function getComboVisionBridgeDecision( hasModelStep = true; const targetModel = s.model; if (typeof targetModel === "string") { - const caps = getResolvedModelCapabilities(targetModel); + const provider = + typeof s.providerId === "string" + ? s.providerId + : typeof s.provider === "string" + ? s.provider + : null; + const caps = getResolvedModelCapabilities({ + provider, + model: targetModel, + }); if (caps.supportsVision === true) { hasVisionCapableStep = true; } else { diff --git a/stryker.conf.json b/stryker.conf.json index 94fa0e643b..baf3b11bb8 100644 --- a/stryker.conf.json +++ b/stryker.conf.json @@ -255,6 +255,7 @@ "tests/unit/github-model-not-supported-lockout.test.ts", "tests/unit/grok-cli-oauth.test.ts", "tests/unit/guardrails-api-3496.test.ts", + "tests/unit/guardrails/combo-vision-provider-id-12112.test.ts", "tests/unit/guardrails/visionBridge-responses-9597.test.ts", "tests/unit/headroom-codex-quota-snapshot-6379.test.ts", "tests/unit/headroom-proxy-lifecycle.test.ts", diff --git a/tests/unit/guardrails/combo-vision-provider-id-12112.test.ts b/tests/unit/guardrails/combo-vision-provider-id-12112.test.ts new file mode 100644 index 0000000000..734e13e2fe --- /dev/null +++ b/tests/unit/guardrails/combo-vision-provider-id-12112.test.ts @@ -0,0 +1,69 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +process.env.DATA_DIR = `/tmp/omniroute-test-12112-${Date.now()}`; + +const { getComboVisionBridgeDecision } = await import( + "../../../src/lib/guardrails/visionBridge.ts" +); +const combosDb = await import("../../../src/lib/db/combos.ts"); +const core = await import("../../../src/lib/db/core.ts"); +const { isVisionIncompatibleTarget } = await import( + "../../../open-sse/services/combo/comboStructure.ts" +); +import type { ResolvedComboTarget } from "../../../open-sse/services/combo/types.ts"; + +test.after(() => { + core.resetDbInstance(); +}); + +test("#12112: checkComboVision respects providerId for namespaced vision models (e.g. nvidia/nemotron-nano-12b-v2-vl)", async () => { + // Model 'nvidia/nemotron-nano-12b-v2-vl' is declared with supportsVision: true in nvidia provider registry. + // It has a slash in model id and requires providerId="nvidia" to resolve capabilities. + await combosDb.createCombo({ + name: "nvidia-vision-combo-12112", + models: [ + { + providerId: "nvidia", + model: "nvidia/nemotron-nano-12b-v2-vl", + weight: 1, + }, + ], + }); + + const decision = await getComboVisionBridgeDecision("nvidia-vision-combo-12112"); + assert.equal( + decision, + "skip", + "Combo with explicit nvidia vision model must resolve as 'skip' (vision supported), not 'no-vision'" + ); +}); + +test("#12112: isVisionIncompatibleTarget passes providerId to resolve vision capability", () => { + const target: ResolvedComboTarget = { + kind: "model", + stepId: "step-1", + executionKey: "step-1", + modelStr: "nvidia/nemotron-nano-12b-v2-vl", + provider: "nvidia", + providerId: "nvidia", + connectionId: "conn-1", + weight: 1, + label: null, + }; + + const incompatible = isVisionIncompatibleTarget(target, { + requiresTools: false, + requiresVision: true, + requiresStructuredOutput: false, + estimatedInputTokens: 10, + requestedOutputTokens: 10, + requiredContextTokens: 10, + }); + + assert.equal( + incompatible, + false, + "Target with providerId='nvidia' and model='nvidia/nemotron-nano-12b-v2-vl' must be vision-compatible" + ); +});