diff --git a/changelog.d/fixes/10702-vision-bridge-alias-credential-mismatch.md b/changelog.d/fixes/10702-vision-bridge-alias-credential-mismatch.md new file mode 100644 index 0000000000..dcd1c488ae --- /dev/null +++ b/changelog.d/fixes/10702-vision-bridge-alias-credential-mismatch.md @@ -0,0 +1 @@ +- fix(guardrails): resolve the public provider alias before querying credentials in the Vision Bridge router, so command-code/opencode (and any alias!=id provider) are no longer reported as "unusable" despite active connections (#10702) diff --git a/src/lib/guardrails/visionBridgeCredentials.ts b/src/lib/guardrails/visionBridgeCredentials.ts index 74dc172358..6b060222c8 100644 --- a/src/lib/guardrails/visionBridgeCredentials.ts +++ b/src/lib/guardrails/visionBridgeCredentials.ts @@ -80,10 +80,16 @@ function loadProvidersModule(): Promise { * Returns `null` when the credential store is unavailable (unit tests / early boot). */ export async function hasUsableCredentialsForModel(model: string): Promise { - const provider = typeof model === "string" ? model.split("/")[0]?.trim() : ""; - if (!provider) return null; + const rawPrefix = typeof model === "string" ? model.split("/")[0]?.trim() : ""; + if (!rawPrefix) return null; try { const { getProviderConnections } = await loadProvidersModule(); + // The model ids this module receives use the PUBLIC ALIAS (PROVIDER_MODELS keys, + // e.g. "cmd" for command-code), but provider_connections.provider is always + // persisted under the raw registry id — resolve the alias first, matching every + // other credential-check path (open-sse/services/model.ts, sse/services/auth.ts). + const { resolveProviderId } = await import("@/shared/constants/providers"); + const provider = resolveProviderId(rawPrefix); const connections = await getProviderConnections({ provider, isActive: true }); if (!Array.isArray(connections)) return null; // Empty active set is a definitive "no" only when the table is readable. diff --git a/tests/unit/guardrails/vision-bridge-credentials-alias-mismatch-10702.test.ts b/tests/unit/guardrails/vision-bridge-credentials-alias-mismatch-10702.test.ts new file mode 100644 index 0000000000..942de03474 --- /dev/null +++ b/tests/unit/guardrails/vision-bridge-credentials-alias-mismatch-10702.test.ts @@ -0,0 +1,52 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-visionbridge-cred-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.DISABLE_SQLITE_AUTO_BACKUP = "true"; + +const core = await import("../../../src/lib/db/core.ts"); +const providersDb = await import("../../../src/lib/db/providers.ts"); +const { hasUsableCredentialsForModel } = await import( + "../../../src/lib/guardrails/visionBridgeCredentials.ts" +); + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("issue #10702: hasUsableCredentialsForModel resolves alias-prefixed model to the raw provider id (command-code / alias cmd)", async () => { + await providersDb.createProviderConnection({ + provider: "command-code", + authType: "apikey", + apiKey: "sk-test-command-code-key", + isActive: true, + }); + + const result = await hasUsableCredentialsForModel("cmd/some-vision-model"); + assert.equal( + result, + true, + "the credentialed command-code connection must be found via its public alias 'cmd'" + ); +}); + +test("issue #10702: hasUsableCredentialsForModel resolves alias-prefixed model to the raw provider id (opencode / alias oc)", async () => { + await providersDb.createProviderConnection({ + provider: "opencode", + authType: "apikey", + apiKey: "sk-test-opencode-key", + isActive: true, + }); + + const result = await hasUsableCredentialsForModel("oc/some-vision-model"); + assert.equal( + result, + true, + "the credentialed opencode connection must be found via its public alias 'oc'" + ); +});