diff --git a/src/server/services/sessionService.ts b/src/server/services/sessionService.ts index b053ca3e..6f7bfc60 100644 --- a/src/server/services/sessionService.ts +++ b/src/server/services/sessionService.ts @@ -967,7 +967,7 @@ export class SessionService { output_tokens: latest.outputTokens, cache_read_input_tokens: latest.cacheReadInputTokens, cache_creation_input_tokens: latest.cacheCreationInputTokens, - }) + }, rawMaxTokens) const percentage = rawMaxTokens > 0 ? Math.round((totalTokens / rawMaxTokens) * 100) : 0 const categories: TranscriptContextEstimate['categories'] = [ { name: 'Input tokens', tokens: latest.inputTokens, color: '#8f3217' }, diff --git a/src/utils/__tests__/context.test.ts b/src/utils/__tests__/context.test.ts index 896de453..2851de55 100644 --- a/src/utils/__tests__/context.test.ts +++ b/src/utils/__tests__/context.test.ts @@ -19,4 +19,38 @@ describe('calculateCurrentContextTokenTotal', () => { output_tokens: 0, })).toBe(29_000) }) + + test('clamps to contextWindow when total would exceed it', () => { + // DeepSeek scenario: input_tokens already near 1M limit, adding output pushes over + expect(calculateCurrentContextTokenTotal(990_000, { + input_tokens: 995_000, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 0, + output_tokens: 8_000, + }, 1_000_000)).toBe(1_000_000) + }) + + test('does not clamp when total is within contextWindow', () => { + expect(calculateCurrentContextTokenTotal(26_000, { + input_tokens: 24_000, + cache_creation_input_tokens: 1_000, + cache_read_input_tokens: 1_000, + output_tokens: 3_000, + }, 200_000)).toBe(29_000) + }) + + test('without contextWindow arg behaves as before — no clamping', () => { + // Passing no contextWindow should not clamp, preserving backward compatibility + expect(calculateCurrentContextTokenTotal(990_000, { + input_tokens: 995_000, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 0, + output_tokens: 8_000, + })).toBe(1_003_000) + }) + + test('returns estimatedTokens when currentUsage is null', () => { + expect(calculateCurrentContextTokenTotal(50_000, null)).toBe(50_000) + expect(calculateCurrentContextTokenTotal(50_000, null, 200_000)).toBe(50_000) + }) }) diff --git a/src/utils/analyzeContext.ts b/src/utils/analyzeContext.ts index 0db8a4ca..54606d48 100644 --- a/src/utils/analyzeContext.ts +++ b/src/utils/analyzeContext.ts @@ -1202,9 +1202,12 @@ export async function analyzeContextUsage( // Use the larger of the local estimate and the latest API-reported context. // This keeps the context meter from dropping when a response completes and // the output tokens become part of the next turn's context. + // Clamp to contextWindow so providers whose input_tokens already approach the + // limit (e.g. DeepSeek with 1M context) don't push the display over 100%. const finalTotalTokens = calculateCurrentContextTokenTotal( totalIncludingReserved, apiUsage, + contextWindow, ) // Pre-calculate grid based on model context window and terminal width diff --git a/src/utils/context.ts b/src/utils/context.ts index 87fae9df..8da26ad3 100644 --- a/src/utils/context.ts +++ b/src/utils/context.ts @@ -170,6 +170,10 @@ export function calculateContextPercentages( * immediately after the model finishes responding. The local estimate is kept * as a lower bound because it includes system/tool/message material that some * provider usage payloads under-report. + * + * Pass `contextWindow` to clamp the result to the model's context window size. + * This prevents display values from exceeding 100% for providers (e.g. DeepSeek) + * whose input_tokens already approach the window limit before output is added. */ export function calculateCurrentContextTokenTotal( estimatedTokens: number, @@ -179,6 +183,7 @@ export function calculateCurrentContextTokenTotal( cache_creation_input_tokens: number cache_read_input_tokens: number } | null, + contextWindow?: number, ): number { if (!currentUsage) return estimatedTokens @@ -188,7 +193,8 @@ export function calculateCurrentContextTokenTotal( currentUsage.cache_read_input_tokens + (currentUsage.output_tokens ?? 0) - return Math.max(estimatedTokens, totalFromAPI) + const total = Math.max(estimatedTokens, totalFromAPI) + return contextWindow !== undefined ? Math.min(total, contextWindow) : total } /**