fix(google): accept Windows ADC manifest paths

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>
This commit is contained in:
clawsweeper[bot]
2026-04-29 22:26:09 -07:00
committed by GitHub
parent 9189b16c1c
commit 1a9763f578
2 changed files with 109 additions and 15 deletions

View File

@@ -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">> = {},
): 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<typeof streamFn>[1],
{
apiKey: "gcp-vertex-credentials",
fetch: tokenFetchMock,
} as Parameters<typeof streamFn>[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: [

View File

@@ -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(