feat(core): add per-model token usage to stream-json output (#21839)

This commit is contained in:
Yongrui Lin
2026-03-10 10:39:04 -07:00
committed by GitHub
parent 18112c474e
commit 4da0366eed
5 changed files with 87 additions and 18 deletions

View File

@@ -4,7 +4,7 @@ exports[`runNonInteractive > should emit appropriate error event in streaming JS
"{"type":"init","timestamp":"<TIMESTAMP>","session_id":"test-session-id","model":"test-model"} "{"type":"init","timestamp":"<TIMESTAMP>","session_id":"test-session-id","model":"test-model"}
{"type":"message","timestamp":"<TIMESTAMP>","role":"user","content":"Loop test"} {"type":"message","timestamp":"<TIMESTAMP>","role":"user","content":"Loop test"}
{"type":"error","timestamp":"<TIMESTAMP>","severity":"warning","message":"Loop detected, stopping execution"} {"type":"error","timestamp":"<TIMESTAMP>","severity":"warning","message":"Loop detected, stopping execution"}
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0}} {"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0,"models":{}}}
" "
`; `;
@@ -12,7 +12,7 @@ exports[`runNonInteractive > should emit appropriate error event in streaming JS
"{"type":"init","timestamp":"<TIMESTAMP>","session_id":"test-session-id","model":"test-model"} "{"type":"init","timestamp":"<TIMESTAMP>","session_id":"test-session-id","model":"test-model"}
{"type":"message","timestamp":"<TIMESTAMP>","role":"user","content":"Max turns test"} {"type":"message","timestamp":"<TIMESTAMP>","role":"user","content":"Max turns test"}
{"type":"error","timestamp":"<TIMESTAMP>","severity":"error","message":"Maximum session turns exceeded"} {"type":"error","timestamp":"<TIMESTAMP>","severity":"error","message":"Maximum session turns exceeded"}
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0}} {"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0,"models":{}}}
" "
`; `;
@@ -23,7 +23,7 @@ exports[`runNonInteractive > should emit appropriate events for streaming JSON o
{"type":"tool_use","timestamp":"<TIMESTAMP>","tool_name":"testTool","tool_id":"tool-1","parameters":{"arg1":"value1"}} {"type":"tool_use","timestamp":"<TIMESTAMP>","tool_name":"testTool","tool_id":"tool-1","parameters":{"arg1":"value1"}}
{"type":"tool_result","timestamp":"<TIMESTAMP>","tool_id":"tool-1","status":"success","output":"Tool executed successfully"} {"type":"tool_result","timestamp":"<TIMESTAMP>","tool_id":"tool-1","status":"success","output":"Tool executed successfully"}
{"type":"message","timestamp":"<TIMESTAMP>","role":"assistant","content":"Final answer","delta":true} {"type":"message","timestamp":"<TIMESTAMP>","role":"assistant","content":"Final answer","delta":true}
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0}} {"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0,"models":{}}}
" "
`; `;

View File

@@ -74,6 +74,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
input: 0, input: 0,
duration_ms: 0, duration_ms: 0,
tool_calls: 0, tool_calls: 0,
models: {},
}), }),
})), })),
uiTelemetryService: { uiTelemetryService: {

View File

@@ -154,6 +154,7 @@ describe('StreamJsonFormatter', () => {
input: 50, input: 50,
duration_ms: 1200, duration_ms: 1200,
tool_calls: 2, tool_calls: 2,
models: {},
}, },
}; };
@@ -180,6 +181,7 @@ describe('StreamJsonFormatter', () => {
input: 50, input: 50,
duration_ms: 1200, duration_ms: 1200,
tool_calls: 0, tool_calls: 0,
models: {},
}, },
}; };
@@ -304,6 +306,15 @@ describe('StreamJsonFormatter', () => {
input: 50, input: 50,
duration_ms: 1200, duration_ms: 1200,
tool_calls: 2, tool_calls: 2,
models: {
'gemini-2.0-flash': {
total_tokens: 80,
input_tokens: 50,
output_tokens: 30,
cached: 0,
input: 50,
},
},
}); });
}); });
@@ -347,6 +358,22 @@ describe('StreamJsonFormatter', () => {
input: 150, input: 150,
duration_ms: 3000, duration_ms: 3000,
tool_calls: 5, tool_calls: 5,
models: {
'gemini-pro': {
total_tokens: 80,
input_tokens: 50,
output_tokens: 30,
cached: 0,
input: 50,
},
'gemini-ultra': {
total_tokens: 170,
input_tokens: 100,
output_tokens: 70,
cached: 0,
input: 100,
},
},
}); });
}); });
@@ -376,6 +403,15 @@ describe('StreamJsonFormatter', () => {
input: 20, input: 20,
duration_ms: 1200, duration_ms: 1200,
tool_calls: 0, tool_calls: 0,
models: {
'gemini-pro': {
total_tokens: 80,
input_tokens: 50,
output_tokens: 30,
cached: 30,
input: 20,
},
},
}); });
}); });
@@ -392,6 +428,7 @@ describe('StreamJsonFormatter', () => {
input: 0, input: 0,
duration_ms: 100, duration_ms: 100,
tool_calls: 0, tool_calls: 0,
models: {},
}); });
}); });
@@ -521,6 +558,7 @@ describe('StreamJsonFormatter', () => {
input: 0, input: 0,
duration_ms: 0, duration_ms: 0,
tool_calls: 0, tool_calls: 0,
models: {},
}, },
} as ResultEvent, } as ResultEvent,
]; ];
@@ -544,6 +582,7 @@ describe('StreamJsonFormatter', () => {
input: 50, input: 50,
duration_ms: 1200, duration_ms: 1200,
tool_calls: 2, tool_calls: 2,
models: {},
}, },
}; };

View File

@@ -4,7 +4,11 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import type { JsonStreamEvent, StreamStats } from './types.js'; import type {
JsonStreamEvent,
ModelStreamStats,
StreamStats,
} from './types.js';
import type { SessionMetrics } from '../telemetry/uiTelemetry.js'; import type { SessionMetrics } from '../telemetry/uiTelemetry.js';
/** /**
@@ -31,7 +35,7 @@ export class StreamJsonFormatter {
/** /**
* Converts SessionMetrics to simplified StreamStats format. * Converts SessionMetrics to simplified StreamStats format.
* Aggregates token counts across all models. * Includes per-model token breakdowns and aggregated totals.
* @param metrics - The session metrics from telemetry * @param metrics - The session metrics from telemetry
* @param durationMs - The session duration in milliseconds * @param durationMs - The session duration in milliseconds
* @returns Simplified stats for streaming output * @returns Simplified stats for streaming output
@@ -40,20 +44,35 @@ export class StreamJsonFormatter {
metrics: SessionMetrics, metrics: SessionMetrics,
durationMs: number, durationMs: number,
): StreamStats { ): StreamStats {
let totalTokens = 0; const { totalTokens, inputTokens, outputTokens, cached, input, models } =
let inputTokens = 0; Object.entries(metrics.models).reduce(
let outputTokens = 0; (acc, [modelName, modelMetrics]) => {
let cached = 0; const modelStats: ModelStreamStats = {
let input = 0; total_tokens: modelMetrics.tokens.total,
input_tokens: modelMetrics.tokens.prompt,
output_tokens: modelMetrics.tokens.candidates,
cached: modelMetrics.tokens.cached,
input: modelMetrics.tokens.input,
};
// Aggregate token counts across all models acc.models[modelName] = modelStats;
for (const modelMetrics of Object.values(metrics.models)) { acc.totalTokens += modelStats.total_tokens;
totalTokens += modelMetrics.tokens.total; acc.inputTokens += modelStats.input_tokens;
inputTokens += modelMetrics.tokens.prompt; acc.outputTokens += modelStats.output_tokens;
outputTokens += modelMetrics.tokens.candidates; acc.cached += modelStats.cached;
cached += modelMetrics.tokens.cached; acc.input += modelStats.input;
input += modelMetrics.tokens.input;
} return acc;
},
{
totalTokens: 0,
inputTokens: 0,
outputTokens: 0,
cached: 0,
input: 0,
models: {} as Record<string, ModelStreamStats>,
},
);
return { return {
total_tokens: totalTokens, total_tokens: totalTokens,
@@ -63,6 +82,7 @@ export class StreamJsonFormatter {
input, input,
duration_ms: durationMs, duration_ms: durationMs,
tool_calls: metrics.tools.totalCalls, tool_calls: metrics.tools.totalCalls,
models,
}; };
} }
} }

View File

@@ -77,6 +77,14 @@ export interface ErrorEvent extends BaseJsonStreamEvent {
message: string; message: string;
} }
export interface ModelStreamStats {
total_tokens: number;
input_tokens: number;
output_tokens: number;
cached: number;
input: number;
}
export interface StreamStats { export interface StreamStats {
total_tokens: number; total_tokens: number;
input_tokens: number; input_tokens: number;
@@ -86,6 +94,7 @@ export interface StreamStats {
input: number; input: number;
duration_ms: number; duration_ms: number;
tool_calls: number; tool_calls: number;
models: Record<string, ModelStreamStats>;
} }
export interface ResultEvent extends BaseJsonStreamEvent { export interface ResultEvent extends BaseJsonStreamEvent {