fix(ai): forward google vertex base url

closes #3619
This commit is contained in:
Mario Zechner
2026-04-24 00:05:43 +02:00
parent 95f4921250
commit 65a6472bdd
3 changed files with 124 additions and 18 deletions

View File

@@ -5,9 +5,11 @@
### Added
- Added GPT-5.5 to OpenAI Codex model generation.
- Added `findEnvKeys()` so callers can identify configured provider API-key environment variables without exposing credential values while preserving `getEnvApiKey()` as the credential-value API.
### Fixed
- Fixed `google-vertex` to forward custom `model.baseUrl` values to `@google/genai`, enabling Vertex proxy and gateway endpoints ([#3619](https://github.com/badlogic/pi-mono/issues/3619))
- Fixed OpenAI-compatible completion usage parsing to stop double-counting reasoning tokens already included in `completion_tokens` ([#3581](https://github.com/badlogic/pi-mono/issues/3581))
- Fixed long cache retention compatibility by adding `compat.supportsLongCacheRetention`, allowing Anthropic Messages and OpenAI-compatible proxies to explicitly disable long-retention fields while enabling long retention by default when requested ([#3543](https://github.com/badlogic/pi-mono/issues/3543))
- Fixed `openai-responses` compatibility by adding `compat.sendSessionIdHeader: false`, allowing strict OpenAI-compatible proxies to omit the underscore-containing `session_id` header while still sending other session-affinity headers ([#3579](https://github.com/badlogic/pi-mono/issues/3579))
@@ -16,6 +18,7 @@
- Fixed `openai-completions` streamed tool-call assembly to coalesce deltas by stable tool index when OpenAI-compatible gateways mutate tool call IDs mid-stream, preventing malformed Kimi K2.6/OpenCode tool streams from splitting one call into multiple bogus tool calls ([#3576](https://github.com/badlogic/pi-mono/issues/3576))
- Fixed `packages/ai` E2E coverage to use currently supported OpenAI Responses and OpenAI Codex models, and updated the Bedrock adaptive-thinking payload expectation to match the current `display: "summarized"` shape.
- Fixed built-in `kimi-coding` model generation to attach `User-Agent: KimiCLI/1.5` to all generated Kimi models, overriding the Anthropic SDK default UA so direct Kimi Coding requests use the provider's expected client identity ([#3586](https://github.com/badlogic/pi-mono/issues/3586))
- Fixed GPT-5.5 Codex capability handling to clamp unsupported minimal reasoning to `low` and apply the model's 2.5x priority service-tier pricing multiplier ([#3618](https://github.com/badlogic/pi-mono/pull/3618) by [@markusylisiurunen](https://github.com/markusylisiurunen))
## [0.69.0] - 2026-04-22

View File

@@ -2,6 +2,8 @@ import {
type GenerateContentConfig,
type GenerateContentParameters,
GoogleGenAI,
type HttpOptions,
ResourceScope,
type ThinkingConfig,
ThinkingLevel,
} from "@google/genai";
@@ -331,20 +333,12 @@ function createClient(
location: string,
optionsHeaders?: Record<string, string>,
): GoogleGenAI {
const httpOptions: { headers?: Record<string, string> } = {};
if (model.headers || optionsHeaders) {
httpOptions.headers = { ...model.headers, ...optionsHeaders };
}
const hasHttpOptions = Object.values(httpOptions).some(Boolean);
return new GoogleGenAI({
vertexai: true,
project,
location,
apiVersion: API_VERSION,
httpOptions: hasHttpOptions ? httpOptions : undefined,
httpOptions: buildHttpOptions(model, optionsHeaders),
});
}
@@ -353,20 +347,50 @@ function createClientWithApiKey(
apiKey: string,
optionsHeaders?: Record<string, string>,
): GoogleGenAI {
const httpOptions: { headers?: Record<string, string> } = {};
return new GoogleGenAI({
vertexai: true,
apiKey,
apiVersion: API_VERSION,
httpOptions: buildHttpOptions(model, optionsHeaders),
});
}
function buildHttpOptions(
model: Model<"google-vertex">,
optionsHeaders?: Record<string, string>,
): HttpOptions | undefined {
const httpOptions: HttpOptions = {};
const baseUrl = resolveCustomBaseUrl(model.baseUrl);
if (baseUrl) {
httpOptions.baseUrl = baseUrl;
httpOptions.baseUrlResourceScope = ResourceScope.COLLECTION;
if (baseUrlIncludesApiVersion(baseUrl)) {
httpOptions.apiVersion = "";
}
}
if (model.headers || optionsHeaders) {
httpOptions.headers = { ...model.headers, ...optionsHeaders };
}
const hasHttpOptions = Object.values(httpOptions).some(Boolean);
return Object.keys(httpOptions).length > 0 ? httpOptions : undefined;
}
return new GoogleGenAI({
vertexai: true,
apiKey,
apiVersion: API_VERSION,
httpOptions: hasHttpOptions ? httpOptions : undefined,
});
function resolveCustomBaseUrl(baseUrl: string): string | undefined {
const trimmed = baseUrl.trim();
if (!trimmed || trimmed.includes("{location}")) {
return undefined;
}
return trimmed;
}
function baseUrlIncludesApiVersion(baseUrl: string): boolean {
try {
const url = new URL(baseUrl);
return url.pathname.split("/").some((part) => /^v\d+(?:beta\d*)?$/.test(part));
} catch {
return /(?:^|\/)v\d+(?:beta\d*)?(?:\/|$)/.test(baseUrl);
}
}
function resolveApiKey(options?: GoogleVertexOptions): string | undefined {

View File

@@ -32,6 +32,9 @@ vi.mock("@google/genai", () => {
return {
GoogleGenAI,
ResourceScope: {
COLLECTION: "COLLECTION",
},
ThinkingLevel: {
THINKING_LEVEL_UNSPECIFIED: "THINKING_LEVEL_UNSPECIFIED",
MINIMAL: "MINIMAL",
@@ -44,7 +47,7 @@ vi.mock("@google/genai", () => {
import { getModel } from "../src/models.js";
import { streamGoogleVertex } from "../src/providers/google-vertex.js";
import type { Context } from "../src/types.js";
import type { Context, Model } from "../src/types.js";
const model = getModel("google-vertex", "gemini-3-flash-preview");
const context: Context = {
@@ -141,4 +144,80 @@ describe("google-vertex api key resolution", () => {
expect(googleGenAiMock.constructorCalls[0]).not.toHaveProperty("project");
expect(googleGenAiMock.constructorCalls[0]).not.toHaveProperty("location");
});
it("does not forward generated Vertex base URL placeholders", async () => {
const stream = streamGoogleVertex(model, context, {
project: "test-project",
location: "us-central1",
});
await stream.result();
expect(googleGenAiMock.constructorCalls).toHaveLength(1);
expect(googleGenAiMock.constructorCalls[0]?.httpOptions).toBeUndefined();
});
it("forwards custom baseUrl to the ADC client", async () => {
const customModel: Model<"google-vertex"> = { ...model, baseUrl: "https://proxy.example.com" };
const stream = streamGoogleVertex(customModel, context, {
project: "test-project",
location: "us-central1",
});
await stream.result();
expect(googleGenAiMock.constructorCalls).toHaveLength(1);
expect(googleGenAiMock.constructorCalls[0]).toMatchObject({
vertexai: true,
project: "test-project",
location: "us-central1",
apiVersion: "v1",
httpOptions: {
baseUrl: "https://proxy.example.com",
baseUrlResourceScope: "COLLECTION",
},
});
});
it("forwards custom baseUrl to the API key client", async () => {
const customModel: Model<"google-vertex"> = { ...model, baseUrl: "https://proxy.example.com" };
const stream = streamGoogleVertex(customModel, context, {
apiKey: "AIzaSyExampleRealisticLookingApiKey123456",
});
await stream.result();
expect(googleGenAiMock.constructorCalls).toHaveLength(1);
expect(googleGenAiMock.constructorCalls[0]).toMatchObject({
vertexai: true,
apiKey: "AIzaSyExampleRealisticLookingApiKey123456",
apiVersion: "v1",
httpOptions: {
baseUrl: "https://proxy.example.com",
baseUrlResourceScope: "COLLECTION",
},
});
});
it("does not append apiVersion when custom baseUrl already includes one", async () => {
const customModel: Model<"google-vertex"> = {
...model,
baseUrl: "https://proxy.example.com/v1/projects/test-project/locations/global",
};
const stream = streamGoogleVertex(customModel, context, {
project: "test-project",
location: "us-central1",
});
await stream.result();
expect(googleGenAiMock.constructorCalls).toHaveLength(1);
expect(googleGenAiMock.constructorCalls[0]).toMatchObject({
httpOptions: {
baseUrl: "https://proxy.example.com/v1/projects/test-project/locations/global",
baseUrlResourceScope: "COLLECTION",
apiVersion: "",
},
});
});
});