repairs for OpenAI token stats/economics

This commit is contained in:
Rob Colbert 2026-05-18 16:40:19 -04:00
parent ef43fa2a00
commit 6696d79a0d
7 changed files with 58 additions and 19 deletions

View File

@ -408,6 +408,10 @@ export interface IWorkOrderCompleteStats {
masterThinkingTokens: number;
toolCallCount: number;
durationMs: number;
/** Aggregate (master + subagent) totals for session-level accounting */
totalInputTokens?: number;
totalOutputTokens?: number;
totalToolCallCount?: number;
}
export interface ChatTurnPrompts {

View File

@ -756,13 +756,15 @@ export default function ChatSessionView() {
}));
// Update session contextWindowUsage for fuel gauge
// Uses aggregate totals (master+subagent) when available, falls back to master-only
setSession(prev => prev ? {
...prev,
contextWindowUsage: (prev.contextWindowUsage ?? 0) + stats.masterInputTokens,
stats: {
...prev.stats,
inputTokens: prev.stats.inputTokens + stats.masterInputTokens,
outputTokens: prev.stats.outputTokens + stats.masterOutputTokens,
inputTokens: prev.stats.inputTokens + (stats.totalInputTokens ?? stats.masterInputTokens),
outputTokens: prev.stats.outputTokens + (stats.totalOutputTokens ?? stats.masterOutputTokens),
toolCallCount: prev.stats.toolCallCount + (stats.totalToolCallCount ?? stats.toolCallCount),
},
} : prev);
}

View File

@ -675,10 +675,12 @@ export class CodeSession extends SocketSession {
this.socket.emit("workOrderComplete", turnId, success, message, stats);
// Update in-memory session stats for UI reactivity
// Uses aggregate totals (master + subagent) when available, falls back to master-only
if (stats && this.chatSession) {
this.chatSession.stats.inputTokens += stats.masterInputTokens;
this.chatSession.stats.outputTokens += stats.masterOutputTokens;
this.chatSession.stats.toolCallCount += stats.toolCallCount;
this.chatSession.stats.inputTokens += stats.totalInputTokens ?? stats.masterInputTokens;
this.chatSession.stats.outputTokens += stats.totalOutputTokens ?? stats.masterOutputTokens;
this.chatSession.stats.toolCallCount += stats.totalToolCallCount ?? stats.toolCallCount;
// contextWindowUsage is intentionally master-only (fuel gauge tracks master context)
this.chatSession.contextWindowUsage = (this.chatSession.contextWindowUsage ?? 0) + stats.masterInputTokens;
}
}

View File

