feat(desktop): per-message usage stats UI (tokens, cost, TTFT, tok/s) (#10210)

Co-authored-by: Douwe M Osinga <douwe@sidewalklabs.com>
This commit is contained in:
filip
2026-07-06 13:51:06 -07:00
committed by GitHub
parent 8694a8d104
commit af2c765c82
40 changed files with 2093 additions and 22 deletions
File diff suppressed because one or more lines are too long
+40 -1
View File
@@ -2533,7 +2533,9 @@ export type GooseSessionUpdate = ({
sessionUpdate: 'usage_update';
} & SessionUsageUpdate) | ({
sessionUpdate: 'status_message';
} & StatusMessageUpdate);
} & StatusMessageUpdate) | ({
sessionUpdate: 'message_usage';
} & MessageUsageUpdate);
/**
* Streaming context-window usage update for a session.
@@ -2562,6 +2564,43 @@ export type StatusMessageUpdate = {
status: StatusMessage;
};
/**
* Wire mirror of the conversation `MessageUsage` (this crate cannot depend on
* goose-provider-types); field names and serde casing MUST stay in parity.
*/
export type MessageUsageData = {
inputTokens?: number | null;
outputTokens?: number | null;
totalTokens?: number | null;
cacheReadTokens?: number | null;
cacheWriteTokens?: number | null;
cost?: number | null;
costSource?: CostSourceData | null;
/**
* Wall-clock generation time, used by the client for a tokens/sec readout.
*/
elapsedMs?: number | null;
timeToFirstTokenMs?: number | null;
/**
* Usage from a compaction/summarization call rather than a normal turn.
*/
isCompaction?: boolean;
};
/**
* Wire mirror of the conversation `CostSource`.
*/
export type CostSourceData = 'provider_reported' | 'estimated';
/**
* Per-message token usage/cost/timing, keyed by the message id used for
* chunk matching. Sent live after a turn's messages and on replay.
*/
export type MessageUsageUpdate = {
messageId?: string | null;
usage: MessageUsageData;
};
export type RequestRecipeParams_unstable = {
sessionId: string;
parameters: Array<RecipeParameterDto>;
+68 -1
View File
@@ -2698,6 +2698,70 @@ export const zStatusMessageUpdate = z.object({
status: zStatusMessage
});
/**
* Wire mirror of the conversation `CostSource`.
*/
export const zCostSourceData = z.union([
z.literal('provider_reported'),
z.literal('estimated')
]);
/**
* Wire mirror of the conversation `MessageUsage` (this crate cannot depend on
* goose-provider-types); field names and serde casing MUST stay in parity.
*/
export const zMessageUsageData = z.object({
inputTokens: z.union([
z.number().int(),
z.null()
]).optional(),
outputTokens: z.union([
z.number().int(),
z.null()
]).optional(),
totalTokens: z.union([
z.number().int(),
z.null()
]).optional(),
cacheReadTokens: z.union([
z.number().int(),
z.null()
]).optional(),
cacheWriteTokens: z.union([
z.number().int(),
z.null()
]).optional(),
cost: z.union([
z.number(),
z.null()
]).optional(),
costSource: z.union([
zCostSourceData,
z.null()
]).optional(),
elapsedMs: z.union([
z.number().int().gte(0),
z.null()
]).optional(),
timeToFirstTokenMs: z.union([
z.number().int().gte(0),
z.null()
]).optional(),
isCompaction: z.boolean().optional().default(false)
});
/**
* Per-message token usage/cost/timing, keyed by the message id used for
* chunk matching. Sent live after a turn's messages and on replay.
*/
export const zMessageUsageUpdate = z.object({
messageId: z.union([
z.string(),
z.null()
]).optional(),
usage: zMessageUsageData
});
/**
* Discriminated union of goose-specific session update payloads.
* Variant tag matches ACP's convention (`sessionUpdate: "<snake_case>"`).
@@ -2712,7 +2776,10 @@ export const zGooseSessionUpdate = z.union([
}).and(zSessionUsageUpdate),
z.object({
sessionUpdate: z.literal('status_message')
}).and(zStatusMessageUpdate)
}).and(zStatusMessageUpdate),
z.object({
sessionUpdate: z.literal('message_usage')
}).and(zMessageUsageUpdate)
]);
/**