fix: google-vertex support multi-region endpoints

This commit is contained in:
bryanster
2026-07-27 19:36:48 +02:00
parent 2f63a07ba9
commit 8ef9428ec4
5 changed files with 70 additions and 7 deletions
+3 -5
View File
@@ -1,4 +1,5 @@
import { $env } from "@oh-my-pi/pi-utils";
import { resolveVertexEndpointHost } from "@oh-my-pi/pi-catalog/hosts";
import * as AIError from "../error";
import type { Context, Model, StreamFunction } from "../types";
import type { AssistantMessageEventStream } from "../utils/event-stream";
@@ -69,7 +70,7 @@ export const streamGoogleVertex: StreamFunction<"google-vertex"> = (
// global-only request.
const explicitLocation = options?.location;
const location = explicitLocation ?? resolveAmbientLocation() ?? "global";
const host = resolveEndpointHost(location);
const host = resolveVertexEndpointHost(location);
const path = `${API_VERSION}/publishers/google/models/${model.id}:streamGenerateContent?alt=sse`;
const useGlobalFallback = !explicitLocation && host !== "aiplatform.googleapis.com";
return {
@@ -87,7 +88,7 @@ export const streamGoogleVertex: StreamFunction<"google-vertex"> = (
const project = resolveProject(options);
const location = resolveLocation(options);
const accessToken = await getVertexAccessToken({ signal: options?.signal, fetch: options?.fetch });
const host = resolveEndpointHost(location);
const host = resolveVertexEndpointHost(location);
const url = `https://${host}/${API_VERSION}/projects/${project}/locations/${location}/publishers/google/models/${model.id}:streamGenerateContent?alt=sse`;
return {
params,
@@ -117,9 +118,6 @@ function resolveProject(options?: GoogleVertexOptions): string {
return project;
}
function resolveEndpointHost(location: string): string {
return location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
}
function resolveAmbientLocation(): string | undefined {
return $env.GOOGLE_VERTEX_LOCATION || $env.GOOGLE_CLOUD_LOCATION || $env.VERTEX_LOCATION || undefined;
}
+2 -2
View File
@@ -5,7 +5,7 @@ import * as path from "node:path";
import { scheduler } from "node:timers/promises";
import { isOfficialAnthropicApiUrl } from "@oh-my-pi/pi-catalog/compat/anthropic";
import type { Effort } from "@oh-my-pi/pi-catalog/effort";
import { isVertexExpressOpenAIUrl, isVertexRawPredictUrl } from "@oh-my-pi/pi-catalog/hosts";
import { isVertexExpressOpenAIUrl, isVertexRawPredictUrl, resolveVertexEndpointHost } from "@oh-my-pi/pi-catalog/hosts";
import {
mapEffortToAnthropicAdaptiveEffort,
mapEffortToGoogleThinkingLevel,
@@ -662,7 +662,7 @@ function resolveVertexRequest(input: string | URL | Request): string | URL | Req
url.includes("{location}") ||
url.includes("%7Bproject%7D") ||
url.includes("%7Blocation%7D");
const host = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
const host = resolveVertexEndpointHost(location);
const rewritten = hasPlaceholder
? url
.replace("https://{location}-aiplatform.googleapis.com", `https://${host}`)
+38
View File
@@ -62,4 +62,42 @@ describe("issue #1270: Vertex AI global endpoint", () => {
"https://aiplatform.googleapis.com/v1/projects/vertex-project/locations/global/publishers/google/models/gemini-3.1-pro-preview:streamGenerateContent?alt=sse",
);
});
it.each([
{
location: "eu",
host: "aiplatform.eu.rep.googleapis.com",
},
{
location: "us",
host: "aiplatform.us.rep.googleapis.com",
},
{
location: "europe-west4",
host: "europe-west4-aiplatform.googleapis.com",
},
] as const)("uses the $host host for location $location", async ({ location, host }) => {
delete Bun.env.GOOGLE_CLOUD_API_KEY;
delete Bun.env.GOOGLE_APPLICATION_CREDENTIALS;
const urls: string[] = [];
const stream = streamGoogleVertex(model, context, {
project: "vertex-project",
location,
fetch: async input => {
const url = input instanceof Request ? input.url : input.toString();
urls.push(url);
if (url === METADATA_TOKEN_URL || url === OAUTH_TOKEN_URL) {
return new Response(JSON.stringify({ access_token: "token", expires_in: 3600 }));
}
return new Response('{"error":{"message":"stop after capture"}}', { status: 400 });
},
});
await stream.result();
expect(urls).toContain(
`https://${host}/v1/projects/vertex-project/locations/${location}/publishers/google/models/gemini-3.1-pro-preview:streamGenerateContent?alt=sse`,
);
});
});
+18
View File
@@ -111,6 +111,24 @@ function includesAsciiCaseInsensitive(value: string, lowerNeedle: string): boole
// --- Endpoint-shape predicates (URL path/verb shapes, not vendor hosts) ---
/**
* Hostname for a Vertex AI GenerateContent / rawPredict / OpenAI-compat
* request for the given location.
*
* - `global` → global endpoint (`aiplatform.googleapis.com`)
* - `eu` / `us` multi-regions → REP endpoints (`aiplatform.{eu|us}.rep.googleapis.com`)
* - every other location → regional (`{location}-aiplatform.googleapis.com`)
*
* Multi-region codes do NOT follow the regional `{location}-aiplatform` pattern;
* interpolating them that way yields hosts like `eu-aiplatform.googleapis.com`
* that 404.
*/
export function resolveVertexEndpointHost(location: string): string {
if (location === "global") return "aiplatform.googleapis.com";
if (location === "eu" || location === "us") return `aiplatform.${location}.rep.googleapis.com`;
return `${location}-aiplatform.googleapis.com`;
}
/** Vertex AI express-mode OpenAI-compatible endpoint (`…/endpoints/openapi`). */
export function isVertexExpressOpenAIUrl(baseUrl: string): boolean {
return baseUrl.includes("/endpoints/openapi");
+9
View File
@@ -5,6 +5,7 @@ import {
isVertexExpressOpenAIUrl,
isVertexRawPredictUrl,
modelMatchesHost,
resolveVertexEndpointHost,
} from "@oh-my-pi/pi-catalog/hosts";
describe("hostMatchesUrl", () => {
@@ -66,6 +67,14 @@ describe("endpoint shape predicates", () => {
).toBe(true);
});
test("resolves Vertex endpoint hosts for global, multi-region, and regional locations", () => {
expect(resolveVertexEndpointHost("global")).toBe("aiplatform.googleapis.com");
expect(resolveVertexEndpointHost("eu")).toBe("aiplatform.eu.rep.googleapis.com");
expect(resolveVertexEndpointHost("us")).toBe("aiplatform.us.rep.googleapis.com");
expect(resolveVertexEndpointHost("europe-west4")).toBe("europe-west4-aiplatform.googleapis.com");
expect(resolveVertexEndpointHost("us-central1")).toBe("us-central1-aiplatform.googleapis.com");
});
test("requires all DashScope compatible-mode URL markers", () => {
expect(isDashscopeCompatibleModeUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")).toBe(true);
expect(isDashscopeCompatibleModeUrl("https://example.aliyuncs.com/compatible-mode/v1")).toBe(false);