BID · Console
Baseline · Intelligence · Decision
src/observability/usage.ts 2,856 bytes · typescript
/**
 * Lightweight Anthropic usage meter.
 *
 * Pure observability — no behaviour. Each agent's LLM wrapper calls
 * `recordUsage(...)` once per successful `messages.create` response.
 * The orchestrator / demo prints `usageSnapshot()` at the end of a
 * run so we can see call counts, token totals, and rough cost per
 * agent without depending on transcripts or external billing pulls.
 */

export interface UsageRecord {
  readonly agent: string;
  readonly model: string;
  readonly inputTokens: number;
  readonly outputTokens: number;
  readonly at: string;
}

export interface UsageRollupRow {
  readonly agent: string;
  readonly model: string;
  readonly calls: number;
  readonly inputTokens: number;
  readonly outputTokens: number;
}

const records: UsageRecord[] = [];

export function recordUsage(agent: string, model: string, inputTokens: number, outputTokens: number): void {
  records.push({ agent, model, inputTokens, outputTokens, at: new Date().toISOString() });
}

export function resetUsage(): void {
  records.length = 0;
}

export function usageSnapshot(): {
  readonly records: readonly UsageRecord[];
  readonly byAgent: readonly UsageRollupRow[];
  readonly totals: { calls: number; inputTokens: number; outputTokens: number };
} {
  const byAgent = new Map<string, UsageRollupRow>();
  for (const r of records) {
    const key = `${r.agent}|${r.model}`;
    const prior = byAgent.get(key);
    if (prior) {
      byAgent.set(key, {
        agent: r.agent,
        model: r.model,
        calls: prior.calls + 1,
        inputTokens: prior.inputTokens + r.inputTokens,
        outputTokens: prior.outputTokens + r.outputTokens,
      });
    } else {
      byAgent.set(key, {
        agent: r.agent,
        model: r.model,
        calls: 1,
        inputTokens: r.inputTokens,
        outputTokens: r.outputTokens,
      });
    }
  }
  let calls = 0, inputTokens = 0, outputTokens = 0;
  for (const row of byAgent.values()) {
    calls += row.calls;
    inputTokens += row.inputTokens;
    outputTokens += row.outputTokens;
  }
  return { records: [...records], byAgent: [...byAgent.values()], totals: { calls, inputTokens, outputTokens } };
}

/** Anthropic Haiku 4.5 public list prices (USD per 1M tokens) at time
 *  of writing — confirm at console.anthropic.com/settings/billing for
 *  the authoritative rate. Used only for rough cost reporting. */
export const HAIKU_4_5_PRICE_PER_M_INPUT = 1.0;
export const HAIKU_4_5_PRICE_PER_M_OUTPUT = 5.0;

export function estimateCostUsd(inputTokens: number, outputTokens: number, model: string): number {
  if (model.startsWith('claude-haiku-4-5')) {
    return (inputTokens / 1_000_000) * HAIKU_4_5_PRICE_PER_M_INPUT
         + (outputTokens / 1_000_000) * HAIKU_4_5_PRICE_PER_M_OUTPUT;
  }
  // Unknown model — return 0 to avoid faking a cost we don't know.
  return 0;
}