@ -306,6 +306,8 @@ export class DroneSession extends SocketSession {
await this.flushBuffer(turnId);
this.streamingBuffers.delete(turnId);
let enrichedStats: IWorkOrderCompleteStats | undefined;
const turn = await ChatTurn.findById(turnId);
if (turn) {
if (success && message === "aborted") {
@ -333,22 +335,30 @@ export class DroneSession extends SocketSession {
// COMPUTE AGGREGATE: walk the turn's subagent records for aggregate totals
let totalInputTokens = stats?.masterInputTokens ?? 0;
let totalOutputTokens = stats?.masterOutputTokens ?? 0;
let totalToolCallCount = stats?.toolCallCount ?? 0;
if (stats) {
for (const tc of turn.toolCalls) {
if ((tc as any).subagent?.stats) {
totalInputTokens += (tc as any).subagent.stats.inputTokens;
totalOutputTokens += (tc as any).subagent.stats.responseTokens + (tc as any).subagent.stats.thinkingTokenCount;
const subStats = (tc as any).subagent.stats;
totalInputTokens += subStats.inputTokens;
totalOutputTokens += subStats.responseTokens + subStats.thinkingTokenCount;
totalToolCallCount += subStats.toolCallCount;
}
}
}
// Build enriched stats with aggregate values for in-memory consumers
enrichedStats = stats
? { ...stats, totalInputTokens, totalOutputTokens, totalToolCallCount }
: undefined;
// INCREMENT SESSION STATS — aggregate totals (master + subagents)
await ChatSession.updateOne(
{ _id: this.chatSessionId },
{ $inc: {
"stats.inputTokens": totalInputTokens,
"stats.outputTokens": totalOutputTokens,
"stats.toolCallCount": stats?.toolCallCount ?? 0,
"stats.toolCallCount": totalToolCallCount,
// Master-only field for the fuel gauge
"contextWindowUsage": stats?.masterInputTokens ?? 0,
}},
@ -358,7 +368,7 @@ export class DroneSession extends SocketSession {
const codeSession = SocketService.getCodeSessionByChatSessionId(
this.chatSessionId,
);
codeSession.onWorkOrderComplete(turnId, success, message, stats);
codeSession.onWorkOrderComplete(turnId, success, message, enrichedStats);
this.currentTurnId = undefined;
} catch (error) {

View File

@ -30,6 +30,7 @@ import {
ChatSessionMode,
ChatTurnStatus,
type IProject,
type IWorkOrderCompleteStats,
} from "@gadget/api";
import AiService from "./ai.ts";
@ -59,13 +60,6 @@ import {
type DroneToolboxEnvironment,
} from "../tools/index.ts";
interface IWorkOrderCompleteStats {
masterInputTokens: number;
masterOutputTokens: number;
masterThinkingTokens: number;
toolCallCount: number;
durationMs: number;
}
import type { IChatExportData } from "@gadget/ai-toolbox";
export interface IAgentWorkOrder {
@ -269,9 +263,9 @@ class AgentService extends GadgetService {
}
// Accumulate real token counts from the AI API response
masterInputTokens += response.stats.tokenCounts.input;
masterOutputTokens += response.stats.tokenCounts.response;
masterThinkingTokens += response.stats.tokenCounts.thinking;
masterInputTokens += response.stats?.tokenCounts?.input ?? 0;
masterOutputTokens += response.stats?.tokenCounts?.response ?? 0;
masterThinkingTokens += response.stats?.tokenCounts?.thinking ?? 0;
// Process tool calls if present
if (response.toolCalls && response.toolCalls.length > 0) {
@ -319,6 +313,7 @@ class AgentService extends GadgetService {
agentId: toolCall.callId,
response: responseText,
subagent: subagentData,
stats: subagentData?.stats as Record<string, unknown> | undefined,
});
} else {
socket.emit("toolCall", toolCall.callId, toolCall.function.name, toolCall.function.arguments, result);

View File

@ -47,6 +47,10 @@ export interface IWorkOrderCompleteStats {
masterThinkingTokens: number;
toolCallCount: number;
durationMs: number;
/** Aggregate (master + subagent) totals for session-level accounting */
totalInputTokens?: number;
totalOutputTokens?: number;
totalToolCallCount?: number;
}
export type WorkOrderCompleteMessage = (

22
repairs.md Normal file
View File

@ -0,0 +1,22 @@
Summary of Changes (6 files, +36/-19 lines)
Root Cause
The token-economics commit (00969cf) correctly added subagent token aggregation to the DB write path (drone-session.ts), but the in-memory and frontend paths only used master-only values. This meant:
-
During a session: UI showed lower token counts than reality
-
After page refresh: numbers jumped up (DB values loaded)
-
Session toolCallCount missed subagent tool calls entirely
Fixes
1. packages/api/src/messages/drone.ts — Extended IWorkOrderCompleteStats with optional aggregate fields (totalInputTokens?, totalOutputTokens?, totalToolCallCount?)
2. gadget-code/src/lib/drone-session.ts — Now computes totalToolCallCount (master + subagent tool calls) alongside existing token aggregates, passes enrichedStats (with aggregate fields populated) to codeSession.onWorkOrderComplete()
3. gadget-code/src/lib/code-session.ts — In-memory session stats use stats.totalInputTokens ?? stats.masterInputTokens (aggregate when available, master fallback)
4. gadget-code/frontend/src/lib/api.ts — Frontend type matches the shared interface
5. gadget-code/frontend/src/pages/ChatSessionView.tsx — Frontend session stats update uses aggregate values; also now correctly increments toolCallCount (was previously missing)
6. gadget-drone/src/services/agent.ts — Three fixes:
-
Removed local IWorkOrderCompleteStats redefinition, now imports from @gadget/api
-
Master loop token accumulation uses optional chaining (response.stats?.tokenCounts?.input ?? 0) matching the subagent loop
-
agent:complete emit now includes stats field from subagent data