refactor: move remaining provider policy into plugins

This commit is contained in:
Peter Steinberger
2026-03-29 23:05:41 +09:00
parent edc58a6864
commit 637b4c8193
11 changed files with 207 additions and 91 deletions

View File

@@ -41,6 +41,10 @@ export default definePluginEntry({
},
},
resolveConfigApiKey: ({ env }) => resolveBedrockConfigApiKey(env),
capabilities: {
providerFamily: "anthropic",
dropThinkingBlockModelHints: ["claude"],
},
wrapStreamFn: ({ modelId, streamFn }) =>
isAnthropicBedrockModel(modelId) ? streamFn : createBedrockNoCacheWrapper(streamFn),
resolveDefaultThinkingLevel: ({ modelId }) =>

View File

@@ -0,0 +1,59 @@
import { describe, expect, it } from "vitest";
import { registerSingleProviderPlugin } from "../../test/helpers/plugins/plugin-registration.js";
import anthropicVertexPlugin from "./index.js";
describe("anthropic-vertex provider plugin", () => {
it("resolves the ADC marker through the provider hook", () => {
const provider = registerSingleProviderPlugin(anthropicVertexPlugin);
expect(
provider.resolveConfigApiKey?.({
provider: "anthropic-vertex",
env: {
ANTHROPIC_VERTEX_USE_GCP_METADATA: "true",
} as NodeJS.ProcessEnv,
} as never),
).toBe("gcp-vertex-credentials");
});
it("merges the implicit Vertex catalog into explicit provider overrides", async () => {
const provider = registerSingleProviderPlugin(anthropicVertexPlugin);
const result = await provider.catalog?.run({
config: {
models: {
providers: {
"anthropic-vertex": {
baseUrl: "https://europe-west4-aiplatform.googleapis.com",
headers: { "x-test-header": "1" },
},
},
},
},
env: {
ANTHROPIC_VERTEX_USE_GCP_METADATA: "true",
GOOGLE_CLOUD_LOCATION: "us-east5",
} as NodeJS.ProcessEnv,
resolveProviderApiKey: () => ({ apiKey: undefined }),
resolveProviderAuth: () => ({
apiKey: undefined,
discoveryApiKey: undefined,
mode: "none",
source: "none",
}),
} as never);
expect(result).toEqual({
provider: {
api: "anthropic-messages",
apiKey: "gcp-vertex-credentials",
baseUrl: "https://europe-west4-aiplatform.googleapis.com",
headers: { "x-test-header": "1" },
models: [
expect.objectContaining({ id: "claude-opus-4-6" }),
expect.objectContaining({ id: "claude-sonnet-4-6" }),
],
},
});
});
});

View File

@@ -0,0 +1,44 @@
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import {
mergeImplicitAnthropicVertexProvider,
resolveAnthropicVertexConfigApiKey,
resolveImplicitAnthropicVertexProvider,
} from "./api.js";
const PROVIDER_ID = "anthropic-vertex";
export default definePluginEntry({
id: PROVIDER_ID,
name: "Anthropic Vertex Provider",
description: "Bundled Anthropic Vertex provider plugin",
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "Anthropic Vertex",
docsPath: "/providers/models",
auth: [],
catalog: {
order: "simple",
run: async (ctx) => {
const implicit = await resolveImplicitAnthropicVertexProvider({
env: ctx.env,
});
if (!implicit) {
return null;
}
return {
provider: mergeImplicitAnthropicVertexProvider({
existing: ctx.config.models?.providers?.[PROVIDER_ID],
implicit,
}),
};
},
},
resolveConfigApiKey: ({ env }) => resolveAnthropicVertexConfigApiKey(env),
capabilities: {
providerFamily: "anthropic",
dropThinkingBlockModelHints: ["claude"],
},
});
},
});

View File

@@ -0,0 +1,10 @@
{
"id": "anthropic-vertex",
"enabledByDefault": true,
"providers": ["anthropic-vertex"],
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}

View File

@@ -0,0 +1,12 @@
{
"name": "@openclaw/anthropic-vertex-provider",
"version": "2026.3.29",
"private": true,
"description": "OpenClaw Anthropic Vertex provider plugin",
"type": "module",
"openclaw": {
"extensions": [
"./index.ts"
]
}
}

View File

@@ -0,0 +1,3 @@
import { describeProviderContracts } from "../../test/helpers/plugins/provider-contract.js";
describeProviderContracts("anthropic-vertex");

