Repository navigation
feat(vertex): dynamic model discovery (surface image models + full catalog) #3712
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
diegosouzapw
merged 1 commit into
cheaper-inference:release/v3.8.23
from
artickc:fix/vertex-dynamic-models
Jun 12, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| /** | ||
| * Parses the Google Generative Language `v1beta/models` listing into discovery models. | ||
| * | ||
| * Each model's `supportedGenerationMethods` is mapped to OmniRoute endpoints: | ||
| * - generateContent / generateAnswer → "chat" | ||
| * - predict / predictLongRunning → "images" (Imagen models) | ||
| * - embedContent → "embeddings" | ||
| * - bidiGenerateContent → "audio" | ||
| * | ||
| * This is shared by the `gemini` discovery config and the `vertex` discovery branch: Vertex AI | ||
| * Express keys (and Service Account JSON via a minted OAuth token) list models from the same | ||
| * endpoint, so image models (imagen-*, gemini-*-image) surface dynamically instead of being | ||
| * limited to the small static registry list. | ||
| */ | ||
| const METHOD_TO_ENDPOINT: Record<string, string> = { | ||
| generateContent: "chat", | ||
| embedContent: "embeddings", | ||
| predict: "images", | ||
| predictLongRunning: "images", | ||
| bidiGenerateContent: "audio", | ||
| generateAnswer: "chat", | ||
| }; | ||
|
|
||
| const IGNORED_METHODS = new Set([ | ||
| "countTokens", | ||
| "countTextTokens", | ||
| "createCachedContent", | ||
| "batchGenerateContent", | ||
| "asyncBatchEmbedContent", | ||
| ]); | ||
|
|
||
| export interface GeminiDiscoveryModel { | ||
| id: string; | ||
| name: string; | ||
| supportedEndpoints: string[]; | ||
| inputTokenLimit?: number; | ||
| outputTokenLimit?: number; | ||
| description?: string; | ||
| supportsThinking?: boolean; | ||
| [key: string]: unknown; | ||
| } | ||
|
|
||
| export function parseGeminiModelsList(data: any): GeminiDiscoveryModel[] { | ||
| return (data?.models || []).map((m: Record<string, unknown>) => { | ||
| const methods: string[] = Array.isArray(m.supportedGenerationMethods) | ||
| ? (m.supportedGenerationMethods as string[]) | ||
| : []; | ||
| const endpoints = [ | ||
| ...new Set( | ||
| methods | ||
| .filter((method) => !IGNORED_METHODS.has(method)) | ||
| .map((method) => METHOD_TO_ENDPOINT[method] || "chat") | ||
| ), | ||
| ]; | ||
| if (endpoints.length === 0) endpoints.push("chat"); | ||
|
|
||
| return { | ||
| ...m, | ||
| id: ((m.name as string) || (m.id as string) || "").replace(/^models\//, ""), | ||
| name: (m.displayName as string) || ((m.name as string) || "").replace(/^models\//, ""), | ||
| supportedEndpoints: endpoints, | ||
| ...(typeof m.inputTokenLimit === "number" ? { inputTokenLimit: m.inputTokenLimit } : {}), | ||
| ...(typeof m.outputTokenLimit === "number" ? { outputTokenLimit: m.outputTokenLimit } : {}), | ||
| ...(typeof m.description === "string" ? { description: m.description } : {}), | ||
| ...(m.thinking === true ? { supportsThinking: true } : {}), | ||
| } as GeminiDiscoveryModel; | ||
| }); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| import test from "node:test"; | ||
| import assert from "node:assert/strict"; | ||
|
|
||
| import { parseGeminiModelsList } from "../../src/lib/providerModels/geminiModelsParser"; | ||
|
|
||
| // A representative slice of the live generativelanguage v1beta/models response — including the | ||
| // image models (gemini-*-image via generateContent, imagen-* via predict) that the Vertex catalog | ||
| // must surface dynamically. | ||
| const SAMPLE = { | ||
| models: [ | ||
| { | ||
| name: "models/gemini-2.5-flash", | ||
| displayName: "Gemini 2.5 Flash", | ||
| inputTokenLimit: 1048576, | ||
| outputTokenLimit: 65536, | ||
| supportedGenerationMethods: ["generateContent", "countTokens", "batchGenerateContent"], | ||
| thinking: true, | ||
| }, | ||
| { | ||
| name: "models/gemini-3-pro-image-preview", | ||
| displayName: "Gemini 3 Pro Image Preview", | ||
| supportedGenerationMethods: ["generateContent", "countTokens"], | ||
| }, | ||
| { | ||
| name: "models/imagen-4.0-generate-001", | ||
| displayName: "Imagen 4.0", | ||
| supportedGenerationMethods: ["predict"], | ||
| }, | ||
| { | ||
| name: "models/text-embedding-004", | ||
| displayName: "Text Embedding 004", | ||
| supportedGenerationMethods: ["embedContent"], | ||
| }, | ||
| { | ||
| name: "models/gemini-live-2.5-flash", | ||
| displayName: "Gemini Live", | ||
| supportedGenerationMethods: ["bidiGenerateContent"], | ||
| }, | ||
| ], | ||
| }; | ||
|
|
||
| test("parseGeminiModelsList strips the models/ prefix and maps display name", () => { | ||
| const models = parseGeminiModelsList(SAMPLE); | ||
| const flash = models.find((m) => m.id === "gemini-2.5-flash"); | ||
| assert.ok(flash, "gemini-2.5-flash should be present"); | ||
| assert.equal(flash!.name, "Gemini 2.5 Flash"); | ||
| assert.equal(flash!.inputTokenLimit, 1048576); | ||
| assert.equal(flash!.outputTokenLimit, 65536); | ||
| assert.equal(flash!.supportsThinking, true); | ||
| assert.deepEqual(flash!.supportedEndpoints, ["chat"]); | ||
| }); | ||
|
|
||
| test("parseGeminiModelsList maps generateContent image models to the chat endpoint", () => { | ||
| const models = parseGeminiModelsList(SAMPLE); | ||
| const proImage = models.find((m) => m.id === "gemini-3-pro-image-preview"); | ||
| assert.ok(proImage, "gemini-3-pro-image-preview should be present"); | ||
| // gemini-*-image models generate images via generateContent (chat-with-modalities path). | ||
| assert.deepEqual(proImage!.supportedEndpoints, ["chat"]); | ||
| }); | ||
|
|
||
| test("parseGeminiModelsList maps Imagen predict models to the images endpoint", () => { | ||
| const models = parseGeminiModelsList(SAMPLE); | ||
| const imagen = models.find((m) => m.id === "imagen-4.0-generate-001"); | ||
| assert.ok(imagen, "imagen-4.0-generate-001 should be present"); | ||
| assert.deepEqual(imagen!.supportedEndpoints, ["images"]); | ||
| }); | ||
|
|
||
| test("parseGeminiModelsList maps embedContent and bidiGenerateContent", () => { | ||
| 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"] | ||
| ); | ||
| }); | ||
|
|
||
| test("parseGeminiModelsList defaults to chat and tolerates empty/missing input", () => { | ||
| assert.deepEqual(parseGeminiModelsList({}), []); | ||
| assert.deepEqual(parseGeminiModelsList(null), []); | ||
| const [m] = parseGeminiModelsList({ models: [{ name: "models/mystery" }] }); | ||
| assert.equal(m.id, "mystery"); | ||
| assert.deepEqual(m.supportedEndpoints, ["chat"]); | ||
| }); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WARNING:
vertex-partnerdynamic discovery can replace the partner-only catalogThis branch treats
vertex-partnerthe same asvertexand returnsbuildApiDiscoveryResponse(allModels)when the Generative Language endpoint returns any models. The live endpoint lists Google Gemini/Imagen models, not the partner models in the staticvertex-partnerregistry (open-sse/config/providerRegistry.ts:3458, e.g.DeepSeek-V4-*,Qwen3.6-*,GLM-5.1-FP8, and Claude partner models). A successful discovery response can therefore drop those partner-only models instead of augmenting the local catalog.Merge discovered models with the local catalog for
vertex-partner, or excludevertex-partnerfrom this branch if it is not served by the Generative Language API.Reply with
@kilocode-bot fix itto have Kilo Code address this issue.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To use Kilo from GitHub you first need to link your GitHub account to Kilo. Link your Kilo account to continue. After linking, mention me again in this issue or pull request.