diff --git a/src/infra/session-cost-usage.test.ts b/src/infra/session-cost-usage.test.ts index f94db87144b..934d18f463c 100644 --- a/src/infra/session-cost-usage.test.ts +++ b/src/infra/session-cost-usage.test.ts @@ -194,13 +194,14 @@ describe("session cost usage", () => { }); }); - it("preserves an operator-configured zero-cost model as a complete $0, not missing", async () => { - const root = await makeSessionCostRoot("cost-intentional-free"); + it("counts token usage for a configured all-zero model as missing because pricing is still unknown", async () => { + const root = await makeSessionCostRoot("cost-configured-zero-unknown"); const sessionsDir = path.join(root, "agents", "main", "sessions"); await fs.mkdir(sessionsDir, { recursive: true }); - // Same shape of turn, but here the operator deliberately priced the model at 0 - // (e.g. a local/self-hosted free model). That intentional $0 must be respected. + // Same shape of turn, with a configured all-zero cost block. After config defaults, + // omitted cost and explicit all-zero cost are indistinguishable, so a zero-rate + // token-burning turn is still safer to report as missing than as complete $0 spend. const entry = { type: "message", timestamp: new Date().toISOString(), @@ -225,8 +226,8 @@ describe("session cost usage", () => { "utf-8", ); - // The operator explicitly configured this model's price as 0 — an intentional - // "free" price that must be preserved as complete $0 cost data. + // This mirrors normalized config where a model declaration without pricing has + // already received default zero rates. const config = { models: { providers: { @@ -247,8 +248,7 @@ describe("session cost usage", () => { const summary = await loadCostUsageSummary({ days: 30, config }); expect(summary.totals.totalTokens).toBe(23287); expect(summary.totals.totalCost).toBe(0); - // Operator-configured $0 is intentional and complete — not a missing entry. - expect(summary.totals.missingCostEntries).toBe(0); + expect(summary.totals.missingCostEntries).toBe(1); }); }); diff --git a/src/infra/session-cost-usage.ts b/src/infra/session-cost-usage.ts index 4e820a9cf24..c12c89fbaaf 100644 --- a/src/infra/session-cost-usage.ts +++ b/src/infra/session-cost-usage.ts @@ -24,7 +24,6 @@ import { normalizeOptionalString } from "../shared/string-coerce.js"; import { countToolResults, extractToolCallNames } from "../utils/transcript-tools.js"; import { estimateUsageCost, - resolveConfiguredModelCost, resolveModelCostConfig, resolveModelCostConfigFingerprint, } from "../utils/usage-format.js"; @@ -1122,23 +1121,16 @@ async function scanTranscriptFile(params: { entry.costBreakdown = undefined; } else if ( !isModelPricingKnown(cost) && - resolveConfiguredModelCost({ - provider: entry.provider, - model: entry.model, - config: params.config, - }) === undefined && (entry.costTotal === undefined || entry.costTotal === 0) && computeUsageTokenTotals(entry.usage).totalTokens > 0 ) { - // Pricing for this model is unknown: it has no positive per-token rate, the - // operator did not explicitly configure its price (so an all-zero value is a - // catalog default, not an intentional "free" price), and there is no trustworthy - // recorded cost — the transport either recorded nothing or a fabricated $0 - // derived from an all-zero catalog entry. Surface this token-burning turn as a - // missing-cost entry instead of recording a confident $0, so budget and spike - // safeguards that read totalCost are not left blind to it. A turn with a real - // positive recorded cost, or a model the operator deliberately priced at 0, is - // preserved by the guards above. + // Pricing for this model is unknown: it has no positive per-token rate and no + // trustworthy recorded cost. The transport either recorded nothing or a + // fabricated $0 derived from an all-zero/default catalog entry. Surface this + // token-burning turn as a missing-cost entry instead of recording a confident + // $0, so budget and spike safeguards that read totalCost are not left blind to + // it. A turn carrying a real positive recorded cost is preserved by the guard + // above. entry.costTotal = undefined; entry.costBreakdown = undefined; } else if (entry.costTotal === undefined) { diff --git a/src/utils/usage-format.ts b/src/utils/usage-format.ts index feb4f95914f..a63748fd4d9 100644 --- a/src/utils/usage-format.ts +++ b/src/utils/usage-format.ts @@ -298,22 +298,6 @@ export function resolveModelCostConfigFingerprint(config?: OpenClawConfig): stri }); } -// Returns model pricing ONLY when the operator explicitly configured it under -// `models.providers` in their OpenClaw config. Unlike resolveModelCostConfig this -// ignores the generated model catalog (models.json) and the gateway pricing cache, so -// callers can tell an intentional operator-set price (e.g. a deliberately free local -// model priced at 0) apart from a price the catalog merely defaulted to zero. -export function resolveConfiguredModelCost(params: { - provider?: string; - model?: string; - config?: OpenClawConfig; -}): ModelCostConfig | undefined { - return ( - findConfiguredProviderCost({ ...params, allowPluginNormalization: false }) ?? - findConfiguredProviderCost(params) - ); -} - export function resolveModelCostConfig(params: { provider?: string; model?: string;