diff --git a/packages/llm/src/schema/events.ts b/packages/llm/src/schema/events.ts index 187291e685..ee755e93e3 100644 --- a/packages/llm/src/schema/events.ts +++ b/packages/llm/src/schema/events.ts @@ -43,7 +43,17 @@ export class Usage extends Schema.Class("LLM.Usage")({ cacheWriteInputTokens: Schema.optional(Schema.Number), totalTokens: Schema.optional(Schema.Number), native: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), -}) {} +}) { + /** Sum of every input-side category. Monotonic under the additive contract. */ + get totalInputTokens() { + return (this.inputTokens ?? 0) + (this.cacheReadInputTokens ?? 0) + (this.cacheWriteInputTokens ?? 0) + } + + /** Sum of every output-side category. Monotonic under the additive contract. */ + get totalOutputTokens() { + return (this.outputTokens ?? 0) + (this.reasoningTokens ?? 0) + } +} export const RequestStart = Schema.Struct({ type: Schema.tag("request-start"), diff --git a/packages/llm/test/provider/anthropic-messages.test.ts b/packages/llm/test/provider/anthropic-messages.test.ts index 85900a1143..6541454cb5 100644 --- a/packages/llm/test/provider/anthropic-messages.test.ts +++ b/packages/llm/test/provider/anthropic-messages.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" -import { CacheHint, LLM, LLMError } from "../../src" +import { CacheHint, LLM, LLMError, Usage } from "../../src" import { LLMClient } from "../../src/route" import * as AnthropicMessages from "../../src/protocols/anthropic-messages" import { it } from "../lib/effect" @@ -152,7 +152,7 @@ describe("Anthropic Messages route", () => { { type: "request-finish", reason: "tool-calls", - usage: { inputTokens: 5, outputTokens: 1, totalTokens: 6, native: { input_tokens: 5, output_tokens: 1 } }, + usage: new Usage({ inputTokens: 5, outputTokens: 1, totalTokens: 6, native: { input_tokens: 5, output_tokens: 1 } }), }, ]) }), diff --git a/packages/llm/test/provider/gemini.test.ts b/packages/llm/test/provider/gemini.test.ts index 7143798e0f..cd34360cce 100644 --- a/packages/llm/test/provider/gemini.test.ts +++ b/packages/llm/test/provider/gemini.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" -import { LLM, LLMError } from "../../src" +import { LLM, LLMError, Usage } from "../../src" import { LLMClient } from "../../src/route" import * as Gemini from "../../src/protocols/gemini" import { it } from "../lib/effect" @@ -210,7 +210,7 @@ describe("Gemini route", () => { { type: "request-finish", reason: "stop", - usage: { + usage: new Usage({ inputTokens: 4, outputTokens: 2, reasoningTokens: 1, @@ -223,7 +223,7 @@ describe("Gemini route", () => { thoughtsTokenCount: 1, cachedContentTokenCount: 1, }, - }, + }), }, ]) }), @@ -257,12 +257,12 @@ describe("Gemini route", () => { { type: "request-finish", reason: "tool-calls", - usage: { + usage: new Usage({ inputTokens: 5, outputTokens: 1, totalTokens: 6, native: { promptTokenCount: 5, candidatesTokenCount: 1 }, - }, + }), }, ]) }), diff --git a/packages/llm/test/provider/openai-chat.test.ts b/packages/llm/test/provider/openai-chat.test.ts index 2c9cf7d3bc..ecb1a81141 100644 --- a/packages/llm/test/provider/openai-chat.test.ts +++ b/packages/llm/test/provider/openai-chat.test.ts @@ -1,7 +1,7 @@ import { describe, expect } from "bun:test" import { Effect, Schema, Stream } from "effect" import { HttpClientRequest } from "effect/unstable/http" -import { LLM, LLMError } from "../../src" +import { LLM, LLMError, Usage } from "../../src" import * as Azure from "../../src/providers/azure" import * as OpenAI from "../../src/providers/openai" import * as OpenAIChat from "../../src/protocols/openai-chat" @@ -230,7 +230,7 @@ describe("OpenAI Chat route", () => { { type: "request-finish", reason: "stop", - usage: { + usage: new Usage({ inputTokens: 4, outputTokens: 2, reasoningTokens: 0, @@ -243,7 +243,7 @@ describe("OpenAI Chat route", () => { prompt_tokens_details: { cached_tokens: 1 }, completion_tokens_details: { reasoning_tokens: 0 }, }, - }, + }), }, ]) }), diff --git a/packages/llm/test/provider/openai-responses.test.ts b/packages/llm/test/provider/openai-responses.test.ts index 787ef9ba90..0723ddf816 100644 --- a/packages/llm/test/provider/openai-responses.test.ts +++ b/packages/llm/test/provider/openai-responses.test.ts @@ -1,7 +1,7 @@ import { describe, expect } from "bun:test" import { ConfigProvider, Effect, Layer, Stream } from "effect" import { Headers, HttpClientRequest } from "effect/unstable/http" -import { LLM, LLMError } from "../../src" +import { LLM, LLMError, Usage } from "../../src" import { Auth, LLMClient, RequestExecutor, WebSocketExecutor } from "../../src/route" import * as Azure from "../../src/providers/azure" import * as OpenAI from "../../src/providers/openai" @@ -342,7 +342,7 @@ describe("OpenAI Responses route", () => { type: "request-finish", reason: "stop", providerMetadata: { openai: { responseId: "resp_1", serviceTier: "default" } }, - usage: { + usage: new Usage({ inputTokens: 4, outputTokens: 2, reasoningTokens: 0, @@ -355,7 +355,7 @@ describe("OpenAI Responses route", () => { input_tokens_details: { cached_tokens: 1 }, output_tokens_details: { reasoning_tokens: 0 }, }, - }, + }), }, ]) }), @@ -411,7 +411,7 @@ describe("OpenAI Responses route", () => { { type: "request-finish", reason: "tool-calls", - usage: { inputTokens: 5, outputTokens: 1, totalTokens: 6, native: { input_tokens: 5, output_tokens: 1 } }, + usage: new Usage({ inputTokens: 5, outputTokens: 1, totalTokens: 6, native: { input_tokens: 5, output_tokens: 1 } }), }, ]) }), diff --git a/packages/llm/test/schema.test.ts b/packages/llm/test/schema.test.ts index a64b0ff71c..9ddfe9e597 100644 --- a/packages/llm/test/schema.test.ts +++ b/packages/llm/test/schema.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import { Schema } from "effect" -import { ContentPart, LLMEvent, LLMRequest, ModelID, ModelLimits, ModelRef, ProviderID } from "../src/schema" +import { ContentPart, LLMEvent, LLMRequest, ModelID, ModelLimits, ModelRef, ProviderID, Usage } from "../src/schema" import { ProviderShared } from "../src/protocols/shared" const model = new ModelRef({ @@ -61,4 +61,16 @@ describe("LLM.Usage additive contract", () => { expect(ProviderShared.subtractTokens(undefined, 3)).toBeUndefined() expect(ProviderShared.subtractTokens(undefined, undefined)).toBeUndefined() }) + + test("totalInputTokens sums every input-side category", () => { + expect(new Usage({ inputTokens: 10, cacheReadInputTokens: 3, cacheWriteInputTokens: 2 }).totalInputTokens).toBe(15) + expect(new Usage({ inputTokens: 10 }).totalInputTokens).toBe(10) + expect(new Usage({}).totalInputTokens).toBe(0) + }) + + test("totalOutputTokens sums every output-side category", () => { + expect(new Usage({ outputTokens: 7, reasoningTokens: 4 }).totalOutputTokens).toBe(11) + expect(new Usage({ outputTokens: 7 }).totalOutputTokens).toBe(7) + expect(new Usage({}).totalOutputTokens).toBe(0) + }) })