/** * Price usage records from configured rates. * * `createPricer` owns the two things that must not be rebuilt per record: the * matching rates and the timezone formatter. Both currencies price a record * from `rates` and `peak` when a key matches. They differ only for a model no * key matches: `USD` adds Pi's recorded dollar amount (its catalog is already * per model), while `CNY` adds nothing and counts the record as unpriced, so the * session can say what its total leaves out. */ import { zeroUsage } from "../../../../lib/usage.ts"; import type { ByteflyConfig, ModelRates } from "./types.ts"; import type { PricedUsage, UsageRecord } from "../types.ts"; import { createRateLookup } from "./rates.ts"; import { isPeak, peakFormatter } from "./schedule.ts"; const TOKENS_PER_MILLION = 1_000_000; /** The cost of one record's tokens at `rates`, which are per million tokens. */ function tokenCost(record: UsageRecord, rates: ModelRates): number { return ( (record.input * rates.input + record.output * rates.output + record.cacheRead * rates.cacheRead + record.cacheWrite * rates.cacheWrite) / TOKENS_PER_MILLION ); } /** * Build a pricer for one config. * * The returned function totals tokens as well as cost, so the bar's meters and * its price always describe the same set of records. */ export function createPricer(config: ByteflyConfig): (records: readonly UsageRecord[]) => PricedUsage { // Both currencies price from `rates` and the tariff; they differ only in what // an unmatched model falls back to, and Pi's recorded cost is already dollars. const dollars = config.currency.code === "USD"; // Both are built once per config, never per record: the table splits every key // and remembers the last match, and the formatter is the expensive part. const lookup = createRateLookup(config.rates); const format = config.peak.windows.length > 0 ? peakFormatter(config.peak.timezone) : null; return (records) => { const totals: PricedUsage = { ...zeroUsage(), unpriced: 0 }; for (const record of records) { totals.input += record.input; totals.output += record.output; totals.cacheRead += record.cacheRead; totals.cacheWrite += record.cacheWrite; const rates = lookup(record.provider, record.model); if (rates !== undefined) { totals.cost += ratedCost(config, format, record, rates); continue; } // No rate: USD still has Pi's own per-model amount, CNY has nothing to add // and only a count to report. if (dollars) totals.cost += record.recordedCost; else totals.unpriced += 1; } return totals; }; } /** One record's cost at its matched rates, after the peak schedule. */ function ratedCost( config: ByteflyConfig, format: Intl.DateTimeFormat | null, record: UsageRecord, rates: ModelRates, ): number { const cost = tokenCost(record, rates); // An undated record is never discounted: we cannot tell which tariff applied. if (format === null || record.at === null) return cost; return isPeak(config.peak, record.at, format) ? cost : cost * rates.offPeakMultiplier; }