From 1a9763f578085e54e0380337dea2b14825ad350e Mon Sep 17 00:00:00 2001 From: "clawsweeper[bot]" <274271284+clawsweeper[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 22:26:09 -0700 Subject: [PATCH] fix(google): accept Windows ADC manifest paths Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com> --- extensions/google/transport-stream.test.ts | 107 ++++++++++++++++++--- extensions/google/vertex-adc.ts | 17 +++- 2 files changed, 109 insertions(+), 15 deletions(-) diff --git a/extensions/google/transport-stream.test.ts b/extensions/google/transport-stream.test.ts index c72fa6c0365..3682237deb5 100644 --- a/extensions/google/transport-stream.test.ts +++ b/extensions/google/transport-stream.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import type { Model } from "@mariozechner/pi-ai"; @@ -51,6 +51,24 @@ function buildGeminiModel( }; } +function buildGoogleVertexModel( + overrides: Partial> = {}, +): Model<"google-vertex"> { + return { + id: "gemini-3.1-pro-preview", + name: "Gemini 3.1 Pro Preview", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128000, + maxTokens: 8192, + ...overrides, + }; +} + function buildSseResponse(events: unknown[]): Response { const sse = `${events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("")}data: [DONE]\n\n`; const encoder = new TextEncoder(); @@ -302,18 +320,7 @@ describe("google transport stream", () => { expect(hasGoogleVertexAuthorizedUserAdcSync()).toBe(true); - const model = { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128000, - maxTokens: 8192, - } satisfies Model<"google-vertex">; + const model = buildGoogleVertexModel(); const streamFn = createGoogleVertexTransportStreamFn(); const stream = await Promise.resolve( @@ -353,6 +360,80 @@ describe("google transport stream", () => { }); }); + it("refreshes authorized_user ADC from the Windows APPDATA fallback for Google Vertex requests", async () => { + const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-appdata-adc-")); + const homeDir = path.join(tempDir, "home"); + const appDataDir = path.join(tempDir, "AppData", "Roaming"); + const fallbackDir = path.join(appDataDir, "gcloud"); + const credentialsPath = path.join(fallbackDir, "application_default_credentials.json"); + await mkdir(fallbackDir, { recursive: true }); + await writeFile( + credentialsPath, + JSON.stringify({ + type: "authorized_user", + client_id: "client-id", + client_secret: "client-secret", + refresh_token: "appdata-refresh-token", + }), + "utf8", + ); + vi.stubEnv("GOOGLE_APPLICATION_CREDENTIALS", undefined); + vi.stubEnv("HOME", homeDir); + vi.stubEnv("APPDATA", appDataDir); + vi.stubEnv("GOOGLE_CLOUD_PROJECT", "vertex-project"); + vi.stubEnv("GOOGLE_CLOUD_LOCATION", "global"); + const tokenFetchMock = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ access_token: "ya29.appdata-token", expires_in: 3600 }), { + status: 200, + headers: { "content-type": "application/json" }, + }), + ); + guardedFetchMock.mockResolvedValueOnce( + buildSseResponse([ + { + candidates: [{ content: { parts: [{ text: "ok" }] }, finishReason: "STOP" }], + }, + ]), + ); + + expect(hasGoogleVertexAuthorizedUserAdcSync()).toBe(true); + + const streamFn = createGoogleVertexTransportStreamFn(); + const stream = await Promise.resolve( + streamFn( + buildGoogleVertexModel(), + { + messages: [{ role: "user", content: "hello", timestamp: 0 }], + } as Parameters[1], + { + apiKey: "gcp-vertex-credentials", + fetch: tokenFetchMock, + } as Parameters[2], + ), + ); + await stream.result(); + + expect(tokenFetchMock).toHaveBeenCalledWith( + "https://oauth2.googleapis.com/token", + expect.objectContaining({ + body: expect.objectContaining({ + get: expect.any(Function), + }), + method: "POST", + }), + ); + const requestBody = tokenFetchMock.mock.calls[0]?.[1]?.body as URLSearchParams | undefined; + expect(requestBody?.get("refresh_token")).toBe("appdata-refresh-token"); + expect(guardedFetchMock).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: "Bearer ya29.appdata-token", + }), + }), + ); + }); + it("coerces replayed malformed tool-call args to an object for Google payloads", () => { const params = buildGoogleGenerativeAiParams(buildGeminiModel(), { messages: [ diff --git a/extensions/google/vertex-adc.ts b/extensions/google/vertex-adc.ts index 36d822330f6..517bd1049fb 100644 --- a/extensions/google/vertex-adc.ts +++ b/extensions/google/vertex-adc.ts @@ -40,8 +40,21 @@ function resolveGoogleApplicationCredentialsPath( return existsSync(explicit) ? explicit : undefined; } const homeDir = normalizeOptionalString(env.HOME) ?? os.homedir(); - const fallback = path.join(homeDir, ".config", "gcloud", "application_default_credentials.json"); - return existsSync(fallback) ? fallback : undefined; + const homeFallback = path.join( + homeDir, + ".config", + "gcloud", + "application_default_credentials.json", + ); + if (existsSync(homeFallback)) { + return homeFallback; + } + const appDataDir = normalizeOptionalString(env.APPDATA); + if (!appDataDir) { + return undefined; + } + const appDataFallback = path.join(appDataDir, "gcloud", "application_default_credentials.json"); + return existsSync(appDataFallback) ? appDataFallback : undefined; } async function readGoogleAuthorizedUserCredentials(