View File

@@ -1,9 +1,5 @@
import type { OpenClawConfig } from "../config/config.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import {
mergeImplicitAnthropicVertexProvider,
resolveImplicitAnthropicVertexProvider,
} from "../plugin-sdk/anthropic-vertex.js";
import {
groupPluginDiscoveryProvidersByOrder,
normalizePluginDiscoveryResult,
@@ -34,20 +30,9 @@ const PROVIDER_IMPLICIT_MERGERS: Partial<
(params: { existing: ProviderConfig | undefined; implicit: ProviderConfig }) => ProviderConfig
>
> = {
"anthropic-vertex": mergeImplicitAnthropicVertexProvider,
ollama: ({ implicit }) => implicit,
};
const CORE_IMPLICIT_PROVIDER_RESOLVERS = [
{
id: "anthropic-vertex",
resolve: async (params: { config?: OpenClawConfig; env: NodeJS.ProcessEnv }) =>
resolveImplicitAnthropicVertexProvider({
env: params.env,
}),
},
] as const;
const PLUGIN_DISCOVERY_ORDERS = ["simple", "profile", "paired", "late"] as const;
type ImplicitProviderParams = {
@@ -309,31 +294,6 @@ async function runProviderCatalogWithTimeout(
}
}
async function mergeCoreImplicitProviders(params: {
config?: OpenClawConfig;
explicitProviders?: Record<string, ProviderConfig> | null;
env: NodeJS.ProcessEnv;
providers: Record<string, ProviderConfig>;
}): Promise<void> {
for (const provider of CORE_IMPLICIT_PROVIDER_RESOLVERS) {
const implicit = await provider.resolve({ config: params.config, env: params.env });
if (!implicit) {
continue;
}
const merge = PROVIDER_IMPLICIT_MERGERS[provider.id];
params.providers[provider.id] = (merge ?? mergeImplicitProviderConfig)({
providerId: provider.id,
existing:
params.providers[provider.id] ??
resolveConfiguredImplicitProvider({
configuredProviders: params.explicitProviders ?? params.config?.models?.providers,
providerIds: [provider.id],
}),
implicit,
});
}
}
export async function resolveImplicitProviders(
params: ImplicitProviderParams,
): Promise<NonNullable<OpenClawConfig["models"]>["providers"]> {
@@ -354,12 +314,5 @@ export async function resolveImplicitProviders(
mergeImplicitProviderSet(providers, await resolvePluginImplicitProviders(context, order));
}
await mergeCoreImplicitProviders({
config: params.config,
explicitProviders: params.explicitProviders,
env,
providers,
});
return providers;
}

View File

@@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
describe("models-config.providers.policy", () => {
it("resolves config apiKey markers through the local bedrock helper", async () => {
it("resolves config apiKey markers through provider plugin hooks", async () => {
const { resolveProviderConfigApiKeyResolver } =
await import("./models-config.providers.policy.js");
const env = {
@@ -12,4 +12,32 @@ describe("models-config.providers.policy", () => {
expect(resolver).toBeTypeOf("function");
expect(resolver?.(env)).toBe("AWS_PROFILE");
});
it("resolves anthropic-vertex ADC markers through provider plugin hooks", async () => {
const { resolveProviderConfigApiKeyResolver } =
await import("./models-config.providers.policy.js");
const resolver = resolveProviderConfigApiKeyResolver("anthropic-vertex");
expect(resolver).toBeTypeOf("function");
expect(
resolver?.({
ANTHROPIC_VERTEX_USE_GCP_METADATA: "true",
} as NodeJS.ProcessEnv),
).toBe("gcp-vertex-credentials");
});
it("normalizes Google provider config through provider plugin hooks", async () => {
const { normalizeProviderSpecificConfig } = await import("./models-config.providers.policy.js");
expect(
normalizeProviderSpecificConfig("google", {
api: "google-generative-ai",
baseUrl: "https://generativelanguage.googleapis.com",
models: [],
}),
).toMatchObject({
api: "google-generative-ai",
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
});
});
});

View File

@@ -1,25 +1,11 @@
import { resolveBedrockConfigApiKey } from "../plugin-sdk/amazon-bedrock.js";
import { resolveAnthropicVertexConfigApiKey } from "../plugin-sdk/anthropic-vertex.js";
import { normalizeGoogleProviderConfig } from "../plugin-sdk/google.js";
import { applyModelStudioNativeStreamingUsageCompat } from "../plugin-sdk/modelstudio.js";
import { applyMoonshotNativeStreamingUsageCompat } from "../plugin-sdk/moonshot.js";
import {
applyProviderNativeStreamingUsageCompatWithPlugin,
normalizeProviderConfigWithPlugin,
resolveProviderConfigApiKeyWithPlugin,
resolveProviderRuntimePlugin,
} from "../plugins/provider-runtime.js";
import type { ProviderConfig } from "./models-config.providers.secrets.js";
const PROVIDER_CONFIG_API_KEY_RESOLVERS: Partial<
Record<string, (env: NodeJS.ProcessEnv) => string | undefined>
> = {
"amazon-bedrock": resolveBedrockConfigApiKey,
"anthropic-vertex": resolveAnthropicVertexConfigApiKey,
};
function shouldNormalizeGoogleProviderConfigLocally(providerKey: string): boolean {
return (
providerKey === "google" ||
providerKey === "google-antigravity" ||
providerKey === "google-vertex"
);
}
export function applyNativeStreamingUsageCompat(
providers: Record<string, ProviderConfig>,
): Record<string, ProviderConfig> {
@@ -28,11 +14,13 @@ export function applyNativeStreamingUsageCompat(
for (const [providerKey, provider] of Object.entries(providers)) {
const nextProvider =
providerKey === "modelstudio"
? applyModelStudioNativeStreamingUsageCompat(provider)
: providerKey === "moonshot"
? applyMoonshotNativeStreamingUsageCompat(provider)
: provider;
applyProviderNativeStreamingUsageCompatWithPlugin({
provider: providerKey,
context: {
provider: providerKey,
providerConfig: provider,
},
}) ?? provider;
nextProviders[providerKey] = nextProvider;
changed ||= nextProvider !== provider;
}
@@ -44,15 +32,32 @@ export function normalizeProviderSpecificConfig(
providerKey: string,
provider: ProviderConfig,
): ProviderConfig {
if (shouldNormalizeGoogleProviderConfigLocally(providerKey)) {
return normalizeGoogleProviderConfig(providerKey, provider);
}
return provider;
return (
normalizeProviderConfigWithPlugin({
provider: providerKey,
context: {
provider: providerKey,
providerConfig: provider,
},
}) ?? provider
);
}
export function resolveProviderConfigApiKeyResolver(
providerKey: string,
): ((env: NodeJS.ProcessEnv) => string | undefined) | undefined {
const fallback = PROVIDER_CONFIG_API_KEY_RESOLVERS[providerKey];
return fallback;
if (!resolveProviderRuntimePlugin({ provider: providerKey })?.resolveConfigApiKey) {
return undefined;
}
return (env) => {
const resolved = resolveProviderConfigApiKeyWithPlugin({
provider: providerKey,
env,
context: {
provider: providerKey,
env,
},
});
return resolved?.trim() || undefined;
};
}

View File

@@ -7,6 +7,16 @@ const resolveProviderCapabilitiesWithPluginMock = vi.fn((params: { provider: str
providerFamily: "anthropic",
dropThinkingBlockModelHints: ["claude"],
};
case "anthropic-vertex":
return {
providerFamily: "anthropic",
dropThinkingBlockModelHints: ["claude"],
};
case "amazon-bedrock":
return {
providerFamily: "anthropic",
dropThinkingBlockModelHints: ["claude"],
};
case "openai":
return {
providerFamily: "openai",

View File

@@ -36,17 +36,6 @@ const DEFAULT_PROVIDER_CAPABILITIES: ProviderCapabilities = {
dropThinkingBlockModelHints: [],
};
const CORE_PROVIDER_CAPABILITIES: Record<string, Partial<ProviderCapabilities>> = {
"anthropic-vertex": {
providerFamily: "anthropic",
dropThinkingBlockModelHints: ["claude"],
},
"amazon-bedrock": {
providerFamily: "anthropic",
dropThinkingBlockModelHints: ["claude"],
},
};
const PLUGIN_CAPABILITIES_FALLBACKS: Record<string, Partial<ProviderCapabilities>> = {
anthropic: {
providerFamily: "anthropic",
@@ -118,7 +107,6 @@ export function resolveProviderCapabilities(
: undefined;
return {
...DEFAULT_PROVIDER_CAPABILITIES,
...CORE_PROVIDER_CAPABILITIES[normalized],
...PLUGIN_CAPABILITIES_FALLBACKS[normalized],
...pluginCapabilities,
};