/** * Issue #2232 — Vision Bridge env override. * * Operators configured `visionBridgeModel: "gemini/gemini-2.0-flash"` (or * other non-anthropic prefixes) and watched every request fail with * `Vision API error 401: You didn't provide an API key` from OpenAI, because * the helper hardcoded `https://api.openai.com/v1` for any model that * wasn't `anthropic/*`. * * The fix adds two new env vars: * - VISION_BRIDGE_BASE_URL: alternate OpenAI-compatible base URL (e.g. * OmniRoute self-loop, Google's Gemini OpenAI-compat endpoint). * - VISION_BRIDGE_API_KEY: alternate API key for that endpoint. * * These tests cover the helpers in isolation; the integration with the * guardrail's `callVisionModel` is exercised by the existing * vision-bridge-settings-schema tests. */ import test from "node:test"; import assert from "node:assert/strict"; const { resolveProviderApiKey, resolveVisionBridgeBaseUrl } = await import("../../src/lib/guardrails/visionBridgeHelpers.ts"); const ENV_KEYS = [ "VISION_BRIDGE_API_KEY", "VISION_BRIDGE_BASE_URL", "OPENAI_API_URL", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GOOGLE_API_KEY", ]; const ORIGINAL_ENV: Record = {}; for (const k of ENV_KEYS) ORIGINAL_ENV[k] = process.env[k]; function clearEnv() { for (const k of ENV_KEYS) delete process.env[k]; } function restoreEnv() { for (const k of ENV_KEYS) { if (ORIGINAL_ENV[k] === undefined) delete process.env[k]; else process.env[k] = ORIGINAL_ENV[k]; } } test.afterEach(restoreEnv); // ─── resolveVisionBridgeBaseUrl ─────────────────────────────────────────── test("#2232 — VISION_BRIDGE_BASE_URL takes precedence over OPENAI_API_URL", () => { clearEnv(); process.env.VISION_BRIDGE_BASE_URL = "http://localhost:20128/v1"; process.env.OPENAI_API_URL = "https://oai.example.com/v1"; assert.equal(resolveVisionBridgeBaseUrl(), "http://localhost:20128/v1"); }); test("#2232 — falls back to OPENAI_API_URL when VISION_BRIDGE_BASE_URL is unset", () => { clearEnv(); process.env.OPENAI_API_URL = "https://oai.example.com/v1"; assert.equal(resolveVisionBridgeBaseUrl(), "https://oai.example.com/v1"); }); test("#2232 — defaults to api.openai.com when both env vars are unset", () => { clearEnv(); assert.equal(resolveVisionBridgeBaseUrl(), "https://api.openai.com/v1"); }); test("#2232 — trailing slashes on VISION_BRIDGE_BASE_URL are stripped", () => { clearEnv(); process.env.VISION_BRIDGE_BASE_URL = "http://localhost:20128/v1///"; assert.equal(resolveVisionBridgeBaseUrl(), "http://localhost:20128/v1"); }); test("#2232 — whitespace-only VISION_BRIDGE_BASE_URL falls through to OPENAI_API_URL", () => { clearEnv(); process.env.VISION_BRIDGE_BASE_URL = " "; process.env.OPENAI_API_URL = "https://oai.example.com/v1"; assert.equal(resolveVisionBridgeBaseUrl(), "https://oai.example.com/v1"); }); test("#2232 — OmniRoute-internal providers default to self-loop when no env vars set", () => { clearEnv(); // Non-standard prefixes (kr/, if/, pol/, groq/) should use OmniRoute self-loop assert.equal(resolveVisionBridgeBaseUrl("kr/claude-sonnet-4-5"), "http://localhost:20128/v1"); assert.equal(resolveVisionBridgeBaseUrl("if/kimi-k2-thinking"), "http://localhost:20128/v1"); assert.equal(resolveVisionBridgeBaseUrl("pol/gpt-5"), "http://localhost:20128/v1"); }); test("#2232 — OpenAI and Anthropic models still default to api.openai.com", () => { clearEnv(); // Standard prefixes should keep default behavior assert.equal(resolveVisionBridgeBaseUrl("openai/gpt-4o"), "https://api.openai.com/v1"); assert.equal( resolveVisionBridgeBaseUrl("anthropic/claude-sonnet-4-5"), "https://api.openai.com/v1" // anthropic goes through a different code path // but if passed here, should not self-loop ); }); test("#2232 — unprefixed model names default to api.openai.com", () => { clearEnv(); // Models without provider prefix should keep default behavior assert.equal(resolveVisionBridgeBaseUrl("gpt-4o-mini"), "https://api.openai.com/v1"); assert.equal(resolveVisionBridgeBaseUrl("deepseek-v4-flash"), "https://api.openai.com/v1"); }); test("#2232 — VISION_BRIDGE_BASE_URL env var takes precedence over self-loop auto-detection", () => { clearEnv(); process.env.VISION_BRIDGE_BASE_URL = "https://custom-proxy.example.com/v1"; assert.equal( resolveVisionBridgeBaseUrl("kr/claude-sonnet-4-5"), "https://custom-proxy.example.com/v1" ); }); // ─── resolveProviderApiKey ──────────────────────────────────────────────── test("#2232 — VISION_BRIDGE_API_KEY wins for non-anthropic models", () => { clearEnv(); process.env.VISION_BRIDGE_API_KEY = "stub-vision-bridge-key"; process.env.OPENAI_API_KEY = "stub-openai-key"; process.env.GOOGLE_API_KEY = "stub-google-key"; assert.equal(resolveProviderApiKey("gemini/gemini-2.0-flash"), "stub-vision-bridge-key"); assert.equal(resolveProviderApiKey("openrouter/nvidia/foo"), "stub-vision-bridge-key"); assert.equal(resolveProviderApiKey("openai/gpt-4o"), "stub-vision-bridge-key"); }); test("#2232 — Anthropic models ignore VISION_BRIDGE_API_KEY (wire format differs)", () => { clearEnv(); process.env.VISION_BRIDGE_API_KEY = "stub-vision-bridge-key"; process.env.ANTHROPIC_API_KEY = "stub-anthropic-key"; assert.equal(resolveProviderApiKey("anthropic/claude-3-haiku"), "stub-anthropic-key"); }); test("#2232 — explicit apiKey wins over VISION_BRIDGE_API_KEY", () => { clearEnv(); process.env.VISION_BRIDGE_API_KEY = "stub-vision-bridge-key"; assert.equal( resolveProviderApiKey("gemini/gemini-2.0-flash", "stub-explicit-key"), "stub-explicit-key" ); }); test("#2232 — without VISION_BRIDGE_API_KEY, falls back to provider-specific env", () => { clearEnv(); process.env.GOOGLE_API_KEY = "stub-google-key"; process.env.OPENAI_API_KEY = "stub-openai-key"; assert.equal(resolveProviderApiKey("google/gemini-pro"), "stub-google-key"); assert.equal(resolveProviderApiKey("openai/gpt-4o"), "stub-openai-key"); }); test("#2232 — empty VISION_BRIDGE_API_KEY falls through to provider-specific env", () => { clearEnv(); process.env.VISION_BRIDGE_API_KEY = " "; process.env.OPENAI_API_KEY = "stub-openai-key"; assert.equal(resolveProviderApiKey("openai/gpt-4o"), "stub-openai-key"); }); test("#2232 — unrecognized prefix without VISION_BRIDGE_API_KEY falls back to OPENAI_API_KEY", () => { clearEnv(); process.env.OPENAI_API_KEY = "stub-openai-fallback"; assert.equal(resolveProviderApiKey("nonexistent/foo-bar"), "stub-openai-fallback"); });