Skip to content

feat(compact): add compactModel config to override compaction model - #1446

Closed
laurentftech wants to merge 9 commits into
Twigpine:mainfrom
laurentftech:feat/compact-model-override
Closed

laurentftech wants to merge 9 commits into
Twigpine:mainfrom
laurentftech:feat/compact-model-override

Conversation

@laurentftech

Copy link
Copy Markdown
Contributor

Closes #1445

Summary

  • Add compactModel?: string to GlobalConfig — optional override for the model used during compaction
  • Expose via /config set compactModel <model-id> with model validation
  • Skip forked-agent cache-sharing path automatically when compactModel differs from mainLoopModel (different model = guaranteed cache miss)

Motivation

When running a slow local reasoning model (e.g. Qwen3.6 27B) as the main loop, compaction blocks on that same slow model — even though summarization doesn't require reasoning. This allows routing compaction to a fast API model (e.g. Gemma 4) without affecting the main conversation model.

No cache tradeoff for local-model users: local inference has no prompt cache anyway.

Usage

claude config set compactModel <your-fast-model-id>

Unset to revert to default (uses mainLoopModel).

Files changed

File Change
src/utils/config.ts compactModel?: string field + GLOBAL_CONFIG_KEYS registration
src/tools/ConfigTool/supportedSettings.ts Config UI entry with validateModel
src/services/compact/compact.ts Read compactModel from config, disable forked path when model differs, pass override to queryModelWithStreaming

🤖 Generated with Claude Code

laurentftech and others added 7 commits May 26, 2026 23:08
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix broken step numbering in system prompt (glob/grep as sub-bullets)
- Remove unnecessary wrapper in getSystemPrompt
- Drop unused beforeEach/afterEach lifecycle in tests
- Remove redundant registration test (beforeAll already throws)
- Fix CLAUDE_CONFIG_DIR leak in beforeAll (restore in finally)
- Replace @ts-ignore with explicit ToolUseContext cast
- Use placeholder in README agentRouting example

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…o gpt-4o)\n\nReplace placeholder model with gpt-4o so the example is immediately usable.
Allows using a fast API model for compaction when the main loop runs
a slow local reasoning model. Skips the forked-agent cache-sharing
path automatically when models differ (different model = cache miss).

Closes Twigpine#1445

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution. I took a pass through the PR intent, the linked issue, the changed paths, and the related open code-reviewer-agent PR, and found issues that need to be addressed before this is ready.

Findings

  • [P2] Remove the unrelated duplicate code-reviewer agent changes
    src/tools/AgentTool/built-in/codeReviewerAgent.ts:72
    This PR is titled and described as adding compactModel for compaction, but it also adds the built-in code-reviewer agent, its tests, README routing entry, and an .openlore/ ignore rule. That agent work is already being reviewed separately in #1381, and this branch contains an older copy of it: for example, #1381 has since added shell-tool denials after review, while this PR still leaves Bash/PowerShell available through wildcard agent tools despite the prompt's read-only guarantee. Please drop the code-reviewer/README/OpenLore-related changes from this compact-model PR and keep that work in the dedicated agent PR.

  • [P2] Check tool-search support against the model that will actually compact
    src/services/compact/compact.ts:1270
    The streaming fallback now sends the request with model: compactModel, but isToolSearchEnabled() is still called with context.options.mainLoopModel. When the main model supports tool references and the configured compact model does not, this can enable ToolSearchTool and deferred MCP-tool handling for a request that is then sent to the unsupported compact model. That makes the primary use case fragile: switching compaction to a fast override model can fail or shape the request incorrectly based on the unrelated main-loop model. Please make the tool-search decision use compactModel consistently for the compact request.

  • [P2] Add a supported way to clear compactModel
    src/tools/ConfigTool/supportedSettings.ts:62
    The linked issue says unset compactModel should restore the existing behavior of using mainLoopModel, and the PR body says "Unset to revert to default". The config tool path added here only supports get vs set; omitting value reads the setting, empty strings fail validateModel, and the only "default" deletion path is hard-coded for remoteControlAtStartup. After a user sets compactModel, they cannot return to the default behavior through the documented config command without manually editing the global config file. Please add an explicit clear/default/unset path for this setting and cover it with a focused test.

