fix(anthropic-vertex): resolve model discovery and auth for GCP ADC (openclaw#65716)

Verified:
- pnpm test extensions/anthropic-vertex/index.test.ts extensions/anthropic-vertex/region.adc.test.ts src/plugins/manifest-registry.test.ts src/plugins/provider-runtime.synthetic-auth-discovery.test.ts

Co-authored-by: feiskyer <676637+feiskyer@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
Pengfei Ni
2026-04-23 22:03:16 +08:00
committed by GitHub
parent 5743d7c8f5
commit be4920f9bc
11 changed files with 241 additions and 14 deletions

View File

@@ -1,8 +1,29 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { registerSingleProviderPlugin } from "../../test/helpers/plugins/plugin-registration.js";
const { hasAnthropicVertexAvailableAuthMock } = vi.hoisted(() => ({
hasAnthropicVertexAvailableAuthMock: vi.fn(),
}));
vi.mock("./api.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./api.js")>();
return {
...actual,
hasAnthropicVertexAvailableAuth: hasAnthropicVertexAvailableAuthMock,
};
});
import anthropicVertexPlugin from "./index.js";
describe("anthropic-vertex provider plugin", () => {
beforeEach(() => {
hasAnthropicVertexAvailableAuthMock.mockReturnValue(true);
});
afterEach(() => {
vi.clearAllMocks();
});
it("resolves the ADC marker through the provider hook", async () => {
const provider = await registerSingleProviderPlugin(anthropicVertexPlugin);
@@ -77,4 +98,34 @@ describe("anthropic-vertex provider plugin", () => {
allowSyntheticToolResults: true,
});
});
it("resolves synthetic auth when ADC is available", async () => {
hasAnthropicVertexAvailableAuthMock.mockReturnValue(true);
const provider = await registerSingleProviderPlugin(anthropicVertexPlugin);
const result = provider.resolveSyntheticAuth?.({
provider: "anthropic-vertex",
config: undefined,
providerConfig: undefined,
} as never);
expect(result).toEqual({
apiKey: "gcp-vertex-credentials",
source: "gcp-vertex-credentials (ADC)",
mode: "api-key",
});
});
it("returns undefined when ADC is not available", async () => {
hasAnthropicVertexAvailableAuthMock.mockReturnValue(false);
const provider = await registerSingleProviderPlugin(anthropicVertexPlugin);
const result = provider.resolveSyntheticAuth?.({
provider: "anthropic-vertex",
config: undefined,
providerConfig: undefined,
} as never);
expect(result).toBeUndefined();
});
});

View File

@@ -1,12 +1,15 @@
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { readConfiguredProviderCatalogEntries } from "openclaw/plugin-sdk/provider-catalog-shared";
import { NATIVE_ANTHROPIC_REPLAY_HOOKS } from "openclaw/plugin-sdk/provider-model-shared";
import {
hasAnthropicVertexAvailableAuth,
mergeImplicitAnthropicVertexProvider,
resolveAnthropicVertexConfigApiKey,
resolveImplicitAnthropicVertexProvider,
} from "./api.js";
const PROVIDER_ID = "anthropic-vertex";
const GCP_VERTEX_CREDENTIALS_MARKER = "gcp-vertex-credentials";
export default definePluginEntry({
id: PROVIDER_ID,
@@ -37,6 +40,21 @@ export default definePluginEntry({
},
resolveConfigApiKey: ({ env }) => resolveAnthropicVertexConfigApiKey(env),
...NATIVE_ANTHROPIC_REPLAY_HOOKS,
resolveSyntheticAuth: () => {
if (!hasAnthropicVertexAvailableAuth()) {
return undefined;
}
return {
apiKey: GCP_VERTEX_CREDENTIALS_MARKER,
source: "gcp-vertex-credentials (ADC)",
mode: "api-key",
};
},
augmentModelCatalog: ({ config }) =>
readConfiguredProviderCatalogEntries({
config,
providerId: PROVIDER_ID,
}),
});
},
});

View File

