From 19de5d1b569dce2375a2692b4cf62dca385fd351 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 5 Apr 2026 09:55:19 +0100 Subject: [PATCH] refactor: move provider discovery config into plugins --- docs/.generated/config-baseline.sha256 | 6 +- docs/gateway/configuration-reference.md | 14 +- docs/help/faq.md | 2 +- docs/providers/bedrock.md | 48 ++--- extensions/amazon-bedrock/discovery.test.ts | 51 +++++- extensions/amazon-bedrock/discovery.ts | 8 +- extensions/amazon-bedrock/index.test.ts | 25 ++- .../amazon-bedrock/openclaw.plugin.json | 49 ++++++ .../amazon-bedrock/register.sync.runtime.ts | 18 +- extensions/github-copilot/index.test.ts | 11 +- extensions/github-copilot/index.ts | 11 +- .../github-copilot/openclaw.plugin.json | 20 ++- extensions/huggingface/index.test.ts | 11 +- extensions/huggingface/index.ts | 11 +- extensions/huggingface/openclaw.plugin.json | 20 ++- extensions/ollama/index.test.ts | 11 +- extensions/ollama/index.ts | 11 +- extensions/ollama/openclaw.plugin.json | 20 ++- src/config/schema.base.generated.ts | 164 ------------------ src/config/schema.help.quality.test.ts | 17 -- src/config/schema.help.ts | 26 --- src/config/schema.labels.ts | 13 -- src/config/types.models.ts | 2 + src/config/zod-schema.core.ts | 11 -- 24 files changed, 295 insertions(+), 285 deletions(-) diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index 995fb2c40e8..f951490ad92 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -1,4 +1,4 @@ -8bbf281d0c63e38098b2132174b77ed58faf2083fb68cb88f90ebe76d7acda1b config-baseline.json -7163170accb9a8b62455ede5437f057d5a9e9ab5da42010cf0f39cbad952071d config-baseline.core.json +d5a737eb69a2b2b64526fa0197ef9fe576b1d5d4b949a5c610a8457d5f5706cd config-baseline.json +b1a181b667568b5860a80945837d544fdec4f946fba34e871936ce0cd3eb689b config-baseline.core.json 3c999707b167138de34f6255e3488b99e404c5132d3fc5879a1fa12d815c31f5 config-baseline.channel.json -76d011c68b8bc44ec862afa826dd8ddd7c577d89ce0b822eed306f8e1e9301ab config-baseline.plugin.json +031b237717ca108ea2cd314413db4c91edfdfea55f808179e3066331f41af134 config-baseline.plugin.json diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index a2ae66988cc..a9006afd7c8 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -2301,13 +2301,13 @@ OpenClaw uses the built-in model catalog. Add custom providers via `models.provi - `models.providers.*.models.*.contextWindow`: native model context window metadata. - `models.providers.*.models.*.contextTokens`: optional runtime context cap. Use this when you want a smaller effective context budget than the model's native `contextWindow`. - `models.providers.*.models.*.compat.supportsDeveloperRole`: optional compatibility hint. For `api: "openai-completions"` with a non-empty non-native `baseUrl` (host not `api.openai.com`), OpenClaw forces this to `false` at runtime. Empty/omitted `baseUrl` keeps default OpenAI behavior. -- `models.bedrockDiscovery`: Bedrock auto-discovery settings root. -- `models.bedrockDiscovery.enabled`: turn discovery polling on/off. -- `models.bedrockDiscovery.region`: AWS region for discovery. -- `models.bedrockDiscovery.providerFilter`: optional provider-id filter for targeted discovery. -- `models.bedrockDiscovery.refreshInterval`: polling interval for discovery refresh. -- `models.bedrockDiscovery.defaultContextWindow`: fallback context window for discovered models. -- `models.bedrockDiscovery.defaultMaxTokens`: fallback max output tokens for discovered models. +- `plugins.entries.amazon-bedrock.config.discovery`: Bedrock auto-discovery settings root. +- `plugins.entries.amazon-bedrock.config.discovery.enabled`: turn implicit discovery on/off. +- `plugins.entries.amazon-bedrock.config.discovery.region`: AWS region for discovery. +- `plugins.entries.amazon-bedrock.config.discovery.providerFilter`: optional provider-id filter for targeted discovery. +- `plugins.entries.amazon-bedrock.config.discovery.refreshInterval`: polling interval for discovery refresh. +- `plugins.entries.amazon-bedrock.config.discovery.defaultContextWindow`: fallback context window for discovered models. +- `plugins.entries.amazon-bedrock.config.discovery.defaultMaxTokens`: fallback max output tokens for discovered models. ### Provider examples diff --git a/docs/help/faq.md b/docs/help/faq.md index 42e2c5b807b..5143731beef 100644 --- a/docs/help/faq.md +++ b/docs/help/faq.md @@ -652,7 +652,7 @@ for usage/billing and raise limits as needed. - Yes. OpenClaw has a bundled **Amazon Bedrock (Converse)** provider. With AWS env markers present, OpenClaw can auto-discover the streaming/text Bedrock catalog and merge it as an implicit `amazon-bedrock` provider; otherwise you can explicitly enable `models.bedrockDiscovery.enabled` or add a manual provider entry. See [Amazon Bedrock](/providers/bedrock) and [Model providers](/providers/models). If you prefer a managed key flow, an OpenAI-compatible proxy in front of Bedrock is still a valid option. + Yes. OpenClaw has a bundled **Amazon Bedrock (Converse)** provider. With AWS env markers present, OpenClaw can auto-discover the streaming/text Bedrock catalog and merge it as an implicit `amazon-bedrock` provider; otherwise you can explicitly enable `plugins.entries.amazon-bedrock.config.discovery.enabled` or add a manual provider entry. See [Amazon Bedrock](/providers/bedrock) and [Model providers](/providers/models). If you prefer a managed key flow, an OpenAI-compatible proxy in front of Bedrock is still a valid option. diff --git a/docs/providers/bedrock.md b/docs/providers/bedrock.md index 9160828d7c8..d689b42e0c2 100644 --- a/docs/providers/bedrock.md +++ b/docs/providers/bedrock.md @@ -27,9 +27,10 @@ cached (default: 1 hour). How the implicit provider is enabled: -- If `models.bedrockDiscovery.enabled` is `true`, OpenClaw will try discovery - even when no AWS env marker is present. -- If `models.bedrockDiscovery.enabled` is unset, OpenClaw only auto-adds the +- If `plugins.entries.amazon-bedrock.config.discovery.enabled` is `true`, + OpenClaw will try discovery even when no AWS env marker is present. +- If `plugins.entries.amazon-bedrock.config.discovery.enabled` is unset, + OpenClaw only auto-adds the implicit Bedrock provider when it sees one of these AWS auth markers: `AWS_BEARER_TOKEN_BEDROCK`, `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY`, or `AWS_PROFILE`. @@ -37,18 +38,24 @@ How the implicit provider is enabled: shared config, SSO, and IMDS instance-role auth can work even when discovery needed `enabled: true` to opt in. -Config options live under `models.bedrockDiscovery`: +Config options live under `plugins.entries.amazon-bedrock.config.discovery`: ```json5 { - models: { - bedrockDiscovery: { - enabled: true, - region: "us-east-1", - providerFilter: ["anthropic", "amazon"], - refreshInterval: 3600, - defaultContextWindow: 32000, - defaultMaxTokens: 4096, + plugins: { + entries: { + "amazon-bedrock": { + config: { + discovery: { + enabled: true, + region: "us-east-1", + providerFilter: ["anthropic", "amazon"], + refreshInterval: 3600, + defaultContextWindow: 32000, + defaultMaxTokens: 4096, + }, + }, + }, }, }, } @@ -120,20 +127,21 @@ export AWS_BEARER_TOKEN_BEDROCK="..." When running OpenClaw on an EC2 instance with an IAM role attached, the AWS SDK can use the instance metadata service (IMDS) for authentication. For Bedrock model discovery, OpenClaw only auto-enables the implicit provider from AWS env -markers unless you explicitly set `models.bedrockDiscovery.enabled: true`. +markers unless you explicitly set +`plugins.entries.amazon-bedrock.config.discovery.enabled: true`. Recommended setup for IMDS-backed hosts: -- Set `models.bedrockDiscovery.enabled` to `true`. -- Set `models.bedrockDiscovery.region` (or export `AWS_REGION`). +- Set `plugins.entries.amazon-bedrock.config.discovery.enabled` to `true`. +- Set `plugins.entries.amazon-bedrock.config.discovery.region` (or export `AWS_REGION`). - You do **not** need a fake API key. - You only need `AWS_PROFILE=default` if you specifically want an env marker for auto mode or status surfaces. ```bash # Recommended: explicit discovery enable + region -openclaw config set models.bedrockDiscovery.enabled true -openclaw config set models.bedrockDiscovery.region us-east-1 +openclaw config set plugins.entries.amazon-bedrock.config.discovery.enabled true +openclaw config set plugins.entries.amazon-bedrock.config.discovery.region us-east-1 # Optional: add an env marker if you want auto mode without explicit enable export AWS_PROFILE=default @@ -176,8 +184,8 @@ aws ec2 associate-iam-instance-profile \ --iam-instance-profile Name=EC2-Bedrock-Access # 3. On the EC2 instance, enable discovery explicitly -openclaw config set models.bedrockDiscovery.enabled true -openclaw config set models.bedrockDiscovery.region us-east-1 +openclaw config set plugins.entries.amazon-bedrock.config.discovery.enabled true +openclaw config set plugins.entries.amazon-bedrock.config.discovery.region us-east-1 # 4. Optional: add an env marker if you want auto mode without explicit enable echo 'export AWS_PROFILE=default' >> ~/.bashrc @@ -194,7 +202,7 @@ openclaw models list - Automatic discovery needs the `bedrock:ListFoundationModels` permission. - If you rely on auto mode, set one of the supported AWS auth env markers on the gateway host. If you prefer IMDS/shared-config auth without env markers, set - `models.bedrockDiscovery.enabled: true`. + `plugins.entries.amazon-bedrock.config.discovery.enabled: true`. - OpenClaw surfaces the credential source in this order: `AWS_BEARER_TOKEN_BEDROCK`, then `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY`, then `AWS_PROFILE`, then the default AWS SDK chain. diff --git a/extensions/amazon-bedrock/discovery.test.ts b/extensions/amazon-bedrock/discovery.test.ts index c7d66500d80..ab2ec323f3e 100644 --- a/extensions/amazon-bedrock/discovery.test.ts +++ b/extensions/amazon-bedrock/discovery.test.ts @@ -5,6 +5,7 @@ import { mergeImplicitBedrockProvider, resetBedrockDiscoveryCacheForTest, resolveBedrockConfigApiKey, + resolveImplicitBedrockProvider, } from "./api.js"; const sendMock = vi.fn(); @@ -147,9 +148,9 @@ describe("bedrock discovery", () => { expect(resolveBedrockConfigApiKey({} as NodeJS.ProcessEnv)).toBeUndefined(); // When AWS_PROFILE is explicitly set, it should return the marker. - expect( - resolveBedrockConfigApiKey({ AWS_PROFILE: "default" } as NodeJS.ProcessEnv), - ).toBe("AWS_PROFILE"); + expect(resolveBedrockConfigApiKey({ AWS_PROFILE: "default" } as NodeJS.ProcessEnv)).toBe( + "AWS_PROFILE", + ); }); it("merges implicit Bedrock models into explicit provider overrides", () => { @@ -179,4 +180,48 @@ describe("bedrock discovery", () => { }).models?.map((model) => model.id), ).toEqual(["amazon.nova-micro-v1:0"]); }); + + it("prefers plugin-owned discovery config and still honors legacy fallback", async () => { + mockSingleActiveSummary(); + + const pluginEnabled = await resolveImplicitBedrockProvider({ + config: { + models: { + bedrockDiscovery: { + enabled: false, + region: "us-west-2", + }, + }, + }, + pluginConfig: { + discovery: { + enabled: true, + region: "us-east-1", + }, + }, + env: {} as NodeJS.ProcessEnv, + clientFactory, + }); + + expect(pluginEnabled?.baseUrl).toBe("https://bedrock-runtime.us-east-1.amazonaws.com"); + expect(sendMock).toHaveBeenCalledTimes(1); + + mockSingleActiveSummary(); + + const legacyEnabled = await resolveImplicitBedrockProvider({ + config: { + models: { + bedrockDiscovery: { + enabled: true, + region: "us-west-2", + }, + }, + }, + env: {} as NodeJS.ProcessEnv, + clientFactory, + }); + + expect(legacyEnabled?.baseUrl).toBe("https://bedrock-runtime.us-west-2.amazonaws.com"); + expect(sendMock).toHaveBeenCalledTimes(2); + }); }); diff --git a/extensions/amazon-bedrock/discovery.ts b/extensions/amazon-bedrock/discovery.ts index 30110fdba7b..04aa13beed0 100644 --- a/extensions/amazon-bedrock/discovery.ts +++ b/extensions/amazon-bedrock/discovery.ts @@ -242,10 +242,15 @@ export async function discoverBedrockModels(params: { export async function resolveImplicitBedrockProvider(params: { config?: { models?: { bedrockDiscovery?: BedrockDiscoveryConfig } }; + pluginConfig?: { discovery?: BedrockDiscoveryConfig }; env?: NodeJS.ProcessEnv; + clientFactory?: (region: string) => BedrockClient; }): Promise { const env = params.env ?? process.env; - const discoveryConfig = params.config?.models?.bedrockDiscovery; + const discoveryConfig = { + ...(params.config?.models?.bedrockDiscovery ?? {}), + ...(params.pluginConfig?.discovery ?? {}), + }; const enabled = discoveryConfig?.enabled; const hasAwsCreds = resolveAwsSdkEnvVarName(env) !== undefined; if (enabled === false) { @@ -259,6 +264,7 @@ export async function resolveImplicitBedrockProvider(params: { const models = await discoverBedrockModels({ region, config: discoveryConfig, + clientFactory: params.clientFactory, }); if (models.length === 0) { return null; diff --git a/extensions/amazon-bedrock/index.test.ts b/extensions/amazon-bedrock/index.test.ts index dda76f60869..e9c1d672aa0 100644 --- a/extensions/amazon-bedrock/index.test.ts +++ b/extensions/amazon-bedrock/index.test.ts @@ -152,12 +152,35 @@ describe("amazon-bedrock provider plugin", () => { }); describe("guardrail config schema", () => { - it("defines guardrail object with correct property types, required fields, and enums", () => { + it("defines discovery and guardrail objects with the expected shape", () => { const pluginJson = JSON.parse( readFileSync(resolve(import.meta.dirname, "openclaw.plugin.json"), "utf-8"), ); + const discovery = pluginJson.configSchema?.properties?.discovery; const guardrail = pluginJson.configSchema?.properties?.guardrail; + expect(discovery).toBeDefined(); + expect(discovery.type).toBe("object"); + expect(discovery.additionalProperties).toBe(false); + expect(discovery.properties.enabled).toEqual({ type: "boolean" }); + expect(discovery.properties.region).toEqual({ type: "string" }); + expect(discovery.properties.providerFilter).toEqual({ + type: "array", + items: { type: "string" }, + }); + expect(discovery.properties.refreshInterval).toEqual({ + type: "integer", + minimum: 0, + }); + expect(discovery.properties.defaultContextWindow).toEqual({ + type: "integer", + minimum: 1, + }); + expect(discovery.properties.defaultMaxTokens).toEqual({ + type: "integer", + minimum: 1, + }); + expect(guardrail).toBeDefined(); expect(guardrail.type).toBe("object"); expect(guardrail.additionalProperties).toBe(false); diff --git a/extensions/amazon-bedrock/openclaw.plugin.json b/extensions/amazon-bedrock/openclaw.plugin.json index 40f74e2e789..69491a0b310 100644 --- a/extensions/amazon-bedrock/openclaw.plugin.json +++ b/extensions/amazon-bedrock/openclaw.plugin.json @@ -6,6 +6,21 @@ "type": "object", "additionalProperties": false, "properties": { + "discovery": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "region": { "type": "string" }, + "providerFilter": { + "type": "array", + "items": { "type": "string" } + }, + "refreshInterval": { "type": "integer", "minimum": 0 }, + "defaultContextWindow": { "type": "integer", "minimum": 1 }, + "defaultMaxTokens": { "type": "integer", "minimum": 1 } + } + }, "guardrail": { "type": "object", "additionalProperties": false, @@ -18,5 +33,39 @@ "required": ["guardrailIdentifier", "guardrailVersion"] } } + }, + "uiHints": { + "discovery": { + "label": "Model Discovery", + "help": "Plugin-owned controls for Amazon Bedrock model auto-discovery." + }, + "discovery.enabled": { + "label": "Enable Discovery", + "help": "When false, OpenClaw keeps the Amazon Bedrock plugin available but skips implicit startup discovery. When true, discovery can run even without AWS auth env markers." + }, + "discovery.region": { + "label": "Discovery Region", + "help": "AWS region to use for Bedrock model discovery. Defaults to AWS_REGION, AWS_DEFAULT_REGION, then us-east-1." + }, + "discovery.providerFilter": { + "label": "Provider Filter", + "help": "Optional Bedrock provider-name allowlist for discovery, such as anthropic or amazon." + }, + "discovery.refreshInterval": { + "label": "Discovery Refresh Interval (s)", + "help": "How long to cache Bedrock discovery results in seconds. Set to 0 to disable caching." + }, + "discovery.defaultContextWindow": { + "label": "Default Context Window", + "help": "Fallback context window to assign to discovered Bedrock models." + }, + "discovery.defaultMaxTokens": { + "label": "Default Max Tokens", + "help": "Fallback max output tokens to assign to discovered Bedrock models." + }, + "guardrail": { + "label": "Guardrail", + "help": "Amazon Bedrock Guardrails settings applied to Bedrock model invocations." + } } } diff --git a/extensions/amazon-bedrock/register.sync.runtime.ts b/extensions/amazon-bedrock/register.sync.runtime.ts index 1b37660d641..179d0b82d93 100644 --- a/extensions/amazon-bedrock/register.sync.runtime.ts +++ b/extensions/amazon-bedrock/register.sync.runtime.ts @@ -19,6 +19,18 @@ type GuardrailConfig = { trace?: "enabled" | "disabled" | "enabled_full"; }; +type AmazonBedrockPluginConfig = { + discovery?: { + enabled?: boolean; + region?: string; + providerFilter?: string[]; + refreshInterval?: number; + defaultContextWindow?: number; + defaultMaxTokens?: number; + }; + guardrail?: GuardrailConfig; +}; + function createGuardrailWrapStreamFn( innerWrapStreamFn: (ctx: { modelId: string; streamFn?: StreamFn }) => StreamFn | null | undefined, guardrailConfig: GuardrailConfig, @@ -57,9 +69,8 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void { const anthropicByModelReplayHooks = buildProviderReplayFamilyHooks({ family: "anthropic-by-model", }); - const guardrail = (api.pluginConfig as Record | undefined)?.guardrail as - | GuardrailConfig - | undefined; + const pluginConfig = (api.pluginConfig ?? {}) as AmazonBedrockPluginConfig; + const guardrail = pluginConfig.guardrail; const baseWrapStreamFn = ({ modelId, streamFn }: { modelId: string; streamFn?: StreamFn }) => isAnthropicBedrockModel(modelId) ? streamFn : createBedrockNoCacheWrapper(streamFn); @@ -79,6 +90,7 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void { run: async (ctx) => { const implicit = await resolveImplicitBedrockProvider({ config: ctx.config, + pluginConfig, env: ctx.env, }); if (!implicit) { diff --git a/extensions/github-copilot/index.test.ts b/extensions/github-copilot/index.test.ts index 2a5d0c7d298..700b3c0c99b 100644 --- a/extensions/github-copilot/index.test.ts +++ b/extensions/github-copilot/index.test.ts @@ -13,6 +13,10 @@ vi.mock("./register.runtime.js", () => ({ import plugin from "./index.js"; function registerProvider() { + return registerProviderWithPluginConfig({}); +} + +function registerProviderWithPluginConfig(pluginConfig: Record) { const registerProviderMock = vi.fn(); plugin.register( @@ -21,6 +25,7 @@ function registerProvider() { name: "GitHub Copilot", source: "test", config: {}, + pluginConfig, runtime: {} as never, registerProvider: registerProviderMock, }), @@ -31,11 +36,11 @@ function registerProvider() { } describe("github-copilot plugin", () => { - it("skips catalog discovery when models.copilotDiscovery.enabled is false", async () => { - const provider = registerProvider(); + it("skips catalog discovery when plugin discovery is disabled", async () => { + const provider = registerProviderWithPluginConfig({ discovery: { enabled: false } }); const result = await provider.catalog.run({ - config: { models: { copilotDiscovery: { enabled: false } } }, + config: {}, agentDir: "/tmp/agent", env: { GH_TOKEN: "gh_test_token" }, resolveProviderApiKey: () => ({ apiKey: "gh_test_token" }), diff --git a/extensions/github-copilot/index.ts b/extensions/github-copilot/index.ts index 8d3856f34ba..75422d2b422 100644 --- a/extensions/github-copilot/index.ts +++ b/extensions/github-copilot/index.ts @@ -11,6 +11,12 @@ import { wrapCopilotProviderStream } from "./stream.js"; const COPILOT_ENV_VARS = ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"]; const COPILOT_XHIGH_MODEL_IDS = ["gpt-5.2", "gpt-5.2-codex"] as const; +type GithubCopilotPluginConfig = { + discovery?: { + enabled?: boolean; + }; +}; + async function loadGithubCopilotRuntime() { return await import("./register.runtime.js"); } @@ -19,6 +25,7 @@ export default definePluginEntry({ name: "GitHub Copilot Provider", description: "Bundled GitHub Copilot provider plugin", register(api) { + const pluginConfig = (api.pluginConfig ?? {}) as GithubCopilotPluginConfig; function resolveFirstGithubToken(params: { agentDir?: string; env: NodeJS.ProcessEnv }): { githubToken: string; hasProfile: boolean; @@ -125,7 +132,9 @@ export default definePluginEntry({ catalog: { order: "late", run: async (ctx) => { - if (ctx.config?.models?.copilotDiscovery?.enabled === false) { + const discoveryEnabled = + pluginConfig.discovery?.enabled ?? ctx.config?.models?.copilotDiscovery?.enabled; + if (discoveryEnabled === false) { return null; } const { DEFAULT_COPILOT_API_BASE_URL, resolveCopilotApiToken } = diff --git a/extensions/github-copilot/openclaw.plugin.json b/extensions/github-copilot/openclaw.plugin.json index 6502677860a..f970121545a 100644 --- a/extensions/github-copilot/openclaw.plugin.json +++ b/extensions/github-copilot/openclaw.plugin.json @@ -20,6 +20,24 @@ "configSchema": { "type": "object", "additionalProperties": false, - "properties": {} + "properties": { + "discovery": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" } + } + } + } + }, + "uiHints": { + "discovery": { + "label": "Model Discovery", + "help": "Plugin-owned controls for GitHub Copilot model auto-discovery." + }, + "discovery.enabled": { + "label": "Enable Discovery", + "help": "When false, OpenClaw keeps the GitHub Copilot plugin available but skips implicit startup discovery from ambient Copilot credentials." + } } } diff --git a/extensions/huggingface/index.test.ts b/extensions/huggingface/index.test.ts index 38aaf58b06f..c692b248397 100644 --- a/extensions/huggingface/index.test.ts +++ b/extensions/huggingface/index.test.ts @@ -21,6 +21,10 @@ vi.mock("./onboard.js", () => ({ import plugin from "./index.js"; function registerProvider() { + return registerProviderWithPluginConfig({}); +} + +function registerProviderWithPluginConfig(pluginConfig: Record) { const registerProviderMock = vi.fn(); plugin.register( @@ -29,6 +33,7 @@ function registerProvider() { name: "Hugging Face", source: "test", config: {}, + pluginConfig, runtime: {} as never, registerProvider: registerProviderMock, }), @@ -39,11 +44,11 @@ function registerProvider() { } describe("huggingface plugin", () => { - it("skips catalog discovery when models.huggingfaceDiscovery.enabled is false", async () => { - const provider = registerProvider(); + it("skips catalog discovery when plugin discovery is disabled", async () => { + const provider = registerProviderWithPluginConfig({ discovery: { enabled: false } }); const result = await provider.catalog.run({ - config: { models: { huggingfaceDiscovery: { enabled: false } } }, + config: {}, resolveProviderApiKey: () => ({ apiKey: "hf_test_token", discoveryApiKey: "hf_test_token", diff --git a/extensions/huggingface/index.ts b/extensions/huggingface/index.ts index a10b60f2e74..61d87686afd 100644 --- a/extensions/huggingface/index.ts +++ b/extensions/huggingface/index.ts @@ -5,11 +5,18 @@ import { buildHuggingfaceProvider } from "./provider-catalog.js"; const PROVIDER_ID = "huggingface"; +type HuggingFacePluginConfig = { + discovery?: { + enabled?: boolean; + }; +}; + export default definePluginEntry({ id: PROVIDER_ID, name: "Hugging Face Provider", description: "Bundled Hugging Face provider plugin", register(api) { + const pluginConfig = (api.pluginConfig ?? {}) as HuggingFacePluginConfig; api.registerProvider({ id: PROVIDER_ID, label: "Hugging Face", @@ -41,7 +48,9 @@ export default definePluginEntry({ catalog: { order: "simple", run: async (ctx) => { - if (ctx.config?.models?.huggingfaceDiscovery?.enabled === false) { + const discoveryEnabled = + pluginConfig.discovery?.enabled ?? ctx.config?.models?.huggingfaceDiscovery?.enabled; + if (discoveryEnabled === false) { return null; } const { apiKey, discoveryApiKey } = ctx.resolveProviderApiKey(PROVIDER_ID); diff --git a/extensions/huggingface/openclaw.plugin.json b/extensions/huggingface/openclaw.plugin.json index 5308872c8f1..343162f0502 100644 --- a/extensions/huggingface/openclaw.plugin.json +++ b/extensions/huggingface/openclaw.plugin.json @@ -24,6 +24,24 @@ "configSchema": { "type": "object", "additionalProperties": false, - "properties": {} + "properties": { + "discovery": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" } + } + } + } + }, + "uiHints": { + "discovery": { + "label": "Model Discovery", + "help": "Plugin-owned controls for Hugging Face model auto-discovery." + }, + "discovery.enabled": { + "label": "Enable Discovery", + "help": "When false, OpenClaw keeps the Hugging Face plugin available but skips implicit startup discovery from ambient Hugging Face credentials." + } } } diff --git a/extensions/ollama/index.test.ts b/extensions/ollama/index.test.ts index 7a58b22954b..16efc6e59ad 100644 --- a/extensions/ollama/index.test.ts +++ b/extensions/ollama/index.test.ts @@ -34,6 +34,10 @@ beforeEach(() => { }); function registerProvider() { + return registerProviderWithPluginConfig({}); +} + +function registerProviderWithPluginConfig(pluginConfig: Record) { const registerProviderMock = vi.fn(); plugin.register( @@ -42,6 +46,7 @@ function registerProvider() { name: "Ollama", source: "test", config: {}, + pluginConfig, runtime: {} as never, registerProvider: registerProviderMock, }), @@ -109,11 +114,11 @@ describe("ollama plugin", () => { }); }); - it("skips ambient discovery when models.ollamaDiscovery.enabled is false", async () => { - const provider = registerProvider(); + it("skips ambient discovery when plugin discovery is disabled", async () => { + const provider = registerProviderWithPluginConfig({ discovery: { enabled: false } }); const result = await provider.discovery.run({ - config: { models: { ollamaDiscovery: { enabled: false } } }, + config: {}, env: {}, resolveProviderApiKey: () => ({ apiKey: "", discoveryApiKey: "" }), } as never); diff --git a/extensions/ollama/index.ts b/extensions/ollama/index.ts index 15c757f7127..68ae925ba5f 100644 --- a/extensions/ollama/index.ts +++ b/extensions/ollama/index.ts @@ -31,6 +31,12 @@ const OPENAI_COMPATIBLE_REPLAY_HOOKS = buildProviderReplayFamilyHooks({ family: "openai-compatible", }); +type OllamaPluginConfig = { + discovery?: { + enabled?: boolean; + }; +}; + function resolveOllamaDiscoveryApiKey(params: { env: NodeJS.ProcessEnv; explicitApiKey?: string; @@ -51,6 +57,7 @@ export default definePluginEntry({ name: "Ollama Provider", description: "Bundled Ollama provider plugin", register(api: OpenClawPluginApi) { + const pluginConfig = (api.pluginConfig ?? {}) as OllamaPluginConfig; api.registerWebSearchProvider(createOllamaWebSearchProvider()); api.registerProvider({ id: PROVIDER_ID, @@ -102,7 +109,9 @@ export default definePluginEntry({ run: async (ctx: ProviderDiscoveryContext) => { const explicit = ctx.config.models?.providers?.ollama; const hasExplicitModels = Array.isArray(explicit?.models) && explicit.models.length > 0; - if (!hasExplicitModels && ctx.config.models?.ollamaDiscovery?.enabled === false) { + const discoveryEnabled = + pluginConfig.discovery?.enabled ?? ctx.config.models?.ollamaDiscovery?.enabled; + if (!hasExplicitModels && discoveryEnabled === false) { return null; } const ollamaKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey; diff --git a/extensions/ollama/openclaw.plugin.json b/extensions/ollama/openclaw.plugin.json index 34703655482..a44ef5956cd 100644 --- a/extensions/ollama/openclaw.plugin.json +++ b/extensions/ollama/openclaw.plugin.json @@ -23,6 +23,24 @@ "configSchema": { "type": "object", "additionalProperties": false, - "properties": {} + "properties": { + "discovery": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" } + } + } + } + }, + "uiHints": { + "discovery": { + "label": "Model Discovery", + "help": "Plugin-owned controls for Ollama model auto-discovery." + }, + "discovery.enabled": { + "label": "Enable Discovery", + "help": "When false, OpenClaw keeps the Ollama plugin available but skips implicit startup discovery of ambient local or remote Ollama models." + } } } diff --git a/src/config/schema.base.generated.ts b/src/config/schema.base.generated.ts index fc10e0ae49a..20626f24a61 100644 --- a/src/config/schema.base.generated.ts +++ b/src/config/schema.base.generated.ts @@ -2893,105 +2893,6 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { description: "Provider map keyed by provider ID containing connection/auth settings and concrete model definitions. Use stable provider keys so references from agents and tooling remain portable across environments.", }, - bedrockDiscovery: { - type: "object", - properties: { - enabled: { - type: "boolean", - title: "Bedrock Discovery Enabled", - description: - "Enables periodic Bedrock model discovery and catalog refresh for Bedrock-backed providers. Keep disabled unless Bedrock is actively used and IAM permissions are correctly configured.", - }, - region: { - type: "string", - title: "Bedrock Discovery Region", - description: - "AWS region used for Bedrock discovery calls when discovery is enabled for your deployment. Use the region where your Bedrock models are provisioned to avoid empty discovery results.", - }, - providerFilter: { - type: "array", - items: { - type: "string", - }, - title: "Bedrock Discovery Provider Filter", - description: - "Optional provider allowlist filter for Bedrock discovery so only selected providers are refreshed. Use this to limit discovery scope in multi-provider environments.", - }, - refreshInterval: { - type: "integer", - minimum: 0, - maximum: 9007199254740991, - title: "Bedrock Discovery Refresh Interval (s)", - description: - "Refresh cadence for Bedrock discovery polling in seconds to detect newly available models over time. Use longer intervals in production to reduce API cost and control-plane noise.", - }, - defaultContextWindow: { - type: "integer", - exclusiveMinimum: 0, - maximum: 9007199254740991, - title: "Bedrock Default Context Window", - description: - "Fallback context-window value applied to discovered models when provider metadata lacks explicit limits. Use realistic defaults to avoid oversized prompts that exceed true provider constraints.", - }, - defaultMaxTokens: { - type: "integer", - exclusiveMinimum: 0, - maximum: 9007199254740991, - title: "Bedrock Default Max Tokens", - description: - "Fallback max-token value applied to discovered models without explicit output token limits. Use conservative defaults to reduce truncation surprises and unexpected token spend.", - }, - }, - additionalProperties: false, - title: "Bedrock Model Discovery", - description: - "Automatic AWS Bedrock model discovery settings used to synthesize provider model entries from account visibility. Keep discovery scoped and refresh intervals conservative to reduce API churn.", - }, - copilotDiscovery: { - type: "object", - properties: { - enabled: { - type: "boolean", - title: "Copilot Discovery Enabled", - description: - "Set to false to prevent Copilot discovery from running even when GitHub tokens are detected. Useful when GH_TOKEN is set for other tools and you do not want Copilot provider auto-registration.", - }, - }, - additionalProperties: false, - title: "Copilot Model Discovery", - description: - "GitHub Copilot implicit discovery settings. Controls whether OpenClaw probes for Copilot API access when GH_TOKEN or GITHUB_TOKEN is present.", - }, - huggingfaceDiscovery: { - type: "object", - properties: { - enabled: { - type: "boolean", - title: "Hugging Face Discovery Enabled", - description: - "Set to false to prevent Hugging Face model discovery from running even when HF_TOKEN is detected. Useful when the token is set for other tools like transformers-cli.", - }, - }, - additionalProperties: false, - title: "Hugging Face Model Discovery", - description: - "Hugging Face implicit discovery settings. Controls whether OpenClaw fetches the Hugging Face model catalog when HF_TOKEN is present.", - }, - ollamaDiscovery: { - type: "object", - properties: { - enabled: { - type: "boolean", - title: "Ollama Discovery Enabled", - description: - "Set to false to prevent Ollama discovery from probing localhost:11434 on startup. Useful when Ollama is not intended for OpenClaw or the local probe causes startup delays.", - }, - }, - additionalProperties: false, - title: "Ollama Model Discovery", - description: - "Ollama implicit discovery settings. Controls whether OpenClaw probes the local Ollama server for available models on startup.", - }, }, additionalProperties: false, title: "Models", @@ -24941,71 +24842,6 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { help: "Declared model list for a provider including identifiers, metadata, and optional compatibility/cost hints. Keep IDs exact to provider catalog values so selection and fallback resolve correctly.", tags: ["models"], }, - "models.bedrockDiscovery": { - label: "Bedrock Model Discovery", - help: "Automatic AWS Bedrock model discovery settings used to synthesize provider model entries from account visibility. Keep discovery scoped and refresh intervals conservative to reduce API churn.", - tags: ["models"], - }, - "models.bedrockDiscovery.enabled": { - label: "Bedrock Discovery Enabled", - help: "Enables periodic Bedrock model discovery and catalog refresh for Bedrock-backed providers. Keep disabled unless Bedrock is actively used and IAM permissions are correctly configured.", - tags: ["models"], - }, - "models.bedrockDiscovery.region": { - label: "Bedrock Discovery Region", - help: "AWS region used for Bedrock discovery calls when discovery is enabled for your deployment. Use the region where your Bedrock models are provisioned to avoid empty discovery results.", - tags: ["models"], - }, - "models.bedrockDiscovery.providerFilter": { - label: "Bedrock Discovery Provider Filter", - help: "Optional provider allowlist filter for Bedrock discovery so only selected providers are refreshed. Use this to limit discovery scope in multi-provider environments.", - tags: ["models"], - }, - "models.bedrockDiscovery.refreshInterval": { - label: "Bedrock Discovery Refresh Interval (s)", - help: "Refresh cadence for Bedrock discovery polling in seconds to detect newly available models over time. Use longer intervals in production to reduce API cost and control-plane noise.", - tags: ["performance", "models"], - }, - "models.bedrockDiscovery.defaultContextWindow": { - label: "Bedrock Default Context Window", - help: "Fallback context-window value applied to discovered models when provider metadata lacks explicit limits. Use realistic defaults to avoid oversized prompts that exceed true provider constraints.", - tags: ["models"], - }, - "models.bedrockDiscovery.defaultMaxTokens": { - label: "Bedrock Default Max Tokens", - help: "Fallback max-token value applied to discovered models without explicit output token limits. Use conservative defaults to reduce truncation surprises and unexpected token spend.", - tags: ["security", "auth", "performance", "models"], - }, - "models.copilotDiscovery": { - label: "Copilot Model Discovery", - help: "GitHub Copilot implicit discovery settings. Controls whether OpenClaw probes for Copilot API access when GH_TOKEN or GITHUB_TOKEN is present.", - tags: ["models"], - }, - "models.copilotDiscovery.enabled": { - label: "Copilot Discovery Enabled", - help: "Set to false to prevent Copilot discovery from running even when GitHub tokens are detected. Useful when GH_TOKEN is set for other tools and you do not want Copilot provider auto-registration.", - tags: ["models"], - }, - "models.huggingfaceDiscovery": { - label: "Hugging Face Model Discovery", - help: "Hugging Face implicit discovery settings. Controls whether OpenClaw fetches the Hugging Face model catalog when HF_TOKEN is present.", - tags: ["models"], - }, - "models.huggingfaceDiscovery.enabled": { - label: "Hugging Face Discovery Enabled", - help: "Set to false to prevent Hugging Face model discovery from running even when HF_TOKEN is detected. Useful when the token is set for other tools like transformers-cli.", - tags: ["models"], - }, - "models.ollamaDiscovery": { - label: "Ollama Model Discovery", - help: "Ollama implicit discovery settings. Controls whether OpenClaw probes the local Ollama server for available models on startup.", - tags: ["models"], - }, - "models.ollamaDiscovery.enabled": { - label: "Ollama Discovery Enabled", - help: "Set to false to prevent Ollama discovery from probing localhost:11434 on startup. Useful when Ollama is not intended for OpenClaw or the local probe causes startup delays.", - tags: ["models"], - }, "auth.cooldowns.billingBackoffHours": { label: "Billing Backoff (hours)", help: "Base backoff (hours) when a profile fails due to billing/insufficient credits (default: 5).", diff --git a/src/config/schema.help.quality.test.ts b/src/config/schema.help.quality.test.ts index 494afa87ce9..95db6c4a621 100644 --- a/src/config/schema.help.quality.test.ts +++ b/src/config/schema.help.quality.test.ts @@ -366,19 +366,6 @@ const TARGET_KEYS = [ "models.providers.*.api", "models.providers.*.headers", "models.providers.*.models", - "models.bedrockDiscovery", - "models.bedrockDiscovery.enabled", - "models.bedrockDiscovery.region", - "models.bedrockDiscovery.providerFilter", - "models.bedrockDiscovery.refreshInterval", - "models.bedrockDiscovery.defaultContextWindow", - "models.bedrockDiscovery.defaultMaxTokens", - "models.copilotDiscovery", - "models.copilotDiscovery.enabled", - "models.huggingfaceDiscovery", - "models.huggingfaceDiscovery.enabled", - "models.ollamaDiscovery", - "models.ollamaDiscovery.enabled", "agents", "agents.defaults", "agents.list", @@ -782,10 +769,6 @@ describe("config help copy quality", () => { expect(modelsMode.includes("SecretRef-managed")).toBe(true); expect(modelsMode.includes("preserve")).toBe(true); - const bedrockRefresh = FIELD_HELP["models.bedrockDiscovery.refreshInterval"]; - expect(/refresh|seconds|interval/i.test(bedrockRefresh)).toBe(true); - expect(/cost|noise|api/i.test(bedrockRefresh)).toBe(true); - const authCooldowns = FIELD_HELP["auth.cooldowns"]; expect(/cooldown|backoff|retry/i.test(authCooldowns)).toBe(true); }); diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index 11a0dd3d9d6..b6c7152303b 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -789,32 +789,6 @@ export const FIELD_HELP: Record = { "Skips upstream TLS certificate verification. Use only for controlled development environments.", "models.providers.*.models": "Declared model list for a provider including identifiers, metadata, and optional compatibility/cost hints. Keep IDs exact to provider catalog values so selection and fallback resolve correctly.", - "models.bedrockDiscovery": - "Automatic AWS Bedrock model discovery settings used to synthesize provider model entries from account visibility. Keep discovery scoped and refresh intervals conservative to reduce API churn.", - "models.bedrockDiscovery.enabled": - "Enables periodic Bedrock model discovery and catalog refresh for Bedrock-backed providers. Keep disabled unless Bedrock is actively used and IAM permissions are correctly configured.", - "models.bedrockDiscovery.region": - "AWS region used for Bedrock discovery calls when discovery is enabled for your deployment. Use the region where your Bedrock models are provisioned to avoid empty discovery results.", - "models.bedrockDiscovery.providerFilter": - "Optional provider allowlist filter for Bedrock discovery so only selected providers are refreshed. Use this to limit discovery scope in multi-provider environments.", - "models.bedrockDiscovery.refreshInterval": - "Refresh cadence for Bedrock discovery polling in seconds to detect newly available models over time. Use longer intervals in production to reduce API cost and control-plane noise.", - "models.bedrockDiscovery.defaultContextWindow": - "Fallback context-window value applied to discovered models when provider metadata lacks explicit limits. Use realistic defaults to avoid oversized prompts that exceed true provider constraints.", - "models.bedrockDiscovery.defaultMaxTokens": - "Fallback max-token value applied to discovered models without explicit output token limits. Use conservative defaults to reduce truncation surprises and unexpected token spend.", - "models.copilotDiscovery": - "GitHub Copilot implicit discovery settings. Controls whether OpenClaw probes for Copilot API access when GH_TOKEN or GITHUB_TOKEN is present.", - "models.copilotDiscovery.enabled": - "Set to false to prevent Copilot discovery from running even when GitHub tokens are detected. Useful when GH_TOKEN is set for other tools and you do not want Copilot provider auto-registration.", - "models.huggingfaceDiscovery": - "Hugging Face implicit discovery settings. Controls whether OpenClaw fetches the Hugging Face model catalog when HF_TOKEN is present.", - "models.huggingfaceDiscovery.enabled": - "Set to false to prevent Hugging Face model discovery from running even when HF_TOKEN is detected. Useful when the token is set for other tools like transformers-cli.", - "models.ollamaDiscovery": - "Ollama implicit discovery settings. Controls whether OpenClaw probes the local Ollama server for available models on startup.", - "models.ollamaDiscovery.enabled": - "Set to false to prevent Ollama discovery from probing localhost:11434 on startup. Useful when Ollama is not intended for OpenClaw or the local probe causes startup delays.", auth: "Authentication profile root used for multi-profile provider credentials and cooldown-based failover ordering. Keep profiles minimal and explicit so automatic failover behavior stays auditable.", "channels.matrix.allowBots": 'Allow messages from other configured Matrix bot accounts to trigger replies (default: false). Set "mentions" to only accept bot messages that visibly mention this bot.', diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index 83273a3ac2c..65ef2d854bf 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -476,19 +476,6 @@ export const FIELD_LABELS: Record = { "models.providers.*.request.tls.serverName": "Model Provider Request TLS Server Name", "models.providers.*.request.tls.insecureSkipVerify": "Model Provider Request TLS Skip Verify", "models.providers.*.models": "Model Provider Model List", - "models.bedrockDiscovery": "Bedrock Model Discovery", - "models.bedrockDiscovery.enabled": "Bedrock Discovery Enabled", - "models.bedrockDiscovery.region": "Bedrock Discovery Region", - "models.bedrockDiscovery.providerFilter": "Bedrock Discovery Provider Filter", - "models.bedrockDiscovery.refreshInterval": "Bedrock Discovery Refresh Interval (s)", - "models.bedrockDiscovery.defaultContextWindow": "Bedrock Default Context Window", - "models.bedrockDiscovery.defaultMaxTokens": "Bedrock Default Max Tokens", - "models.copilotDiscovery": "Copilot Model Discovery", - "models.copilotDiscovery.enabled": "Copilot Discovery Enabled", - "models.huggingfaceDiscovery": "Hugging Face Model Discovery", - "models.huggingfaceDiscovery.enabled": "Hugging Face Discovery Enabled", - "models.ollamaDiscovery": "Ollama Model Discovery", - "models.ollamaDiscovery.enabled": "Ollama Discovery Enabled", "auth.cooldowns.billingBackoffHours": "Billing Backoff (hours)", "auth.cooldowns.billingBackoffHoursByProvider": "Billing Backoff Overrides", "auth.cooldowns.billingMaxHours": "Billing Backoff Cap (hours)", diff --git a/src/config/types.models.ts b/src/config/types.models.ts index a33eede3b69..afd82ee6806 100644 --- a/src/config/types.models.ts +++ b/src/config/types.models.ts @@ -99,6 +99,8 @@ export type DiscoveryToggleConfig = { export type ModelsConfig = { mode?: "merge" | "replace"; providers?: Record; + // Deprecated legacy compat aliases. Kept in the runtime type surface so + // doctor/runtime fallbacks can read older configs until migration completes. bedrockDiscovery?: BedrockDiscoveryConfig; copilotDiscovery?: DiscoveryToggleConfig; huggingfaceDiscovery?: DiscoveryToggleConfig; diff --git a/src/config/zod-schema.core.ts b/src/config/zod-schema.core.ts index 98f3afe85cf..e4af9740a30 100644 --- a/src/config/zod-schema.core.ts +++ b/src/config/zod-schema.core.ts @@ -342,21 +342,10 @@ export const BedrockDiscoverySchema = z .strict() .optional(); -export const DiscoveryToggleSchema = z - .object({ - enabled: z.boolean().optional(), - }) - .strict() - .optional(); - export const ModelsConfigSchema = z .object({ mode: z.union([z.literal("merge"), z.literal("replace")]).optional(), providers: z.record(z.string(), ModelProviderSchema).optional(), - bedrockDiscovery: BedrockDiscoverySchema, - copilotDiscovery: DiscoveryToggleSchema, - huggingfaceDiscovery: DiscoveryToggleSchema, - ollamaDiscovery: DiscoveryToggleSchema, }) .strict() .optional();