diff --git a/CHANGELOG.md b/CHANGELOG.md index a073e259b0a..a5799630313 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai ### Changes - Docs/showcase: add a scannable hero, complete section jump links, and a responsive video grid for community examples. (#48493) Thanks @jchopard69. +- Agents/local models: add `agents.defaults.localModelMode: "lean"` to drop heavyweight default tools like `browser`, `cron`, and `message`, reducing prompt size for weaker local-model setups without changing the normal path. Thanks @ImLukeF. ### Fixes diff --git a/docs/gateway/local-models.md b/docs/gateway/local-models.md index 208330d70a2..c54e2165c0e 100644 --- a/docs/gateway/local-models.md +++ b/docs/gateway/local-models.md @@ -164,8 +164,10 @@ Compatibility notes for stricter OpenAI-compatible backends: - Some smaller or stricter local backends are unstable with OpenClaw's full agent-runtime prompt shape, especially when tool schemas are included. If the backend works for tiny direct `/v1/chat/completions` calls but fails on normal - OpenClaw agent turns, try - `models.providers..models[].compat.supportsTools: false` first. + OpenClaw agent turns, first try + `agents.defaults.localModelMode: "lean"` to drop heavyweight default tools + like `browser`, `cron`, and `message`; if that still fails, try + `models.providers..models[].compat.supportsTools: false`. - If the backend still fails only on larger OpenClaw runs, the remaining issue is usually upstream model/server capacity or a backend bug, not OpenClaw's transport layer. diff --git a/src/agents/pi-tools.model-provider-collision.test.ts b/src/agents/pi-tools.model-provider-collision.test.ts index a577e50e80d..c04f8a8e6fb 100644 --- a/src/agents/pi-tools.model-provider-collision.test.ts +++ b/src/agents/pi-tools.model-provider-collision.test.ts @@ -115,4 +115,56 @@ describe("applyModelProviderToolPolicy", () => { expect(toolNames(filtered)).toEqual(["read", "web_search", "exec"]); }); + + it("drops heavyweight tools when lean local-model mode is enabled", () => { + const filtered = __testing.applyModelProviderToolPolicy( + [ + { name: "read" }, + { name: "browser" }, + { name: "cron" }, + { name: "message" }, + { name: "exec" }, + ] as unknown as AnyAgentTool[], + { + config: { + agents: { + defaults: { + localModelMode: "lean", + }, + }, + }, + modelProvider: "openai", + modelApi: "openai-responses", + modelId: "gpt-5.4", + }, + ); + + expect(toolNames(filtered)).toEqual(["read", "exec"]); + }); + + it("keeps heavyweight tools when lean local-model mode is not enabled", () => { + const filtered = __testing.applyModelProviderToolPolicy( + [ + { name: "read" }, + { name: "browser" }, + { name: "cron" }, + { name: "message" }, + { name: "exec" }, + ] as unknown as AnyAgentTool[], + { + config: { + agents: { + defaults: { + localModelMode: "default", + }, + }, + }, + modelProvider: "openai", + modelApi: "openai-responses", + modelId: "gpt-5.4", + }, + ); + + expect(toolNames(filtered)).toEqual(["read", "browser", "cron", "message", "exec"]); + }); }); diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index 268f56cabb9..4a1679c70e9 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -131,6 +131,11 @@ function applyModelProviderToolPolicy( modelCompat?: ModelCompatConfig; }, ): AnyAgentTool[] { + if (params?.config?.agents?.defaults?.localModelMode === "lean") { + const leanDeny = new Set(["browser", "cron", "message"]); + tools = tools.filter((tool) => !leanDeny.has(tool.name)); + } + if ( shouldSuppressManagedWebSearchTool({ config: params?.config, diff --git a/src/config/schema.base.generated.ts b/src/config/schema.base.generated.ts index 7cd28ec3664..2da7b522cd9 100644 --- a/src/config/schema.base.generated.ts +++ b/src/config/schema.base.generated.ts @@ -3243,6 +3243,21 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { description: "Max total characters across all injected workspace bootstrap files (default: 150000).", }, + localModelMode: { + anyOf: [ + { + type: "string", + const: "default", + }, + { + type: "string", + const: "lean", + }, + ], + title: "Local Model Mode", + description: + 'Local-model prompt profile: "default" keeps the standard tool surface, while "lean" drops heavyweight non-essential tools for smaller or weaker models.', + }, bootstrapPromptTruncationWarning: { anyOf: [ { @@ -24513,6 +24528,11 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { help: "Max total characters across all injected workspace bootstrap files (default: 150000).", tags: ["performance"], }, + "agents.defaults.localModelMode": { + label: "Local Model Mode", + help: 'Local-model prompt profile: "default" keeps the standard tool surface, while "lean" drops heavyweight non-essential tools for smaller or weaker models.', + tags: ["advanced"], + }, "agents.defaults.bootstrapPromptTruncationWarning": { label: "Bootstrap Prompt Truncation Warning", help: 'Inject agent-visible warning text when bootstrap files are truncated: "off", "once" (default), or "always".', diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index 3fbd679e5df..9534dfe208b 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -848,6 +848,8 @@ export const FIELD_HELP: Record = { "Max characters of each workspace bootstrap file injected into the system prompt before truncation (default: 20000).", "agents.defaults.bootstrapTotalMaxChars": "Max total characters across all injected workspace bootstrap files (default: 150000).", + "agents.defaults.localModelMode": + 'Local-model prompt profile: "default" keeps the standard tool surface, while "lean" drops heavyweight non-essential tools for smaller or weaker models.', "agents.defaults.bootstrapPromptTruncationWarning": 'Inject agent-visible warning text when bootstrap files are truncated: "off", "once" (default), or "always".', "agents.defaults.startupContext": diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index a87a995b6af..89ae176cf77 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -343,6 +343,7 @@ export const FIELD_LABELS: Record = { "agents.defaults.contextInjection": "Context Injection", "agents.defaults.bootstrapMaxChars": "Bootstrap Max Chars", "agents.defaults.bootstrapTotalMaxChars": "Bootstrap Total Max Chars", + "agents.defaults.localModelMode": "Local Model Mode", "agents.defaults.bootstrapPromptTruncationWarning": "Bootstrap Prompt Truncation Warning", "agents.defaults.startupContext": "Startup Context", "agents.defaults.startupContext.enabled": "Enable Startup Context", diff --git a/src/config/types.agent-defaults.ts b/src/config/types.agent-defaults.ts index 132759f56b7..005ae3feab6 100644 --- a/src/config/types.agent-defaults.ts +++ b/src/config/types.agent-defaults.ts @@ -13,6 +13,7 @@ import type { MemorySearchConfig } from "./types.tools.js"; export type AgentContextInjection = "always" | "continuation-skip"; export type EmbeddedPiExecutionContract = "default" | "strict-agentic"; +export type LocalModelMode = "default" | "lean"; export type AgentModelEntryConfig = { alias?: string; @@ -198,6 +199,12 @@ export type AgentDefaultsConfig = { bootstrapMaxChars?: number; /** Max total chars across all injected bootstrap files (default: 150000). */ bootstrapTotalMaxChars?: number; + /** + * Optional local-model prompt profile: + * - default: keep the standard tool surface + * - lean: drop heavyweight non-essential tools for smaller or weaker models + */ + localModelMode?: LocalModelMode; /** * Agent-visible bootstrap truncation warning mode: * - off: do not inject warning text diff --git a/src/config/zod-schema.agent-defaults.ts b/src/config/zod-schema.agent-defaults.ts index c8e7486d6e6..5ed294f6c2c 100644 --- a/src/config/zod-schema.agent-defaults.ts +++ b/src/config/zod-schema.agent-defaults.ts @@ -52,6 +52,7 @@ export const AgentDefaultsSchema = z contextInjection: z.union([z.literal("always"), z.literal("continuation-skip")]).optional(), bootstrapMaxChars: z.number().int().positive().optional(), bootstrapTotalMaxChars: z.number().int().positive().optional(), + localModelMode: z.union([z.literal("default"), z.literal("lean")]).optional(), bootstrapPromptTruncationWarning: z .union([z.literal("off"), z.literal("once"), z.literal("always")]) .optional(),