plugins: add lightweight anthropic vertex discovery

This commit is contained in:
Peter Steinberger
2026-04-09 01:43:32 +01:00
parent 67a030dfe8
commit f0ea5bf393
5 changed files with 252 additions and 7 deletions

View File

@@ -2,6 +2,7 @@
"id": "anthropic-vertex",
"enabledByDefault": true,
"providers": ["anthropic-vertex"],
"providerDiscoveryEntry": "./provider-discovery.ts",
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@@ -0,0 +1,10 @@
import { describe, expect, it } from "vitest";
describe("anthropic-vertex provider discovery entry", () => {
it("imports without loading the full plugin entry", async () => {
const module = await import("./provider-discovery.js");
expect(module.default.id).toBe("anthropic-vertex");
expect(module.default.catalog.order).toBe("simple");
});
});

View File

@@ -0,0 +1,215 @@
import { readFileSync } from "node:fs";
import { homedir, platform } from "node:os";
import { join } from "node:path";
import type { ProviderCatalogContext } from "openclaw/plugin-sdk/provider-catalog-shared";
import type {
ModelDefinitionConfig,
ModelProviderConfig,
} from "openclaw/plugin-sdk/provider-model-shared";
const PROVIDER_ID = "anthropic-vertex";
const ANTHROPIC_VERTEX_DEFAULT_REGION = "global";
const ANTHROPIC_VERTEX_REGION_RE = /^[a-z0-9-]+$/;
const ANTHROPIC_VERTEX_DEFAULT_CONTEXT_WINDOW = 1_000_000;
const GCP_VERTEX_CREDENTIALS_MARKER = "gcp-vertex-credentials";
const GCLOUD_DEFAULT_ADC_PATH = join(
homedir(),
".config",
"gcloud",
"application_default_credentials.json",
);
type AnthropicVertexProviderPlugin = {
id: string;
label: string;
docsPath: string;
auth: [];
catalog: {
order: "simple";
run: (ctx: ProviderCatalogContext) => ReturnType<typeof runAnthropicVertexCatalog>;
};
resolveConfigApiKey: (params: { env: NodeJS.ProcessEnv }) => string | undefined;
};
type AdcProjectFile = {
project_id?: unknown;
quota_project_id?: unknown;
};
function normalizeOptionalString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function normalizeLowercaseStringOrEmpty(value: unknown): string {
return normalizeOptionalString(value)?.toLowerCase() ?? "";
}
function resolveAnthropicVertexRegion(env: NodeJS.ProcessEnv = process.env): string {
const region =
normalizeOptionalString(env.GOOGLE_CLOUD_LOCATION) ||
normalizeOptionalString(env.CLOUD_ML_REGION);
return region && ANTHROPIC_VERTEX_REGION_RE.test(region)
? region
: ANTHROPIC_VERTEX_DEFAULT_REGION;
}
function hasAnthropicVertexMetadataServerAdc(env: NodeJS.ProcessEnv = process.env): boolean {
const explicitMetadataOptIn = normalizeOptionalString(env.ANTHROPIC_VERTEX_USE_GCP_METADATA);
return (
explicitMetadataOptIn === "1" ||
normalizeLowercaseStringOrEmpty(explicitMetadataOptIn) === "true"
);
}
function resolveAnthropicVertexDefaultAdcPath(env: NodeJS.ProcessEnv = process.env): string {
return platform() === "win32"
? join(
env.APPDATA ?? join(homedir(), "AppData", "Roaming"),
"gcloud",
"application_default_credentials.json",
)
: GCLOUD_DEFAULT_ADC_PATH;
}
function resolveAnthropicVertexAdcCredentialsPathCandidate(
env: NodeJS.ProcessEnv = process.env,
): string | undefined {
const explicit = normalizeOptionalString(env.GOOGLE_APPLICATION_CREDENTIALS);
if (explicit) {
return explicit;
}
if (env !== process.env) {
return undefined;
}
return resolveAnthropicVertexDefaultAdcPath(env);
}
function readAnthropicVertexAdc(env: NodeJS.ProcessEnv = process.env): AdcProjectFile | null {
const credentialsPath = resolveAnthropicVertexAdcCredentialsPathCandidate(env);
if (!credentialsPath) {
return null;
}
try {
return JSON.parse(readFileSync(credentialsPath, "utf8")) as AdcProjectFile;
} catch {
return null;
}
}
function hasAnthropicVertexAvailableAuth(env: NodeJS.ProcessEnv = process.env): boolean {
return hasAnthropicVertexMetadataServerAdc(env) || readAnthropicVertexAdc(env) !== null;
}
function resolveAnthropicVertexConfigApiKey(
env: NodeJS.ProcessEnv = process.env,
): string | undefined {
return hasAnthropicVertexAvailableAuth(env) ? GCP_VERTEX_CREDENTIALS_MARKER : undefined;
}
function buildAnthropicVertexModel(params: {
id: string;
name: string;
reasoning: boolean;
input: ModelDefinitionConfig["input"];
cost: ModelDefinitionConfig["cost"];
maxTokens: number;
}): ModelDefinitionConfig {
return {
id: params.id,
name: params.name,
reasoning: params.reasoning,
input: params.input,
cost: params.cost,
contextWindow: ANTHROPIC_VERTEX_DEFAULT_CONTEXT_WINDOW,
maxTokens: params.maxTokens,
};
}
function buildAnthropicVertexProvider(params?: { env?: NodeJS.ProcessEnv }): ModelProviderConfig {
const region = resolveAnthropicVertexRegion(params?.env);
const baseUrl =
normalizeLowercaseStringOrEmpty(region) === "global"
? "https://aiplatform.googleapis.com"
: `https://${region}-aiplatform.googleapis.com`;
return {
baseUrl,
api: "anthropic-messages",
apiKey: GCP_VERTEX_CREDENTIALS_MARKER,
models: [
buildAnthropicVertexModel({
id: "claude-opus-4-6",
name: "Claude Opus 4.6",
reasoning: true,
input: ["text", "image"],
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
maxTokens: 128000,
}),
buildAnthropicVertexModel({
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
reasoning: true,
input: ["text", "image"],
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
maxTokens: 128000,
}),
],
};
}
function mergeImplicitAnthropicVertexProvider(params: {
existing?: ModelProviderConfig;
implicit: ModelProviderConfig;
}) {
const { existing, implicit } = params;
if (!existing) {
return implicit;
}
return {
...implicit,
...existing,
models:
Array.isArray(existing.models) && existing.models.length > 0
? existing.models
: implicit.models,
};
}
function resolveImplicitAnthropicVertexProvider(params?: { env?: NodeJS.ProcessEnv }) {
const env = params?.env ?? process.env;
if (!hasAnthropicVertexAvailableAuth(env)) {
return null;
}
return buildAnthropicVertexProvider({ env });
}
async function runAnthropicVertexCatalog(ctx: ProviderCatalogContext) {
const implicit = resolveImplicitAnthropicVertexProvider({
env: ctx.env,
});
if (!implicit) {
return null;
}
return {
provider: mergeImplicitAnthropicVertexProvider({
existing: ctx.config.models?.providers?.[PROVIDER_ID],
implicit,
}),
};
}
export const anthropicVertexProviderDiscovery: AnthropicVertexProviderPlugin = {
id: PROVIDER_ID,
label: "Anthropic Vertex",
docsPath: "/providers/models",
auth: [],
catalog: {
order: "simple",
run: runAnthropicVertexCatalog,
},
resolveConfigApiKey: ({ env }) => resolveAnthropicVertexConfigApiKey(env),
};
export default anthropicVertexProviderDiscovery;

