























@@ -1,4 +1,4 @@
1-import { mkdtemp, writeFile } from "node:fs/promises";
1+import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
22import os from "node:os";
33import path from "node:path";
44import type { Model } from "@mariozechner/pi-ai";
@@ -51,6 +51,24 @@ function buildGeminiModel(
5151};
5252}
535354+function buildGoogleVertexModel(
55+overrides: Partial<Model<"google-vertex">> = {},
56+): Model<"google-vertex"> {
57+return {
58+id: "gemini-3.1-pro-preview",
59+name: "Gemini 3.1 Pro Preview",
60+api: "google-vertex",
61+provider: "google-vertex",
62+baseUrl: "https://{location}-aiplatform.googleapis.com",
63+reasoning: true,
64+input: ["text"],
65+cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
66+contextWindow: 128000,
67+maxTokens: 8192,
68+ ...overrides,
69+};
70+}
71+5472function buildSseResponse(events: unknown[]): Response {
5573const sse = `${events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("")}data: [DONE]\n\n`;
5674const encoder = new TextEncoder();
@@ -302,18 +320,7 @@ describe("google transport stream", () => {
302320303321expect(hasGoogleVertexAuthorizedUserAdcSync()).toBe(true);
304322305-const model = {
306-id: "gemini-3.1-pro-preview",
307-name: "Gemini 3.1 Pro Preview",
308-api: "google-vertex",
309-provider: "google-vertex",
310-baseUrl: "https://{location}-aiplatform.googleapis.com",
311-reasoning: true,
312-input: ["text"],
313-cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
314-contextWindow: 128000,
315-maxTokens: 8192,
316-} satisfies Model<"google-vertex">;
323+const model = buildGoogleVertexModel();
317324318325const streamFn = createGoogleVertexTransportStreamFn();
319326const stream = await Promise.resolve(
@@ -353,6 +360,80 @@ describe("google transport stream", () => {
353360});
354361});
355362363+it("refreshes authorized_user ADC from the Windows APPDATA fallback for Google Vertex requests", async () => {
364+const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-appdata-adc-"));
365+const homeDir = path.join(tempDir, "home");
366+const appDataDir = path.join(tempDir, "AppData", "Roaming");
367+const fallbackDir = path.join(appDataDir, "gcloud");
368+const credentialsPath = path.join(fallbackDir, "application_default_credentials.json");
369+await mkdir(fallbackDir, { recursive: true });
370+await writeFile(
371+credentialsPath,
372+JSON.stringify({
373+type: "authorized_user",
374+client_id: "client-id",
375+client_secret: "client-secret",
376+refresh_token: "appdata-refresh-token",
377+}),
378+"utf8",
379+);
380+vi.stubEnv("GOOGLE_APPLICATION_CREDENTIALS", undefined);
381+vi.stubEnv("HOME", homeDir);
382+vi.stubEnv("APPDATA", appDataDir);
383+vi.stubEnv("GOOGLE_CLOUD_PROJECT", "vertex-project");
384+vi.stubEnv("GOOGLE_CLOUD_LOCATION", "global");
385+const tokenFetchMock = vi.fn().mockResolvedValue(
386+new Response(JSON.stringify({ access_token: "ya29.appdata-token", expires_in: 3600 }), {
387+status: 200,
388+headers: { "content-type": "application/json" },
389+}),
390+);
391+guardedFetchMock.mockResolvedValueOnce(
392+buildSseResponse([
393+{
394+candidates: [{ content: { parts: [{ text: "ok" }] }, finishReason: "STOP" }],
395+},
396+]),
397+);
398+399+expect(hasGoogleVertexAuthorizedUserAdcSync()).toBe(true);
400+401+const streamFn = createGoogleVertexTransportStreamFn();
402+const stream = await Promise.resolve(
403+streamFn(
404+buildGoogleVertexModel(),
405+{
406+messages: [{ role: "user", content: "hello", timestamp: 0 }],
407+} as Parameters<typeof streamFn>[1],
408+{
409+apiKey: "gcp-vertex-credentials",
410+fetch: tokenFetchMock,
411+} as Parameters<typeof streamFn>[2],
412+),
413+);
414+await stream.result();
415+416+expect(tokenFetchMock).toHaveBeenCalledWith(
417+"https://oauth2.googleapis.com/token",
418+expect.objectContaining({
419+body: expect.objectContaining({
420+get: expect.any(Function),
421+}),
422+method: "POST",
423+}),
424+);
425+const requestBody = tokenFetchMock.mock.calls[0]?.[1]?.body as URLSearchParams | undefined;
426+expect(requestBody?.get("refresh_token")).toBe("appdata-refresh-token");
427+expect(guardedFetchMock).toHaveBeenCalledWith(
428+expect.any(String),
429+expect.objectContaining({
430+headers: expect.objectContaining({
431+Authorization: "Bearer ya29.appdata-token",
432+}),
433+}),
434+);
435+});
436+356437it("coerces replayed malformed tool-call args to an object for Google payloads", () => {
357438const params = buildGoogleGenerativeAiParams(buildGeminiModel(), {
358439messages: [
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。