diff --git a/packages/llm/src/protocols/anthropic-messages.ts b/packages/llm/src/protocols/anthropic-messages.ts index 1c0dcd32a433..80642101a87a 100644 --- a/packages/llm/src/protocols/anthropic-messages.ts +++ b/packages/llm/src/protocols/anthropic-messages.ts @@ -171,7 +171,7 @@ const AnthropicMessagesBody = Schema.Struct(AnthropicBodyFields) export type AnthropicMessagesBody = Schema.Schema.Type const AnthropicUsage = Schema.Struct({ - input_tokens: Schema.optional(Schema.Number), + input_tokens: optionalNull(Schema.Number), output_tokens: Schema.optional(Schema.Number), cache_creation_input_tokens: optionalNull(Schema.Number), cache_read_input_tokens: optionalNull(Schema.Number), @@ -572,7 +572,7 @@ const mapFinishReason = (reason: string | null | undefined): FinishReason => { // `outputTokens` carries the combined total. const mapUsage = (usage: AnthropicUsage | undefined): Usage | undefined => { if (!usage) return undefined - const nonCached = usage.input_tokens + const nonCached = usage.input_tokens ?? undefined const cacheRead = usage.cache_read_input_tokens ?? undefined const cacheWrite = usage.cache_creation_input_tokens ?? undefined const inputTokens = ProviderShared.sumTokens(nonCached, cacheRead, cacheWrite) diff --git a/packages/llm/test/provider/anthropic-messages.test.ts b/packages/llm/test/provider/anthropic-messages.test.ts index 898931295849..90a0a90344a2 100644 --- a/packages/llm/test/provider/anthropic-messages.test.ts +++ b/packages/llm/test/provider/anthropic-messages.test.ts @@ -407,6 +407,34 @@ describe("Anthropic Messages route", () => { }), ) + it.effect("retains earlier input usage when the terminal count is null", () => + Effect.gen(function* () { + const body = sseEvents( + { type: "message_start", message: { usage: { input_tokens: 5 } } }, + { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } }, + { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "Hello" } }, + { type: "content_block_stop", index: 0 }, + { + type: "message_delta", + delta: { stop_reason: "end_turn" }, + usage: { input_tokens: null, output_tokens: 2 }, + }, + { type: "message_stop" }, + ) + const response = yield* LLMClient.generate(request).pipe(Effect.provide(fixedResponse(body))) + + expect(response.usage).toMatchObject({ + inputTokens: 5, + outputTokens: 2, + nonCachedInputTokens: 5, + totalTokens: 7, + providerMetadata: { + anthropic: { input_tokens: null, output_tokens: 2 }, + }, + }) + }), + ) + it.effect("assembles streamed tool call input", () => Effect.gen(function* () { const body = sseEvents(