mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-25 14:55:28 +00:00
feat(llm): add Usage.totalInputTokens / totalOutputTokens getters
Match the `LLMResponse.text` / `reasoning` / `toolCalls` getter pattern
in the same file — `usage.totalInputTokens` reads naturally and lives
where the Usage data does. Both sums are monotonic under the additive
contract, so callers no longer need to remember which fields are
non-overlapping.
Test fixtures that previously asserted with `usage: { ... }` plain
literals are now wrapped with `new Usage({...})` to match the runtime
shape the mappers actually produce (an instance, not a struct).
This commit is contained in:
parent
0d4f8d126f
commit
f5d199db62
6 changed files with 38 additions and 16 deletions
|
|
@ -43,7 +43,17 @@ export class Usage extends Schema.Class<Usage>("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"),
|
||||
|
|
|
|||
|
|
@ -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 } }),
|
||||
},
|
||||
])
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
},
|
||||
}),
|
||||
},
|
||||
])
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
])
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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 } }),
|
||||
},
|
||||
])
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue