diff --git a/changelog.d/fixes/10591-gemini-live-catalog.md b/changelog.d/fixes/10591-gemini-live-catalog.md new file mode 100644 index 0000000000..7663591649 --- /dev/null +++ b/changelog.d/fixes/10591-gemini-live-catalog.md @@ -0,0 +1 @@ +- Stop advertising Gemini Live-only models as supported audio endpoints until OmniRoute proxies the bidirectional Live protocol. diff --git a/src/lib/providerModels/geminiModelsParser.ts b/src/lib/providerModels/geminiModelsParser.ts index e4fd1bfdd7..9d4aa68de8 100644 --- a/src/lib/providerModels/geminiModelsParser.ts +++ b/src/lib/providerModels/geminiModelsParser.ts @@ -6,7 +6,7 @@ * - predict → "images" (Imagen image generation) * - predictLongRunning → "videos" (Veo video generation) * - embedContent → "embeddings" - * - bidiGenerateContent → "audio" (Live real-time audio) + * - bidiGenerateContent → ignored (Gemini Live is not proxied) * * Model-id heuristics refine the long-running bucket because Google exposes both * Imagen and Veo via long-running methods on the same endpoint: @@ -19,7 +19,7 @@ * * This is shared by the `gemini` discovery config and the `vertex` / * `vertex-partner` (incl. Vertex AI Express key) discovery branches, so every - * model the account can access — chat, image, video, audio and embeddings — + * supported model the account can access — chat, image, video and embeddings — * surfaces dynamically instead of being limited to the small static registry. */ const METHOD_TO_ENDPOINT: Record = { @@ -27,7 +27,6 @@ const METHOD_TO_ENDPOINT: Record = { embedContent: "embeddings", predict: "images", predictLongRunning: "videos", - bidiGenerateContent: "audio", generateAnswer: "chat", }; @@ -37,6 +36,7 @@ const IGNORED_METHODS = new Set([ "createCachedContent", "batchGenerateContent", "asyncBatchEmbedContent", + "bidiGenerateContent", ]); const RETIRED_GEMINI_MODEL_IDS = new Set(["gemini-3.5-flash"]); @@ -78,6 +78,13 @@ export function parseGeminiModelsList(data: any): GeminiDiscoveryModel[] { endpoints.add("images"); } + if ( + endpoints.size === 0 && + methods.length > 0 && + methods.every((method) => IGNORED_METHODS.has(method)) + ) { + return null; + } if (endpoints.size === 0) endpoints.add("chat"); return { @@ -91,5 +98,8 @@ export function parseGeminiModelsList(data: any): GeminiDiscoveryModel[] { ...(m.thinking === true ? { supportsThinking: true } : {}), } as GeminiDiscoveryModel; }) - .filter((model: GeminiDiscoveryModel) => !RETIRED_GEMINI_MODEL_IDS.has(model.id)); + .filter( + (model: GeminiDiscoveryModel | null): model is GeminiDiscoveryModel => + Boolean(model) && !RETIRED_GEMINI_MODEL_IDS.has(model.id) + ); } diff --git a/tests/unit/gemini-models-parser.test.ts b/tests/unit/gemini-models-parser.test.ts index 8c9da3dac4..f9463d61b5 100644 --- a/tests/unit/gemini-models-parser.test.ts +++ b/tests/unit/gemini-models-parser.test.ts @@ -72,14 +72,21 @@ test("parseGeminiModelsList maps generateContent image models to the chat endpoi assert.deepEqual(proImage!.supportedEndpoints, ["chat"]); }); -test("parseGeminiModelsList maps embedContent and bidiGenerateContent", () => { +test("parseGeminiModelsList maps embeddings without advertising unsupported Gemini Live", () => { const models = parseGeminiModelsList(SAMPLE); assert.deepEqual(models.find((m) => m.id === "text-embedding-004")!.supportedEndpoints, [ "embeddings", ]); - assert.deepEqual(models.find((m) => m.id === "gemini-live-2.5-flash")!.supportedEndpoints, [ - "audio", - ]); + assert.equal(models.some((m) => m.id === "gemini-live-2.5-flash"), false); + const [hybrid] = parseGeminiModelsList({ + models: [ + { + name: "models/gemini-live-hybrid", + supportedGenerationMethods: ["generateContent", "bidiGenerateContent"], + }, + ], + }); + assert.deepEqual(hybrid.supportedEndpoints, ["chat"]); }); test("parseGeminiModelsList maps Veo predictLongRunning models to the videos endpoint", () => {