Skip to content

Commit caf1b84

Browse files
feat: allow compaction model override via config (#38753)
Merged via squash. Prepared head SHA: a3d6d6c Co-authored-by: starbuck100 <[email protected]> Co-authored-by: jalehman <[email protected]> Reviewed-by: @jalehman
1 parent b6520d7 commit caf1b84

File tree

12 files changed

+143
-4
lines changed

12 files changed

+143
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Docs: https://docs.openclaw.ai
5555
- Mattermost/model picker: add Telegram-style interactive provider/model browsing for `/oc_model` and `/oc_models`, fix picker callback updates, and emit a normal confirmation reply when a model is selected. (#38767) thanks @mukhtharcm.
5656
- Docker/multi-stage build: restructure Dockerfile as a multi-stage build to produce a minimal runtime image without build tools, source code, or Bun; add `OPENCLAW_VARIANT=slim` build arg for a bookworm-slim variant. (#38479) Thanks @sallyom.
5757
- Google/Gemini 3.1 Flash-Lite: add first-class `google/gemini-3.1-flash-lite-preview` support across model-id normalization, default aliases, media-understanding image lookups, Google Gemini CLI forward-compat fallback, and docs.
58+
- Agents/compaction model override: allow `agents.defaults.compaction.model` to route compaction summarization through a different model than the main session, and document the override across config help/reference surfaces. (#38753) thanks @starbuck100.
5859

5960
### Breaking
6061

docs/concepts/compaction.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,36 @@ Compaction **persists** in the session’s JSONL history.
2424
Use the `agents.defaults.compaction` setting in your `openclaw.json` to configure compaction behavior (mode, target tokens, etc.).
2525
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`.
2626

27+
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:
28+
29+
```json
30+
{
31+
"agents": {
32+
"defaults": {
33+
"compaction": {
34+
"model": "openrouter/anthropic/claude-sonnet-4-5"
35+
}
36+
}
37+
}
38+
}
39+
```
40+
41+
This also works with local models, for example a second Ollama model dedicated to summarization or a fine-tuned compaction specialist:
42+
43+
```json
44+
{
45+
"agents": {
46+
"defaults": {
47+
"compaction": {
48+
"model": "ollama/llama3.1:8b"
49+
}
50+
}
51+
}
52+
}
53+
```
54+
55+
When unset, compaction uses the agent's primary model.
56+
2757
## Auto-compaction (default on)
2858

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

docs/gateway/configuration-reference.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,6 +1005,7 @@ Periodic heartbeat runs.
10051005
identifierPolicy: "strict", // strict | off | custom
10061006
identifierInstructions: "Preserve deployment IDs, ticket IDs, and host:port pairs exactly.", // used when identifierPolicy=custom
10071007
postCompactionSections: ["Session Startup", "Red Lines"], // [] disables reinjection
1008+
model: "openrouter/anthropic/claude-sonnet-4-5", // optional compaction-only model override
10081009
memoryFlush: {
10091010
enabled: true,
10101011
softThresholdTokens: 6000,
@@ -1021,6 +1022,7 @@ Periodic heartbeat runs.
10211022
- `identifierPolicy`: `strict` (default), `off`, or `custom`. `strict` prepends built-in opaque identifier retention guidance during compaction summarization.
10221023
- `identifierInstructions`: optional custom identifier-preservation text used when `identifierPolicy=custom`.
10231024
- `postCompactionSections`: optional AGENTS.md H2/H3 section names to re-inject after compaction. Defaults to `["Session Startup", "Red Lines"]`; set `[]` to disable reinjection. When unset or explicitly set to that default pair, older `Every Session`/`Safety` headings are also accepted as a legacy fallback.
1025+
- `model`: optional `provider/model-id` override for compaction summarization only. Use this when the main session should keep one model but compaction summaries should run on another; when unset, compaction uses the session's primary model.
10241026
- `memoryFlush`: silent agentic turn before auto-compaction to store durable memories. Skipped when workspace is read-only.
10251027

10261028
### `agents.defaults.contextPruning`

scripts/test-parallel.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ const unitIsolatedFilesRaw = [
8686
"src/slack/monitor/slash.test.ts",
8787
// Uses process-level unhandledRejection listeners; keep it off vmForks to avoid cross-file leakage.
8888
"src/imessage/monitor.shutdown.unhandled-rejection.test.ts",
89+
// Mutates process.cwd() and mocks core module loaders; isolate from the shared fast lane.
90+
"src/infra/git-commit.test.ts",
8991
];
9092
const unitIsolatedFiles = unitIsolatedFilesRaw.filter((file) => fs.existsSync(file));
9193

src/agents/models-config.merge.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ describe("models-config merge helpers", () => {
5252
it("merges explicit providers onto trimmed keys", () => {
5353
const merged = mergeProviders({
5454
explicit: {
55-
" custom ": { api: "openai-responses", models: [] } as ProviderConfig,
55+
" custom ": {
56+
api: "openai-responses",
57+
models: [] as ProviderConfig["models"],
58+
} as ProviderConfig,
5659
},
5760
});
5861

src/agents/pi-embedded-runner/compact.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -271,8 +271,31 @@ export async function compactEmbeddedPiSessionDirect(
271271
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
272272
const prevCwd = process.cwd();
273273

274-
const provider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
275-
const modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL;
274+
// Resolve compaction model: prefer config override, then fall back to caller-supplied model
275+
const compactionModelOverride = params.config?.agents?.defaults?.compaction?.model?.trim();
276+
let provider: string;
277+
let modelId: string;
278+
// When switching provider via override, drop the primary auth profile to avoid
279+
// sending the wrong credentials (e.g. OpenAI profile token to OpenRouter).
280+
let authProfileId: string | undefined = params.authProfileId;
281+
if (compactionModelOverride) {
282+
const slashIdx = compactionModelOverride.indexOf("/");
283+
if (slashIdx > 0) {
284+
provider = compactionModelOverride.slice(0, slashIdx).trim();
285+
modelId = compactionModelOverride.slice(slashIdx + 1).trim() || DEFAULT_MODEL;
286+
// Provider changed — drop primary auth profile so getApiKeyForModel
287+
// falls back to provider-based key resolution for the override model.
288+
if (provider !== (params.provider ?? "").trim()) {
289+
authProfileId = undefined;
290+
}
291+
} else {
292+
provider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
293+
modelId = compactionModelOverride;
294+
}
295+
} else {
296+
provider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
297+
modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL;
298+
}
276299
const fail = (reason: string): EmbeddedPiCompactResult => {
277300
log.warn(
278301
`[compaction-diag] end runId=${runId} sessionKey=${params.sessionKey ?? params.sessionId} ` +
@@ -302,7 +325,7 @@ export async function compactEmbeddedPiSessionDirect(
302325
const apiKeyInfo = await getApiKeyForModel({
303326
model,
304327
cfg: params.config,
305-
profileId: params.authProfileId,
328+
profileId: authProfileId,
306329
agentDir,
307330
});
308331

src/agents/pi-embedded-runner/run/attempt.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,72 @@ describe("prependSystemPromptAddition", () => {
639639
});
640640

641641
describe("buildAfterTurnLegacyCompactionParams", () => {
642+
it("uses primary model when compaction.model is not set", () => {
643+
const legacy = buildAfterTurnLegacyCompactionParams({
644+
attempt: {
645+
sessionKey: "agent:main:session:abc",
646+
messageChannel: "slack",
647+
messageProvider: "slack",
648+
agentAccountId: "acct-1",
649+
authProfileId: "openai:p1",
650+
config: {} as OpenClawConfig,
651+
skillsSnapshot: undefined,
652+
senderIsOwner: true,
653+
provider: "openai-codex",
654+
modelId: "gpt-5.3-codex",
655+
thinkLevel: "off",
656+
reasoningLevel: "on",
657+
extraSystemPrompt: "extra",
658+
ownerNumbers: ["+15555550123"],
659+
},
660+
workspaceDir: "/tmp/workspace",
661+
agentDir: "/tmp/agent",
662+
});
663+
664+
expect(legacy).toMatchObject({
665+
provider: "openai-codex",
666+
model: "gpt-5.3-codex",
667+
});
668+
});
669+
670+
it("passes primary model through even when compaction.model is set (override resolved in compactDirect)", () => {
671+
const legacy = buildAfterTurnLegacyCompactionParams({
672+
attempt: {
673+
sessionKey: "agent:main:session:abc",
674+
messageChannel: "slack",
675+
messageProvider: "slack",
676+
agentAccountId: "acct-1",
677+
authProfileId: "openai:p1",
678+
config: {
679+
agents: {
680+
defaults: {
681+
compaction: {
682+
model: "openrouter/anthropic/claude-sonnet-4-5",
683+
},
684+
},
685+
},
686+
} as OpenClawConfig,
687+
skillsSnapshot: undefined,
688+
senderIsOwner: true,
689+
provider: "openai-codex",
690+
modelId: "gpt-5.3-codex",
691+
thinkLevel: "off",
692+
reasoningLevel: "on",
693+
extraSystemPrompt: "extra",
694+
ownerNumbers: ["+15555550123"],
695+
},
696+
workspaceDir: "/tmp/workspace",
697+
agentDir: "/tmp/agent",
698+
});
699+
700+
// buildAfterTurnLegacyCompactionParams no longer resolves the override;
701+
// compactEmbeddedPiSessionDirect does it centrally for both auto + manual paths.
702+
expect(legacy).toMatchObject({
703+
provider: "openai-codex",
704+
model: "gpt-5.3-codex",
705+
});
706+
});
707+
642708
it("includes resolved auth profile fields for context-engine afterTurn compaction", () => {
643709
const legacy = buildAfterTurnLegacyCompactionParams({
644710
attempt: {

src/config/schema.help.quality.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ const TARGET_KEYS = [
378378
"agents.defaults.compaction.qualityGuard.enabled",
379379
"agents.defaults.compaction.qualityGuard.maxRetries",
380380
"agents.defaults.compaction.postCompactionSections",
381+
"agents.defaults.compaction.model",
381382
"agents.defaults.compaction.memoryFlush",
382383
"agents.defaults.compaction.memoryFlush.enabled",
383384
"agents.defaults.compaction.memoryFlush.softThresholdTokens",
@@ -810,6 +811,9 @@ describe("config help copy quality", () => {
810811
expect(/Every Session|Safety/i.test(postCompactionSections)).toBe(true);
811812
expect(/\[\]|disable/i.test(postCompactionSections)).toBe(true);
812813

814+
const compactionModel = FIELD_HELP["agents.defaults.compaction.model"];
815+
expect(/provider\/model|different model|primary agent model/i.test(compactionModel)).toBe(true);
816+
813817
const flush = FIELD_HELP["agents.defaults.compaction.memoryFlush.enabled"];
814818
expect(/pre-compaction|memory flush|token/i.test(flush)).toBe(true);
815819
});

src/config/schema.help.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,8 @@ export const FIELD_HELP: Record<string, string> = {
10131013
"Maximum number of regeneration retries after a failed safeguard summary quality audit. Use small values to bound extra latency and token cost.",
10141014
"agents.defaults.compaction.postCompactionSections":
10151015
'AGENTS.md H2/H3 section names re-injected after compaction so the agent reruns critical startup guidance. Leave unset to use "Session Startup"/"Red Lines" with legacy fallback to "Every Session"/"Safety"; set to [] to disable reinjection entirely.',
1016+
"agents.defaults.compaction.model":
1017+
"Optional provider/model override used only for compaction summarization. Set this when you want compaction to run on a different model than the session default, and leave it unset to keep using the primary agent model.",
10161018
"agents.defaults.compaction.memoryFlush":
10171019
"Pre-compaction memory flush settings that run an agentic memory write before heavy compaction. Keep enabled for long sessions so salient context is persisted before aggressive trimming.",
10181020
"agents.defaults.compaction.memoryFlush.enabled":

src/config/schema.labels.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,7 @@ export const FIELD_LABELS: Record<string, string> = {
458458
"agents.defaults.compaction.qualityGuard.enabled": "Compaction Quality Guard Enabled",
459459
"agents.defaults.compaction.qualityGuard.maxRetries": "Compaction Quality Guard Max Retries",
460460
"agents.defaults.compaction.postCompactionSections": "Post-Compaction Context Sections",
461+
"agents.defaults.compaction.model": "Compaction Model Override",
461462
"agents.defaults.compaction.memoryFlush": "Compaction Memory Flush",
462463
"agents.defaults.compaction.memoryFlush.enabled": "Compaction Memory Flush Enabled",
463464
"agents.defaults.compaction.memoryFlush.softThresholdTokens":

0 commit comments

Comments
 (0)