diff --git a/packages/proxy/schema/model_list.json b/packages/proxy/schema/model_list.json index 72177f0a..a59bf077 100644 --- a/packages/proxy/schema/model_list.json +++ b/packages/proxy/schema/model_list.json @@ -2924,9 +2924,6 @@ "displayName": "Gemini 3.1 Pro (Preview)", "reasoning": true, "reasoning_budget": true, - "supported_regions": [ - "global" - ], "max_input_tokens": 1048576, "max_output_tokens": 65536, "available_providers": [ @@ -2961,9 +2958,6 @@ "displayName": "Gemini 3.1 Flash-Lite (Preview)", "reasoning": true, "reasoning_budget": true, - "supported_regions": [ - "global" - ], "max_input_tokens": 1048576, "max_output_tokens": 65536, "available_providers": [ @@ -2999,9 +2993,6 @@ "displayName": "Gemini 3 Flash (Preview)", "reasoning": true, "reasoning_budget": true, - "supported_regions": [ - "global" - ], "max_input_tokens": 1048576, "max_output_tokens": 65535, "available_providers": [ @@ -3019,32 +3010,6 @@ "displayName": "Gemini 2.5 Flash", "reasoning": true, "reasoning_budget": true, - "supported_regions": [ - "global", - "asia-northeast1", - "asia-northeast3", - "asia-south1", - "asia-southeast1", - "australia-southeast1", - "europe-central2", - "europe-north1", - "europe-southwest1", - "europe-west1", - "europe-west2", - "europe-west3", - "europe-west4", - "europe-west8", - "europe-west9", - "northamerica-northeast1", - "southamerica-east1", - "us-central1", - "us-east1", - "us-east4", - "us-east5", - "us-south1", - "us-west1", - "us-west4" - ], "max_input_tokens": 1048576, "max_output_tokens": 65535, "available_providers": [ @@ -3062,25 +3027,6 @@ "displayName": "Gemini 2.5 Pro", "reasoning": true, "reasoning_budget": true, - "supported_regions": [ - "global", - "asia-northeast1", - "europe-central2", - "europe-north1", - "europe-southwest1", - "europe-west1", - "europe-west4", - "europe-west8", - "europe-west9", - "northamerica-northeast1", - "us-central1", - "us-east1", - "us-east4", - "us-east5", - "us-south1", - "us-west1", - "us-west4" - ], "max_input_tokens": 1048576, "max_output_tokens": 65535, "available_providers": [ @@ -3099,9 +3045,6 @@ "reasoning_budget": true, "experimental": false, "parent": "gemini-2.5-flash", - "supported_regions": [ - "global" - ], "max_input_tokens": 1048576, "max_output_tokens": 65535, "available_providers": [ @@ -3229,9 +3172,6 @@ "reasoning_budget": true, "experimental": true, "parent": "gemini-2.5-flash-lite", - "supported_regions": [ - "global" - ], "max_input_tokens": 1048576, "max_output_tokens": 65535, "available_providers": [ @@ -3269,23 +3209,6 @@ "displayName": "Gemini 2.5 Flash-Lite", "reasoning": true, "reasoning_budget": true, - "supported_regions": [ - "global", - "europe-central2", - "europe-north1", - "europe-southwest1", - "europe-west1", - "europe-west4", - "europe-west8", - "europe-west9", - "us-central1", - "us-east1", - "us-east4", - "us-east5", - "us-south1", - "us-west1", - "us-west4" - ], "max_input_tokens": 1048576, "max_output_tokens": 65535, "available_providers": [ @@ -3364,9 +3287,6 @@ "input_cost_per_mil_tokens": 2, "output_cost_per_mil_tokens": 12, "displayName": "Gemini 3 Pro Image Preview", - "supported_regions": [ - "global" - ], "max_input_tokens": 65536, "max_output_tokens": 32768, "available_providers": [ @@ -5181,6 +5101,9 @@ "input_cost_per_mil_tokens": 2, "output_cost_per_mil_tokens": 12, "displayName": "Gemini 3 Pro Image Preview", + "locations": [ + "global" + ], "max_input_tokens": 65536, "max_output_tokens": 32768 }, @@ -5292,6 +5215,12 @@ "displayName": "Claude 4.6 Sonnet", "reasoning": true, "reasoning_budget": true, + "locations": [ + "global", + "us-east5", + "asia-southeast1", + "europe-west1" + ], "max_input_tokens": 200000, "max_output_tokens": 64000 }, @@ -5303,7 +5232,13 @@ "output_cost_per_mil_tokens": 15, "displayName": "Claude 4.5 Sonnet", "reasoning": true, - "reasoning_budget": true + "reasoning_budget": true, + "locations": [ + "global", + "us-east5", + "asia-southeast1", + "europe-west1" + ] }, "publishers/anthropic/models/claude-sonnet-4-5@20250929": { "format": "anthropic", @@ -5314,7 +5249,13 @@ "reasoning": true, "reasoning_budget": true, "experimental": true, - "parent": "publishers/anthropic/models/claude-sonnet-4-5" + "parent": "publishers/anthropic/models/claude-sonnet-4-5", + "locations": [ + "global", + "us-east5", + "asia-southeast1", + "europe-west1" + ] }, "publishers/anthropic/models/claude-sonnet-4": { "format": "anthropic", @@ -5324,7 +5265,13 @@ "output_cost_per_mil_tokens": 15, "displayName": "Claude 4 Sonnet", "reasoning": true, - "reasoning_budget": true + "reasoning_budget": true, + "locations": [ + "global", + "us-east5", + "asia-east1", + "europe-west1" + ] }, "publishers/anthropic/models/claude-sonnet-4@20250514": { "format": "anthropic", @@ -5335,7 +5282,13 @@ "reasoning": true, "reasoning_budget": true, "experimental": true, - "parent": "publishers/anthropic/models/claude-sonnet-4" + "parent": "publishers/anthropic/models/claude-sonnet-4", + "locations": [ + "global", + "us-east5", + "asia-east1", + "europe-west1" + ] }, "publishers/anthropic/models/claude-3-7-sonnet": { "format": "anthropic", @@ -5346,7 +5299,12 @@ "displayName": "Claude 3.7 Sonnet", "reasoning": true, "reasoning_budget": true, - "deprecation_date": "2026-02-19" + "deprecation_date": "2026-02-19", + "locations": [ + "global", + "us-east5", + "europe-west1" + ] }, "publishers/anthropic/models/claude-3-7-sonnet@20250219": { "format": "anthropic", @@ -5358,7 +5316,12 @@ "reasoning_budget": true, "experimental": true, "deprecation_date": "2026-02-19", - "parent": "publishers/anthropic/models/claude-3-7-sonnet" + "parent": "publishers/anthropic/models/claude-3-7-sonnet", + "locations": [ + "global", + "us-east5", + "europe-west1" + ] }, "publishers/anthropic/models/claude-haiku-4-5": { "format": "anthropic", @@ -5371,6 +5334,12 @@ "displayName": "Claude 4.5 Haiku", "reasoning": true, "reasoning_budget": true, + "locations": [ + "global", + "us-east5", + "asia-east1", + "europe-west1" + ], "max_input_tokens": 200000, "max_output_tokens": 64000 }, @@ -5385,6 +5354,12 @@ "reasoning": true, "reasoning_budget": true, "parent": "publishers/anthropic/models/claude-haiku-4-5", + "locations": [ + "global", + "us-east5", + "asia-east1", + "europe-west1" + ], "max_input_tokens": 200000, "max_output_tokens": 64000 }, @@ -5398,6 +5373,10 @@ "input_cache_write_cost_per_mil_tokens": 1.25, "displayName": "Claude 3.5 Haiku", "deprecation_date": "2026-02-19", + "locations": [ + "us-east5", + "europe-west1" + ], "max_input_tokens": 200000, "max_output_tokens": 8192 }, @@ -5411,6 +5390,10 @@ "input_cache_write_cost_per_mil_tokens": 1.25, "deprecation_date": "2026-02-19", "parent": "publishers/anthropic/models/claude-3-5-haiku", + "locations": [ + "us-east5", + "europe-west1" + ], "max_input_tokens": 200000, "max_output_tokens": 8192 }, @@ -5454,7 +5437,11 @@ "output_cost_per_mil_tokens": 75, "displayName": "Claude 4 Opus", "reasoning": true, - "reasoning_budget": true + "reasoning_budget": true, + "locations": [ + "global", + "us-east5" + ] }, "publishers/anthropic/models/claude-opus-4-6": { "format": "anthropic", @@ -5467,6 +5454,12 @@ "displayName": "Claude 4.6 Opus", "reasoning": true, "reasoning_budget": true, + "locations": [ + "global", + "us-east5", + "asia-southeast1", + "europe-west1" + ], "max_input_tokens": 200000, "max_output_tokens": 128000 }, @@ -5481,6 +5474,12 @@ "displayName": "Claude 4.5 Opus", "reasoning": true, "reasoning_budget": true, + "locations": [ + "global", + "us-east5", + "asia-southeast1", + "europe-west1" + ], "max_input_tokens": 200000, "max_output_tokens": 64000 }, @@ -5495,6 +5494,10 @@ "displayName": "Claude 4.1 Opus", "reasoning": true, "reasoning_budget": true, + "locations": [ + "global", + "us-east5" + ], "max_input_tokens": 200000, "max_output_tokens": 32000 }, @@ -5506,7 +5509,11 @@ "output_cost_per_mil_tokens": 75, "reasoning": true, "reasoning_budget": true, - "parent": "publishers/anthropic/models/claude-opus-4" + "parent": "publishers/anthropic/models/claude-opus-4", + "locations": [ + "global", + "us-east5" + ] }, "publishers/anthropic/models/claude-3-opus": { "format": "anthropic", @@ -5530,7 +5537,12 @@ "multimodal": true, "input_cost_per_mil_tokens": 0.25, "output_cost_per_mil_tokens": 1.25, - "displayName": "Claude 3 Haiku" + "displayName": "Claude 3 Haiku", + "locations": [ + "us-east5", + "asia-southeast1", + "europe-west1" + ] }, "publishers/anthropic/models/claude-3-haiku@20240307": { "format": "anthropic", @@ -5538,7 +5550,12 @@ "multimodal": true, "input_cost_per_mil_tokens": 0.25, "output_cost_per_mil_tokens": 1.25, - "parent": "publishers/anthropic/models/claude-3-haiku" + "parent": "publishers/anthropic/models/claude-3-haiku", + "locations": [ + "us-east5", + "asia-southeast1", + "europe-west1" + ] }, "publishers/meta/models/llama-3.1-401b-instruct-maas": { "format": "openai", @@ -5552,7 +5569,11 @@ "flavor": "chat", "input_cost_per_mil_tokens": 2, "output_cost_per_mil_tokens": 6, - "displayName": "Mistral Large (24.11)" + "displayName": "Mistral Large (24.11)", + "locations": [ + "us-central1", + "europe-west4" + ] }, "publishers/mistralai/models/mistral-nemo": { "format": "openai", @@ -5566,7 +5587,11 @@ "flavor": "chat", "input_cost_per_mil_tokens": 0.3, "output_cost_per_mil_tokens": 0.9, - "displayName": "Codestral (25.01)" + "displayName": "Codestral (25.01)", + "locations": [ + "us-central1", + "europe-west4" + ] }, "publishers/google/models/gemini-2.5-pro": { "format": "google", @@ -5577,6 +5602,25 @@ "displayName": "Gemini 2.5 Pro", "reasoning": true, "reasoning_budget": true, + "locations": [ + "global", + "us-central1", + "us-east5", + "asia-northeast1", + "europe-central2", + "europe-north1", + "europe-southwest1", + "europe-west1", + "europe-west4", + "europe-west8", + "europe-west9", + "northamerica-northeast1", + "us-east1", + "us-east4", + "us-south1", + "us-west1", + "us-west4" + ], "max_input_tokens": 1048576, "max_output_tokens": 65535 }, @@ -5585,14 +5629,52 @@ "flavor": "chat", "multimodal": true, "deprecated": true, - "parent": "publishers/google/models/gemini-2.5-pro" + "parent": "publishers/google/models/gemini-2.5-pro", + "locations": [ + "global", + "us-central1", + "us-east5", + "asia-northeast1", + "europe-central2", + "europe-north1", + "europe-southwest1", + "europe-west1", + "europe-west4", + "europe-west8", + "europe-west9", + "northamerica-northeast1", + "us-east1", + "us-east4", + "us-south1", + "us-west1", + "us-west4" + ] }, "publishers/google/models/gemini-2.5-pro-preview-03-25": { "format": "google", "flavor": "chat", "multimodal": true, "deprecated": true, - "parent": "publishers/google/models/gemini-2.5-pro" + "parent": "publishers/google/models/gemini-2.5-pro", + "locations": [ + "global", + "us-central1", + "us-east5", + "asia-northeast1", + "europe-central2", + "europe-north1", + "europe-southwest1", + "europe-west1", + "europe-west4", + "europe-west8", + "europe-west9", + "northamerica-northeast1", + "us-east1", + "us-east4", + "us-south1", + "us-west1", + "us-west4" + ] }, "publishers/google/models/gemini-2.5-pro-exp-03-25": { "format": "google", @@ -5600,7 +5682,26 @@ "multimodal": true, "displayName": "Gemini 2.5 Pro Experimental", "experimental": true, - "parent": "publishers/google/models/gemini-2.5-pro" + "parent": "publishers/google/models/gemini-2.5-pro", + "locations": [ + "global", + "us-central1", + "us-east5", + "asia-northeast1", + "europe-central2", + "europe-north1", + "europe-southwest1", + "europe-west1", + "europe-west4", + "europe-west8", + "europe-west9", + "northamerica-northeast1", + "us-east1", + "us-east4", + "us-south1", + "us-west1", + "us-west4" + ] }, "publishers/google/models/gemini-2.5-flash": { "format": "google", @@ -5611,6 +5712,32 @@ "displayName": "Gemini 2.5 Flash", "reasoning": true, "reasoning_budget": true, + "locations": [ + "global", + "us-central1", + "us-east5", + "asia-northeast1", + "asia-northeast3", + "asia-south1", + "asia-southeast1", + "australia-southeast1", + "europe-central2", + "europe-north1", + "europe-southwest1", + "europe-west1", + "europe-west2", + "europe-west3", + "europe-west4", + "europe-west8", + "europe-west9", + "northamerica-northeast1", + "southamerica-east1", + "us-east1", + "us-east4", + "us-south1", + "us-west1", + "us-west4" + ], "max_input_tokens": 1048576, "max_output_tokens": 65535 }, @@ -5620,7 +5747,10 @@ "multimodal": true, "reasoning": true, "reasoning_budget": true, - "parent": "publishers/google/models/gemini-2.5-flash" + "parent": "publishers/google/models/gemini-2.5-flash", + "locations": [ + "global" + ] }, "publishers/google/models/gemini-2.5-flash-preview-05-20": { "format": "google", @@ -5629,7 +5759,33 @@ "reasoning": true, "reasoning_budget": true, "deprecated": true, - "parent": "publishers/google/models/gemini-2.5-flash" + "parent": "publishers/google/models/gemini-2.5-flash", + "locations": [ + "global", + "us-central1", + "us-east5", + "asia-northeast1", + "asia-northeast3", + "asia-south1", + "asia-southeast1", + "australia-southeast1", + "europe-central2", + "europe-north1", + "europe-southwest1", + "europe-west1", + "europe-west2", + "europe-west3", + "europe-west4", + "europe-west8", + "europe-west9", + "northamerica-northeast1", + "southamerica-east1", + "us-east1", + "us-east4", + "us-south1", + "us-west1", + "us-west4" + ] }, "publishers/google/models/gemini-2.5-flash-preview-04-17": { "format": "google", @@ -5640,7 +5796,33 @@ "reasoning": true, "reasoning_budget": true, "deprecated": true, - "parent": "publishers/google/models/gemini-2.5-flash" + "parent": "publishers/google/models/gemini-2.5-flash", + "locations": [ + "global", + "us-central1", + "us-east5", + "asia-northeast1", + "asia-northeast3", + "asia-south1", + "asia-southeast1", + "australia-southeast1", + "europe-central2", + "europe-north1", + "europe-southwest1", + "europe-west1", + "europe-west2", + "europe-west3", + "europe-west4", + "europe-west8", + "europe-west9", + "northamerica-northeast1", + "southamerica-east1", + "us-east1", + "us-east4", + "us-south1", + "us-west1", + "us-west4" + ] }, "publishers/google/models/gemini-2.5-flash-lite-preview-09-2025": { "format": "google", @@ -5652,6 +5834,9 @@ "reasoning_budget": true, "experimental": true, "parent": "publishers/google/models/gemini-2.5-flash-lite", + "locations": [ + "global" + ], "max_input_tokens": 1048576, "max_output_tokens": 65535 }, @@ -5666,6 +5851,23 @@ "experimental": true, "deprecated": true, "parent": "publishers/google/models/gemini-2.5-flash-lite", + "locations": [ + "global", + "us-central1", + "us-east5", + "europe-central2", + "europe-north1", + "europe-southwest1", + "europe-west1", + "europe-west4", + "europe-west8", + "europe-west9", + "us-east1", + "us-east4", + "us-south1", + "us-west1", + "us-west4" + ], "max_input_tokens": 1048576, "max_output_tokens": 65535 }, @@ -5679,6 +5881,23 @@ "displayName": "Gemini 2.5 Flash-Lite", "reasoning": true, "reasoning_budget": true, + "locations": [ + "global", + "us-central1", + "us-east5", + "europe-central2", + "europe-north1", + "europe-southwest1", + "europe-west1", + "europe-west4", + "europe-west8", + "europe-west9", + "us-east1", + "us-east4", + "us-south1", + "us-west1", + "us-west4" + ], "max_input_tokens": 1048576, "max_output_tokens": 65535 }, @@ -5695,7 +5914,10 @@ "format": "openai", "flavor": "chat", "displayName": "Llama 3.3 70B Instruct", - "experimental": true + "experimental": true, + "locations": [ + "us-central1" + ] }, "publishers/meta/models/llama-3.2-90b-vision-instruct-maas": { "format": "openai", diff --git a/packages/proxy/schema/models.ts b/packages/proxy/schema/models.ts index 89138f51..af1946d9 100644 --- a/packages/proxy/schema/models.ts +++ b/packages/proxy/schema/models.ts @@ -80,8 +80,7 @@ export const ModelSchema = z.object({ .describe("Date after which the model will be treated as deprecated"), parent: z.string().nullish().describe("The model was replaced this model."), endpoint_types: z.array(z.enum(ModelEndpointType)).nullish(), - locations: z.array(z.string()).nullish(), - supported_regions: z + locations: z .array(z.string()) .nullish() .describe("Documented supported regions for the model on the provider."), diff --git a/packages/proxy/scripts/sync_models.ts b/packages/proxy/scripts/sync_models.ts index 05dbfef5..e4d723fe 100644 --- a/packages/proxy/scripts/sync_models.ts +++ b/packages/proxy/scripts/sync_models.ts @@ -1295,7 +1295,7 @@ async function updateModelsCommand(argv: any) { ? supportedRegions.join(", ") : "(cleared)"; console.log( - ` ${argv.write ? "[WRITE]" : "[DRY RUN]"} Updating supported_regions for ${modelName}: ${regions}`, + ` ${argv.write ? "[WRITE]" : "[DRY RUN]"} Updating locations for ${modelName}: ${regions}`, ); } } @@ -1529,7 +1529,7 @@ async function addModelsCommand(argv: any) { ? supportedRegions.join(", ") : "(cleared)"; console.log( - ` ${argv.write ? "[WRITE]" : "[DRY RUN]"} Updating supported_regions for ${modelName}: ${regions}`, + ` ${argv.write ? "[WRITE]" : "[DRY RUN]"} Updating locations for ${modelName}: ${regions}`, ); } } diff --git a/packages/proxy/scripts/sync_vertex_regions.ts b/packages/proxy/scripts/sync_vertex_regions.ts index c646ddc0..24c47988 100644 --- a/packages/proxy/scripts/sync_vertex_regions.ts +++ b/packages/proxy/scripts/sync_vertex_regions.ts @@ -4,15 +4,18 @@ import { ModelSpec } from "../schema/models"; export const GOOGLE_VERTEX_LOCATIONS_URL = "https://docs.cloud.google.com/vertex-ai/generative-ai/docs/learn/locations"; -// Puts "global" first, then sorts the rest alphabetically +// Sorts alphabetically, but prioritizes "global", "us-central1", then +// "us-east5". function sortRegionsDeterministically(regions: string[]): string[] { const uniqueRegions = Array.from(new Set(regions)); return uniqueRegions.sort((a, b) => { - if (a === "global" && b !== "global") { - return -1; - } - if (a !== "global" && b === "global") { - return 1; + for (const region of ["global", "us-central1", "us-east5"]) { + if (a === region && b !== region) { + return -1; + } + if (a !== region && b === region) { + return 1; + } } return a.localeCompare(b); }); @@ -44,52 +47,157 @@ function stripPublisherGooglePrefix(modelName: string): string { return modelName.replace(/^publishers\/google\/models\//, ""); } -function isVertexGoogleModel(modelName: string, model?: ModelSpec): boolean { - const normalizedName = stripPublisherGooglePrefix(modelName); - const isGoogleModel = normalizedName.startsWith("gemini"); - const hasVertexProvider = - model?.available_providers?.includes("vertex") || - model?.endpoint_types?.includes("vertex"); - return isGoogleModel && !!hasVertexProvider; +function isPublisherModel(modelName: string): boolean { + return modelName.startsWith("publishers/"); } -export function parseVertexSupportedRegionsFromLocationsPage( - html: string, -): Map { - const googleModelsStart = html.indexOf('

, + key: string, + regions: string[], +) { + const existing = map.get(key) ?? []; + map.set(key, sortRegionsDeterministically([...existing, ...regions])); +} + +/** + * Parse one section of the page (a geographic tab) whose rows have model IDs + * in tags, calling keyFn to convert the raw code text to a map key. + * Rows where keyFn returns null are skipped. + */ +function parseSectionWithCodeTags( + tableHtml: string, + regionCodes: string[], + keyFn: (codeText: string) => string | null, + out: Map, +) { + for (const rowMatch of tableHtml.matchAll(/[\s\S]*?<\/tr>/g)) { + const rowHtml = rowMatch[0]; + const codeText = rowHtml.match(/]*>\(([^)<]+)\)<\/code>/)?.[1]; + if (!codeText) { + continue; + } + const key = keyFn(codeText); + if (!key) { + continue; + } + const cells = Array.from(rowHtml.matchAll(//g)).slice(1); + const supported = cells.flatMap((cellMatch, index) => { + const cellHtml = cellMatch[0]; + return cellHtml.includes('aria-label="Supported"') || + cellHtml.includes("compare-yes") + ? [regionCodes[index]] + : []; + }); + if (supported.length > 0) { + addRegions(out, key, supported); + } } +} - const partnerModelsStart = html.indexOf( - '

tags), matching via displayNameToModelId. + */ +function parseSectionWithDisplayNames( + tableHtml: string, + regionCodes: string[], + displayNameToModelId: ReadonlyMap, + out: Map, +) { + for (const rowMatch of tableHtml.matchAll(/[\s\S]*?<\/tr>/g)) { + const rowHtml = rowMatch[0]; + if (rowHtml.includes("vertex-ai-table-heading")) { + continue; + } + const firstTd = rowHtml.match(/([\s\S]*?)<\/td>/); + if (!firstTd) { + continue; + } + const displayName = firstTd[1].replace(/<[^>]+>/g, "").trim(); + const modelId = displayNameToModelId.get(displayName); + if (!modelId) { + continue; + } + const cells = Array.from(rowHtml.matchAll(//g)).slice(1); + const supported = cells.flatMap((cellMatch, index) => { + const cellHtml = cellMatch[0]; + return cellHtml.includes('aria-label="Supported"') || + cellHtml.includes("compare-yes") + ? [regionCodes[index]] + : []; + }); + if (supported.length > 0) { + addRegions(out, modelId, supported); + } + } +} - const modelRegions = new Map(); - const sectionRegex = - /
]*>([\s\S]*?)<\/h3>([\s\S]*?)<\/section>/g; - for (const sectionMatch of sectionHtml.matchAll(sectionRegex)) { - const tableHtml = sectionMatch[2].match(/[\s\S]*?<\/table>/)?.[0]; +/** + * Iterate over every geographic-tab
within an h2 block and call + * the appropriate parse function for each table. + */ +function parseH2Block( + html: string, + h2Id: string, + callback: (tableHtml: string, regionCodes: string[]) => void, +) { + const blockStart = html.indexOf(`

]*>[\s\S]*?<\/h3>([\s\S]*?)<\/section>/g, + )) { + const tableHtml = sectionMatch[1].match(/

[\s\S]*?<\/table>/)?.[0]; if (!tableHtml) { continue; } - const regionCodes = Array.from( tableHtml.matchAll( /[\s\S]*?<\/tr>/g)) { const rowHtml = rowMatch[0]; @@ -103,50 +211,102 @@ export function parseVertexSupportedRegionsFromLocationsPage( if (!inGeminiSection) { continue; } - const modelName = rowHtml.match( /]*>\((gemini[^)<]*)\)<\/code>/, )?.[1]; if (!modelName) { continue; } - const cells = Array.from(rowHtml.matchAll(//g)).slice( 1, ); - if (cells.length === 0) { - continue; - } - - const supportedRegions = cells.flatMap((cellMatch, index) => { + const supported = cells.flatMap((cellMatch, index) => { const cellHtml = cellMatch[0]; return cellHtml.includes('aria-label="Supported"') || cellHtml.includes("compare-yes") ? [regionCodes[index]] : []; }); - - if (supportedRegions.length > 0) { - const existingRegions = modelRegions.get(modelName) ?? []; - modelRegions.set( - modelName, - sortRegionsDeterministically([ - ...existingRegions, - ...supportedRegions, - ]), - ); + if (supported.length > 0) { + addRegions(modelRegions, modelName, supported); } } - } + }); + + // --- Open models: tags → keyed by slug; display-name rows → via map --- + parseH2Block(html, "genai-open-models", (tableHtml, regionCodes) => { + parseSectionWithCodeTags( + tableHtml, + regionCodes, + (slug) => slug, + modelRegions, + ); + parseSectionWithDisplayNames( + tableHtml, + regionCodes, + displayNameToModelId, + modelRegions, + ); + }); + + // --- Partner models (Anthropic, Mistral): display names only --- + parseH2Block(html, "genai-partner-models", (tableHtml, regionCodes) => { + parseSectionWithDisplayNames( + tableHtml, + regionCodes, + displayNameToModelId, + modelRegions, + ); + }); return modelRegions; } +/** + * Maps the display name used on the Vertex AI locations page to the + * corresponding publisher model ID in model_list.json. + * + * Used for models in the partner/open sections that lack model ID tags. + * Update this map whenever new partner models are added to the page. + */ +export const PARTNER_MODEL_PAGE_NAME_TO_ID: ReadonlyMap = + new Map([ + // Anthropic + ["Claude Opus 4.6", "publishers/anthropic/models/claude-opus-4-6"], + ["Claude Opus 4.5", "publishers/anthropic/models/claude-opus-4-5@20251101"], + ["Claude Opus 4.1", "publishers/anthropic/models/claude-opus-4-1@20250805"], + ["Claude Opus 4", "publishers/anthropic/models/claude-opus-4"], + ["Claude Sonnet 4.6", "publishers/anthropic/models/claude-sonnet-4-6"], + ["Claude Sonnet 4.5", "publishers/anthropic/models/claude-sonnet-4-5"], + ["Claude Sonnet 4", "publishers/anthropic/models/claude-sonnet-4"], + ["Claude Haiku 4.5", "publishers/anthropic/models/claude-haiku-4-5"], + [ + "Anthropic's Claude 3.7 Sonnet", + "publishers/anthropic/models/claude-3-7-sonnet", + ], + [ + "Anthropic's Claude 3.5 Haiku", + "publishers/anthropic/models/claude-3-5-haiku", + ], + [ + "Anthropic's Claude 3 Haiku (deprecated)", + "publishers/anthropic/models/claude-3-haiku", + ], + // Mistral + ["Mistral Large (24.07)", "publishers/mistralai/models/mistral-large-2411"], + ["Codestral 2", "publishers/mistralai/models/codestral-2501"], + // Llama + ["Llama 3.3 70B", "publishers/meta/models/llama-3.3-70b-instruct-maas"], + ]); + export async function fetchVertexSupportedRegions(): Promise< Map > { const html = await fetchText(GOOGLE_VERTEX_LOCATIONS_URL); - return parseVertexSupportedRegionsFromLocationsPage(html); + return parseVertexSupportedRegionsFromLocationsPage( + html, + PARTNER_MODEL_PAGE_NAME_TO_ID, + ); } export function syncVertexSupportedRegions>( @@ -155,31 +315,109 @@ export function syncVertexSupportedRegions>( ): Map { const updates = new Map(); + // Step 1: build a slug→fullKey index for open models + // (e.g. "qwen3-235b-a22b-instruct-2507-maas" → "publishers/qwen/models/qwen3-235b-a22b-instruct-2507-maas") + const slugToPublisherKey = new Map(); + for (const modelName of Object.keys(localModels)) { + if (!isPublisherModel(modelName)) { + continue; + } + const slug = modelName.split("/models/")[1]; + if (slug) { + slugToPublisherKey.set(slug, modelName); + } + } + + // Step 2: resolve direct scraped entries for all publisher models. + // - Gemini models: lookup key is short name (e.g. "gemini-2.5-pro") + // - Partner models (Anthropic/Mistral/Llama): lookup key is full publisher ID + // - Open models (Qwen/Kimi/etc.): lookup key is the slug + const resolvedLocations = new Map(); + for (const modelName of Object.keys(localModels)) { + if (!isPublisherModel(modelName)) { + continue; + } + let lookupKey: string; + if (modelName.startsWith("publishers/google/models/gemini")) { + lookupKey = stripPublisherGooglePrefix(modelName); + } else { + lookupKey = modelName; + } + // Try direct lookup first, then slug lookup for open models + const regions = + supportedRegionsByModel.get(lookupKey) ?? + supportedRegionsByModel.get(modelName.split("/models/")[1] ?? ""); + if (regions) { + resolvedLocations.set(modelName, sortRegionsDeterministically(regions)); + } + } + + // Step 3: propagate to versioned children (e.g. claude-opus-4@20250514). + // Done as a separate pass so inheritance is order-independent with respect + // to JSON key order. + for (const [modelName, model] of Object.entries(localModels)) { + if ( + !isPublisherModel(modelName) || + resolvedLocations.has(modelName) || + !model.parent + ) { + continue; + } + const parentRegions = resolvedLocations.get(model.parent); + if (parentRegions) { + resolvedLocations.set(modelName, parentRegions); + } + } + + // Step 4: determine which models are "scraper-owned" so we don't accidentally + // clear static locations on models the scraper doesn't know about. + const scrapedModelIds = new Set(); + // Gemini models covered by the scraper + for (const key of supportedRegionsByModel.keys()) { + if (key.startsWith("gemini")) { + scrapedModelIds.add(`publishers/google/models/${key}`); + } + } + // Partner/open models covered by explicit mapping or slug lookup + for (const modelId of PARTNER_MODEL_PAGE_NAME_TO_ID.values()) { + scrapedModelIds.add(modelId); + } + for (const [slug, fullKey] of slugToPublisherKey) { + if (supportedRegionsByModel.has(slug)) { + scrapedModelIds.add(fullKey); + } + } + // Versioned children of scraped models + for (const [modelName, model] of Object.entries(localModels)) { + if (model.parent && scrapedModelIds.has(model.parent)) { + scrapedModelIds.add(modelName); + } + } + + // Step 5: apply updates. for (const [modelName, model] of Object.entries(localModels)) { - if (!isVertexGoogleModel(modelName, model)) { + if (!isPublisherModel(modelName)) { continue; } - const normalizedName = stripPublisherGooglePrefix(modelName); - const supportedRegions = supportedRegionsByModel.get(normalizedName); - const currentRegions = model.supported_regions; + const nextRegions = resolvedLocations.get(modelName); + const currentRegions = model.locations; - if (!supportedRegions) { - if (currentRegions) { - delete (model as Partial).supported_regions; + if (!nextRegions) { + if (currentRegions && scrapedModelIds.has(modelName)) { + delete (model as Partial).locations; updates.set(modelName, []); } continue; } - const nextRegions = sortRegionsDeterministically(supportedRegions); const same = Array.isArray(currentRegions) && currentRegions.length === nextRegions.length && currentRegions.every((region, index) => region === nextRegions[index]); if (!same) { - model.supported_regions = nextRegions; + model.locations = nextRegions; updates.set(modelName, nextRegions); } } diff --git a/packages/proxy/src/providers/anthropic.test.ts b/packages/proxy/src/providers/anthropic.test.ts index 0dd33b2d..cfa41c41 100644 --- a/packages/proxy/src/providers/anthropic.test.ts +++ b/packages/proxy/src/providers/anthropic.test.ts @@ -649,9 +649,10 @@ it("should use model's max_output_tokens as default when max_tokens not specifie it("should default Vertex Anthropic calls to us-east5 when location is omitted", async () => { const { fetch, requests } = createCapturingFetch({ captureOnly: true }); + // Use a model with no locations set so the default location is used. await callProxyV1({ body: { - model: "publishers/anthropic/models/claude-sonnet-4", + model: "publishers/anthropic/models/claude-3-5-sonnet", messages: [{ role: "user", content: "Hello" }], stream: false, }, @@ -679,9 +680,10 @@ it("should default Vertex Anthropic calls to us-east5 when location is omitted", it("should honor Vertex metadata location for Anthropic calls", async () => { const { fetch, requests } = createCapturingFetch({ captureOnly: true }); + // Use a model with no locations set so metadata.location is respected. await callProxyV1({ body: { - model: "publishers/anthropic/models/claude-sonnet-4", + model: "publishers/anthropic/models/claude-3-5-sonnet", messages: [{ role: "user", content: "Hello" }], stream: false, },
[\s\S]*?\(([a-z0-9-]+)\)<\/th>/g, ), - (match) => match[1], + (m) => m[1], ); - if (regionCodes.length === 0) { continue; } + callback(tableHtml, regionCodes); + } +} + +/** + * Parse region support from the Google Vertex AI locations page. + * + * Returns a map keyed by: + * - Short gemini model name (e.g. "gemini-2.5-pro") for Google models + * (genai-google-models section, matched via tags). + * - Model slug (e.g. "qwen3-235b-a22b-instruct-2507-maas") for open models + * (genai-open-models section, matched via tags). + * - Full publisher model ID (e.g. "publishers/anthropic/models/claude-opus-4") + * for partner/open models that lack tags (Anthropic, Mistral, Llama), + * resolved via displayNameToModelId. + */ +export function parseVertexSupportedRegionsFromLocationsPage( + html: string, + displayNameToModelId: ReadonlyMap = new Map(), +): Map { + const modelRegions = new Map(); + if (html.indexOf('

{ let inGeminiSection = false; for (const rowMatch of tableHtml.matchAll(/