From 134ab8cabb760e883de2d8545b39dbdec8fc2aa3 Mon Sep 17 00:00:00 2001 From: Ravi Tharuma <25951435+RaviTharuma@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:52:16 +0200 Subject: [PATCH] fix(api): name working OpenRouter ids when Gemini embed creds are missing (#10565) Native gemini-embedding-2 400s with a dead-end credentials error even though openrouter/google/gemini-embedding-2 already serves 3072-d vectors. Co-authored-by: Ravi Tharuma Co-authored-by: Cursor --- .../fixes/embed-gemini-missing-creds-hint.md | 1 + src/lib/embeddings/errors.ts | 50 +++++++++++++++++++ src/lib/embeddings/service.ts | 8 ++- .../unit/embeddings-gemini-creds-hint.test.ts | 38 ++++++++++++++ 4 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 changelog.d/fixes/embed-gemini-missing-creds-hint.md create mode 100644 src/lib/embeddings/errors.ts create mode 100644 tests/unit/embeddings-gemini-creds-hint.test.ts diff --git a/changelog.d/fixes/embed-gemini-missing-creds-hint.md b/changelog.d/fixes/embed-gemini-missing-creds-hint.md new file mode 100644 index 0000000000..41b61713f7 --- /dev/null +++ b/changelog.d/fixes/embed-gemini-missing-creds-hint.md @@ -0,0 +1 @@ +- **fix(api):** `/v1/embeddings` 400s for native `gemini-embedding-2` now name the working OpenRouter ids (`openrouter/google/gemini-embedding-2` and the preview alias) instead of only `No credentials for embedding provider: gemini` — thanks @RaviTharuma diff --git a/src/lib/embeddings/errors.ts b/src/lib/embeddings/errors.ts new file mode 100644 index 0000000000..47479a5a15 --- /dev/null +++ b/src/lib/embeddings/errors.ts @@ -0,0 +1,50 @@ +/** + * Operator-facing embedding credential / provider errors. + * + * Native `gemini-embedding-2` is registered, but a host without a `gemini` + * Google AI Studio key still 400s. OpenRouter already serves the same model + * under `openrouter/google/gemini-embedding-2` (live default 3072-d). Name + * that working id in the 400 so Hindsight / Memorix operators are not stuck + * on a dead native id. + */ + +export const OPENROUTER_GEMINI_EMBEDDING_2 = "openrouter/google/gemini-embedding-2"; +export const OPENROUTER_GEMINI_EMBEDDING_2_PREVIEW = + "openrouter/google/gemini-embedding-2-preview"; +export const NATIVE_GEMINI_EMBEDDING_2 = "gemini/gemini-embedding-2"; + +const OPENROUTER_HINT = + `use \`${OPENROUTER_GEMINI_EMBEDDING_2}\` or \`${OPENROUTER_GEMINI_EMBEDDING_2_PREVIEW}\` ` + + "when OpenRouter is configured"; + +/** + * 400 body when the resolved embedding provider has no usable credentials. + */ +export function formatMissingEmbeddingCredentialsError(provider: string): string { + const base = `No credentials for embedding provider: ${provider}`; + if (provider === "gemini") { + return ( + `${base}. Native Gemini embeddings require a Google AI Studio API key on the \`gemini\` ` + + `provider. Without that key, ${OPENROUTER_HINT}.` + ); + } + return base; +} + +/** + * 400 body when the first path segment is not a known embedding provider. + */ +export function formatUnknownEmbeddingProviderError( + provider: string, + model: string | null = null +): string { + const base = + `Unknown embedding provider: ${provider}. No matching hardcoded or local provider found.`; + const looksLikeGeminiEmbedding2 = + provider === "google" || (typeof model === "string" && model.includes("gemini-embedding-2")); + if (!looksLikeGeminiEmbedding2) return base; + return ( + `${base} For Gemini Embedding 2, ${OPENROUTER_HINT}, or call \`${NATIVE_GEMINI_EMBEDDING_2}\` ` + + "after adding a Google AI Studio key on the `gemini` provider." + ); +} diff --git a/src/lib/embeddings/service.ts b/src/lib/embeddings/service.ts index a615958b18..cd429569bf 100644 --- a/src/lib/embeddings/service.ts +++ b/src/lib/embeddings/service.ts @@ -22,6 +22,10 @@ import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts"; import { handleComboChat } from "@omniroute/open-sse/services/combo.ts"; import { resolveBareModelToConnectionDefault } from "@omniroute/open-sse/services/model.ts"; import { findEmbeddingComboDimensionConflict } from "./familyGuard"; +import { + formatMissingEmbeddingCredentialsError, + formatUnknownEmbeddingProviderError, +} from "./errors"; import { isPrivateHost, isCloudMetadataHost } from "@/shared/network/outboundUrlGuard"; import { calculateCost } from "@/lib/usage/costCalculator"; import { attachOmniRouteMetaHeaders } from "@/domain/omnirouteResponseMeta"; @@ -217,7 +221,7 @@ export async function createEmbeddingResponse( if (!providerConfig) { return errorResponse( HTTP_STATUS.BAD_REQUEST, - `Unknown embedding provider: ${provider}. No matching hardcoded or local provider found.` + formatUnknownEmbeddingProviderError(provider, resolvedModel) ); } @@ -227,7 +231,7 @@ export async function createEmbeddingResponse( if (!credentials) { return errorResponse( HTTP_STATUS.BAD_REQUEST, - `No credentials for embedding provider: ${provider}` + formatMissingEmbeddingCredentialsError(provider) ); } if ("allRateLimited" in credentials && credentials.allRateLimited) { diff --git a/tests/unit/embeddings-gemini-creds-hint.test.ts b/tests/unit/embeddings-gemini-creds-hint.test.ts new file mode 100644 index 0000000000..c6ab316ba2 --- /dev/null +++ b/tests/unit/embeddings-gemini-creds-hint.test.ts @@ -0,0 +1,38 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + formatMissingEmbeddingCredentialsError, + formatUnknownEmbeddingProviderError, + OPENROUTER_GEMINI_EMBEDDING_2, + OPENROUTER_GEMINI_EMBEDDING_2_PREVIEW, + NATIVE_GEMINI_EMBEDDING_2, +} from "../../src/lib/embeddings/errors.ts"; + +test("missing gemini embed credentials name the working OpenRouter ids", () => { + const message = formatMissingEmbeddingCredentialsError("gemini"); + assert.match(message, /^No credentials for embedding provider: gemini/); + assert.match(message, new RegExp(OPENROUTER_GEMINI_EMBEDDING_2)); + assert.match(message, new RegExp(OPENROUTER_GEMINI_EMBEDDING_2_PREVIEW)); + assert.match(message, /Google AI Studio/); +}); + +test("missing credentials for other providers stay a short 400", () => { + assert.equal( + formatMissingEmbeddingCredentialsError("openai"), + "No credentials for embedding provider: openai" + ); +}); + +test("unknown google provider names Gemini Embedding 2 workarounds", () => { + const message = formatUnknownEmbeddingProviderError("google", "gemini-embedding-2"); + assert.match(message, /^Unknown embedding provider: google/); + assert.match(message, new RegExp(OPENROUTER_GEMINI_EMBEDDING_2)); + assert.match(message, new RegExp(NATIVE_GEMINI_EMBEDDING_2)); +}); + +test("unknown unrelated providers stay a short 400", () => { + assert.equal( + formatUnknownEmbeddingProviderError("not-a-provider", "text-embedding-3-small"), + "Unknown embedding provider: not-a-provider. No matching hardcoded or local provider found." + ); +});