Merge pull request #2914 from pocharlies/fix/context-usage-server-total
fix(ui): stop the context meter from counting every internal round-trip
This commit is contained in:
@@ -14,8 +14,8 @@ describe('computeContextUsage', () => {
|
||||
});
|
||||
|
||||
test('reports the latest turn rather than a sum across turns', () => {
|
||||
// Each assistant turn reports the whole window it saw, so adding them up
|
||||
// would report several times the real fill.
|
||||
// A turn's tokens describe that turn's window, so adding turns up would
|
||||
// report several times the real fill.
|
||||
const usage = computeContextUsage(
|
||||
[
|
||||
assistant({ input: 400, output: 0, reasoning: 0 }, 'old'),
|
||||
@@ -61,4 +61,24 @@ describe('computeContextUsage', () => {
|
||||
const usage = computeContextUsage([assistant({ input: 10 })], 100);
|
||||
expect(usage?.totalTokens).toBe(10);
|
||||
});
|
||||
|
||||
test('prefers the server-reported total over summing round-trip fields', () => {
|
||||
// Real payload from opencode 1.18.18: ~14 tool-call round-trips accumulated
|
||||
// cache.read to 3.29M while the 1M window really held 232,872. Summing
|
||||
// rendered 330.6%; the reported total renders the real 23.3%.
|
||||
const usage = computeContextUsage(
|
||||
[assistant({ total: 232_872, input: 0, output: 14_523, reasoning: 0, cache: { read: 3_291_956, write: 0 } })],
|
||||
1_000_000,
|
||||
);
|
||||
expect(usage?.totalTokens).toBe(232_872);
|
||||
expect(usage?.percent.toFixed(4)).toBe('23.2872');
|
||||
});
|
||||
|
||||
test('selects a message whose only signal is the reported total', () => {
|
||||
const usage = computeContextUsage(
|
||||
[assistant({ total: 5_000, input: 0, output: 0, reasoning: 0 })],
|
||||
100_000,
|
||||
);
|
||||
expect(usage?.totalTokens).toBe(5_000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,11 @@
|
||||
* global read to race with.
|
||||
*/
|
||||
|
||||
import { contextTokensFromBreakdown } from '@/stores/utils/tokenUtils';
|
||||
|
||||
type MessageTokens = {
|
||||
/** Server-reported window of the turn's final round-trip; absent on older servers. */
|
||||
total?: number;
|
||||
input?: number;
|
||||
output?: number;
|
||||
reasoning?: number;
|
||||
@@ -37,18 +41,12 @@ type WorkStatusContextUsage = {
|
||||
/** The store's own fallback when a model exposes no context limit. */
|
||||
export const DEFAULT_CONTEXT_LIMIT = 200_000;
|
||||
|
||||
const sumTokens = (tokens: MessageTokens): number => (
|
||||
(tokens.input ?? 0)
|
||||
+ (tokens.output ?? 0)
|
||||
+ (tokens.reasoning ?? 0)
|
||||
+ (tokens.cache?.read ?? 0)
|
||||
+ (tokens.cache?.write ?? 0)
|
||||
);
|
||||
|
||||
/**
|
||||
* Usage from the newest assistant message that reported a non-zero token count.
|
||||
* Each assistant turn reports the whole window it saw, so the latest one is the
|
||||
* current fill — not a sum across turns.
|
||||
* The latest turn describes the current fill — not a sum across turns. Within
|
||||
* a turn, the server-reported `total` is the final round-trip's window;
|
||||
* summing the breakdown fields instead overstates multi-step turns, whose
|
||||
* input/cache fields accumulate across round-trips.
|
||||
*/
|
||||
export const computeContextUsage = (
|
||||
messages: readonly MessageLike[],
|
||||
@@ -60,7 +58,7 @@ export const computeContextUsage = (
|
||||
const message = messages[index];
|
||||
if (message?.role !== 'assistant' || !message.tokens) continue;
|
||||
|
||||
const totalTokens = sumTokens(message.tokens);
|
||||
const totalTokens = contextTokensFromBreakdown(message.tokens);
|
||||
if (totalTokens <= 0) continue;
|
||||
|
||||
const limit = contextLimit > 0 ? contextLimit : DEFAULT_CONTEXT_LIMIT;
|
||||
|
||||
@@ -92,6 +92,7 @@ const extractTokenBreakdown = (message: SessionMessage): TokenBreakdown => {
|
||||
}
|
||||
|
||||
const breakdown = source as {
|
||||
total?: unknown;
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
reasoning?: unknown;
|
||||
@@ -103,6 +104,10 @@ const extractTokenBreakdown = (message: SessionMessage): TokenBreakdown => {
|
||||
const reasoning = toNonNegativeNumber(breakdown.reasoning);
|
||||
const cacheRead = toNonNegativeNumber(breakdown.cache?.read);
|
||||
const cacheWrite = toNonNegativeNumber(breakdown.cache?.write);
|
||||
// Multi-step turns accumulate the fields across API round-trips (every tool
|
||||
// call re-reads the whole cached prompt), so summing them overstates the
|
||||
// window. The server-reported total is the final round-trip's window.
|
||||
const reportedTotal = toNonNegativeNumber(breakdown.total);
|
||||
|
||||
return {
|
||||
input,
|
||||
@@ -110,7 +115,7 @@ const extractTokenBreakdown = (message: SessionMessage): TokenBreakdown => {
|
||||
reasoning,
|
||||
cacheRead,
|
||||
cacheWrite,
|
||||
total: input + output + reasoning + cacheRead + cacheWrite,
|
||||
total: reportedTotal > 0 ? reportedTotal : input + output + reasoning + cacheRead + cacheWrite,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useViewportStore } from '@/sync/viewport-store';
|
||||
import { useSessions, useDirectorySync, useSessionMessages, useSessionMessagesResolved } from '@/sync/sync-context';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { resolveGlobalSessionDirectory, useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore';
|
||||
import { contextTokensFromBreakdown } from '@/stores/utils/tokenUtils';
|
||||
import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay';
|
||||
import { McpDropdown } from '@/components/mcp/McpDropdown';
|
||||
import { ArchiveAllDropdown } from '@/components/session/ArchiveAllDropdown';
|
||||
@@ -702,7 +703,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
|
||||
}
|
||||
|
||||
if (!lastTokens && message.tokens) {
|
||||
const total = message.tokens.input + message.tokens.output + message.tokens.reasoning + (message.tokens.cache?.read ?? 0) + (message.tokens.cache?.write ?? 0);
|
||||
const total = contextTokensFromBreakdown(message.tokens);
|
||||
if (total > 0) {
|
||||
lastTokens = message.tokens;
|
||||
lastMessageId = (currentSessionMessages[i] as { id?: string }).id;
|
||||
@@ -730,7 +731,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
|
||||
}
|
||||
|
||||
const lastTokens = headerMessageSummary.lastTokens;
|
||||
const totalTokens = lastTokens.input + lastTokens.output + lastTokens.reasoning + (lastTokens.cache?.read ?? 0) + (lastTokens.cache?.write ?? 0);
|
||||
const totalTokens = contextTokensFromBreakdown(lastTokens);
|
||||
const thresholdLimit = contextLimit > 0 ? contextLimit : 200000;
|
||||
const percentage = contextLimit > 0 ? Math.round((totalTokens / contextLimit) * 100) : 0;
|
||||
const normalizedOutput = outputLimit > 0 ? Math.round((lastTokens.output / outputLimit) * 100) : undefined;
|
||||
|
||||
@@ -18,6 +18,7 @@ import { useGitBranchLabel, useGitStore } from '@/stores/useGitStore';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||
import { contextTokensFromBreakdown } from '@/stores/utils/tokenUtils';
|
||||
import type { SessionContextUsage } from '@/stores/types/sessionTypes';
|
||||
|
||||
type MiniChatMode = 'session' | 'draft';
|
||||
@@ -157,7 +158,7 @@ const MiniChatHeader: React.FC<{ mode: MiniChatMode }> = ({ mode }) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
type AssistantTokens = { input: number; output: number; reasoning: number; cache: { read: number; write: number } };
|
||||
type AssistantTokens = { total?: number; input: number; output: number; reasoning: number; cache: { read: number; write: number } };
|
||||
let lastTokens: AssistantTokens | undefined;
|
||||
let lastMessageId: string | undefined;
|
||||
|
||||
@@ -166,7 +167,7 @@ const MiniChatHeader: React.FC<{ mode: MiniChatMode }> = ({ mode }) => {
|
||||
if (message.role !== 'assistant') continue;
|
||||
const tokens = (message as { tokens?: AssistantTokens }).tokens;
|
||||
if (!tokens) continue;
|
||||
const total = tokens.input + tokens.output + tokens.reasoning + (tokens.cache?.read ?? 0) + (tokens.cache?.write ?? 0);
|
||||
const total = contextTokensFromBreakdown(tokens);
|
||||
if (total > 0) {
|
||||
lastTokens = tokens;
|
||||
lastMessageId = message.id;
|
||||
@@ -178,7 +179,7 @@ const MiniChatHeader: React.FC<{ mode: MiniChatMode }> = ({ mode }) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const totalTokens = lastTokens.input + lastTokens.output + lastTokens.reasoning + (lastTokens.cache?.read ?? 0) + (lastTokens.cache?.write ?? 0);
|
||||
const totalTokens = contextTokensFromBreakdown(lastTokens);
|
||||
const thresholdLimit = contextLimit > 0 ? contextLimit : 200000;
|
||||
const percentage = contextLimit > 0 ? Math.round((totalTokens / contextLimit) * 100) : 0;
|
||||
const normalizedOutput = outputLimit > 0 ? Math.round((lastTokens.output / outputLimit) * 100) : undefined;
|
||||
|
||||
Reference in New Issue
Block a user