mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
feat(gemini/vertex): surface Veo video models in dynamic discovery (#3839)
Gemini / Vertex / Vertex AI Express already discover their catalog dynamically from v1beta/models, but video (Veo) models use predictLongRunning, which was not mapped — so they never surfaced. parseGeminiModelsList now recognizes predictLongRunning and exposes Veo video models alongside chat/image/embedding/audio. Integrated into release/v3.8.25. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
This commit is contained in:
@@ -101,7 +101,7 @@
|
||||
"src/shared/constants/providers.ts": 3147,
|
||||
"src/shared/constants/sidebarVisibility.ts": 1006,
|
||||
"src/shared/services/cliRuntime.ts": 1090,
|
||||
"src/shared/validation/schemas.ts": 2522,
|
||||
"src/shared/validation/schemas.ts": 2523,
|
||||
"src/sse/handlers/chat.ts": 1425,
|
||||
"src/sse/services/auth.ts": 2207
|
||||
},
|
||||
@@ -126,5 +126,6 @@
|
||||
"_rebaseline_2026_06_14_3825_combo_stickiness": "Re-baseline #3825 (sessionless combo stickiness + reasoning-aware readiness): combo.ts 5164→5198 (+34, pós-prettier). Crescimento = deriveComboSessionKey() + effectiveSessionId threading nos sites de read/write do pin server-side. streamReadinessPolicy.ts não-frozen (sob cap). Lógica coesa no handler de combo; não-extraível.",
|
||||
"_rebaseline_2026_06_14_r3_3835_kiro_pricing": "PR #3835 own growth: pricing.ts 1470→1508 (+38 = missing Kiro pricing rows, claude-sonnet-4.6 etc., pure data). Also carries inherited release/v3.8.25 drift not yet frozen by prior r2 merges: RequestLoggerV2.tsx 1276→1282 (#3820 resizable log table) and combo.ts 5198→5203 (#3811 round-robin replay-response fix). This PR does not touch RequestLoggerV2/combo.ts source; updating the frozen values restores Fast Quality Gates on the current release branch.",
|
||||
"_rebaseline_2026_06_14_r3_3849_transient_hide": "PR #3849 own growth: providerPageHelpers.ts 939→955 (+16 = expanded JSDoc on the auto-hide policy + transient-failure guard in evaluateTestAllEntry). Cohesive helper logic; not extractable.",
|
||||
"_rebaseline_2026_06_14_r3_3838_opencode_quota": "PR #3838 own growth: usage.ts 3394→3408 (+14 = clearer OpenCode Go missing-quota-API messages with OMNIROUTE_OPENCODE_GO_QUOTA_URL override hint + upstream issue links). Message text only; no logic extractable."
|
||||
"_rebaseline_2026_06_14_r3_3838_opencode_quota": "PR #3838 own growth: usage.ts 3394→3408 (+14 = clearer OpenCode Go missing-quota-API messages with OMNIROUTE_OPENCODE_GO_QUOTA_URL override hint + upstream issue links). Message text only; no logic extractable.",
|
||||
"_rebaseline_2026_06_14_r3_3839_veo_video": "PR #3839 own growth: schemas.ts 2522→2523 (+1 = Veo video model (predictLongRunning) validation for Gemini/Vertex dynamic discovery)."
|
||||
}
|
||||
|
||||
@@ -3,20 +3,30 @@
|
||||
*
|
||||
* Each model's `supportedGenerationMethods` is mapped to OmniRoute endpoints:
|
||||
* - generateContent / generateAnswer → "chat"
|
||||
* - predict / predictLongRunning → "images" (Imagen models)
|
||||
* - embedContent → "embeddings"
|
||||
* - bidiGenerateContent → "audio"
|
||||
* - predict → "images" (Imagen image generation)
|
||||
* - predictLongRunning → "video" (Veo video generation)
|
||||
* - embedContent → "embeddings"
|
||||
* - bidiGenerateContent → "audio" (Live real-time 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.
|
||||
* Model-id heuristics refine the long-running bucket because Google exposes both
|
||||
* Imagen and Veo via long-running methods on the same endpoint:
|
||||
* - id contains "veo" → ensure "video"
|
||||
* - id contains "imagen" → force "images" (never "video")
|
||||
*
|
||||
* Note: `gemini-*-image` models (e.g. gemini-3-pro-image) generate images via the
|
||||
* regular `generateContent` path, so they stay "chat" (image output is a chat
|
||||
* modality) and are intentionally NOT reclassified as "images".
|
||||
*
|
||||
* 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 —
|
||||
* surfaces dynamically instead of being limited to the small static registry.
|
||||
*/
|
||||
const METHOD_TO_ENDPOINT: Record<string, string> = {
|
||||
generateContent: "chat",
|
||||
embedContent: "embeddings",
|
||||
predict: "images",
|
||||
predictLongRunning: "images",
|
||||
predictLongRunning: "video",
|
||||
bidiGenerateContent: "audio",
|
||||
generateAnswer: "chat",
|
||||
};
|
||||
@@ -45,20 +55,33 @@ export function parseGeminiModelsList(data: any): GeminiDiscoveryModel[] {
|
||||
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");
|
||||
|
||||
const endpoints = new Set<string>(
|
||||
methods
|
||||
.filter((method) => !IGNORED_METHODS.has(method))
|
||||
.map((method) => METHOD_TO_ENDPOINT[method] || "chat")
|
||||
);
|
||||
|
||||
const id = ((m.name as string) || (m.id as string) || "").replace(/^models\//, "");
|
||||
const lowerId = id.toLowerCase();
|
||||
|
||||
// Google exposes Imagen (image) and Veo (video) via long-running methods; the
|
||||
// method alone can't always distinguish them, so refine by model id.
|
||||
if (lowerId.includes("veo")) {
|
||||
endpoints.add("video");
|
||||
}
|
||||
if (lowerId.includes("imagen")) {
|
||||
endpoints.delete("video");
|
||||
endpoints.add("images");
|
||||
}
|
||||
|
||||
if (endpoints.size === 0) endpoints.add("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,
|
||||
id,
|
||||
name: (m.displayName as string) || id,
|
||||
supportedEndpoints: [...endpoints],
|
||||
...(typeof m.inputTokenLimit === "number" ? { inputTokenLimit: m.inputTokenLimit } : {}),
|
||||
...(typeof m.outputTokenLimit === "number" ? { outputTokenLimit: m.outputTokenLimit } : {}),
|
||||
...(typeof m.description === "string" ? { description: m.description } : {}),
|
||||
|
||||
@@ -1056,6 +1056,7 @@ export const providerModelMutationSchema = z.object({
|
||||
"rerank",
|
||||
"images",
|
||||
"audio",
|
||||
"video",
|
||||
"audio-transcriptions",
|
||||
"audio-speech",
|
||||
"images-generations",
|
||||
|
||||
@@ -36,6 +36,18 @@ const SAMPLE = {
|
||||
displayName: "Gemini Live",
|
||||
supportedGenerationMethods: ["bidiGenerateContent"],
|
||||
},
|
||||
{
|
||||
name: "models/veo-3.0-generate-001",
|
||||
displayName: "Veo 3.0",
|
||||
supportedGenerationMethods: ["predictLongRunning"],
|
||||
},
|
||||
{
|
||||
// Defensive: an Imagen model exposed via a long-running method must stay
|
||||
// "images", never "video".
|
||||
name: "models/imagen-future-preview",
|
||||
displayName: "Imagen Future",
|
||||
supportedGenerationMethods: ["predictLongRunning"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -77,6 +89,20 @@ test("parseGeminiModelsList maps embedContent and bidiGenerateContent", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("parseGeminiModelsList maps Veo predictLongRunning models to the video endpoint", () => {
|
||||
const models = parseGeminiModelsList(SAMPLE);
|
||||
const veo = models.find((m) => m.id === "veo-3.0-generate-001");
|
||||
assert.ok(veo, "veo-3.0-generate-001 should be present");
|
||||
assert.deepEqual(veo!.supportedEndpoints, ["video"]);
|
||||
});
|
||||
|
||||
test("parseGeminiModelsList keeps Imagen as images even via a long-running method", () => {
|
||||
const models = parseGeminiModelsList(SAMPLE);
|
||||
const imagen = models.find((m) => m.id === "imagen-future-preview");
|
||||
assert.ok(imagen, "imagen-future-preview should be present");
|
||||
assert.deepEqual(imagen!.supportedEndpoints, ["images"]);
|
||||
});
|
||||
|
||||
test("parseGeminiModelsList defaults to chat and tolerates empty/missing input", () => {
|
||||
assert.deepEqual(parseGeminiModelsList({}), []);
|
||||
assert.deepEqual(parseGeminiModelsList(null), []);
|
||||
|
||||
Reference in New Issue
Block a user