diff --git a/open-sse/config/embeddingRegistry.ts b/open-sse/config/embeddingRegistry.ts index a96327e8..6cf867df 100644 --- a/open-sse/config/embeddingRegistry.ts +++ b/open-sse/config/embeddingRegistry.ts @@ -17,6 +17,7 @@ export interface EmbeddingProvider { } export interface EmbeddingProviderNodeRow { + id?: string; prefix: string; name: string; baseUrl: string; diff --git a/src/app/api/v1/embeddings/route.ts b/src/app/api/v1/embeddings/route.ts index 99c439cb..6bb7169c 100644 --- a/src/app/api/v1/embeddings/route.ts +++ b/src/app/api/v1/embeddings/route.ts @@ -160,6 +160,7 @@ export async function POST(request) { // Resolve provider config — dynamic first (local override), then hardcoded let providerConfig: EmbeddingProvider | null = dynamicProviders.find((dp) => dp.id === provider) || getEmbeddingProvider(provider) || null; + let credentialsProviderId = provider; // #496: Fallback — resolve from ALL provider_nodes (not just localhost) // This enables custom embedding models (e.g. google/gemini-embedding-001) whose @@ -180,6 +181,7 @@ export async function POST(request) { authHeader: "bearer", models: [], }; + credentialsProviderId = matchingNode.id || provider; log.info( "EMBED", `Resolved custom embedding provider: ${provider} → ${providerConfig.baseUrl}` @@ -200,7 +202,7 @@ export async function POST(request) { // Get credentials — skip for local providers (authType: "none") let credentials = null; if (providerConfig && providerConfig.authType !== "none") { - credentials = await getProviderCredentials(provider); + credentials = await getProviderCredentials(credentialsProviderId); if (!credentials) { return errorResponse( HTTP_STATUS.BAD_REQUEST, diff --git a/tests/unit/auth-clear-provider-routes.test.mjs b/tests/unit/auth-clear-provider-routes.test.mjs index e67cb524..49befc2e 100644 --- a/tests/unit/auth-clear-provider-routes.test.mjs +++ b/tests/unit/auth-clear-provider-routes.test.mjs @@ -106,3 +106,60 @@ test("embeddings route clears stale provider error metadata on success", async ( globalThis.fetch = originalFetch; } }); + +test("embeddings route uses provider node id for compatible provider credentials", async () => { + await resetStorage(); + + const providerNode = await providersDb.createProviderNode({ + id: "openai-compatible-responses-google-embeddings", + type: "openai-compatible", + name: "Gemini Embeddings", + prefix: "google", + apiType: "responses", + baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai", + }); + + const created = await providersDb.createProviderConnection({ + provider: providerNode.id, + authType: "apikey", + email: null, + name: "google-compatible-key", + apiKey: "google-compatible-test-key", + testStatus: "active", + lastError: null, + lastErrorType: "token_refresh_failed", + lastErrorSource: "oauth", + errorCode: "refresh_failed", + rateLimitedUntil: null, + backoffLevel: 2, + }); + + const originalFetch = globalThis.fetch; + globalThis.fetch = async (url, init) => { + assert.equal(url, "https://generativelanguage.googleapis.com/v1beta/openai/embeddings"); + assert.equal(init?.headers?.Authorization, "Bearer google-compatible-test-key"); + return Response.json({ + data: [{ object: "embedding", index: 0, embedding: [0.1, 0.2] }], + usage: { prompt_tokens: 3, total_tokens: 3 }, + }); + }; + + try { + const request = new Request("http://localhost/v1/embeddings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ model: "google/gemini-embedding-001", input: "hello" }), + }); + + const response = await embeddingsRoute.POST(request); + assert.equal(response.status, 200); + + const updated = await readConnection(created.id); + assert.equal(updated.testStatus, "active"); + assert.equal(updated.errorCode, undefined); + assert.equal(updated.lastErrorType, undefined); + assert.equal(updated.lastErrorSource, undefined); + } finally { + globalThis.fetch = originalFetch; + } +});