diff --git a/src/sse/services/auth.ts b/src/sse/services/auth.ts index 0fee030e4f..4f7c3c6bb1 100644 --- a/src/sse/services/auth.ts +++ b/src/sse/services/auth.ts @@ -964,7 +964,17 @@ async function getProviderSearchPool(provider: string): Promise { // internal provider ids like openai-compatible-responses-. try { const providerNodes = await getCachedProviderNodes(); - for (const node of Array.isArray(providerNodes) ? providerNodes : []) { + const compatibleNodes = Array.isArray(providerNodes) ? providerNodes : []; + const nodeTypes = new Map(); + for (const node of compatibleNodes) { + const nodeRecord = asRecord(node); + const nodeId = typeof nodeRecord.id === "string" ? nodeRecord.id.trim() : ""; + if (!nodeId) continue; + const derivedType = nodeTypeFromId(nodeId); + nodeTypes.set(derivedType, (nodeTypes.get(derivedType) || 0) + 1); + } + + for (const node of compatibleNodes) { const nodeRecord = asRecord(node); const nodePrefix = typeof nodeRecord.prefix === "string" ? nodeRecord.prefix.trim() : ""; const nodeId = typeof nodeRecord.id === "string" ? nodeRecord.id.trim() : ""; @@ -984,8 +994,9 @@ async function getProviderSearchPool(provider: string): Promise { // looks up the concrete node id, and vice versa. const derivedType = nodeTypeFromId(nodeId); if (derivedType && derivedType !== nodeId) { + const typeIsUnambiguous = nodeTypes.get(derivedType) === 1; if (nodeId === provider || nodeId === canonicalProvider || nodeId === canonicalAlias) { - searchPool.add(derivedType); + if (typeIsUnambiguous) searchPool.add(derivedType); } if ( derivedType === provider || diff --git a/tests/unit/10085-compatible-generic-vs-uuid-credential.test.ts b/tests/unit/10085-compatible-generic-vs-uuid-credential.test.ts index a2f9c49261..40df04c963 100644 --- a/tests/unit/10085-compatible-generic-vs-uuid-credential.test.ts +++ b/tests/unit/10085-compatible-generic-vs-uuid-credential.test.ts @@ -29,6 +29,7 @@ const auth = await import("../../src/sse/services/auth.ts"); const NODE_PREFIX = "my-compat-10085"; const NODE_ID = `openai-compatible-chat-458d982b-0000-4000-8000-000000000000`; +const NODE_B_ID = `openai-compatible-chat-558d982b-0000-4000-8000-000000000000`; async function resetStorage() { core.resetDbInstance(); @@ -52,6 +53,17 @@ async function seedNode() { }); } +async function seedSecondNode() { + await nodesDb.createProviderNode({ + id: NODE_B_ID, + type: "openai-compatible", + name: "My Compat B", + prefix: "my-compat-10085-b", + apiType: "chat", + baseUrl: "https://example-b.test/v1", + }); +} + test("a connection stored under the GENERIC type id is reachable when chat resolves the uuid node id (#10085)", async () => { await resetStorage(); await seedNode(); @@ -99,6 +111,34 @@ test("the bridge works in the other direction too: a uuid-stored connection is r ); }); +test("a concrete second node does not inherit the first node's generic credentials", async () => { + await resetStorage(); + await seedNode(); + await seedSecondNode(); + await providersDb.createProviderConnection({ + provider: "openai-compatible-chat", + authType: "apikey", + apiKey: "sk-test-10085-node-a", + name: "test-compat-node-a", + isActive: true, + testStatus: "active", + priority: 1, + providerSpecificData: { + nodeId: NODE_ID, + prefix: NODE_PREFIX, + baseUrl: "https://example-a.test/v1", + }, + }); + + const creds = await auth.getProviderCredentials(NODE_B_ID); + + assert.equal( + creds, + null, + "node B must not receive node A's generic connection when both nodes share a type" + ); +}); + test("control: a connection stored under the uuid node id is found by a uuid node id lookup", async () => { await resetStorage(); await seedNode();