diff --git a/packages/core/src/session/usage.ts b/packages/core/src/session/usage.ts index e55f4802f1..518415122c 100644 --- a/packages/core/src/session/usage.ts +++ b/packages/core/src/session/usage.ts @@ -5,7 +5,8 @@ import { Money } from "@opencode-ai/schema/money" import type { TokenUsage } from "@opencode-ai/schema/token-usage" import type { Model } from "../model.js" -const safe = (value: number | undefined) => Math.max(0, Number.isFinite(value) ? (value ?? 0) : 0) +const finite = (value: number) => (Number.isFinite(value) ? value : 0) +const safe = (value: number | undefined) => Math.max(0, finite(value ?? 0)) export const tokens = (usage: Usage | undefined): TokenUsage.Info => ({ input: safe(usage?.nonCachedInputTokens), @@ -26,10 +27,10 @@ export function calculateCost(costs: Model.Info["cost"], usage: TokenUsage.Info) const cost = tier ?? costs.find((cost) => cost.tier === undefined) if (!cost) return Money.USD.zero return Money.USD.make( - (usage.input * cost.input + - (usage.output + usage.reasoning) * cost.output + - usage.cache.read * cost.cache.read + - usage.cache.write * cost.cache.write) / + (usage.input * finite(cost.input) + + (usage.output + usage.reasoning) * finite(cost.output) + + usage.cache.read * finite(cost.cache.read) + + usage.cache.write * finite(cost.cache.write)) / 1_000_000, ) } diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index e55f1d8e27..d0167a6cec 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -197,6 +197,29 @@ test("calculates step cost using the matching context tier", () => { ).toBeCloseTo(0.0002926) }) +test("ignores malformed model cost fields", () => { + const costs = [ + { + input: Money.USDPerMillionTokens.make(3), + output: Money.USDPerMillionTokens.make(15), + cache: { + read: Money.USDPerMillionTokens.make(0.3), + write: Money.USDPerMillionTokens.make(3.75), + }, + }, + ] + Object.assign(costs[0], { input: {} }) + + expect( + SessionUsage.calculateCost(costs, { + input: 1_000_000, + output: 100_000, + reasoning: 0, + cache: { read: 0, write: 0 }, + }), + ).toBe(Money.USD.make(1.5)) +}) + test("does not apply an ineligible tier without base pricing", () => { expect( SessionUsage.calculateCost(