1+ import { mkdtemp , writeFile } from "node:fs/promises" ;
2+ import os from "node:os" ;
3+ import path from "node:path" ;
14import 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
47const { buildGuardedModelFetchMock, guardedFetchMock } = vi . hoisted ( ( ) => ( {
58 buildGuardedModelFetchMock : vi . fn ( ) ,
@@ -13,6 +16,8 @@ vi.mock("openclaw/plugin-sdk/provider-transport-runtime", async (importOriginal)
1316
1417let buildGoogleGenerativeAiParams : typeof import ( "./transport-stream.js" ) . buildGoogleGenerativeAiParams ;
1518let 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
1722const MODEL_PROVIDER_REQUEST_TRANSPORT_SYMBOL = Symbol . for (
1823 "openclaw.modelProviderRequestTransport" ,
@@ -63,8 +68,12 @@ function buildSseResponse(events: unknown[]): Response {
6368
6469describe ( "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