@@ -3,6 +3,7 @@
"enabledByDefault": true,
"providers": ["anthropic-vertex"],
"providerDiscoveryEntry": "./provider-discovery.ts",
"syntheticAuthRefs": ["anthropic-vertex"],
"nonSecretAuthMarkers": ["gcp-vertex-credentials"],
"configSchema": {
"type": "object",

View File

@@ -4,6 +4,7 @@ import { buildAnthropicVertexProvider } from "./provider-catalog.js";
import { hasAnthropicVertexAvailableAuth, resolveAnthropicVertexConfigApiKey } from "./region.js";
const PROVIDER_ID = "anthropic-vertex";
const GCP_VERTEX_CREDENTIALS_MARKER = "gcp-vertex-credentials";
type AnthropicVertexProviderPlugin = {
id: string;
@@ -15,6 +16,13 @@ type AnthropicVertexProviderPlugin = {
run: (ctx: ProviderCatalogContext) => ReturnType<typeof runAnthropicVertexCatalog>;
};
resolveConfigApiKey: (params: { env: NodeJS.ProcessEnv }) => string | undefined;
resolveSyntheticAuth: () =>
| {
apiKey: string;
source: string;
mode: "api-key";
}
| undefined;
};
function mergeImplicitAnthropicVertexProvider(params: {
@@ -69,6 +77,16 @@ export const anthropicVertexProviderDiscovery: AnthropicVertexProviderPlugin = {
run: runAnthropicVertexCatalog,
},
resolveConfigApiKey: ({ env }) => resolveAnthropicVertexConfigApiKey(env),
resolveSyntheticAuth: () => {
if (!hasAnthropicVertexAvailableAuth()) {
return undefined;
}
return {
apiKey: GCP_VERTEX_CREDENTIALS_MARKER,
source: "gcp-vertex-credentials (ADC)",
mode: "api-key",
};
},
};
export default anthropicVertexProviderDiscovery;

View File

@@ -46,4 +46,28 @@ describe("anthropic-vertex ADC reads", () => {
expect(existsSyncMock).not.toHaveBeenCalled();
expect(readFileSyncMock).toHaveBeenCalledWith("/tmp/vertex-adc.json", "utf8");
});
it("respects HOME when probing the default ADC path from a copied env snapshot", () => {
const env = {
HOME: "/tmp/vertex-home",
} as NodeJS.ProcessEnv;
readFileSyncMock.mockImplementation((pathname, options) =>
String(pathname) === "/tmp/vertex-home/.config/gcloud/application_default_credentials.json"
? '{"project_id":"vertex-project"}'
: String(pathname) === "/tmp/vertex-adc.json"
? '{"project_id":"vertex-project"}'
: (() => {
throw new Error(`unexpected readFileSync(${String(pathname)}, ${String(options)})`);
})(),
);
expect(resolveAnthropicVertexProjectId(env)).toBe("vertex-project");
expect(hasAnthropicVertexAvailableAuth(env)).toBe(true);
expect(existsSyncMock).not.toHaveBeenCalled();
expect(readFileSyncMock).toHaveBeenCalledWith(
"/tmp/vertex-home/.config/gcloud/application_default_credentials.json",
"utf8",
);
});
});

View File

@@ -7,12 +7,6 @@ import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtim
const ANTHROPIC_VERTEX_DEFAULT_REGION = "global";
const ANTHROPIC_VERTEX_REGION_RE = /^[a-z0-9-]+$/;
const GCP_VERTEX_CREDENTIALS_MARKER = "gcp-vertex-credentials";
const GCLOUD_DEFAULT_ADC_PATH = join(
homedir(),
".config",
"gcloud",
"application_default_credentials.json",
);
type AdcProjectFile = {
project_id?: unknown;
@@ -71,14 +65,28 @@ function hasAnthropicVertexMetadataServerAdc(env: NodeJS.ProcessEnv = process.en
);
}
function resolveAnthropicVertexHomeDir(env: NodeJS.ProcessEnv = process.env): string {
return (
normalizeOptionalSecretInput(env.HOME) ||
normalizeOptionalSecretInput(env.USERPROFILE) ||
homedir()
);
}
function resolveAnthropicVertexDefaultAdcPath(env: NodeJS.ProcessEnv = process.env): string {
return platform() === "win32"
? join(
env.APPDATA ?? join(homedir(), "AppData", "Roaming"),
normalizeOptionalSecretInput(env.APPDATA) ??
join(resolveAnthropicVertexHomeDir(env), "AppData", "Roaming"),
"gcloud",
"application_default_credentials.json",
)
: GCLOUD_DEFAULT_ADC_PATH;
: join(
resolveAnthropicVertexHomeDir(env),
".config",
"gcloud",
"application_default_credentials.json",
);
}
function resolveAnthropicVertexAdcCredentialsPathCandidate(
@@ -88,9 +96,6 @@ function resolveAnthropicVertexAdcCredentialsPathCandidate(
if (explicit) {
return explicit;
}
if (env !== process.env) {
return undefined;
}
return resolveAnthropicVertexDefaultAdcPath(env);
}