View File

@@ -4,12 +4,19 @@ import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { resolveImplicitProvidersForTest } from "./models-config.e2e-harness.js";
const ANTHROPIC_VERTEX_DISCOVERY_ENV = {
OPENCLAW_TEST_ONLY_PROVIDER_PLUGIN_IDS: "anthropic",
} satisfies NodeJS.ProcessEnv;
describe("anthropic-vertex implicit provider", () => {
it("does not auto-enable from GOOGLE_CLOUD_PROJECT_ID alone", async () => {
const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-"));
const providers = await resolveImplicitProvidersForTest({
agentDir,
env: { GOOGLE_CLOUD_PROJECT_ID: "vertex-project" },
env: {
...ANTHROPIC_VERTEX_DISCOVERY_ENV,
GOOGLE_CLOUD_PROJECT_ID: "vertex-project",
},
});
expect(providers?.["anthropic-vertex"]).toBeUndefined();
});
@@ -24,6 +31,7 @@ describe("anthropic-vertex implicit provider", () => {
const providers = await resolveImplicitProvidersForTest({
agentDir,
env: {
...ANTHROPIC_VERTEX_DISCOVERY_ENV,
GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
GOOGLE_CLOUD_LOCATION: "us-east1",
},
@@ -50,6 +58,7 @@ describe("anthropic-vertex implicit provider", () => {
const providers = await resolveImplicitProvidersForTest({
agentDir,
env: {
...ANTHROPIC_VERTEX_DISCOVERY_ENV,
GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
GOOGLE_CLOUD_LOCATION: "us-east5",
},
@@ -72,6 +81,7 @@ describe("anthropic-vertex implicit provider", () => {
const providers = await resolveImplicitProvidersForTest({
agentDir,
env: {
...ANTHROPIC_VERTEX_DISCOVERY_ENV,
GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
GOOGLE_CLOUD_LOCATION: "europe-west4",
},
@@ -94,6 +104,7 @@ describe("anthropic-vertex implicit provider", () => {
const providers = await resolveImplicitProvidersForTest({
agentDir,
env: {
...ANTHROPIC_VERTEX_DISCOVERY_ENV,
GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
GOOGLE_CLOUD_LOCATION: "us-central1.attacker.example",
},
@@ -114,6 +125,7 @@ describe("anthropic-vertex implicit provider", () => {
const providers = await resolveImplicitProvidersForTest({
agentDir,
env: {
...ANTHROPIC_VERTEX_DISCOVERY_ENV,
GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
GOOGLE_CLOUD_LOCATION: "global",
},
@@ -129,6 +141,7 @@ describe("anthropic-vertex implicit provider", () => {
const providers = await resolveImplicitProvidersForTest({
agentDir,
env: {
...ANTHROPIC_VERTEX_DISCOVERY_ENV,
ANTHROPIC_VERTEX_USE_GCP_METADATA: "true",
GOOGLE_CLOUD_LOCATION: "us-east5",
},
@@ -143,6 +156,7 @@ describe("anthropic-vertex implicit provider", () => {
const providers = await resolveImplicitProvidersForTest({
agentDir,
env: {
...ANTHROPIC_VERTEX_DISCOVERY_ENV,
ANTHROPIC_VERTEX_USE_GCP_METADATA: "true",
GOOGLE_CLOUD_LOCATION: "us-east5",
},
@@ -170,6 +184,7 @@ describe("anthropic-vertex implicit provider", () => {
const providers = await resolveImplicitProvidersForTest({
agentDir,
env: {
...ANTHROPIC_VERTEX_DISCOVERY_ENV,
KUBERNETES_SERVICE_HOST: "10.0.0.1",
GOOGLE_CLOUD_LOCATION: "us-east5",
},

View File

@@ -3,7 +3,6 @@ import os from "node:os";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { ensureAuthProfileStore } from "./auth-profiles.js";
import { NON_ENV_SECRETREF_MARKER } from "./model-auth-markers.js";
import { normalizeProviders } from "./models-config.providers.normalize.js";
import { resolveApiKeyFromProfiles } from "./models-config.providers.secrets.js";
@@ -183,13 +182,18 @@ describe("normalizeProviders", () => {
"utf8",
);
const store = ensureAuthProfileStore(agentDir, {
allowKeychainPrompt: false,
});
const resolved = resolveApiKeyFromProfiles({
provider: "minimax",
store,
store: {
version: 1,
profiles: {
"minimax:default": {
type: "api_key",
provider: "minimax",
keyRef: { source: "env", provider: "default", id: "MINIMAX_API_KEY" },
},
},
},
env: process.env,
});