mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-28 09:32:12 +00:00
fix: treat zero-rate usage cost as unknown
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user