refactor: move provider discovery config into plugins

This commit is contained in:
Peter Steinberger
2026-04-05 09:55:19 +01:00
parent 4613f121ad
commit 19de5d1b56
24 changed files with 295 additions and 285 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -652,7 +652,7 @@ for usage/billing and raise limits as needed.
</Accordion>
<Accordion title="Is AWS Bedrock supported?">
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.
</Accordion>
<Accordion title="How does Codex auth work?">

View File

@@ -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.

View File

@@ -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);
});
});

View File

@@ -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<ModelProviderConfig | null> {
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;

View File

@@ -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);

View File

@@ -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."
}
}
}

View File

@@ -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<string, unknown> | 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) {

View File

@@ -13,6 +13,10 @@ vi.mock("./register.runtime.js", () => ({
import plugin from "./index.js";
function registerProvider() {
return registerProviderWithPluginConfig({});
}
function registerProviderWithPluginConfig(pluginConfig: Record<string, unknown>) {
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" }),

View File

@@ -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 } =

View File

@@ -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."
}
}
}

View File

@@ -21,6 +21,10 @@ vi.mock("./onboard.js", () => ({
import plugin from "./index.js";
function registerProvider() {
return registerProviderWithPluginConfig({});
}
function registerProviderWithPluginConfig(pluginConfig: Record<string, unknown>) {
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",

View File

@@ -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);

View File

@@ -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."
}
}
}

View File

@@ -34,6 +34,10 @@ beforeEach(() => {
});
function registerProvider() {
return registerProviderWithPluginConfig({});
}
function registerProviderWithPluginConfig(pluginConfig: Record<string, unknown>) {
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);

View File

@@ -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;

View File

@@ -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."
}
}
}

View File

@@ -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).",

View File

@@ -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);
});

View File

@@ -789,32 +789,6 @@ export const FIELD_HELP: Record<string, string> = {
"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.',

View File

@@ -476,19 +476,6 @@ export const FIELD_LABELS: Record<string, string> = {
"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)",

View File

@@ -99,6 +99,8 @@ export type DiscoveryToggleConfig = {
export type ModelsConfig = {
mode?: "merge" | "replace";
providers?: Record<string, ModelProviderConfig>;
// 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;

View File

@@ -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();