fix: clamp context token display to model context window size

Providers like DeepSeek with 1M context windows can report
input_tokens that, when combined with output_tokens, exceed the
declared context window — causing the UI to show >100% usage.

Add an optional contextWindow parameter to calculateCurrentContextTokenTotal
that clamps the result to the window size. Pass it from analyzeContext
and sessionService where the window size is already available.

Backward compatible: callers that omit the parameter keep existing behavior.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
程序员阿江(Relakkes) 2026-05-10 18:19:57 +08:00
parent ffef5de9ef
commit 1aa0774cbf
4 changed files with 45 additions and 2 deletions

View File

@ -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' },

View File

@ -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)
})
})

View File

@ -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

View File

@ -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
}
/**