Skip to content

Commit 0b59964

Browse files
committed
fix(google): support Vertex authorized_user ADC
1 parent 601596b commit 0b59964

5 files changed

Lines changed: 433 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai
3232
- CLI/browser: preserve parent flags while lazy-loading browser subcommands, so `openclaw browser --json open` and `openclaw browser --json tabs` keep machine-readable output after reparsing. Fixes #74574. Thanks @devintegeritsm.
3333
- Plugins/runtime-deps: add `openclaw plugins deps` inspection and repair with script-free package-manager defaults shared across plugin installers, so operators can repair missing bundled runtime deps without corrupting JSON output or blocking unrelated conflict-free deps. Thanks @vincentkoc.
3434
- Agents/output: strip internal `[tool calls omitted]` replay placeholders from user-facing replies while preserving visible reply whitespace. Fixes #74573. Thanks @blaspat.
35+
- Providers/Google Vertex: route authorized_user ADC credentials through OpenClaw's REST transport so Docker installs using gcloud application-default credentials no longer crash in the Google SDK before requests are sent. Fixes #74628. Thanks @frankhal2001-design.
3536
- Agents/sessions: emit a terminal lifecycle backstop when embedded timeout/error turns return without `agent_end`, so Gateway sessions no longer stay stuck in `running` after failover surfaces a timeout. Fixes #74607. Thanks @millerc79.
3637
- Gateway/diagnostics: include stuck-session reason hints and recovery skip causes in warnings, so operators can tell whether a lane is waiting on active work, queued work, or stale bookkeeping. Thanks @vincentkoc.
3738
- Agents/Codex: bound embedded-run cleanup, trajectory flushing, and command-lane task timeouts after runtime failures, so Discord and other chat sessions return to idle instead of staying stuck in processing. Thanks @vincentkoc.

extensions/google/provider-registration.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import {
99
normalizeGoogleProviderConfig,
1010
resolveGoogleGenerativeAiTransport,
1111
} from "./provider-policy.js";
12-
import { createGoogleGenerativeAiTransportStreamFn } from "./transport-stream.js";
12+
import {
13+
createGoogleGenerativeAiTransportStreamFn,
14+
createGoogleVertexTransportStreamFn,
15+
} from "./transport-stream.js";
16+
import { hasGoogleVertexAuthorizedUserAdcSync } from "./vertex-adc.js";
1317

1418
export function buildGoogleProvider(): ProviderPlugin {
1519
return {
@@ -49,10 +53,15 @@ export function buildGoogleProvider(): ProviderPlugin {
4953
providerId: ctx.provider,
5054
ctx,
5155
}),
52-
createStreamFn: ({ model }) =>
53-
model.api === "google-generative-ai"
54-
? createGoogleGenerativeAiTransportStreamFn()
55-
: undefined,
56+
createStreamFn: ({ model }) => {
57+
if (model.api === "google-generative-ai") {
58+
return createGoogleGenerativeAiTransportStreamFn();
59+
}
60+
if (model.api === "google-vertex" && hasGoogleVertexAuthorizedUserAdcSync()) {
61+
return createGoogleVertexTransportStreamFn();
62+
}
63+
return undefined;
64+
},
5665
...GOOGLE_GEMINI_PROVIDER_HOOKS,
5766
isModernModelRef: ({ modelId }) => isModernGoogleModel(modelId),
5867
};

extensions/google/transport-stream.test.ts

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import { mkdtemp, writeFile } from "node:fs/promises";
2+
import os from "node:os";
3+
import path from "node:path";
14
import type { Model } from "@mariozechner/pi-ai";
2-
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
5+
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
36

