From ff1ea1232459dd0956d130e5593c5bb3bc1d223c Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 18 Mar 2026 22:51:14 +0100 Subject: [PATCH] fix(ai): ignore placeholder vertex api keys closes #2335 --- packages/ai/CHANGELOG.md | 4 + packages/ai/src/providers/google-vertex.ts | 10 +- .../google-vertex-api-key-resolution.test.ts | 125 ++++++++++++++++++ 3 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 packages/ai/test/google-vertex-api-key-resolution.test.ts diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index e3ee6a8a..837c8357 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -6,6 +6,10 @@ - Added `gpt-5.4-mini` model support for the `openai-codex` provider with Codex pricing metadata and unit coverage. +### Fixed + +- Fixed `google-vertex` API key resolution to ignore placeholder auth markers like `` and fall back to ADC instead of sending them as literal API keys ([#2335](https://github.com/badlogic/pi-mono/issues/2335)) + ## [0.60.0] - 2026-03-18 ### Fixed diff --git a/packages/ai/src/providers/google-vertex.ts b/packages/ai/src/providers/google-vertex.ts index a56aae36..99bd57ba 100644 --- a/packages/ai/src/providers/google-vertex.ts +++ b/packages/ai/src/providers/google-vertex.ts @@ -368,7 +368,15 @@ function createClientWithApiKey( } function resolveApiKey(options?: GoogleVertexOptions): string | undefined { - return options?.apiKey || process.env.GOOGLE_CLOUD_API_KEY; + const apiKey = options?.apiKey?.trim() || process.env.GOOGLE_CLOUD_API_KEY?.trim(); + if (!apiKey || isPlaceholderApiKey(apiKey)) { + return undefined; + } + return apiKey; +} + +function isPlaceholderApiKey(apiKey: string): boolean { + return /^<[^>]+>$/.test(apiKey); } function resolveProject(options?: GoogleVertexOptions): string { diff --git a/packages/ai/test/google-vertex-api-key-resolution.test.ts b/packages/ai/test/google-vertex-api-key-resolution.test.ts new file mode 100644 index 00000000..38e0fac4 --- /dev/null +++ b/packages/ai/test/google-vertex-api-key-resolution.test.ts @@ -0,0 +1,125 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const googleGenAiMock = vi.hoisted(() => ({ + constructorCalls: [] as Array>, +})); + +vi.mock("@google/genai", () => { + class GoogleGenAI { + models = { + generateContentStream: async function* () { + yield { + responseId: "vertex-response-id", + candidates: [ + { + content: { parts: [{ text: "ok" }] }, + finishReason: "STOP", + }, + ], + usageMetadata: { + promptTokenCount: 1, + candidatesTokenCount: 1, + totalTokenCount: 2, + }, + }; + }, + }; + + constructor(config: Record) { + googleGenAiMock.constructorCalls.push(config); + } + } + + return { + GoogleGenAI, + ThinkingLevel: { + THINKING_LEVEL_UNSPECIFIED: "THINKING_LEVEL_UNSPECIFIED", + MINIMAL: "MINIMAL", + LOW: "LOW", + MEDIUM: "MEDIUM", + HIGH: "HIGH", + }, + }; +}); + +import { getModel } from "../src/models.js"; +import { streamGoogleVertex } from "../src/providers/google-vertex.js"; +import type { Context } from "../src/types.js"; + +const model = getModel("google-vertex", "gemini-3-flash-preview"); +const context: Context = { + messages: [{ role: "user", content: "hello", timestamp: Date.now() }], +}; + +const originalGoogleCloudApiKey = process.env.GOOGLE_CLOUD_API_KEY; + +beforeEach(() => { + googleGenAiMock.constructorCalls.length = 0; + delete process.env.GOOGLE_CLOUD_API_KEY; +}); + +afterEach(() => { + if (originalGoogleCloudApiKey === undefined) { + delete process.env.GOOGLE_CLOUD_API_KEY; + } else { + process.env.GOOGLE_CLOUD_API_KEY = originalGoogleCloudApiKey; + } +}); + +describe("google-vertex api key resolution", () => { + it("falls back to ADC when options.apiKey is a placeholder marker", async () => { + const stream = streamGoogleVertex(model, context, { + apiKey: "", + 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", + }); + expect(googleGenAiMock.constructorCalls[0]).not.toHaveProperty("apiKey"); + }); + + it("falls back to ADC when GOOGLE_CLOUD_API_KEY is a placeholder marker", async () => { + process.env.GOOGLE_CLOUD_API_KEY = ""; + + const stream = streamGoogleVertex(model, 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", + }); + expect(googleGenAiMock.constructorCalls[0]).not.toHaveProperty("apiKey"); + }); + + it("still uses the API key client for real API keys", async () => { + const stream = streamGoogleVertex(model, context, { + apiKey: "AIzaSyExampleRealisticLookingApiKey123456", + }); + + await stream.result(); + + expect(googleGenAiMock.constructorCalls).toHaveLength(1); + expect(googleGenAiMock.constructorCalls[0]).toMatchObject({ + vertexai: true, + apiKey: "AIzaSyExampleRealisticLookingApiKey123456", + apiVersion: "v1", + }); + expect(googleGenAiMock.constructorCalls[0]).not.toHaveProperty("project"); + expect(googleGenAiMock.constructorCalls[0]).not.toHaveProperty("location"); + }); +});