From d67206819a72d7fc634fdcb48aa854d4abe9de76 Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Thu, 29 Jan 2026 20:51:03 +0800 Subject: [PATCH] fix(contextWindowSize): fix context window size update on model switch and ACP agent config priority - Fix contextWindowSize not updating when switching models via setModel() - Fix ACP agent to respect provider-configured contextWindowSize before auto-detection - Simplify getTruncateToolOutputThreshold to use static threshold - Add support for GLM-4.7, Kimi-2.5, and MiniMax-M2.1 models - Update context usage display to require explicit contextWindowSize Co-authored-by: Qwen-Coder --- packages/cli/src/acp-integration/acpAgent.ts | 10 ++-- .../src/ui/components/ContextUsageDisplay.tsx | 6 +-- packages/cli/src/ui/components/Footer.tsx | 10 ++-- packages/core/src/config/config.test.ts | 49 ++++--------------- packages/core/src/config/config.ts | 13 +---- packages/core/src/core/tokenLimits.ts | 12 +++-- packages/core/src/models/modelRegistry.ts | 1 + packages/core/src/models/modelsConfig.ts | 21 +++++--- packages/core/src/models/types.ts | 2 + 9 files changed, 47 insertions(+), 77 deletions(-) diff --git a/packages/cli/src/acp-integration/acpAgent.ts b/packages/cli/src/acp-integration/acpAgent.ts index 0e934d881..12cf69890 100644 --- a/packages/cli/src/acp-integration/acpAgent.ts +++ b/packages/cli/src/acp-integration/acpAgent.ts @@ -379,9 +379,7 @@ class GeminiAgent { name: model.label, description: model.description ?? null, _meta: { - // Each model should have its own context window size based on its capabilities - // Use tokenLimit to get the model-specific context window size - contextLimit: tokenLimit(model.id), + contextLimit: model.contextWindowSize ?? tokenLimit(model.id), }, })); @@ -389,13 +387,15 @@ class GeminiAgent { currentModelId && !mappedAvailableModels.some((model) => model.modelId === currentModelId) ) { + const currentContextWindowSize = + config.getContentGeneratorConfig()?.contextWindowSize ?? + tokenLimit(currentModelId); mappedAvailableModels.unshift({ modelId: currentModelId, name: currentModelId, description: null, _meta: { - // Get context window size specific to the current model - contextLimit: tokenLimit(currentModelId), + contextLimit: currentContextWindowSize, }, }); } diff --git a/packages/cli/src/ui/components/ContextUsageDisplay.tsx b/packages/cli/src/ui/components/ContextUsageDisplay.tsx index 56a6f665f..dc2e22d7c 100644 --- a/packages/cli/src/ui/components/ContextUsageDisplay.tsx +++ b/packages/cli/src/ui/components/ContextUsageDisplay.tsx @@ -6,7 +6,6 @@ import { Text } from 'ink'; import { theme } from '../semantic-colors.js'; -import { DEFAULT_TOKEN_LIMIT } from '@qwen-code/qwen-code-core'; export const ContextUsageDisplay = ({ promptTokenCount, @@ -15,14 +14,13 @@ export const ContextUsageDisplay = ({ }: { promptTokenCount: number; terminalWidth: number; - contextWindowSize?: number; + contextWindowSize: number; }) => { if (promptTokenCount === 0) { return null; } - const contextLimit = contextWindowSize ?? DEFAULT_TOKEN_LIMIT; - const percentage = promptTokenCount / contextLimit; + const percentage = promptTokenCount / contextWindowSize; const percentageUsed = (percentage * 100).toFixed(1); const label = terminalWidth < 100 ? '% used' : '% context used'; diff --git a/packages/cli/src/ui/components/Footer.tsx b/packages/cli/src/ui/components/Footer.tsx index 9d8fa1d8a..b55923a84 100644 --- a/packages/cli/src/ui/components/Footer.tsx +++ b/packages/cli/src/ui/components/Footer.tsx @@ -5,7 +5,6 @@ */ import type React from 'react'; -import { useMemo } from 'react'; import { Box, Text } from 'ink'; import { theme } from '../semantic-colors.js'; import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js'; @@ -56,11 +55,8 @@ export const Footer: React.FC = () => { // Check if debug mode is enabled const debugMode = config.getDebugMode(); - // Memoize contextWindowSize to avoid recalculating on every render - const contextWindowSize = useMemo( - () => config.getContentGeneratorConfig()?.contextWindowSize, - [config], - ); + const contextWindowSize = + config.getContentGeneratorConfig()?.contextWindowSize; // Left section should show exactly ONE thing at any time, in priority order. const leftContent = uiState.ctrlCPressedOnce ? ( @@ -93,7 +89,7 @@ export const Footer: React.FC = () => { node: Debug Mode, }); } - if (promptTokenCount > 0) { + if (promptTokenCount > 0 && contextWindowSize) { rightItems.push({ key: 'context', node: ( diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts index 475e19524..e6a87941e 100644 --- a/packages/core/src/config/config.test.ts +++ b/packages/core/src/config/config.test.ts @@ -189,13 +189,8 @@ vi.mock('../ide/ide-client.js', () => ({ })); import { BaseLlmClient } from '../core/baseLlmClient.js'; -import { tokenLimit } from '../core/tokenLimits.js'; -import { uiTelemetryService } from '../telemetry/index.js'; vi.mock('../core/baseLlmClient.js'); -vi.mock('../core/tokenLimits.js', () => ({ - tokenLimit: vi.fn(), -})); describe('Server Config (config.ts)', () => { const MODEL = 'qwen3-coder-plus'; @@ -1036,29 +1031,8 @@ describe('Server Config (config.ts)', () => { }); describe('getTruncateToolOutputThreshold', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('should return the calculated threshold when it is smaller than the default', () => { + it('should return the default threshold', () => { const config = new Config(baseParams); - vi.mocked(tokenLimit).mockReturnValue(8000); - vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue( - 2000, - ); - // 4 * (8000 - 2000) = 4 * 6000 = 24000 - // default is 25_000 - expect(config.getTruncateToolOutputThreshold()).toBe(25000); - }); - - it('should return the default threshold when the calculated value is larger', () => { - const config = new Config(baseParams); - vi.mocked(tokenLimit).mockReturnValue(2_000_000); - vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue( - 500_000, - ); - // 4 * (2_000_000 - 500_000) = 4 * 1_500_000 = 6_000_000 - // default is 25_000 expect(config.getTruncateToolOutputThreshold()).toBe(25_000); }); @@ -1068,21 +1042,18 @@ describe('Server Config (config.ts)', () => { truncateToolOutputThreshold: 50000, }; const config = new Config(customParams); - vi.mocked(tokenLimit).mockReturnValue(8000); - vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue( - 2000, - ); - // 4 * (8000 - 2000) = 4 * 6000 = 24000 - // custom threshold is 50000 expect(config.getTruncateToolOutputThreshold()).toBe(50000); + }); - vi.mocked(tokenLimit).mockReturnValue(32000); - vi.mocked(uiTelemetryService.getLastPromptTokenCount).mockReturnValue( - 1000, + it('should return infinity when truncation is disabled', () => { + const customParams = { + ...baseParams, + enableToolOutputTruncation: false, + }; + const config = new Config(customParams); + expect(config.getTruncateToolOutputThreshold()).toBe( + Number.POSITIVE_INFINITY, ); - // 4 * (32000 - 1000) = 124000 - // custom threshold is 50000 - expect(config.getTruncateToolOutputThreshold()).toBe(50000); }); }); }); diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index f6063aa6c..af2d28555 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -79,7 +79,6 @@ import { RipgrepFallbackEvent, StartSessionEvent, type TelemetryTarget, - uiTelemetryService, } from '../telemetry/index.js'; import { ExtensionManager, @@ -1513,17 +1512,7 @@ export class Config { return Number.POSITIVE_INFINITY; } - const contextWindowSize = - this.getContentGeneratorConfig()?.contextWindowSize; - if (!contextWindowSize) { - return this.truncateToolOutputThreshold; - } - - return Math.min( - // Estimate remaining context window in characters (1 token ~= 4 chars). - 4 * (contextWindowSize - uiTelemetryService.getLastPromptTokenCount()), - this.truncateToolOutputThreshold, - ); + return this.truncateToolOutputThreshold; } getTruncateToolOutputLines(): number { diff --git a/packages/core/src/core/tokenLimits.ts b/packages/core/src/core/tokenLimits.ts index bafb38f99..c20bd16a7 100644 --- a/packages/core/src/core/tokenLimits.ts +++ b/packages/core/src/core/tokenLimits.ts @@ -161,6 +161,7 @@ const PATTERNS: Array<[RegExp, TokenCount]> = [ [/^glm-4\.5-air(?:-.*)?$/, LIMITS['128k']], [/^glm-4\.5(?:-.*)?$/, LIMITS['128k']], [/^glm-4\.6(?:-.*)?$/, 202_752 as unknown as TokenCount], // exact limit from the model config file + [/^glm-4\.7(?:-.*)?$/, LIMITS['200k']], // ------------------- // DeepSeek @@ -170,10 +171,8 @@ const PATTERNS: Array<[RegExp, TokenCount]> = [ // ------------------- // Moonshot / Kimi // ------------------- - [/^kimi-k2-0905$/, LIMITS['256k']], // Kimi-k2-0905-preview: 256K context - [/^kimi-k2-turbo.*$/, LIMITS['256k']], // Kimi-k2-turbo-preview: 256K context - [/^kimi-k2-0711$/, LIMITS['128k']], // Kimi-k2-0711-preview: 128K context - [/^kimi-k2-instruct.*$/, LIMITS['128k']], // Kimi-k2-instruct: 128K context + [/^kimi-2\.5.*$/, LIMITS['256k']], // Kimi-2.5: 256K context + [/^kimi-k2.*$/, LIMITS['256k']], // Kimi-k2 variants: 256K context // ------------------- // GPT-OSS / Llama & Mistral examples @@ -181,6 +180,11 @@ const PATTERNS: Array<[RegExp, TokenCount]> = [ [/^gpt-oss.*$/, LIMITS['128k']], [/^llama-4-scout.*$/, LIMITS['10m']], [/^mistral-large-2.*$/, LIMITS['128k']], + + // ------------------- + // MiniMax + // ------------------- + [/^minimax-m2\.1.*$/i, LIMITS['200k']], // MiniMax-M2.1: 200K context ]; /** diff --git a/packages/core/src/models/modelRegistry.ts b/packages/core/src/models/modelRegistry.ts index cec6ebb94..e288dd772 100644 --- a/packages/core/src/models/modelRegistry.ts +++ b/packages/core/src/models/modelRegistry.ts @@ -110,6 +110,7 @@ export class ModelRegistry { capabilities: model.capabilities, authType: model.authType, isVision: model.capabilities?.vision ?? false, + contextWindowSize: model.generationConfig.contextWindowSize, })); } diff --git a/packages/core/src/models/modelsConfig.ts b/packages/core/src/models/modelsConfig.ts index 5700703e9..74f7d250c 100644 --- a/packages/core/src/models/modelsConfig.ts +++ b/packages/core/src/models/modelsConfig.ts @@ -242,6 +242,11 @@ export class ModelsConfig { kind: 'programmatic', detail: metadata?.reason || 'setModel', }; + + // Notify Config to update contentGeneratorConfig + if (this.onModelChange) { + await this.onModelChange(AuthType.QWEN_OAUTH, false); + } return; } @@ -578,12 +583,16 @@ export class ModelsConfig { detail: 'generationConfig.reasoning', }; - // Apply contextWindowSize with fallback logic - // Priority: user config > model auto-detection - // Note: User-set contextWindowSize is already in _generationConfig and its source - // is already tracked in resolveContentGeneratorConfigWithSources - if (this._generationConfig.contextWindowSize === undefined) { - // Auto-detect from model if not explicitly set by user + // Context window size: use provider value if set, otherwise auto-detect from model + if (gc.contextWindowSize !== undefined) { + this._generationConfig.contextWindowSize = gc.contextWindowSize; + this.generationConfigSources['contextWindowSize'] = { + kind: 'modelProviders', + authType: model.authType, + modelId: model.id, + detail: 'generationConfig.contextWindowSize', + }; + } else { this._generationConfig.contextWindowSize = tokenLimit(model.id, 'input'); this.generationConfigSources['contextWindowSize'] = { kind: 'computed', diff --git a/packages/core/src/models/types.ts b/packages/core/src/models/types.ts index f6987d89a..1a4d0c897 100644 --- a/packages/core/src/models/types.ts +++ b/packages/core/src/models/types.ts @@ -33,6 +33,7 @@ export type ModelGenerationConfig = Pick< | 'reasoning' | 'customHeaders' | 'extra_body' + | 'contextWindowSize' >; /** @@ -90,6 +91,7 @@ export interface AvailableModel { capabilities?: ModelCapabilities; authType: AuthType; isVision?: boolean; + contextWindowSize?: number; } /**