mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 12:40:32 +00:00
@@ -1768,17 +1768,20 @@ function totalTokens(usage: UsageSnapshot): number {
|
||||
/** Project a nullable API usage object into our non-null snapshot shape.
|
||||
* Both SDK message_start and assistant message `usage` have `number | null`
|
||||
* cache fields; we coerce absent values to 0 so `totalTokens` never hits
|
||||
* NaN. Delta events have different semantics (cumulative + prev fallback)
|
||||
* and are handled inline. */
|
||||
* NaN. `input_tokens`/`output_tokens` are typed `number` by the SDK but
|
||||
* synthetic or third-party-backend stream events have been observed emitting
|
||||
* them as null/undefined — coerce those too so a malformed upstream event
|
||||
* can't leak NaN into the wire `used` field. Delta events have different
|
||||
* semantics (cumulative + prev fallback) and are handled inline. */
|
||||
function snapshotFromUsage(usage: {
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
cache_read_input_tokens: number | null;
|
||||
cache_creation_input_tokens: number | null;
|
||||
input_tokens?: number | null;
|
||||
output_tokens?: number | null;
|
||||
cache_read_input_tokens?: number | null;
|
||||
cache_creation_input_tokens?: number | null;
|
||||
}): UsageSnapshot {
|
||||
return {
|
||||
input_tokens: usage.input_tokens,
|
||||
output_tokens: usage.output_tokens,
|
||||
input_tokens: usage.input_tokens ?? 0,
|
||||
output_tokens: usage.output_tokens ?? 0,
|
||||
cache_read_input_tokens: usage.cache_read_input_tokens ?? 0,
|
||||
cache_creation_input_tokens: usage.cache_creation_input_tokens ?? 0,
|
||||
};
|
||||
|
||||
@@ -1937,6 +1937,58 @@ describe("usage_update computation", () => {
|
||||
expect(usageUpdate.update.used).toBe(1800);
|
||||
});
|
||||
|
||||
it("coerces null input/output tokens so wire `used` is never null", async () => {
|
||||
// Synthetic or third-party-backend stream events have been observed
|
||||
// emitting input_tokens/output_tokens as null. Without coercion the
|
||||
// snapshot leaks NaN into totalTokens(), and JSON.stringify(NaN) === "null"
|
||||
// produces a malformed `used: null` that schema-validating ACP clients reject.
|
||||
const { agent, updates } = createMockAgentWithCapture();
|
||||
injectSession(agent, [
|
||||
createAssistantMessage({
|
||||
model: "claude-opus-4-20250514",
|
||||
usage: {
|
||||
input_tokens: null,
|
||||
output_tokens: null,
|
||||
cache_read_input_tokens: 200,
|
||||
cache_creation_input_tokens: 100,
|
||||
} as unknown as {
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
cache_read_input_tokens: number;
|
||||
cache_creation_input_tokens: number;
|
||||
},
|
||||
}),
|
||||
createResultMessageWithModel({
|
||||
modelUsage: {
|
||||
"claude-opus-4-20250514": {
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheReadInputTokens: 200,
|
||||
cacheCreationInputTokens: 100,
|
||||
webSearchRequests: 0,
|
||||
costUSD: 0.01,
|
||||
contextWindow: 1000000,
|
||||
maxOutputTokens: 16384,
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ type: "system", subtype: "session_state_changed", state: "idle" },
|
||||
]);
|
||||
|
||||
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "test" }] });
|
||||
|
||||
const usageUpdates = updates.filter((u: any) => u.update?.sessionUpdate === "usage_update");
|
||||
expect(usageUpdates.length).toBeGreaterThan(0);
|
||||
for (const u of usageUpdates) {
|
||||
expect(u.update.used).not.toBeNull();
|
||||
expect(Number.isFinite(u.update.used)).toBe(true);
|
||||
// Round-trip through JSON to catch the NaN -> "null" serialization bug.
|
||||
const wire = JSON.parse(JSON.stringify(u.update));
|
||||
expect(wire.used).not.toBeNull();
|
||||
expect(typeof wire.used).toBe("number");
|
||||
}
|
||||
});
|
||||
|
||||
it("stream_event message_start emits usage_update before result", async () => {
|
||||
const { agent, updates } = createMockAgentWithCapture();
|
||||
injectSession(agent, [
|
||||
|
||||
Reference in New Issue
Block a user