refactor(llm): drop Usage.totalInput / totalOutput helpers

The additive contract delivers value at the mapper boundary — every
field is non-overlapping and non-negative, so any caller summing
arbitrary subsets is correct by construction. Two-line helpers that
just sum three or two known fields add API surface without paying for
themselves, and there are no in-tree consumers today. If v2 wants them
at integration time, the right place is a getter on the `Schema.Class`
(matching the `LLMResponse.text` / `reasoning` / `toolCalls` pattern in
the same file), not a static namespace helper.
This commit is contained in:
Kit Langton
2026-05-10 19:15:46 -04:00
parent 478f3ae50c
commit 0d4f8d126f
2 changed files with 3 additions and 24 deletions

View File

@@ -45,15 +45,6 @@ export class Usage extends Schema.Class<Usage>("LLM.Usage")({
native: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
}) {}
export namespace Usage {
/** Sum of every input-side category. Monotonic under the additive contract. */
export const totalInput = (usage: Usage) =>
(usage.inputTokens ?? 0) + (usage.cacheReadInputTokens ?? 0) + (usage.cacheWriteInputTokens ?? 0)
/** Sum of every output-side category. Monotonic under the additive contract. */
export const totalOutput = (usage: Usage) => (usage.outputTokens ?? 0) + (usage.reasoningTokens ?? 0)
}
export const RequestStart = Schema.Struct({
type: Schema.tag("request-start"),
id: ResponseID,

View File

@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"
import { Schema } from "effect"
import { ContentPart, LLMEvent, LLMRequest, ModelID, ModelLimits, ModelRef, ProviderID, Usage } from "../src/schema"
import { ContentPart, LLMEvent, LLMRequest, ModelID, ModelLimits, ModelRef, ProviderID } from "../src/schema"
import { ProviderShared } from "../src/protocols/shared"
const model = new ModelRef({
@@ -53,24 +53,12 @@ describe("llm schema", () => {
describe("LLM.Usage additive contract", () => {
test("subtractTokens clamps non-sensical breakdowns to zero", () => {
// Defense against a provider reporting cached_tokens > prompt_tokens or
// reasoning_tokens > completion_tokens. The clamp prevents the negative
// values that triggered opencode#26620 from ever entering the pipeline.
// reasoning_tokens > completion_tokens — the negative would otherwise
// round-trip through the pipeline and crash strict downstream schemas.
expect(ProviderShared.subtractTokens(5, 3)).toBe(2)
expect(ProviderShared.subtractTokens(5, 10)).toBe(0)
expect(ProviderShared.subtractTokens(5, undefined)).toBe(5)
expect(ProviderShared.subtractTokens(undefined, 3)).toBeUndefined()
expect(ProviderShared.subtractTokens(undefined, undefined)).toBeUndefined()
})
test("totalInput sums every input-side category", () => {
expect(Usage.totalInput(new Usage({ inputTokens: 10, cacheReadInputTokens: 3, cacheWriteInputTokens: 2 }))).toBe(15)
expect(Usage.totalInput(new Usage({ inputTokens: 10 }))).toBe(10)
expect(Usage.totalInput(new Usage({}))).toBe(0)
})
test("totalOutput sums every output-side category", () => {
expect(Usage.totalOutput(new Usage({ outputTokens: 7, reasoningTokens: 4 }))).toBe(11)
expect(Usage.totalOutput(new Usage({ outputTokens: 7 }))).toBe(7)
expect(Usage.totalOutput(new Usage({}))).toBe(0)
})
})