feat: add implicit discovery toggles

This commit is contained in:
Peter Steinberger
2026-04-05 09:27:33 +01:00
parent bff55b55cb
commit 455c642acb
13 changed files with 254 additions and 6 deletions

View File

@@ -1,4 +1,4 @@
20a882f9991e17310013471756ac7ec62c272e29490daeede9c0901bd51c0e69 config-baseline.json
8ba6e5c959d5fc3eee9e6c5d1d8f764f164052f4207c0352bb39e2a7dbad64a8 config-baseline.core.json
ca6d1fa8a3507566979ea2da2b88a6a7ae49d650f3ebd3eee14a22ed18e5be89 config-baseline.channel.json
17fd37605bf6cb087932ec2ebcfa9dd22e669fa6b8b93081ab2deac9d24821c5 config-baseline.plugin.json
8bbf281d0c63e38098b2132174b77ed58faf2083fb68cb88f90ebe76d7acda1b config-baseline.json
7163170accb9a8b62455ede5437f057d5a9e9ab5da42010cf0f39cbad952071d config-baseline.core.json
3c999707b167138de34f6255e3488b99e404c5132d3fc5879a1fa12d815c31f5 config-baseline.channel.json
76d011c68b8bc44ec862afa826dd8ddd7c577d89ce0b822eed306f8e1e9301ab config-baseline.plugin.json

View File

@@ -0,0 +1,47 @@
import { describe, expect, it, vi } from "vitest";
import { createTestPluginApi } from "../../test/helpers/plugins/plugin-api.js";
const resolveCopilotApiTokenMock = vi.hoisted(() => vi.fn());
vi.mock("./register.runtime.js", () => ({
DEFAULT_COPILOT_API_BASE_URL: "https://api.githubcopilot.test",
resolveCopilotApiToken: resolveCopilotApiTokenMock,
githubCopilotLoginCommand: vi.fn(),
fetchCopilotUsage: vi.fn(),
}));
import plugin from "./index.js";
function registerProvider() {
const registerProviderMock = vi.fn();
plugin.register(
createTestPluginApi({
id: "github-copilot",
name: "GitHub Copilot",
source: "test",
config: {},
runtime: {} as never,
registerProvider: registerProviderMock,
}),
);
expect(registerProviderMock).toHaveBeenCalledTimes(1);
return registerProviderMock.mock.calls[0]?.[0];
}
describe("github-copilot plugin", () => {
it("skips catalog discovery when models.copilotDiscovery.enabled is false", async () => {
const provider = registerProvider();
const result = await provider.catalog.run({
config: { models: { copilotDiscovery: { enabled: false } } },
agentDir: "/tmp/agent",
env: { GH_TOKEN: "gh_test_token" },
resolveProviderApiKey: () => ({ apiKey: "gh_test_token" }),
} as never);
expect(result).toBeNull();
expect(resolveCopilotApiTokenMock).not.toHaveBeenCalled();
});
});

View File