47
const { buildGuardedModelFetchMock, guardedFetchMock } = vi.hoisted(() => ({
58
buildGuardedModelFetchMock: vi.fn(),
@@ -13,6 +16,8 @@ vi.mock("openclaw/plugin-sdk/provider-transport-runtime", async (importOriginal)
1316

1417
let buildGoogleGenerativeAiParams: typeof import("./transport-stream.js").buildGoogleGenerativeAiParams;
1518
let createGoogleGenerativeAiTransportStreamFn: typeof import("./transport-stream.js").createGoogleGenerativeAiTransportStreamFn;
19+
let createGoogleVertexTransportStreamFn: typeof import("./transport-stream.js").createGoogleVertexTransportStreamFn;
20+
let hasGoogleVertexAuthorizedUserAdcSync: typeof import("./vertex-adc.js").hasGoogleVertexAuthorizedUserAdcSync;
1621

1722
const MODEL_PROVIDER_REQUEST_TRANSPORT_SYMBOL = Symbol.for(
1823
"openclaw.modelProviderRequestTransport",
@@ -63,8 +68,12 @@ function buildSseResponse(events: unknown[]): Response {
6368

6469
describe("google transport stream", () => {
6570
beforeAll(async () => {
66-
({ buildGoogleGenerativeAiParams, createGoogleGenerativeAiTransportStreamFn } =
67-
await import("./transport-stream.js"));
71+
({
72+
buildGoogleGenerativeAiParams,
73+
createGoogleGenerativeAiTransportStreamFn,
74+
createGoogleVertexTransportStreamFn,
75+
} = await import("./transport-stream.js"));
76+
({ hasGoogleVertexAuthorizedUserAdcSync } = await import("./vertex-adc.js"));
6877
});
6978

7079
beforeEach(() => {
@@ -73,6 +82,10 @@ describe("google transport stream", () => {
7382
buildGuardedModelFetchMock.mockReturnValue(guardedFetchMock);
7483
});
7584

85+
afterEach(() => {
86+
vi.unstubAllEnvs();
87+
});
88+
7689
it("uses the guarded fetch transport and parses Gemini SSE output", async () => {
7790
guardedFetchMock.mockResolvedValueOnce(
7891
buildSseResponse([
@@ -257,6 +270,89 @@ describe("google transport stream", () => {
257270
);
258271
});
259272

273+
it("refreshes authorized_user ADC before Google Vertex requests", async () => {
274+
const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-adc-"));
275+
const credentialsPath = path.join(tempDir, "application_default_credentials.json");
276+
await writeFile(
277+
credentialsPath,
278+
JSON.stringify({
279+
type: "authorized_user",
280+
client_id: "client-id",
281+
client_secret: "client-secret",
282+
refresh_token: "refresh-token",
283+
}),
284+
"utf8",
285+
);
286+
vi.stubEnv("GOOGLE_APPLICATION_CREDENTIALS", credentialsPath);
287+
vi.stubEnv("GOOGLE_CLOUD_PROJECT", "vertex-project");
288+
vi.stubEnv("GOOGLE_CLOUD_LOCATION", "global");
289+
const tokenFetchMock = vi.fn().mockResolvedValue(
290+
new Response(JSON.stringify({ access_token: "ya29.vertex-token", expires_in: 3600 }), {
291+
status: 200,
292+
headers: { "content-type": "application/json" },
293+
}),
294+
);
295+
guardedFetchMock.mockResolvedValueOnce(
296+
buildSseResponse([
297+
{
298+
candidates: [{ content: { parts: [{ text: "ok" }] }, finishReason: "STOP" }],
299+
},
300+
]),
301+
);
302+
303+
expect(hasGoogleVertexAuthorizedUserAdcSync()).toBe(true);
304+
305+
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">;
317+
318+
const streamFn = createGoogleVertexTransportStreamFn();
319+
const stream = await Promise.resolve(
320+
streamFn(
321+
model,
322+
{
323+
messages: [{ role: "user", content: "hello", timestamp: 0 }],
324+
} as Parameters<typeof streamFn>[1],
325+
{
326+
apiKey: "gcp-vertex-credentials",
327+
fetch: tokenFetchMock,
328+
} as Parameters<typeof streamFn>[2],
329+
),
330+
);
331+
const result = await stream.result();
332+
333+
expect(tokenFetchMock).toHaveBeenCalledWith(
334+
"https://oauth2.googleapis.com/token",
335+
expect.objectContaining({ method: "POST" }),
336+
);
337+
expect(guardedFetchMock).toHaveBeenCalledWith(
338+
"https://aiplatform.googleapis.com/v1/projects/vertex-project/locations/global/publishers/google/models/gemini-3.1-pro-preview:streamGenerateContent?alt=sse",
339+
expect.objectContaining({
340+
method: "POST",
341+
headers: expect.objectContaining({
342+
Authorization: "Bearer ya29.vertex-token",
343+
"Content-Type": "application/json",
344+
accept: "text/event-stream",
345+
}),
346+
}),
347+
);
348+
expect(result).toMatchObject({
349+
api: "google-vertex",
350+
provider: "google-vertex",
351+
stopReason: "stop",
352+
content: [{ type: "text", text: "ok" }],
353+
});
354+
});
355+
260356
it("coerces replayed malformed tool-call args to an object for Google payloads", () => {
261357
const params = buildGoogleGenerativeAiParams(buildGeminiModel(), {
262358
messages: [

0 commit comments

Comments
 (0)