claw-web-v2 / server /runtime /usage.ts
Claw Web
Claw Web v1.0 β€” AI Agent Web Interface with MiMo-V2-Flash
7540aea
/**
* Usage tracking module β€” matches original rust/crates/runtime/src/usage.rs exactly.
*
* Provides:
* - ModelPricing with per-model cost rates
* - pricing_for_model() lookup
* - TokenUsage with cache_creation/cache_read tokens
* - UsageCostEstimate with total_cost_usd()
* - UsageTracker with record(), cumulative_usage(), current_turn_usage(), turns()
* - summary_lines_for_model() with pricing=estimated-default fallback
* - format_usd() helper
*/
// ─── Constants (match original DEFAULT_*_COST_PER_MILLION) ──────────────────
const DEFAULT_INPUT_COST_PER_MILLION = 15.0;
const DEFAULT_OUTPUT_COST_PER_MILLION = 75.0;
const DEFAULT_CACHE_CREATION_COST_PER_MILLION = 18.75;
const DEFAULT_CACHE_READ_COST_PER_MILLION = 1.5;
// ─── ModelPricing ───────────────────────────────────────────────────────────
export interface ModelPricing {
input_cost_per_million: number;
output_cost_per_million: number;
cache_creation_cost_per_million: number;
cache_read_cost_per_million: number;
}
export function defaultSonnetTierPricing(): ModelPricing {
return {
input_cost_per_million: DEFAULT_INPUT_COST_PER_MILLION,
output_cost_per_million: DEFAULT_OUTPUT_COST_PER_MILLION,
cache_creation_cost_per_million: DEFAULT_CACHE_CREATION_COST_PER_MILLION,
cache_read_cost_per_million: DEFAULT_CACHE_READ_COST_PER_MILLION,
};
}
/**
* Matches original pricing_for_model() β€” returns pricing for known models,
* null for unknown models (caller should use default).
*/
export function pricingForModel(model: string): ModelPricing | null {
const normalized = model.toLowerCase();
if (normalized.includes("haiku")) {
return {
input_cost_per_million: 1.0,
output_cost_per_million: 5.0,
cache_creation_cost_per_million: 1.25,
cache_read_cost_per_million: 0.1,
};
}
if (normalized.includes("opus")) {
return {
input_cost_per_million: 15.0,
output_cost_per_million: 75.0,
cache_creation_cost_per_million: 18.75,
cache_read_cost_per_million: 1.5,
};
}
if (normalized.includes("sonnet")) {
return defaultSonnetTierPricing();
}
return null;
}
// ─── TokenUsage ─────────────────────────────────────────────────────────────
export interface TokenUsage {
input_tokens: number;
output_tokens: number;
cache_creation_input_tokens: number;
cache_read_input_tokens: number;
}
export function emptyTokenUsage(): TokenUsage {
return {
input_tokens: 0,
output_tokens: 0,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
};
}
export function totalTokens(usage: TokenUsage): number {
return (
usage.input_tokens +
usage.output_tokens +
usage.cache_creation_input_tokens +
usage.cache_read_input_tokens
);
}
// ─── UsageCostEstimate ──────────────────────────────────────────────────────
export interface UsageCostEstimate {
input_cost_usd: number;
output_cost_usd: number;
cache_creation_cost_usd: number;
cache_read_cost_usd: number;
}
export function totalCostUsd(estimate: UsageCostEstimate): number {
return (
estimate.input_cost_usd +
estimate.output_cost_usd +
estimate.cache_creation_cost_usd +
estimate.cache_read_cost_usd
);
}
function costForTokens(tokens: number, costPerMillion: number): number {
return (tokens / 1_000_000) * costPerMillion;
}
export function estimateCostUsd(usage: TokenUsage): UsageCostEstimate {
return estimateCostUsdWithPricing(usage, defaultSonnetTierPricing());
}
export function estimateCostUsdWithPricing(
usage: TokenUsage,
pricing: ModelPricing
): UsageCostEstimate {
return {
input_cost_usd: costForTokens(usage.input_tokens, pricing.input_cost_per_million),
output_cost_usd: costForTokens(usage.output_tokens, pricing.output_cost_per_million),
cache_creation_cost_usd: costForTokens(
usage.cache_creation_input_tokens,
pricing.cache_creation_cost_per_million
),
cache_read_cost_usd: costForTokens(
usage.cache_read_input_tokens,
pricing.cache_read_cost_per_million
),
};
}
// ─── format_usd ─────────────────────────────────────────────────────────────
export function formatUsd(amount: number): string {
return `$${amount.toFixed(4)}`;
}
// ─── summary_lines_for_model ────────────────────────────────────────────────
export function summaryLines(usage: TokenUsage, label: string): string[] {
return summaryLinesForModel(usage, label, undefined);
}
export function summaryLinesForModel(
usage: TokenUsage,
label: string,
model?: string
): string[] {
const pricing = model ? pricingForModel(model) : null;
const effectivePricing = pricing ?? defaultSonnetTierPricing();
const cost = estimateCostUsdWithPricing(usage, effectivePricing);
const total = totalCostUsd(cost);
const pricingLabel = pricing ? `model=${model}` : "pricing=estimated-default";
const line1 = [
`${label}:`,
`estimated_cost=${formatUsd(total)}`,
pricingLabel,
`input=${formatUsd(cost.input_cost_usd)}`,
`output=${formatUsd(cost.output_cost_usd)}`,
].join(" ");
const line2 = [
` tokens:`,
`input=${usage.input_tokens}`,
`output=${usage.output_tokens}`,
`cache_creation=${usage.cache_creation_input_tokens}`,
`cache_read=${usage.cache_read_input_tokens}`,
`cache_creation=${formatUsd(cost.cache_creation_cost_usd)}`,
`cache_read=${formatUsd(cost.cache_read_cost_usd)}`,
].join(" ");
return [line1, line2];
}
// ─── UsageTracker ───────────────────────────────────────────────────────────
export interface SessionMessage {
usage?: TokenUsage | null;
}
export class UsageTracker {
private latestTurn: TokenUsage = emptyTokenUsage();
private cumulative: TokenUsage = emptyTokenUsage();
private _turns: number = 0;
static new(): UsageTracker {
return new UsageTracker();
}
/**
* Matches original UsageTracker::from_session() β€” reconstructs tracker
* from all messages that have usage data.
*/
static fromSession(messages: SessionMessage[]): UsageTracker {
const tracker = new UsageTracker();
for (const msg of messages) {
if (msg.usage) {
tracker.record(msg.usage);
}
}
return tracker;
}
record(usage: TokenUsage): void {
this.latestTurn = usage;
this.cumulative.input_tokens += usage.input_tokens;
this.cumulative.output_tokens += usage.output_tokens;
this.cumulative.cache_creation_input_tokens += usage.cache_creation_input_tokens;
this.cumulative.cache_read_input_tokens += usage.cache_read_input_tokens;
this._turns += 1;
}
currentTurnUsage(): TokenUsage {
return { ...this.latestTurn };
}
cumulativeUsage(): TokenUsage {
return { ...this.cumulative };
}
turns(): number {
return this._turns;
}
}