From 15dd25b8d2f372f0aaf0dac184de9235263dd873 Mon Sep 17 00:00:00 2001 From: "rajeshwaran.r" Date: Sun, 28 Jun 2026 17:29:27 +0530 Subject: [PATCH 1/2] fix(core): use fast model for chat compression side queries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chat compression now uses config.getFastModel?.() (e.g., Qwen/Qwen3-Alpha) instead of the main model for compression side queries. This reduces cost while maintaining functionality. - Removed `model` parameter from compress() method signature - Pass config.getFastModel?.() to runSideQuery for compression - Update test to verify fast model is passed correctly - Test renamed: 'forwards model...' → 'passes getFastModel to runSideQuery for compression' --- .../core/src/services/chatCompressionService.test.ts | 10 ++++++++-- packages/core/src/services/chatCompressionService.ts | 5 +++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/core/src/services/chatCompressionService.test.ts b/packages/core/src/services/chatCompressionService.test.ts index 8a1c457b51e..2d98eb99ca3 100644 --- a/packages/core/src/services/chatCompressionService.test.ts +++ b/packages/core/src/services/chatCompressionService.test.ts @@ -53,6 +53,7 @@ describe('ChatCompressionService', () => { getContentGeneratorConfig: vi.fn().mockReturnValue({}), getHookSystem: mockGetHookSystem, getModel: () => 'test-model', + getFastModel: vi.fn(), getApprovalMode: () => 'default', getDebugLogger: () => ({ warn: vi.fn(), @@ -838,7 +839,10 @@ describe('ChatCompressionService', () => { expect(serialized).toContain('[image: image/png]'); }); - it('forwards model, maxAttempts, and thinkingConfig to runSideQuery', async () => { + it('passes getFastModel to runSideQuery for compression', async () => { + // Compression passes config.getFastModel?.() to runSideQuery so it uses + // the fast model (e.g., Qwen/Qwen3-Alpha) instead of the expensive main + // model, reducing cost. See https://github.com/QwenLM/qwen-code/issues/5956 const history: Content[] = [ { role: 'user', parts: [{ text: 'msg1' }] }, { role: 'model', parts: [{ text: 'msg2' }] }, @@ -848,6 +852,7 @@ describe('ChatCompressionService', () => { vi.mocked(mockChat.getHistory).mockReturnValue(history); vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue(100); vi.mocked(tokenLimit).mockReturnValue(1000); + vi.mocked(mockConfig.getFastModel).mockReturnValue('fast-model-v1'); const mockGenerateText = vi.fn().mockResolvedValue({ text: 'Summary', @@ -874,9 +879,10 @@ describe('ChatCompressionService', () => { // inconsistent) and the output is hard-capped by COMPACT_MAX_OUTPUT_TOKENS // so subsequent threshold math has a predictable reserve. maxAttempts=1 // keeps the call best-effort (next turn re-triggers on failure). + // Model is set to getFastModel?.() to use the fast model for cost efficiency. expect(mockGenerateText).toHaveBeenCalledWith( expect.objectContaining({ - model: mockModel, + model: 'fast-model-v1', maxAttempts: 1, config: expect.objectContaining({ thinkingConfig: { includeThoughts: false }, diff --git a/packages/core/src/services/chatCompressionService.ts b/packages/core/src/services/chatCompressionService.ts index 73c5129596a..9d6a2a16a98 100644 --- a/packages/core/src/services/chatCompressionService.ts +++ b/packages/core/src/services/chatCompressionService.ts @@ -316,7 +316,6 @@ export class ChatCompressionService { const { promptId, force, - model, config, consecutiveFailures, originalTokenCount, @@ -493,7 +492,9 @@ export class ChatCompressionService { const summaryResult = await runSideQuery(config, { purpose: 'chat-compression', skipOutputLanguagePreference: true, - model, + model: config.getFastModel?.(), + // Compression uses the fast model (config.getFastModel?.()) to reduce cost. + // See https://github.com/QwenLM/qwen-code/issues/5956 // Stream so a slow compression inference keeps the HTTP connection alive. // Non-streaming returns no bytes until the whole summary is generated, so // behind a BFF gateway with a short `proxy_read_timeout` a long inference From 2d65e71111855afd1ffc690676baf0d2c1bc0e03 Mon Sep 17 00:00:00 2001 From: "rajeshwaran.r" Date: Mon, 29 Jun 2026 16:57:37 +0530 Subject: [PATCH 2/2] feat(cli): add /model --compaction for configurable chat compression model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce dedicated compaction model setting (configurable via /model --compaction) with fallback chain: compactionModel → fastModel → main model. Adds UI dialog mode, slash command flag, and runtime snapshot guard so compaction models are never presented as runtime selections. --- packages/cli/src/config/config.ts | 1 + packages/cli/src/config/settingsSchema.ts | 11 ++ packages/cli/src/ui/AppContainer.tsx | 3 + .../cli/src/ui/commands/modelCommand.test.ts | 2 +- packages/cli/src/ui/commands/modelCommand.ts | 123 ++++++++++++++++-- packages/cli/src/ui/commands/types.ts | 1 + .../cli/src/ui/components/DialogManager.tsx | 1 + .../cli/src/ui/components/ModelDialog.tsx | 22 +++- .../cli/src/ui/contexts/UIActionsContext.tsx | 1 + .../cli/src/ui/contexts/UIStateContext.tsx | 1 + .../cli/src/ui/hooks/slashCommandProcessor.ts | 4 + packages/cli/src/ui/hooks/useModelCommand.ts | 16 ++- packages/core/src/config/config.ts | 64 +++++++++ packages/core/src/models/types.ts | 2 + 14 files changed, 235 insertions(+), 17 deletions(-) diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 9d9be7bcecb..d17599dabb7 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -2102,6 +2102,7 @@ export async function loadCliConfig( : (settings.memory?.autoSkillConfirm ?? true), fastModel: settings.fastModel || undefined, visionModel: settings.visionModel || undefined, + compactionModel: settings.compactionModel || undefined, // Use separated hooks if provided, otherwise fall back to merged hooks userHooks: bareMode || safeMode diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 60dbe7f6203..006f6eff092 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -1275,6 +1275,17 @@ const SETTINGS_SCHEMA = { showInDialog: false, }, + compactionModel: { + type: 'string', + label: 'Compaction Model', + category: 'Model', + requiresRestart: false, + default: '', + description: + 'Model used for chat compression (auto-compaction). Set with /model --compaction. Leave empty to fall back to fastModel or the main model. A smaller/faster model reduces compression latency and cost.', + showInDialog: false, + }, + model: { type: 'object', label: 'Model', diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 177f9508b54..dcf184b5a1e 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -1097,6 +1097,7 @@ export const AppContainer = (props: AppContainerProps) => { isFastModelMode, isVoiceModelMode, isVisionModelMode, + isCompactionModelMode, openModelDialog, closeModelDialog, } = useModelCommand(); @@ -3664,6 +3665,7 @@ export const AppContainer = (props: AppContainerProps) => { isFastModelMode, isVoiceModelMode, isVisionModelMode, + isCompactionModelMode, isTrustDialogOpen, activeArenaDialog, isPermissionsDialogOpen, @@ -3803,6 +3805,7 @@ export const AppContainer = (props: AppContainerProps) => { isFastModelMode, isVoiceModelMode, isVisionModelMode, + isCompactionModelMode, isTrustDialogOpen, activeArenaDialog, isPermissionsDialogOpen, diff --git a/packages/cli/src/ui/commands/modelCommand.test.ts b/packages/cli/src/ui/commands/modelCommand.test.ts index 74c7e04680d..a1271801ea7 100644 --- a/packages/cli/src/ui/commands/modelCommand.test.ts +++ b/packages/cli/src/ui/commands/modelCommand.test.ts @@ -45,7 +45,7 @@ describe('modelCommand', () => { it('should have the correct name and description', () => { expect(modelCommand.name).toBe('model'); expect(modelCommand.description).toBe( - 'Switch the model for this session (--fast for suggestion model, --voice for voice transcription model, --vision for the vision bridge model, [model-id] to switch immediately).', + '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, [model-id] to switch immediately).', ); }); diff --git a/packages/cli/src/ui/commands/modelCommand.ts b/packages/cli/src/ui/commands/modelCommand.ts index 4ffebcb8d15..b2c784715f3 100644 --- a/packages/cli/src/ui/commands/modelCommand.ts +++ b/packages/cli/src/ui/commands/modelCommand.ts @@ -33,6 +33,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 to set it, or leave it unset to auto-pick a same-provider vision model.'; @@ -79,7 +82,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[], @@ -97,7 +100,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` + @@ -106,10 +111,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, @@ -153,6 +158,18 @@ function formatUnavailableVisionModelMessage( ); } +function formatUnavailableCompactionModelMessage( + modelName: string, + availableModels: AvailableModel[], +): string { + return formatUnavailableAuxModelMessage( + 'Compaction model', + modelName, + availableModels, + COMPACTION_MODEL_CONFIGURATION_HINT, + ); +} + // Shown when a user pins a model that isn't known to accept images. The pin is // still honored, but the bridge will send images to it, so flag it. Reuses the // same translated key the model dialog emits (ModelDialog.tsx) so both paths @@ -191,7 +208,7 @@ 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; @@ -199,7 +216,7 @@ function getAvailableModelIds( 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; @@ -212,10 +229,10 @@ 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, [model-id] to switch immediately).', + '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, [model-id] to switch immediately).', ); }, - argumentHint: '[--fast|--voice|--vision] []', + argumentHint: '[--fast|--voice|--vision|--compaction] []', kind: CommandKind.BUILT_IN, supportedModes: ['interactive', 'non_interactive', 'acp'] as const, completion: async (context, partialArg) => { @@ -237,13 +254,19 @@ 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)', + ), + }, ].filter((item) => item.value.startsWith(partialArg)); if (flagCompletions.length > 0) { return flagCompletions; } const trimmed = partialArg.trim(); if (trimmed) { - let mode: 'main' | 'fast' | 'voice' | 'vision' = 'main'; + let mode: 'main' | 'fast' | 'voice' | 'vision' | 'compaction' = 'main'; let modelPrefix = trimmed; if (trimmed.startsWith('--fast ')) { mode = 'fast'; @@ -254,6 +277,9 @@ export const modelCommand: SlashCommand = { } else if (trimmed.startsWith('--vision ')) { mode = 'vision'; modelPrefix = trimmed.slice('--vision '.length); + } else if (trimmed.startsWith('--compaction ')) { + mode = 'compaction'; + modelPrefix = trimmed.slice('--compaction '.length); } return getAvailableModelIds(context, mode).filter((id) => id.startsWith(modelPrefix), @@ -537,6 +563,85 @@ export const modelCommand: SlashCommand = { }; } + const isCompactionModelCommand = + args === '--compaction' || args.startsWith('--compaction '); + if (isCompactionModelCommand) { + const modelName = args.replace('--compaction', '').trim(); + if (!modelName) { + if (context.executionMode !== 'interactive') { + 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( + 'Current compaction model: {{compactionModel}}\nUse "/model --compaction " to set compaction model, or "/model --compaction " to clear the override.', + { compactionModel }, + ), + }; + } + return { + type: 'dialog', + dialog: 'compaction-model', + }; + } + 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)) { + return { + type: 'message', + messageType: 'error', + content: selector.authType + ? formatUnavailableModelMessage( + 'Compaction model', + selector.modelId, + selector.authType, + availableModels, + ) + : formatUnavailableCompactionModelMessage( + modelName, + availableModels, + ), + }; + } + + persistSetting(settings, 'compactionModel', modelName); + // 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 { diff --git a/packages/cli/src/ui/commands/types.ts b/packages/cli/src/ui/commands/types.ts index 89293a9f46d..e633ca08d75 100644 --- a/packages/cli/src/ui/commands/types.ts +++ b/packages/cli/src/ui/commands/types.ts @@ -181,6 +181,7 @@ export interface OpenDialogActionReturn { | 'fast-model' | 'voice-model' | 'vision-model' + | 'compaction-model' | 'subagent_create' | 'subagent_list' | 'skills_manage' diff --git a/packages/cli/src/ui/components/DialogManager.tsx b/packages/cli/src/ui/components/DialogManager.tsx index e2160503fc6..176641fa28a 100644 --- a/packages/cli/src/ui/components/DialogManager.tsx +++ b/packages/cli/src/ui/components/DialogManager.tsx @@ -271,6 +271,7 @@ export const DialogManager = ({ isFastModelMode={uiState.isFastModelMode} isVoiceModelMode={uiState.isVoiceModelMode} isVisionModelMode={uiState.isVisionModelMode} + isCompactionModelMode={uiState.isCompactionModelMode} /> ); } diff --git a/packages/cli/src/ui/components/ModelDialog.tsx b/packages/cli/src/ui/components/ModelDialog.tsx index 7857a5ed711..826af309fdf 100644 --- a/packages/cli/src/ui/components/ModelDialog.tsx +++ b/packages/cli/src/ui/components/ModelDialog.tsx @@ -105,6 +105,7 @@ interface ModelDialogProps { isFastModelMode?: boolean; isVoiceModelMode?: boolean; isVisionModelMode?: boolean; + isCompactionModelMode?: boolean; } function maskApiKey(apiKey: string | undefined): string { @@ -229,6 +230,7 @@ export function ModelDialog({ isFastModelMode, isVoiceModelMode, isVisionModelMode, + isCompactionModelMode, }: ModelDialogProps): React.JSX.Element { const config = useContext(ConfigContext); const uiState = useContext(UIStateContext); @@ -251,7 +253,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), ); // Group registry models by authType @@ -305,7 +309,14 @@ export function ModelDialog({ } return result; - }, [authType, config, isFastModelMode, isVoiceModelMode]); + }, [ + authType, + config, + isFastModelMode, + isVoiceModelMode, + isVisionModelMode, + isCompactionModelMode, + ]); const MODEL_OPTIONS = useMemo( () => @@ -401,8 +412,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() diff --git a/packages/cli/src/ui/contexts/UIActionsContext.tsx b/packages/cli/src/ui/contexts/UIActionsContext.tsx index 7d10da5c3a9..eeafc3b203b 100644 --- a/packages/cli/src/ui/contexts/UIActionsContext.tsx +++ b/packages/cli/src/ui/contexts/UIActionsContext.tsx @@ -52,6 +52,7 @@ export interface UIActions { fastModelMode?: boolean; voiceModelMode?: boolean; visionModelMode?: boolean; + compactionModelMode?: boolean; }) => void; openArenaDialog: (type: Exclude) => void; closeArenaDialog: () => void; diff --git a/packages/cli/src/ui/contexts/UIStateContext.tsx b/packages/cli/src/ui/contexts/UIStateContext.tsx index 8a2ea8b0922..0517ea95309 100644 --- a/packages/cli/src/ui/contexts/UIStateContext.tsx +++ b/packages/cli/src/ui/contexts/UIStateContext.tsx @@ -69,6 +69,7 @@ export interface UIState { isFastModelMode: boolean; isVoiceModelMode: boolean; isVisionModelMode: boolean; + isCompactionModelMode: boolean; isTrustDialogOpen: boolean; activeArenaDialog: ArenaDialogType; isPermissionsDialogOpen: boolean; diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts index fcc5d81e0d3..5ff2d3668c3 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts @@ -122,6 +122,7 @@ export interface SlashCommandProcessorActions { fastModelMode?: boolean; voiceModelMode?: boolean; visionModelMode?: boolean; + compactionModelMode?: boolean; }) => void; openTrustDialog: () => void; openPermissionsDialog: () => void; @@ -832,6 +833,9 @@ export const useSlashCommandProcessor = ( case 'vision-model': actions.openModelDialog({ visionModelMode: true }); return { type: 'handled' }; + case 'compaction-model': + actions.openModelDialog({ compactionModelMode: true }); + return { type: 'handled' }; case 'trust': actions.openTrustDialog(); return { type: 'handled' }; diff --git a/packages/cli/src/ui/hooks/useModelCommand.ts b/packages/cli/src/ui/hooks/useModelCommand.ts index 67ff02b2d12..678ebc7a966 100644 --- a/packages/cli/src/ui/hooks/useModelCommand.ts +++ b/packages/cli/src/ui/hooks/useModelCommand.ts @@ -11,10 +11,12 @@ interface UseModelCommandReturn { isFastModelMode: boolean; isVoiceModelMode: boolean; isVisionModelMode: boolean; + isCompactionModelMode: boolean; openModelDialog: (options?: { fastModelMode?: boolean; voiceModelMode?: boolean; visionModelMode?: boolean; + compactionModelMode?: boolean; }) => void; closeModelDialog: () => void; } @@ -24,25 +26,31 @@ export const useModelCommand = (): UseModelCommandReturn => { const [isFastModelMode, setIsFastModelMode] = useState(false); const [isVoiceModelMode, setIsVoiceModelMode] = useState(false); const [isVisionModelMode, setIsVisionModelMode] = useState(false); + const [isCompactionModelMode, setIsCompactionModelMode] = useState(false); const openModelDialog = useCallback( (options?: { fastModelMode?: boolean; voiceModelMode?: boolean; visionModelMode?: boolean; + compactionModelMode?: boolean; }) => { const voiceModelMode = options?.voiceModelMode ?? false; const visionModelMode = options?.visionModelMode ?? false; + const compactionModelMode = options?.compactionModelMode ?? false; // Modes are mutually exclusive; a specialized mode suppresses fast mode. setIsFastModelMode( - voiceModelMode || visionModelMode + voiceModelMode || visionModelMode || compactionModelMode ? false : (options?.fastModelMode ?? false), ); // Vision wins over voice when both are passed, so the dialog can't end up // in two specialized modes at once (mismatched title vs. highlighted row). - setIsVoiceModelMode(visionModelMode ? false : voiceModelMode); - setIsVisionModelMode(visionModelMode); + setIsVoiceModelMode( + visionModelMode || compactionModelMode ? false : voiceModelMode, + ); + setIsVisionModelMode(compactionModelMode ? false : visionModelMode); + setIsCompactionModelMode(compactionModelMode); setIsModelDialogOpen(true); }, [], @@ -53,6 +61,7 @@ export const useModelCommand = (): UseModelCommandReturn => { setIsFastModelMode(false); setIsVoiceModelMode(false); setIsVisionModelMode(false); + setIsCompactionModelMode(false); }, []); return { @@ -60,6 +69,7 @@ export const useModelCommand = (): UseModelCommandReturn => { isFastModelMode, isVoiceModelMode, isVisionModelMode, + isCompactionModelMode, openModelDialog, closeModelDialog, }; diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index b4afc61db34..af817e59186 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -1023,6 +1023,12 @@ export interface ConfigParameters { * (configurable via `/model --vision`). */ visionModel?: string; + /** + * Dedicated model for chat compression (auto-compaction). Falls back to + * fastModel, then the main model. Corresponds to the `compactionModel` setting + * (configurable via `/model --compaction`). + */ + compactionModel?: string; /** * Disable all hooks (default: false, hooks enabled). * Migration note: This replaces the deprecated hooksConfig.enabled setting. @@ -1487,6 +1493,7 @@ export class Config { private readonly autoSkillConfirm: boolean; private fastModel?: string; private visionModel?: string; + private compactionModel?: string; private readonly disableAllHooks: boolean; private readonly stopHookBlockingCap: number; /** User-level hooks (always loaded regardless of trust) */ @@ -1778,6 +1785,7 @@ export class Config { this.autoSkillConfirm = params.autoSkillConfirm ?? true; this.fastModel = params.fastModel || undefined; this.visionModel = params.visionModel || undefined; + this.compactionModel = params.compactionModel || undefined; this.disableAllHooks = params.disableAllHooks ?? false; this.stopHookBlockingCap = resolveStopHookBlockingCap( params.stopHookBlockingCap, @@ -2957,6 +2965,62 @@ export class Config { this.visionModel = model || undefined; } + /** + * Resolve the compaction model for chat compression (auto-compaction). + * Priority: compactionModel (if set) → fastModel (if set) → main model. + */ + getCompactionModel(): string | undefined { + const selector = this.resolveCompactionModelSelector(); + if (selector) { + const available = selector.authType + ? this.getAllConfiguredModels([selector.authType]) + : this.getAllConfiguredModels(); + if (!available.some((m) => m.id === selector.modelId)) { + return undefined; + } + const rawSelector = resolveModelId(this.compactionModel); + return rawSelector?.authType + ? `${rawSelector.authType}:${selector.modelId}` + : selector.modelId; + } + // Fallback: fastModel → main model + return this.getFastModel() ?? this.getModel(); + } + + private resolveCompactionModelSelector() { + if (!this.compactionModel) return undefined; + try { + const rawSelector = resolveModelId(this.compactionModel); + if (!rawSelector) return undefined; + if (rawSelector.authType) return rawSelector; + + const currentAuthType = this.getContentGeneratorConfig()?.authType; + if (!currentAuthType) { + this.debugLogger.debug( + 'No active auth type; skipping bare compaction model resolution', + ); + return undefined; + } + + return resolveModelId(this.compactionModel, { + currentAuthType, + getAvailableModels: () => + this.getAllConfiguredModels([currentAuthType]), + }); + } catch { + return undefined; + } + } + + /** + * Update the compaction model at runtime (e.g. `/model --compaction `). + * Pass undefined or an empty string to clear the override and fall back to + * fastModel, then the main model. + */ + setCompactionModel(model: string | undefined): void { + this.compactionModel = model || undefined; + } + /** * Whether `model` is the same entry as the current primary model — matched on * the provider identity (auth type, and baseUrl when both carry one), not just diff --git a/packages/core/src/models/types.ts b/packages/core/src/models/types.ts index 5bb954d18f1..4049590036e 100644 --- a/packages/core/src/models/types.ts +++ b/packages/core/src/models/types.ts @@ -126,6 +126,8 @@ export interface AvailableModel { fastOnly?: boolean; /** When true, this model only appears in the voice model selector */ voiceOnly?: boolean; + /** When true, this model only appears in the vision model selector */ + visionOnly?: boolean; /** Whether this is a runtime model (not from modelProviders) */ isRuntimeModel?: boolean;