refactor(llm): trim Usage helpers + Bedrock subtraction
Review pass: - Drop `Pick<>` type aliases on `Usage.totalInput` / `Usage.totalOutput` — the helpers can take `Usage` directly since every field is optional. - Collapse Bedrock's nested `subtractTokens(subtractTokens(...))` into a single subtraction against the summed cache subtotals. - Drop arithmetic-walkthrough comments in test fixtures (the raw fixture values are right next to the expected outputs). - Generalize the comment on `mapUsage` in `openai-chat.ts` so the rationale outlives the PR reference.
This commit is contained in:
parent
b9451175a6
commit
478f3ae50c
6 changed files with 8 additions and 29 deletions
|
|
@ -370,10 +370,8 @@ const mapFinishReason = (reason: string): FinishReason => {
|
|||
// reasoning tokens for any current model.
|
||||
const mapUsage = (usage: BedrockUsageSchema | undefined): Usage | undefined => {
|
||||
if (!usage) return undefined
|
||||
const inputTokens = ProviderShared.subtractTokens(
|
||||
ProviderShared.subtractTokens(usage.inputTokens, usage.cacheReadInputTokens),
|
||||
usage.cacheWriteInputTokens,
|
||||
)
|
||||
const cacheTotal = (usage.cacheReadInputTokens ?? 0) + (usage.cacheWriteInputTokens ?? 0)
|
||||
const inputTokens = ProviderShared.subtractTokens(usage.inputTokens, cacheTotal)
|
||||
return new Usage({
|
||||
inputTokens,
|
||||
outputTokens: usage.outputTokens,
|
||||
|
|
|
|||
|
|
@ -292,9 +292,8 @@ const mapFinishReason = (reason: string | null | undefined): FinishReason => {
|
|||
|
||||
// OpenAI Chat reports `prompt_tokens` as the total prompt (cached tokens
|
||||
// included) and `completion_tokens` as the total output (reasoning tokens
|
||||
// included). The additive `LLM.Usage` contract pulls each subtotal out at
|
||||
// the boundary so consumers never subtract — eliminating the underflow
|
||||
// class addressed by opencode#26620.
|
||||
// included). Pull each subtotal out at the boundary so the additive
|
||||
// `LLM.Usage` contract holds and consumers never subtract.
|
||||
const mapUsage = (usage: OpenAIChatEvent["usage"]): Usage | undefined => {
|
||||
if (!usage) return undefined
|
||||
const cached = usage.prompt_tokens_details?.cached_tokens
|
||||
|
|
|
|||
|
|
@ -46,21 +46,12 @@ export class Usage extends Schema.Class<Usage>("LLM.Usage")({
|
|||
}) {}
|
||||
|
||||
export namespace Usage {
|
||||
type InputFields = Pick<Usage, "inputTokens" | "cacheReadInputTokens" | "cacheWriteInputTokens">
|
||||
type OutputFields = Pick<Usage, "outputTokens" | "reasoningTokens">
|
||||
|
||||
/**
|
||||
* Sum of every input-side category: non-cached input + cache reads +
|
||||
* cache writes. Monotonic; cannot underflow under the additive contract.
|
||||
*/
|
||||
export const totalInput = (usage: InputFields) =>
|
||||
/** 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: visible output + reasoning.
|
||||
* Monotonic; cannot underflow under the additive contract.
|
||||
*/
|
||||
export const totalOutput = (usage: OutputFields) => (usage.outputTokens ?? 0) + (usage.reasoningTokens ?? 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({
|
||||
|
|
|
|||
|
|
@ -197,9 +197,6 @@ describe("Gemini route", () => {
|
|||
expect(response.text).toBe("Hello!")
|
||||
expect(response.reasoning).toBe("thinking")
|
||||
expect(response.usage).toMatchObject({
|
||||
// Additive contract: promptTokenCount=5 includes 1 cached, so
|
||||
// inputTokens=4 + cacheReadInputTokens=1. Gemini already splits
|
||||
// candidates from thoughts, so outputTokens=2 + reasoningTokens=1.
|
||||
inputTokens: 4,
|
||||
outputTokens: 2,
|
||||
reasoningTokens: 1,
|
||||
|
|
|
|||
|
|
@ -231,9 +231,6 @@ describe("OpenAI Chat route", () => {
|
|||
type: "request-finish",
|
||||
reason: "stop",
|
||||
usage: {
|
||||
// Additive contract: prompt_tokens=5 includes 1 cached, so
|
||||
// inputTokens=4 (non-cached) + cacheReadInputTokens=1.
|
||||
// completion_tokens=2 includes 0 reasoning, so outputTokens=2.
|
||||
inputTokens: 4,
|
||||
outputTokens: 2,
|
||||
reasoningTokens: 0,
|
||||
|
|
|
|||
|
|
@ -343,9 +343,6 @@ describe("OpenAI Responses route", () => {
|
|||
reason: "stop",
|
||||
providerMetadata: { openai: { responseId: "resp_1", serviceTier: "default" } },
|
||||
usage: {
|
||||
// Additive contract: input_tokens=5 includes 1 cached, so
|
||||
// inputTokens=4 + cacheReadInputTokens=1.
|
||||
// output_tokens=2 includes 0 reasoning, so outputTokens=2.
|
||||
inputTokens: 4,
|
||||
outputTokens: 2,
|
||||
reasoningTokens: 0,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue