Repository navigation
feat(cli): add /model --compaction for configurable chat compression model #6019
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 11 commits
15dd25b
2d65e71
e295f31
25b8b3a
20468b2
8a7bd15
9bf7fc3
de94523
4bd3c53
00b481a
6f287e3
e2adc66
fec830f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,6 +38,9 @@ const MAIN_MODEL_CONFIGURATION_HINT = | |
| const FAST_MODEL_CONFIGURATION_HINT = | ||
| 'Configure models in settings.modelProviders and ensure the required environment variables are set. In interactive mode, run /auth to configure or switch providers, or run /model --fast without a model to choose from configured models.'; | ||
|
|
||
| const COMPACTION_MODEL_CONFIGURATION_HINT = | ||
| 'Configure models in settings.modelProviders and ensure the required environment variables are set. In interactive mode, run /auth to configure or switch providers, or run /model --compaction without a model to choose from configured models.'; | ||
|
|
||
| const VISION_MODEL_CONFIGURATION_HINT = | ||
| 'Configure an image-capable model in settings.modelProviders and ensure the required environment variables are set. Run /model --vision <model-id> to set it, or leave it unset to auto-pick a same-provider vision model.'; | ||
|
|
||
|
|
@@ -143,7 +146,7 @@ async function switchMainModel( | |
| } | ||
|
|
||
| function formatUnavailableModelMessage( | ||
| kind: 'Model' | 'Fast model' | 'Vision model', | ||
| kind: 'Model' | 'Fast model' | 'Vision model' | 'Compaction model', | ||
| modelName: string, | ||
| authType: AuthType, | ||
| availableModels: AvailableModel[], | ||
|
|
@@ -161,7 +164,9 @@ function formatUnavailableModelMessage( | |
| ? FAST_MODEL_CONFIGURATION_HINT | ||
| : kind === 'Vision model' | ||
| ? VISION_MODEL_CONFIGURATION_HINT | ||
| : MAIN_MODEL_CONFIGURATION_HINT; | ||
| : kind === 'Compaction model' | ||
| ? COMPACTION_MODEL_CONFIGURATION_HINT | ||
| : MAIN_MODEL_CONFIGURATION_HINT; | ||
|
|
||
| return ( | ||
| `${kind} '${modelName}' is not available for auth type '${authType}'.\n` + | ||
|
|
@@ -170,10 +175,10 @@ function formatUnavailableModelMessage( | |
| ); | ||
| } | ||
|
|
||
| // Fast and vision share the same "not configured for any auth type" message | ||
| // Fast, vision, and compaction share the same "not configured for any auth type" message | ||
| // shape, differing only in the label and the configuration hint. | ||
| function formatUnavailableAuxModelMessage( | ||
| label: 'Fast model' | 'Vision model', | ||
| label: 'Fast model' | 'Vision model' | 'Compaction model', | ||
| modelName: string, | ||
| availableModels: AvailableModel[], | ||
| hint: string, | ||
|
|
@@ -217,6 +222,18 @@ function formatUnavailableVisionModelMessage( | |
| ); | ||
| } | ||
|
|
||
| function formatUnavailableCompactionModelMessage( | ||
| modelName: string, | ||
| availableModels: AvailableModel[], | ||
| ): string { | ||
| return formatUnavailableAuxModelMessage( | ||
| 'Compaction model', | ||
| modelName, | ||
| availableModels, | ||
| COMPACTION_MODEL_CONFIGURATION_HINT, | ||
| ); | ||
| } | ||
|
|
||
| function formatAmbiguousVisionModelMessage( | ||
| modelName: string, | ||
| matchingModels: AvailableModel[], | ||
|
|
@@ -294,15 +311,15 @@ function formatUnavailableVoiceModelMessage( | |
| // Get an array of the available model IDs as strings, filtered by mode | ||
| function getAvailableModelIds( | ||
| context: CommandContext, | ||
| mode: 'main' | 'fast' | 'voice' | 'vision' = 'main', | ||
| mode: 'main' | 'fast' | 'voice' | 'vision' | 'compaction' = 'main', | ||
| ) { | ||
| const { services } = context; | ||
| const { config } = services; | ||
| if (!config) { | ||
| return []; | ||
| } | ||
| const availableModels = config.getAvailableModels().filter((m) => { | ||
| if (mode === 'fast') return !m.voiceOnly; | ||
| if (mode === 'fast' || mode === 'compaction') return !m.voiceOnly; | ||
| if (mode === 'voice') return !m.fastOnly; | ||
|
|
||
| // 'vision' and 'main' both exclude fast/voice-only models. | ||
| return !m.fastOnly && !m.voiceOnly; | ||
|
|
||
|
|
@@ -315,11 +332,11 @@ export const modelCommand: SlashCommand = { | |
| completionPriority: 100, | ||
| get description() { | ||
| return t( | ||
| 'Switch the model for this session (--fast for suggestion model, --voice for voice transcription model, --vision for the vision bridge model, --project to persist to project settings, --global to persist to user settings, [model-id] to switch immediately, or [model-id] [prompt] to run a one-off prompt on another model; the inline prompt is sent verbatim without @file expansion).', | ||
| 'Switch the model for this session (--fast for suggestion model, --voice for voice transcription model, --vision for the vision bridge model, --compaction for chat compression model, --project to persist to project settings, --global to persist to user settings, [model-id] to switch immediately, or [model-id] [prompt] to run a one-off prompt on another model; the inline prompt is sent verbatim without @file expansion).', | ||
| ); | ||
| }, | ||
| argumentHint: | ||
| '[--fast|--voice|--vision] [--project|--global] [<model-id>] | <model-id> <prompt>', | ||
| '[--fast|--voice|--vision|--compaction] [--project|--global] [<model-id>] | <model-id> <prompt>', | ||
| kind: CommandKind.BUILT_IN, | ||
| supportedModes: ['interactive', 'non_interactive', 'acp'] as const, | ||
| completion: async (context, partialArg) => { | ||
|
|
@@ -341,6 +358,12 @@ export const modelCommand: SlashCommand = { | |
| 'Set the image-capable model used to transcribe images for a text-only main model', | ||
| ), | ||
| }, | ||
| { | ||
| value: '--compaction', | ||
| description: t( | ||
| 'Set the model used for chat compression (auto-compaction)', | ||
| ), | ||
| }, | ||
| { | ||
| value: '--project', | ||
| description: t( | ||
|
|
@@ -359,18 +382,20 @@ export const modelCommand: SlashCommand = { | |
| } | ||
| const trimmed = partialArg.trim(); | ||
| if (trimmed) { | ||
| let mode: 'main' | 'fast' | 'voice' | 'vision' = 'main'; | ||
| let mode: 'main' | 'fast' | 'voice' | 'vision' | 'compaction' = 'main'; | ||
| // Strip all known flags to isolate the model prefix for completion | ||
| const modelPrefix = trimmed | ||
| .replace(/(?:^|\s)--fast(?:\s|$)/, ' ') | ||
| .replace(/(?:^|\s)--voice(?:\s|$)/, ' ') | ||
| .replace(/(?:^|\s)--vision(?:\s|$)/, ' ') | ||
| .replace(/(?:^|\s)--compaction(?:\s|$)/, ' ') | ||
| .replace(/(?:^|\s)--project(?:\s|$)/, ' ') | ||
| .replace(/(?:^|\s)--global(?:\s|$)/, ' ') | ||
| .trim(); | ||
| if (/(?:^|\s)--fast(?:\s|$)/.test(trimmed)) mode = 'fast'; | ||
| else if (/(?:^|\s)--voice(?:\s|$)/.test(trimmed)) mode = 'voice'; | ||
| else if (/(?:^|\s)--vision(?:\s|$)/.test(trimmed)) mode = 'vision'; | ||
| else if (/(?:^|\s)--compaction(?:\s|$)/.test(trimmed)) mode = 'compaction'; | ||
| return getAvailableModelIds(context, mode).filter((id) => | ||
| id.startsWith(modelPrefix), | ||
| ); | ||
|
|
@@ -711,6 +736,85 @@ export const modelCommand: SlashCommand = { | |
| }; | ||
| } | ||
|
|
||
| const isCompactionModelCommand = | ||
| args === '--compaction' || args.startsWith('--compaction '); | ||
| if (isCompactionModelCommand) { | ||
|
Comment on lines
+820
to
+822
Comment on lines
+820
to
+822
|
||
| const modelName = args.replace('--compaction', '').trim(); | ||
|
Comment on lines
+820
to
+823
|
||
| if (!modelName) { | ||
| if (context.executionMode !== 'interactive') { | ||
|
Comment on lines
+820
to
+841
|
||
| const compactionModel = | ||
| context.services.settings?.merged?.compactionModel?.trim() || | ||
| t('not set (falls back to fast model, then main model)'); | ||
| return { | ||
| type: 'message', | ||
| messageType: 'info', | ||
| content: t( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] Misleading "clear" instruction — no clear mechanism exists This message tells users to use Either implement the clear path (mirroring how — qwen3.7-max via Qwen Code /review |
||
| 'Current compaction model: {{compactionModel}}\nUse "/model --compaction <model-id>" to set compaction model, or "/model --compaction " to clear the override.', | ||
|
|
||
| { compactionModel }, | ||
| ), | ||
|
Comment on lines
+848
to
+851
Comment on lines
+848
to
+851
Comment on lines
+848
to
+851
|
||
| }; | ||
| } | ||
| return { | ||
| type: 'dialog', | ||
| dialog: 'compaction-model', | ||
| }; | ||
|
Comment on lines
+854
to
+858
Comment on lines
+854
to
+858
Comment on lines
+854
to
+858
Comment on lines
+854
to
+858
|
||
| } | ||
|
Comment on lines
+854
to
+859
Comment on lines
+854
to
+859
|
||
| if (!settings) { | ||
| return { | ||
| type: 'message', | ||
| messageType: 'error', | ||
| content: t('Settings service not available.'), | ||
| }; | ||
| } | ||
|
|
||
| const selector = (() => { | ||
| try { | ||
| return resolveModelId(modelName); | ||
| } catch { | ||
| return undefined; | ||
| } | ||
| })(); | ||
| if (!selector) { | ||
| return { | ||
| type: 'message', | ||
| messageType: 'error', | ||
| content: formatUnavailableCompactionModelMessage(modelName, []), | ||
| }; | ||
| } | ||
|
|
||
| const availableModels = ( | ||
| selector.authType | ||
| ? config.getAvailableModelsForAuthType(selector.authType) | ||
| : config.getAllConfiguredModels() | ||
| ).filter((m) => !m.voiceOnly); | ||
|
|
||
| if (!availableModels.some((model) => model.id === selector.modelId)) { | ||
|
Comment on lines
+883
to
+890
|
||
| return { | ||
| type: 'message', | ||
| messageType: 'error', | ||
| content: selector.authType | ||
| ? formatUnavailableModelMessage( | ||
| 'Compaction model', | ||
| selector.modelId, | ||
| selector.authType, | ||
| availableModels, | ||
| ) | ||
| : formatUnavailableCompactionModelMessage( | ||
| modelName, | ||
| availableModels, | ||
| ), | ||
| }; | ||
| } | ||
|
|
||
| persistSetting(settings, 'compactionModel', modelName); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] Zero test coverage for 85+ lines of new command handling code with no tests. The existing
— qwen3.7-max via Qwen Code /review |
||
| // Sync runtime Config so the compression service picks it up immediately. | ||
| config.setCompactionModel(modelName); | ||
|
|
||
| return { | ||
| type: 'message', | ||
| messageType: 'info', | ||
| content: t('Compaction Model') + ': ' + modelName, | ||
|
|
||
| }; | ||
|
|
||
| } | ||
|
|
||
| const contentGeneratorConfig = config.getContentGeneratorConfig(); | ||
| if (!contentGeneratorConfig) { | ||
| return { | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] The newly added filter condition
(isVisionModelMode || !m.visionOnly) && // ← existing line: shows all models in vision mode
(isCompactionModelMode || !m.visionOnly), // ← new line added by this PRBoth conditions are ANDed. When
The combined AND result is (isCompactionModelMode || isVisionModelMode || !m.visionOnly),— claude-sonnet-4-6 via Qwen Code /review Generated by Claude Code
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] The fast/voice/vision handlers each early-return with mode-specific persistence ( if (isCompactionModelMode) {
const compactionModel = encodeAuxModelSelector(selected);
const scope = getPersistScopeForModelSelection(settings);
settings.setValue(scope, 'compactionModel', compactionModel);
config?.setCompactionModel?.(compactionModel);
uiState?.historyManager.addItem({ type: 'success', text: `Compaction Model: ${compactionModel}` }, Date.now());
onClose();
return;
}Without this, — claude-sonnet-4-6 via Qwen Code /review Generated by Claude Code |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -116,6 +116,7 @@ interface ModelDialogProps { | |||||
| isFastModelMode?: boolean; | ||||||
| isVoiceModelMode?: boolean; | ||||||
| isVisionModelMode?: boolean; | ||||||
| isCompactionModelMode?: boolean; | ||||||
| /** Override which settings scope to persist the selection to. */ | ||||||
| persistScope?: 'workspace' | 'user'; | ||||||
| availableTerminalHeight?: number; | ||||||
|
|
@@ -285,6 +286,7 @@ export function ModelDialog({ | |||||
| isFastModelMode, | ||||||
| isVoiceModelMode, | ||||||
| isVisionModelMode, | ||||||
| isCompactionModelMode, | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] Three
Each should mirror the pattern used by — qwen3.7-max via Qwen Code /review |
||||||
| persistScope, | ||||||
|
Comment on lines
295
to
300
|
||||||
| availableTerminalHeight, | ||||||
| }: ModelDialogProps): React.JSX.Element { | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] The callback has handlers for Add an
Also add — qwen3.7-max via Qwen Code /review |
||||||
|
|
@@ -309,7 +311,9 @@ export function ModelDialog({ | |||||
| (m.authType !== AuthType.QWEN_OAUTH || | ||||||
| authType === AuthType.QWEN_OAUTH) && | ||||||
| (isFastModelMode || !m.fastOnly) && | ||||||
| (isVoiceModelMode || !m.voiceOnly), | ||||||
| (isVoiceModelMode || !m.voiceOnly) && | ||||||
| (isVisionModelMode || !m.visionOnly) && | ||||||
| (isCompactionModelMode || !m.visionOnly), | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] Copy-paste bug: The compaction filter reuses
Suggested change
— qwen3.7-max via Qwen Code /review |
||||||
| ); | ||||||
|
|
||||||
| // Group registry models by authType | ||||||
|
|
@@ -363,7 +367,14 @@ export function ModelDialog({ | |||||
| } | ||||||
|
|
||||||
| return result; | ||||||
| }, [authType, config, isFastModelMode, isVoiceModelMode]); | ||||||
| }, [ | ||||||
| authType, | ||||||
| config, | ||||||
| isFastModelMode, | ||||||
| isVoiceModelMode, | ||||||
| isVisionModelMode, | ||||||
| isCompactionModelMode, | ||||||
| ]); | ||||||
|
|
||||||
| const MODEL_OPTIONS = useMemo( | ||||||
| () => | ||||||
|
|
@@ -488,8 +499,11 @@ export function ModelDialog({ | |||||
| // Check if current model is a runtime model | ||||||
| // Runtime snapshot ID is already in $runtime|${authType}|${modelId} format | ||||||
| const activeRuntimeSnapshot = | ||||||
| isFastModelMode || isVoiceModelMode || isVisionModelMode | ||||||
| ? undefined // fast/voice/vision models are never runtime model selections | ||||||
| isFastModelMode || | ||||||
| isVoiceModelMode || | ||||||
| isVisionModelMode || | ||||||
| isCompactionModelMode | ||||||
| ? undefined // fast/voice/vision/compaction models are never runtime model selections | ||||||
|
|
||||||
| : config?.getActiveRuntimeModelSnapshot?.(); | ||||||
| const currentBaseUrl = config | ||||||
| ?.getModelsConfig() | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This adds a
compactionModelsetting, but one already exists inmain(added via PR #7818 at line ~1448). The diff creates a duplicate key — JS/TS object literal last-wins semantics means the added block overwrites the existing correct one with an outdated description ("fall back to fastModel or the main model"vs main's"fall back to the main model"). This will:.github/scripts/check-settings-schema.shno-dupe-keysThe feature this PR originally introduced (
/model --compaction) already landed on main via PR #7818. The remaining diff is solely this duplicate key — a side effect of git's auto-merge producing a second entry that conflict resolution did not catch.Fix: delete this block from the PR branch.