mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-22 12:48:10 +00:00
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
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
// Google tests cover index plugin behavior.
|
||||
import { mkdtemp, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { Context, Model } from "openclaw/plugin-sdk/llm";
|
||||
import type {
|
||||
ProviderReplaySessionEntry,
|
||||
@@ -14,6 +17,7 @@ import type { RealtimeVoiceProviderPlugin } from "openclaw/plugin-sdk/realtime-v
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { registerGoogleGeminiCliProvider } from "./gemini-cli-provider.js";
|
||||
import googlePlugin from "./index.js";
|
||||
import googleProviderDiscovery from "./provider-discovery.js";
|
||||
import { registerGoogleProvider } from "./provider-registration.js";
|
||||
|
||||
const googleProviderPlugin = {
|
||||
@@ -163,6 +167,59 @@ describe("google provider plugin hooks", () => {
|
||||
).toBe("native");
|
||||
});
|
||||
|
||||
it("resolves Google Vertex ADC auth evidence to the config marker", async () => {
|
||||
const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-config-key-"));
|
||||
const credentialsPath = path.join(tempDir, "application_default_credentials.json");
|
||||
await writeFile(
|
||||
credentialsPath,
|
||||
JSON.stringify({
|
||||
type: "authorized_user",
|
||||
client_id: "client-id",
|
||||
client_secret: "client-secret",
|
||||
refresh_token: "refresh-token",
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
const { providers } = await registerProviderPlugin({
|
||||
plugin: googleProviderPlugin,
|
||||
id: "google",
|
||||
name: "Google Provider",
|
||||
});
|
||||
const provider = requireRegisteredProvider(providers, "google-vertex");
|
||||
|
||||
expect(
|
||||
provider.resolveConfigApiKey?.({
|
||||
provider: "google-vertex",
|
||||
env: {
|
||||
GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
|
||||
GOOGLE_CLOUD_PROJECT: "vertex-project",
|
||||
GOOGLE_CLOUD_LOCATION: "global",
|
||||
},
|
||||
}),
|
||||
).toBe("gcp-vertex-credentials");
|
||||
expect(
|
||||
provider.resolveConfigApiKey?.({
|
||||
provider: "google-vertex",
|
||||
env: {
|
||||
GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
|
||||
GOOGLE_CLOUD_PROJECT: "",
|
||||
GCLOUD_PROJECT: "vertex-project",
|
||||
GOOGLE_CLOUD_LOCATION: "global",
|
||||
},
|
||||
}),
|
||||
).toBe("gcp-vertex-credentials");
|
||||
expect(
|
||||
googleProviderDiscovery.resolveConfigApiKey?.({
|
||||
provider: "google-vertex",
|
||||
env: {
|
||||
GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
|
||||
GOOGLE_CLOUD_PROJECT: "vertex-project",
|
||||
GOOGLE_CLOUD_LOCATION: "global",
|
||||
},
|
||||
}),
|
||||
).toBe("gcp-vertex-credentials");
|
||||
});
|
||||
|
||||
it("owns Gemini tool schema normalization for direct and CLI providers", async () => {
|
||||
const { providers } = await registerProviderPlugin({
|
||||
plugin: googleProviderPlugin,
|
||||
|
||||
@@ -4,12 +4,15 @@ import {
|
||||
buildGoogleStaticCatalogProvider,
|
||||
buildGoogleVertexStaticCatalogProvider,
|
||||
} from "./provider-catalog.js";
|
||||
import { resolveGoogleVertexConfigApiKey } from "./vertex-adc.js";
|
||||
|
||||
const googleProviderDiscovery: ProviderPlugin = {
|
||||
id: "google",
|
||||
label: "Google AI Studio",
|
||||
docsPath: "/providers/models",
|
||||
auth: [],
|
||||
resolveConfigApiKey: ({ provider, env }) =>
|
||||
provider === "google-vertex" ? resolveGoogleVertexConfigApiKey(env) : undefined,
|
||||
staticCatalog: {
|
||||
order: "simple",
|
||||
run: async () => ({
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
createGoogleGenerativeAiTransportStreamFn,
|
||||
createGoogleVertexTransportStreamFn,
|
||||
} from "./transport-stream.js";
|
||||
import { resolveGoogleVertexConfigApiKey } from "./vertex-adc.js";
|
||||
|
||||
function resolveGoogleReasoningOutputMode(
|
||||
ctx: ProviderReasoningOutputModeContext,
|
||||
@@ -68,6 +69,8 @@ export function buildGoogleProvider(): ProviderPlugin {
|
||||
resolveGoogleGenerativeAiTransport({ provider, api, baseUrl }),
|
||||
normalizeConfig: ({ provider, providerConfig }) =>
|
||||
normalizeGoogleProviderConfig(provider, providerConfig),
|
||||
resolveConfigApiKey: ({ provider, env }) =>
|
||||
provider === "google-vertex" ? resolveGoogleVertexConfigApiKey(env) : undefined,
|
||||
staticCatalog: {
|
||||
order: "simple",
|
||||
run: async () => ({
|
||||
|
||||
@@ -93,6 +93,17 @@ export function isGoogleVertexCredentialsMarker(
|
||||
return apiKey === undefined || apiKey === GCP_VERTEX_CREDENTIALS_MARKER;
|
||||
}
|
||||
|
||||
function hasGoogleVertexProjectEnv(env: NodeJS.ProcessEnv): boolean {
|
||||
return Boolean(
|
||||
normalizeOptionalString(env.GOOGLE_CLOUD_PROJECT) ||
|
||||
normalizeOptionalString(env.GCLOUD_PROJECT),
|
||||
);
|
||||
}
|
||||
|
||||
function hasGoogleVertexLocationEnv(env: NodeJS.ProcessEnv): boolean {
|
||||
return Boolean(normalizeOptionalString(env.GOOGLE_CLOUD_LOCATION));
|
||||
}
|
||||
|
||||
function resolveGoogleApplicationCredentialsPath(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string | undefined {
|
||||
@@ -183,6 +194,16 @@ export function hasGoogleVertexAuthorizedUserAdcSync(
|
||||
return false;
|
||||
}
|
||||
|
||||
export function resolveGoogleVertexConfigApiKey(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string | undefined {
|
||||
return hasGoogleVertexProjectEnv(env) &&
|
||||
hasGoogleVertexLocationEnv(env) &&
|
||||
hasGoogleVertexAuthorizedUserAdcSync(env)
|
||||
? GCP_VERTEX_CREDENTIALS_MARKER
|
||||
: undefined;
|
||||
}
|
||||
|
||||
async function refreshGoogleVertexAuthorizedUserAccessToken(params: {
|
||||
credentialsPath: string;
|
||||
credentials: GoogleAuthorizedUserCredentials;
|
||||
|
||||
@@ -495,6 +495,60 @@ describe("models-config", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps google-vertex static catalog rows when ADC auth evidence supplies the marker", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-adc-models-"));
|
||||
const credentialsPath = path.join(agentDir, "application_default_credentials.json");
|
||||
await fs.writeFile(credentialsPath, JSON.stringify({ type: "authorized_user" }), "utf8");
|
||||
try {
|
||||
const plan = await planOpenClawModelsJsonWithDeps(
|
||||
{
|
||||
cfg: {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"google-vertex/gemini-2.5-pro": {},
|
||||
},
|
||||
model: { primary: "google-vertex/gemini-2.5-pro" },
|
||||
},
|
||||
},
|
||||
models: { providers: {} },
|
||||
},
|
||||
agentDir,
|
||||
env: {
|
||||
GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
|
||||
GOOGLE_CLOUD_PROJECT: "vertex-project",
|
||||
GOOGLE_CLOUD_LOCATION: "global",
|
||||
} as NodeJS.ProcessEnv,
|
||||
existingRaw: "",
|
||||
existingParsed: null,
|
||||
},
|
||||
{
|
||||
resolveImplicitProviders: async () => ({
|
||||
"google-vertex": createImplicitGoogleVertexProvider(),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
expect(plan.action).toBe("write");
|
||||
if (plan.action !== "write") {
|
||||
throw new Error("Expected models.json write plan");
|
||||
}
|
||||
const parsed = JSON.parse(plan.contents) as {
|
||||
providers?: Record<
|
||||
string,
|
||||
{ apiKey?: string; api?: string; models?: Array<{ id?: string }> }
|
||||
>;
|
||||
};
|
||||
expect(parsed.providers?.["google-vertex"]?.api).toBe("google-vertex");
|
||||
expect(parsed.providers?.["google-vertex"]?.apiKey).toBe("gcp-vertex-credentials");
|
||||
expect(parsed.providers?.["google-vertex"]?.models?.map((model) => model.id)).toEqual([
|
||||
"gemini-2.5-pro",
|
||||
]);
|
||||
} finally {
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("uses config env.vars entries for implicit provider discovery without mutating process.env", async () => {
|
||||
await withTempEnv(["OPENROUTER_API_KEY", TEST_ENV_VAR], async () => {
|
||||
unsetEnv(["OPENROUTER_API_KEY", TEST_ENV_VAR]);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
// Exercises startup provider discovery scoping without loading real plugin manifests.
|
||||
import { mkdtemp, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { PluginMetadataSnapshotOwnerMaps } from "../plugins/plugin-metadata-snapshot.js";
|
||||
import type { ProviderPlugin } from "../plugins/types.js";
|
||||
@@ -205,6 +208,38 @@ describe("resolveImplicitProviders startup discovery scope", () => {
|
||||
expect(mocks.runProviderCatalog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("fills missing static catalog apiKey from Google Vertex ADC auth evidence", async () => {
|
||||
const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-adc-"));
|
||||
const credentialsPath = path.join(tempDir, "application_default_credentials.json");
|
||||
await writeFile(credentialsPath, JSON.stringify({ type: "authorized_user" }));
|
||||
mocks.resolveRuntimePluginDiscoveryProviders.mockResolvedValue([
|
||||
createStaticOnlyProvider("google"),
|
||||
]);
|
||||
mocks.runProviderStaticCatalog.mockResolvedValue({
|
||||
providers: {
|
||||
"google-vertex": {
|
||||
baseUrl: "https://aiplatform.googleapis.com",
|
||||
api: "google-vertex" as const,
|
||||
models: [createTextModel("gemini-3.1-pro-preview", "Gemini 3.1 Pro Preview")],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const providers = await resolveImplicitProviders({
|
||||
agentDir: "/tmp/openclaw-agent",
|
||||
config: {},
|
||||
env: {
|
||||
GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
|
||||
GOOGLE_CLOUD_PROJECT: "vertex-project",
|
||||
GOOGLE_CLOUD_LOCATION: "global",
|
||||
} as NodeJS.ProcessEnv,
|
||||
explicitProviders: {},
|
||||
providerDiscoveryEntriesOnly: true,
|
||||
});
|
||||
|
||||
expect(providers?.["google-vertex"]?.apiKey).toBe("gcp-vertex-credentials");
|
||||
});
|
||||
|
||||
it("falls back to static provider catalogs when runtime discovery has no rows", async () => {
|
||||
mocks.resolveRuntimePluginDiscoveryProviders.mockResolvedValue([
|
||||
createProviderWithStaticCatalog("minimax"),
|
||||
|
||||
@@ -38,6 +38,7 @@ import type {
|
||||
import {
|
||||
createProviderApiKeyResolver,
|
||||
createProviderAuthResolver,
|
||||
resolveMissingProviderApiKey,
|
||||
} from "./models-config.providers.secrets.js";
|
||||
|
||||
const log = createSubsystemLogger("agents/model-providers");
|
||||
@@ -293,6 +294,19 @@ function mergeImplicitProviderConfig(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveImplicitProviderAuthMarker(params: {
|
||||
ctx: ImplicitProviderContext;
|
||||
providerId: string;
|
||||
provider: ProviderConfig;
|
||||
}): ProviderConfig {
|
||||
return resolveMissingProviderApiKey({
|
||||
providerKey: params.providerId,
|
||||
provider: params.provider,
|
||||
env: params.ctx.env,
|
||||
profileApiKey: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveConfiguredImplicitProvider(params: {
|
||||
configuredProviders?: Record<string, ProviderConfig> | null;
|
||||
providerIds: readonly string[];
|
||||
@@ -430,7 +444,7 @@ async function resolvePluginImplicitProviders(
|
||||
result,
|
||||
});
|
||||
for (const [providerId, implicitProvider] of Object.entries(normalizedResult)) {
|
||||
discovered[providerId] = mergeImplicitProviderConfig({
|
||||
const mergedProvider = mergeImplicitProviderConfig({
|
||||
providerId,
|
||||
existing:
|
||||
discovered[providerId] ??
|
||||
@@ -449,6 +463,11 @@ async function resolvePluginImplicitProviders(
|
||||
providerId,
|
||||
}),
|
||||
});
|
||||
discovered[providerId] = resolveImplicitProviderAuthMarker({
|
||||
ctx,
|
||||
providerId,
|
||||
provider: mergedProvider,
|
||||
});
|
||||
}
|
||||
}
|
||||
return Object.keys(discovered).length > 0 ? discovered : undefined;
|
||||
|
||||
@@ -98,6 +98,18 @@ export function resolveAwsSdkApiKeyVarName(
|
||||
return resolveAwsSdkEnvVarName(env);
|
||||
}
|
||||
|
||||
function resolveEnvAuthEvidenceApiKeyMarker(
|
||||
provider: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): string | undefined {
|
||||
const resolved = resolveEnvApiKey(provider, env);
|
||||
const apiKey = resolved?.apiKey?.trim();
|
||||
if (!apiKey || !isNonSecretApiKeyMarker(apiKey, { includeEnvVarName: false })) {
|
||||
return undefined;
|
||||
}
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
/** Rewrites secret-backed provider headers to stable marker values. */
|
||||
export function normalizeHeaderValues(params: {
|
||||
headers: ProviderConfig["headers"] | undefined;
|
||||
@@ -334,11 +346,14 @@ export function resolveMissingProviderApiKey(params: {
|
||||
}
|
||||
|
||||
const fromEnv = resolveEnvApiKeyVarName(params.providerKey, params.env);
|
||||
const apiKey = fromEnv ?? params.profileApiKey?.apiKey;
|
||||
const fromAuthEvidence = fromEnv
|
||||
? undefined
|
||||
: resolveEnvAuthEvidenceApiKeyMarker(params.providerKey, params.env);
|
||||
const apiKey = fromEnv ?? fromAuthEvidence ?? params.profileApiKey?.apiKey;
|
||||
if (!apiKey?.trim()) {
|
||||
return params.provider;
|
||||
}
|
||||
if (params.profileApiKey && params.profileApiKey.source !== "plaintext") {
|
||||
if (fromAuthEvidence || (params.profileApiKey && params.profileApiKey.source !== "plaintext")) {
|
||||
params.secretRefManagedProviders?.add(params.providerKey);
|
||||
}
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user