fix: google-vertex support multi-region endpoints
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user