@@ -125,6 +125,9 @@ export default definePluginEntry({
catalog: {
order: "late",
run: async (ctx) => {
if (ctx.config?.models?.copilotDiscovery?.enabled === false) {
return null;
}
const { DEFAULT_COPILOT_API_BASE_URL, resolveCopilotApiToken } =
await loadGithubCopilotRuntime();
const { githubToken, hasProfile } = resolveFirstGithubToken({

View File

@@ -0,0 +1,56 @@
import { describe, expect, it, vi } from "vitest";
import { createTestPluginApi } from "../../test/helpers/plugins/plugin-api.js";
const buildHuggingfaceProviderMock = vi.hoisted(() =>
vi.fn(async () => ({
baseUrl: "https://router.huggingface.co/v1",
api: "openai-completions",
models: [],
})),
);
vi.mock("./provider-catalog.js", () => ({
buildHuggingfaceProvider: buildHuggingfaceProviderMock,
}));
vi.mock("./onboard.js", () => ({
applyHuggingfaceConfig: vi.fn((cfg) => cfg),
HUGGINGFACE_DEFAULT_MODEL_REF: "huggingface/deepseek-ai/DeepSeek-R1",
}));
import plugin from "./index.js";
function registerProvider() {
const registerProviderMock = vi.fn();
plugin.register(
createTestPluginApi({
id: "huggingface",
name: "Hugging Face",
source: "test",
config: {},
runtime: {} as never,
registerProvider: registerProviderMock,
}),
);
expect(registerProviderMock).toHaveBeenCalledTimes(1);
return registerProviderMock.mock.calls[0]?.[0];
}
describe("huggingface plugin", () => {
it("skips catalog discovery when models.huggingfaceDiscovery.enabled is false", async () => {
const provider = registerProvider();
const result = await provider.catalog.run({
config: { models: { huggingfaceDiscovery: { enabled: false } } },
resolveProviderApiKey: () => ({
apiKey: "hf_test_token",
discoveryApiKey: "hf_test_token",
}),
} as never);
expect(result).toBeNull();
expect(buildHuggingfaceProviderMock).not.toHaveBeenCalled();
});
});

View File

@@ -41,6 +41,9 @@ export default definePluginEntry({
catalog: {
order: "simple",
run: async (ctx) => {
if (ctx.config?.models?.huggingfaceDiscovery?.enabled === false) {
return null;
}
const { apiKey, discoveryApiKey } = ctx.resolveProviderApiKey(PROVIDER_ID);
if (!apiKey) {
return null;

View File

@@ -1,4 +1,4 @@
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createTestPluginApi } from "../../test/helpers/plugins/plugin-api.js";
import plugin from "./index.js";
@@ -18,14 +18,21 @@ const promptAndConfigureOllamaMock = vi.hoisted(() =>
})),
);
const ensureOllamaModelPulledMock = vi.hoisted(() => vi.fn(async () => {}));
const buildOllamaProviderMock = vi.hoisted(() => vi.fn());
vi.mock("./api.js", () => ({
promptAndConfigureOllama: promptAndConfigureOllamaMock,
ensureOllamaModelPulled: ensureOllamaModelPulledMock,
configureOllamaNonInteractive: vi.fn(),
buildOllamaProvider: vi.fn(),
buildOllamaProvider: buildOllamaProviderMock,
}));
beforeEach(() => {
promptAndConfigureOllamaMock.mockClear();
ensureOllamaModelPulledMock.mockClear();
buildOllamaProviderMock.mockReset();
});
function registerProvider() {
const registerProviderMock = vi.fn();
@@ -102,6 +109,19 @@ describe("ollama plugin", () => {
});
});
it("skips ambient discovery when models.ollamaDiscovery.enabled is false", async () => {
const provider = registerProvider();
const result = await provider.discovery.run({
config: { models: { ollamaDiscovery: { enabled: false } } },
env: {},
resolveProviderApiKey: () => ({ apiKey: "", discoveryApiKey: "" }),
} as never);
expect(result).toBeNull();
expect(buildOllamaProviderMock).not.toHaveBeenCalled();
});
it("wraps OpenAI-compatible payloads with num_ctx for Ollama compat routes", () => {
const provider = registerProvider();
let payloadSeen: Record<string, unknown> | undefined;

View File

@@ -102,6 +102,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) {
return null;
}
const ollamaKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
const explicitApiKey = typeof explicit?.apiKey === "string" ? explicit.apiKey : undefined;
if (hasExplicitModels && explicit) {

View File

@@ -2947,6 +2947,51 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
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",
@@ -24931,6 +24976,36 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
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

@@ -373,6 +373,12 @@ const TARGET_KEYS = [
"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",

View File

@@ -803,6 +803,18 @@ export const FIELD_HELP: Record<string, string> = {
"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

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

@@ -92,8 +92,15 @@ export type BedrockDiscoveryConfig = {
defaultMaxTokens?: number;
};
export type DiscoveryToggleConfig = {
enabled?: boolean;
};
export type ModelsConfig = {
mode?: "merge" | "replace";
providers?: Record<string, ModelProviderConfig>;
bedrockDiscovery?: BedrockDiscoveryConfig;
copilotDiscovery?: DiscoveryToggleConfig;
huggingfaceDiscovery?: DiscoveryToggleConfig;
ollamaDiscovery?: DiscoveryToggleConfig;
};

View File

@@ -342,11 +342,21 @@ 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();