feat: add usage cost reporting

This commit is contained in:
Peter Steinberger
2026-01-09 02:21:17 +00:00
parent dfbee10377
commit 151523f47b
29 changed files with 696 additions and 184 deletions

View File

@@ -0,0 +1,60 @@
import { describe, expect, it } from "vitest";
import type { ClawdbotConfig } from "../config/config.js";
import {
estimateUsageCost,
formatTokenCount,
formatUsd,
resolveModelCostConfig,
} from "./usage-format.js";
describe("usage-format", () => {
it("formats token counts", () => {
expect(formatTokenCount(999)).toBe("999");
expect(formatTokenCount(1234)).toBe("1.2k");
expect(formatTokenCount(12000)).toBe("12k");
expect(formatTokenCount(2_500_000)).toBe("2.5m");
});
it("formats USD values", () => {
expect(formatUsd(1.234)).toBe("$1.23");
expect(formatUsd(0.5)).toBe("$0.50");
expect(formatUsd(0.0042)).toBe("$0.0042");
});
it("resolves model cost config and estimates usage cost", () => {
const config = {
models: {
providers: {
test: {
models: [
{
id: "m1",
cost: { input: 1, output: 2, cacheRead: 0.5, cacheWrite: 0 },
},
],
},
},
},
} as ClawdbotConfig;
const cost = resolveModelCostConfig({
provider: "test",
model: "m1",
config,
});
expect(cost).toEqual({
input: 1,
output: 2,
cacheRead: 0.5,
cacheWrite: 0,
});
const total = estimateUsageCost({
usage: { input: 1000, output: 500, cacheRead: 2000 },
cost,
});
expect(total).toBeCloseTo(0.003);
});
});

69
src/utils/usage-format.ts Normal file
View File

@@ -0,0 +1,69 @@
import type { NormalizedUsage } from "../agents/usage.js";
import type { ClawdbotConfig } from "../config/config.js";
export type ModelCostConfig = {
input: number;
output: number;
cacheRead: number;
cacheWrite: number;
};
export type UsageTotals = {
input?: number;
output?: number;
cacheRead?: number;
cacheWrite?: number;
total?: number;
};
export function formatTokenCount(value?: number): string {
if (value === undefined || !Number.isFinite(value)) return "0";
const safe = Math.max(0, value);
if (safe >= 1_000_000) return `${(safe / 1_000_000).toFixed(1)}m`;
if (safe >= 1_000)
return `${(safe / 1_000).toFixed(safe >= 10_000 ? 0 : 1)}k`;
return String(Math.round(safe));
}
export function formatUsd(value?: number): string | undefined {
if (value === undefined || !Number.isFinite(value)) return undefined;
if (value >= 1) return `$${value.toFixed(2)}`;
if (value >= 0.01) return `$${value.toFixed(2)}`;
return `$${value.toFixed(4)}`;
}
export function resolveModelCostConfig(params: {
provider?: string;
model?: string;
config?: ClawdbotConfig;
}): ModelCostConfig | undefined {
const provider = params.provider?.trim();
const model = params.model?.trim();
if (!provider || !model) return undefined;
const providers = params.config?.models?.providers ?? {};
const entry = providers[provider]?.models?.find((item) => item.id === model);
return entry?.cost;
}
const toNumber = (value: number | undefined): number =>
typeof value === "number" && Number.isFinite(value) ? value : 0;
export function estimateUsageCost(params: {
usage?: NormalizedUsage | UsageTotals | null;
cost?: ModelCostConfig;
}): number | undefined {
const usage = params.usage;
const cost = params.cost;
if (!usage || !cost) return undefined;
const input = toNumber(usage.input);
const output = toNumber(usage.output);
const cacheRead = toNumber(usage.cacheRead);
const cacheWrite = toNumber(usage.cacheWrite);
const total =
input * cost.input +
output * cost.output +
cacheRead * cost.cacheRead +
cacheWrite * cost.cacheWrite;
if (!Number.isFinite(total)) return undefined;
return total / 1_000_000;
}