From c44cc77fa6ce1860c809343221d666141ec48565 Mon Sep 17 00:00:00 2001 From: starbuck100 Date: Sat, 7 Mar 2026 10:23:48 +0100 Subject: [PATCH] compaction: add configurable model override Allow users to specify a different model for compaction summarization via `agents.defaults.compaction.model`. When set, compaction uses this model instead of the agent's primary model. This is useful for hybrid local+cloud setups where the primary model is a small local model (e.g. Ollama/Qwen) but compaction benefits from a more capable summarizer (e.g. cloud Claude), or a dedicated local model fine-tuned for summarization. Config example: "compaction": { "model": "openrouter/anthropic/claude-sonnet-4-5" } Falls back to the primary model when unset (no behavior change for existing configs). Closes #7926 Co-Authored-By: Claude Opus 4.6 --- docs/concepts/compaction.md | 30 +++++++++ .../pi-embedded-runner/run/attempt.test.ts | 64 +++++++++++++++++++ src/agents/pi-embedded-runner/run/attempt.ts | 18 +++++- src/config/types.agent-defaults.ts | 4 ++ src/config/zod-schema.agent-defaults.ts | 1 + 5 files changed, 115 insertions(+), 2 deletions(-) diff --git a/docs/concepts/compaction.md b/docs/concepts/compaction.md index 8d243bf234d..73f6372c3f7 100644 --- a/docs/concepts/compaction.md +++ b/docs/concepts/compaction.md @@ -24,6 +24,36 @@ Compaction **persists** in the session’s JSONL history. Use the `agents.defaults.compaction` setting in your `openclaw.json` to configure compaction behavior (mode, target tokens, etc.). Compaction summarization preserves opaque identifiers by default (`identifierPolicy: "strict"`). You can override this with `identifierPolicy: "off"` or provide custom text with `identifierPolicy: "custom"` and `identifierInstructions`. +You can optionally specify a different model for compaction summarization via `agents.defaults.compaction.model`. This is useful when your primary model is a local or small model and you want compaction summaries produced by a more capable model. The override accepts any `provider/model-id` string: + +```json +{ + "agents": { + "defaults": { + "compaction": { + "model": "openrouter/anthropic/claude-sonnet-4-5" + } + } + } +} +``` + +This also works with local models, for example a second Ollama model dedicated to summarization or a fine-tuned compaction specialist: + +```json +{ + "agents": { + "defaults": { + "compaction": { + "model": "ollama/llama3.1:8b" + } + } + } +} +``` + +When unset, compaction uses the agent's primary model. + ## Auto-compaction (default on) When a session nears or exceeds the model’s context window, OpenClaw triggers auto-compaction and may retry the original request using the compacted context. diff --git a/src/agents/pi-embedded-runner/run/attempt.test.ts b/src/agents/pi-embedded-runner/run/attempt.test.ts index 197a2903183..ad307ce483a 100644 --- a/src/agents/pi-embedded-runner/run/attempt.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.test.ts @@ -639,6 +639,70 @@ describe("prependSystemPromptAddition", () => { }); describe("buildAfterTurnLegacyCompactionParams", () => { + it("uses primary model when compaction.model is not set", () => { + const legacy = buildAfterTurnLegacyCompactionParams({ + attempt: { + sessionKey: "agent:main:session:abc", + messageChannel: "slack", + messageProvider: "slack", + agentAccountId: "acct-1", + authProfileId: "openai:p1", + config: {} as OpenClawConfig, + skillsSnapshot: undefined, + senderIsOwner: true, + provider: "openai-codex", + modelId: "gpt-5.3-codex", + thinkLevel: "off", + reasoningLevel: "on", + extraSystemPrompt: "extra", + ownerNumbers: ["+15555550123"], + }, + workspaceDir: "/tmp/workspace", + agentDir: "/tmp/agent", + }); + + expect(legacy).toMatchObject({ + provider: "openai-codex", + model: "gpt-5.3-codex", + }); + }); + + it("uses compaction.model override when set, splitting provider from model id", () => { + const legacy = buildAfterTurnLegacyCompactionParams({ + attempt: { + sessionKey: "agent:main:session:abc", + messageChannel: "slack", + messageProvider: "slack", + agentAccountId: "acct-1", + authProfileId: "openai:p1", + config: { + agents: { + defaults: { + compaction: { + model: "openrouter/anthropic/claude-sonnet-4-5", + }, + }, + }, + } as OpenClawConfig, + skillsSnapshot: undefined, + senderIsOwner: true, + provider: "openai-codex", + modelId: "gpt-5.3-codex", + thinkLevel: "off", + reasoningLevel: "on", + extraSystemPrompt: "extra", + ownerNumbers: ["+15555550123"], + }, + workspaceDir: "/tmp/workspace", + agentDir: "/tmp/agent", + }); + + expect(legacy).toMatchObject({ + provider: "openrouter", + model: "anthropic/claude-sonnet-4-5", + }); + }); + it("includes resolved auth profile fields for context-engine afterTurn compaction", () => { const legacy = buildAfterTurnLegacyCompactionParams({ attempt: { diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 467b8e1501f..40684d43671 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -659,6 +659,20 @@ export function buildAfterTurnLegacyCompactionParams(params: { workspaceDir: string; agentDir: string; }): Partial { + // Resolve compaction model: use config override or fall back to primary model + const compactionModelOverride = params.attempt.config?.agents?.defaults?.compaction?.model; + let compactionProvider = params.attempt.provider; + let compactionModelId = params.attempt.modelId; + if (compactionModelOverride) { + const slashIdx = compactionModelOverride.indexOf("/"); + if (slashIdx > 0) { + compactionProvider = compactionModelOverride.slice(0, slashIdx); + compactionModelId = compactionModelOverride.slice(slashIdx + 1); + } else { + compactionModelId = compactionModelOverride; + } + } + return { sessionKey: params.attempt.sessionKey, messageChannel: params.attempt.messageChannel, @@ -670,8 +684,8 @@ export function buildAfterTurnLegacyCompactionParams(params: { config: params.attempt.config, skillsSnapshot: params.attempt.skillsSnapshot, senderIsOwner: params.attempt.senderIsOwner, - provider: params.attempt.provider, - model: params.attempt.modelId, + provider: compactionProvider, + model: compactionModelId, thinkLevel: params.attempt.thinkLevel, reasoningLevel: params.attempt.reasoningLevel, bashElevated: params.attempt.bashElevated, diff --git a/src/config/types.agent-defaults.ts b/src/config/types.agent-defaults.ts index a242d0bbcc1..9124e4084d8 100644 --- a/src/config/types.agent-defaults.ts +++ b/src/config/types.agent-defaults.ts @@ -322,6 +322,10 @@ export type AgentCompactionConfig = { * Set to [] to disable post-compaction context injection entirely. */ postCompactionSections?: string[]; + /** Optional model override for compaction summarization (e.g. "openrouter/anthropic/claude-sonnet-4-5"). + * When set, compaction uses this model instead of the agent's primary model. + * Falls back to the primary model when unset. */ + model?: string; }; export type AgentCompactionMemoryFlushConfig = { diff --git a/src/config/zod-schema.agent-defaults.ts b/src/config/zod-schema.agent-defaults.ts index 1e83a92f54c..242d6959729 100644 --- a/src/config/zod-schema.agent-defaults.ts +++ b/src/config/zod-schema.agent-defaults.ts @@ -104,6 +104,7 @@ export const AgentDefaultsSchema = z .strict() .optional(), postCompactionSections: z.array(z.string()).optional(), + model: z.string().optional(), memoryFlush: z .object({ enabled: z.boolean().optional(),