- Drop code-reviewer agent (belongs in dedicated PR Twigpine#1381)
- Fix isToolSearchEnabled to use compactModel, not mainLoopModel
- Add clearable support so /config compactModel default unsets the key

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@laurentftech

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review. Addressed all three P2 findings in 8499c32:

[P2] Code-reviewer agent removed — dropped codeReviewerAgent.ts, its test, the builtInAgents.ts registration, the README routing entry, and the .openlore/ gitignore rule. That work stays in #1381.

[P2] isToolSearchEnabled now uses compactModel (compact.ts:1271) — the tool-search capability check now uses the same model as the compaction request, so a fast override model that doesn't support tool references won't get ToolSearchTool injected.

[P2] compactModel unset path added — introduced clearable?: boolean on SettingConfig (generic, not hardcoded per-key). When clearable: true and value is "default", ConfigTool deletes the key from global config before reaching validateOnWrite. Running /config compactModel default now restores the main-loop-model fallback. compactModel description updated to document the "default" escape hatch.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update. I rechecked the changed paths and found issues that still need to be addressed.

Findings

  • [P2] Resolve compactModel aliases before sending compaction requests
    src/services/compact/compact.ts:1159
    compactModel is validated with the same validateModel path as the main model setting, and that validator explicitly accepts aliases such as sonnet, opus, and haiku as valid. Unlike the main-loop model path, though, this value is read straight from global config and passed to queryModelWithStreaming as model: compactModel, and the API request only strips context tags rather than expanding aliases. That means /config set compactModel sonnet succeeds, but the next compaction sends model: "sonnet" instead of the resolved model ID, so the documented config surface can fail at runtime. Please normalize/parse the stored compact model the same way the main model path does before comparing it to mainLoopModel and before passing it to the API.

  • [P3] Return the cleared value in the config-tool result
    src/tools/ConfigTool/ConfigTool.ts:205
    The new clearable-setting branch successfully deletes compactModel, but it returns only value: null. Both the visible ConfigTool UI and mapToolResultToToolResultBlockParam render successful set operations from content.newValue, so /config set compactModel default reports Set compactModel to undefined to the user and to the model even though the key was cleared. Please return the same result shape that the set path expects, and add a focused regression test for the clear/default case so the reset behavior and confirmation stay in sync.

Resolve compactModel aliases (sonnet/opus/haiku) via parseUserSpecifiedModel
before sending to the API — raw aliases caused runtime failures since the API
does not expand them.

Fix clearable-setting result shape: return newValue:null instead of value:null
so mapToolResultToToolResultBlockParam renders "Set compactModel to null"
rather than "Set compactModel to undefined".

Add ConfigTool regression tests covering set/clear/get/error rendering paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@laurentftech

Copy link
Copy Markdown
Contributor Author

Both findings addressed in ed1aad0.

[P2] Alias resolution — compactModel is now resolved via parseUserSpecifiedModel() before any downstream use. The raw config value (e.g. "sonnet") is expanded to the full model ID before being passed to isToolSearchEnabled, getMaxOutputTokensForModel, and queryModelWithStreaming. The cache-sharing comparison (compactModel === context.options.mainLoopModel) now compares two resolved IDs on both sides, since mainLoopModel is already resolved at startup via the same function.

[P3] Clearable result shape — the clearable branch now returns newValue: null instead of value: null. mapToolResultToToolResultBlockParam renders set operations from content.newValue, so /config set compactModel default now reports Set compactModel to null rather than Set compactModel to undefined. Added a regression test covering the set/clear/get/error rendering paths in ConfigTool.test.ts.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update. I rechecked the changed paths and found one issue that still needs to be addressed.

Findings

  • [P3] Coerce compactModel values before validation
    src/tools/ConfigTool/supportedSettings.ts:68
    The ConfigTool input schema still allows value to be a string, boolean, or number, but the new compactModel setting passes v as string directly into validateModel. That cast does not convert the runtime value, and validateModel immediately calls model.trim(), so a malformed tool call such as { "setting": "compactModel", "value": true } throws TypeError: model.trim is not a function instead of returning a normal validation error. The existing model setting avoids this by calling validateModel(String(v)); please apply the same coercion or explicitly reject non-string values before validation, and add a small regression test for the bad-value path.

@jatmn

jatmn commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

closing as abandoned and newer #1629 has overlap

@jatmn jatmn closed this Jun 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add compactModel config option to use a different model for compaction

2 participants