Skip to content

Commit a4f7e4c

Browse files
authored
fix(google): preserve Vertex ADC catalog auth (#90609)
* fix: preserve Google Vertex ADC catalog auth * fix: register Google Vertex ADC config marker * fix: fill Vertex ADC static catalog auth
1 parent 6da3b1f commit a4f7e4c

8 files changed

Lines changed: 210 additions & 3 deletions

extensions/google/index.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
// Google tests cover index plugin behavior.
2+
import { mkdtemp, writeFile } from "node:fs/promises";
3+
import os from "node:os";
4+
import path from "node:path";
25
import type { Context, Model } from "openclaw/plugin-sdk/llm";
36
import type {
47
ProviderReplaySessionEntry,
@@ -14,6 +17,7 @@ import type { RealtimeVoiceProviderPlugin } from "openclaw/plugin-sdk/realtime-v
1417
import { describe, expect, it, vi } from "vitest";
1518
import { registerGoogleGeminiCliProvider } from "./gemini-cli-provider.js";
1619
import googlePlugin from "./index.js";
20+
import googleProviderDiscovery from "./provider-discovery.js";
1721
import { registerGoogleProvider } from "./provider-registration.js";
1822

1923
const googleProviderPlugin = {
@@ -163,6 +167,59 @@ describe("google provider plugin hooks", () => {
163167
).toBe("native");
164168
});
165169

170+
it("resolves Google Vertex ADC auth evidence to the config marker", async () => {
171+
const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-config-key-"));
172+
const credentialsPath = path.join(tempDir, "application_default_credentials.json");
173+
await writeFile(
174+
credentialsPath,
175+
JSON.stringify({
176+
type: "authorized_user",
177+
client_id: "client-id",
178+
client_secret: "client-secret",
179+
refresh_token: "refresh-token",
180+
}),
181+
"utf8",
182+
);
183+
const { providers } = await registerProviderPlugin({
184+
plugin: googleProviderPlugin,
185+
id: "google",
186+
name: "Google Provider",
187+
});
188+
const provider = requireRegisteredProvider(providers, "google-vertex");
189+
190+
expect(
191+
provider.resolveConfigApiKey?.({
192+
provider: "google-vertex",
193+
env: {
194+
GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
195+
GOOGLE_CLOUD_PROJECT: "vertex-project",
196+
GOOGLE_CLOUD_LOCATION: "global",
197+
},
198+
}),
199+
).toBe("gcp-vertex-credentials");
200+
expect(
201+
provider.resolveConfigApiKey?.({
202+
provider: "google-vertex",
203+
env: {
204+
GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
205+
GOOGLE_CLOUD_PROJECT: "",
206+
GCLOUD_PROJECT: "vertex-project",
207+
GOOGLE_CLOUD_LOCATION: "global",
208+
},
209+
}),
210+
).toBe("gcp-vertex-credentials");
211+
expect(
212+
googleProviderDiscovery.resolveConfigApiKey?.({
213+
provider: "google-vertex",
214+
env: {
215+
GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
216+
GOOGLE_CLOUD_PROJECT: "vertex-project",
217+
GOOGLE_CLOUD_LOCATION: "global",
218+
},
219+
}),
220+
).toBe("gcp-vertex-credentials");
221+
});
222+
166223
it("owns Gemini tool schema normalization for direct and CLI providers", async () => {
167224
const { providers } = await registerProviderPlugin({
168225
plugin: googleProviderPlugin,

extensions/google/provider-discovery.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ import {
44
buildGoogleStaticCatalogProvider,
55
buildGoogleVertexStaticCatalogProvider,
66
} from "./provider-catalog.js";
7+
import { resolveGoogleVertexConfigApiKey } from "./vertex-adc.js";
78

89
const googleProviderDiscovery: ProviderPlugin = {
910
id: "google",
1011
label: "Google AI Studio",
1112
docsPath: "/providers/models",
1213
auth: [],
14+
resolveConfigApiKey: ({ provider, env }) =>
15+
provider === "google-vertex" ? resolveGoogleVertexConfigApiKey(env) : undefined,
1316
staticCatalog: {
1417
order: "simple",
1518
run: async () => ({

extensions/google/provider-registration.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
createGoogleGenerativeAiTransportStreamFn,
2323
createGoogleVertexTransportStreamFn,
2424
} from "./transport-stream.js";
25+
import { resolveGoogleVertexConfigApiKey } from "./vertex-adc.js";
2526

2627
function resolveGoogleReasoningOutputMode(
2728
ctx: ProviderReasoningOutputModeContext,
@@ -68,6 +69,8 @@ export function buildGoogleProvider(): ProviderPlugin {
6869
resolveGoogleGenerativeAiTransport({ provider, api, baseUrl }),
6970
normalizeConfig: ({ provider, providerConfig }) =>
7071
normalizeGoogleProviderConfig(provider, providerConfig),
72+
resolveConfigApiKey: ({ provider, env }) =>
73+
provider === "google-vertex" ? resolveGoogleVertexConfigApiKey(env) : undefined,
7174
staticCatalog: {
7275
order: "simple",
7376
run: async () => ({

extensions/google/vertex-adc.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,17 @@ export function isGoogleVertexCredentialsMarker(
9393
return apiKey === undefined || apiKey === GCP_VERTEX_CREDENTIALS_MARKER;
9494
}
9595

96+
function hasGoogleVertexProjectEnv(env: NodeJS.ProcessEnv): boolean {
97+
return Boolean(
98+
normalizeOptionalString(env.GOOGLE_CLOUD_PROJECT) ||
99+
normalizeOptionalString(env.GCLOUD_PROJECT),
100+
);
101+
}
102+
103+
function hasGoogleVertexLocationEnv(env: NodeJS.ProcessEnv): boolean {
104+
return Boolean(normalizeOptionalString(env.GOOGLE_CLOUD_LOCATION));
105+
}
106+
96107
function resolveGoogleApplicationCredentialsPath(
97108
env: NodeJS.ProcessEnv = process.env,
98109
): string | undefined {
@@ -183,6 +194,16 @@ export function hasGoogleVertexAuthorizedUserAdcSync(
183194
return false;
184195
}
185196

197+
export function resolveGoogleVertexConfigApiKey(
198+
env: NodeJS.ProcessEnv = process.env,
199+
): string | undefined {
200+
return hasGoogleVertexProjectEnv(env) &&
201+
hasGoogleVertexLocationEnv(env) &&
202+
hasGoogleVertexAuthorizedUserAdcSync(env)
203+
? GCP_VERTEX_CREDENTIALS_MARKER
204+
: undefined;
205+
}
206+
186207
async function refreshGoogleVertexAuthorizedUserAccessToken(params: {
187208
credentialsPath: string;
188209
credentials: GoogleAuthorizedUserCredentials;

src/agents/models-config.applies-config-env-vars.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,60 @@ describe("models-config", () => {
495495
}
496496
});
497497

498+
it("keeps google-vertex static catalog rows when ADC auth evidence supplies the marker", async () => {
499+
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-adc-models-"));
500+
const credentialsPath = path.join(agentDir, "application_default_credentials.json");
501+
await fs.writeFile(credentialsPath, JSON.stringify({ type: "authorized_user" }), "utf8");
502+
try {
503+
const plan = await planOpenClawModelsJsonWithDeps(
504+
{
505+
cfg: {
506+
agents: {
507+
defaults: {
508+
models: {
509+
"google-vertex/gemini-2.5-pro": {},
510+
},
511+
model: { primary: "google-vertex/gemini-2.5-pro" },
512+
},
513+
},
514+
models: { providers: {} },
515+
},
516+
agentDir,
517+
env: {
518+
GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
519+
GOOGLE_CLOUD_PROJECT: "vertex-project",
520+
GOOGLE_CLOUD_LOCATION: "global",
521+
} as NodeJS.ProcessEnv,
522+
existingRaw: "",
523+
existingParsed: null,
524+
},
525+
{
526+
resolveImplicitProviders: async () => ({
527+
"google-vertex": createImplicitGoogleVertexProvider(),
528+
}),
529+
},
530+
);
531+
532+
expect(plan.action).toBe("write");
533+
if (plan.action !== "write") {
534+
throw new Error("Expected models.json write plan");
535+
}
536+
const parsed = JSON.parse(plan.contents) as {
537+
providers?: Record<
538+
string,
539+
{ apiKey?: string; api?: string; models?: Array<{ id?: string }> }
540+
>;
541+
};
542+
expect(parsed.providers?.["google-vertex"]?.api).toBe("google-vertex");
543+
expect(parsed.providers?.["google-vertex"]?.apiKey).toBe("gcp-vertex-credentials");
544+
expect(parsed.providers?.["google-vertex"]?.models?.map((model) => model.id)).toEqual([
545+
"gemini-2.5-pro",
546+
]);
547+
} finally {
548+
await fs.rm(agentDir, { recursive: true, force: true });
549+
}
550+
});
551+
498552
it("uses config env.vars entries for implicit provider discovery without mutating process.env", async () => {
499553
await withTempEnv(["OPENROUTER_API_KEY", TEST_ENV_VAR], async () => {
500554
unsetEnv(["OPENROUTER_API_KEY", TEST_ENV_VAR]);

src/agents/models-config.providers.implicit.discovery-scope.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
// Exercises startup provider discovery scoping without loading real plugin manifests.
2+
import { mkdtemp, writeFile } from "node:fs/promises";
3+
import os from "node:os";
4+
import path from "node:path";
25
import { beforeEach, describe, expect, it, vi } from "vitest";
36
import type { PluginMetadataSnapshotOwnerMaps } from "../plugins/plugin-metadata-snapshot.js";
47
import type { ProviderPlugin } from "../plugins/types.js";
@@ -205,6 +208,38 @@ describe("resolveImplicitProviders startup discovery scope", () => {
205208
expect(mocks.runProviderCatalog).not.toHaveBeenCalled();
206209
});
207210

211+
it("fills missing static catalog apiKey from Google Vertex ADC auth evidence", async () => {
212+
const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-adc-"));
213+
const credentialsPath = path.join(tempDir, "application_default_credentials.json");
214+
await writeFile(credentialsPath, JSON.stringify({ type: "authorized_user" }));
215+
mocks.resolveRuntimePluginDiscoveryProviders.mockResolvedValue([
216+
createStaticOnlyProvider("google"),
217+
]);
218+
mocks.runProviderStaticCatalog.mockResolvedValue({
219+
providers: {
220+
"google-vertex": {
221+
baseUrl: "https://aiplatform.googleapis.com",
222+
api: "google-vertex" as const,
223+
models: [createTextModel("gemini-3.1-pro-preview", "Gemini 3.1 Pro Preview")],
224+
},
225+
},
226+
});
227+
228+
const providers = await resolveImplicitProviders({
229+
agentDir: "/tmp/openclaw-agent",
230+
config: {},
231+
env: {
232+
GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
233+
GOOGLE_CLOUD_PROJECT: "vertex-project",
234+
GOOGLE_CLOUD_LOCATION: "global",
235+
} as NodeJS.ProcessEnv,
236+
explicitProviders: {},
237+
providerDiscoveryEntriesOnly: true,
238+
});
239+
240+
expect(providers?.["google-vertex"]?.apiKey).toBe("gcp-vertex-credentials");
241+
});
242+
208243
it("falls back to static provider catalogs when runtime discovery has no rows", async () => {
209244
mocks.resolveRuntimePluginDiscoveryProviders.mockResolvedValue([
210245
createProviderWithStaticCatalog("minimax"),

src/agents/models-config.providers.implicit.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import type {
3838
import {
3939
createProviderApiKeyResolver,
4040
createProviderAuthResolver,
41+
resolveMissingProviderApiKey,
4142
} from "./models-config.providers.secrets.js";
4243

4344
const log = createSubsystemLogger("agents/model-providers");
@@ -293,6 +294,19 @@ function mergeImplicitProviderConfig(params: {
293294
};
294295
}
295296

297+
function resolveImplicitProviderAuthMarker(params: {
298+
ctx: ImplicitProviderContext;
299+
providerId: string;
300+
provider: ProviderConfig;
301+
}): ProviderConfig {
302+
return resolveMissingProviderApiKey({
303+
providerKey: params.providerId,
304+
provider: params.provider,
305+
env: params.ctx.env,
306+
profileApiKey: undefined,
307+
});
308+
}
309+
296310
function resolveConfiguredImplicitProvider(params: {
297311
configuredProviders?: Record<string, ProviderConfig> | null;
298312
providerIds: readonly string[];
@@ -430,7 +444,7 @@ async function resolvePluginImplicitProviders(
430444
result,
431445
});
432446
for (const [providerId, implicitProvider] of Object.entries(normalizedResult)) {
433-
discovered[providerId] = mergeImplicitProviderConfig({
447+
const mergedProvider = mergeImplicitProviderConfig({
434448
providerId,
435449
existing:
436450
discovered[providerId] ??
@@ -449,6 +463,11 @@ async function resolvePluginImplicitProviders(
449463
providerId,
450464
}),
451465
});
466+
discovered[providerId] = resolveImplicitProviderAuthMarker({
467+
ctx,
468+
providerId,
469+
provider: mergedProvider,
470+
});
452471
}
453472
}
454473
return Object.keys(discovered).length > 0 ? discovered : undefined;

src/agents/models-config.providers.secret-helpers.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,18 @@ export function resolveAwsSdkApiKeyVarName(
9898
return resolveAwsSdkEnvVarName(env);
9999
}
100100

101+
function resolveEnvAuthEvidenceApiKeyMarker(
102+
provider: string,
103+
env: NodeJS.ProcessEnv,
104+
): string | undefined {
105+
const resolved = resolveEnvApiKey(provider, env);
106+
const apiKey = resolved?.apiKey?.trim();
107+
if (!apiKey || !isNonSecretApiKeyMarker(apiKey, { includeEnvVarName: false })) {
108+
return undefined;
109+
}
110+
return apiKey;
111+
}
112+
101113
/** Rewrites secret-backed provider headers to stable marker values. */
102114
export function normalizeHeaderValues(params: {
103115
headers: ProviderConfig["headers"] | undefined;
@@ -334,11 +346,14 @@ export function resolveMissingProviderApiKey(params: {
334346
}
335347

336348
const fromEnv = resolveEnvApiKeyVarName(params.providerKey, params.env);
337-
const apiKey = fromEnv ?? params.profileApiKey?.apiKey;
349+
const fromAuthEvidence = fromEnv
350+
? undefined
351+
: resolveEnvAuthEvidenceApiKeyMarker(params.providerKey, params.env);
352+
const apiKey = fromEnv ?? fromAuthEvidence ?? params.profileApiKey?.apiKey;
338353
if (!apiKey?.trim()) {
339354
return params.provider;
340355
}
341-
if (params.profileApiKey && params.profileApiKey.source !== "plaintext") {
356+
if (fromAuthEvidence || (params.profileApiKey && params.profileApiKey.source !== "plaintext")) {
342357
params.secretRefManagedProviders?.add(params.providerKey);
343358
}
344359
return {

0 commit comments

Comments
 (0)