/** * 用量记录存储与查询。 * 零 pi 依赖的纯业务模块。 * * 把每次响应带出的 usage 追加到本地日志(NDJSON),可按时间/会话聚合汇总。 */ import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { homedir } from "node:os"; import type { UsageRecord, UsageStats } from "./pricing.ts"; import { summarizeStats } from "./pricing.ts"; export interface StoreIO { exists(path: string): boolean; append(path: string, line: string): void; readLines(path: string): string[]; mkdir(dir: string): void; dirname(path: string): string; } const nodeStore: StoreIO = { exists: existsSync, append: (p, l) => appendFileSync(p, l + "\n"), readLines: (p) => (existsSync(p) ? readFileSync(p, "utf-8").split("\n").filter(Boolean) : []), mkdir: (d) => mkdirSync(d, { recursive: true }), dirname, }; const ENV_STORE_PATH = "PI_OPENCODE_USAGE_STORE"; export function defaultStorePath(): string { return join(homedir(), ".pi", "agent", "opencode-usage.jsonl"); } export function getStorePath(env = process.env): string { return env[ENV_STORE_PATH] ?? defaultStorePath(); } export class UsageStore { private io: StoreIO; private path: string; constructor(path = getStorePath(), io: StoreIO = nodeStore) { this.path = path; this.io = io; } get pathString(): string { return this.path; } append(record: UsageRecord): void { this.io.mkdir(this.io.dirname(this.path)); this.io.append(this.path, JSON.stringify(record)); } readAll(): UsageRecord[] { const out: UsageRecord[] = []; for (const line of this.io.readLines(this.path)) { try { out.push(JSON.parse(line) as UsageRecord); } catch { /* skip malformed */ } } return out; } /** 按时间范围(epoch ms)过滤汇总。since 未给则从头;until 未给则到现在。 */ summarize(opts: { since?: number; until?: number; sessionId?: string } = {}): UsageStats { const now = Date.now(); const until = opts.until ?? now; const since = opts.since ?? 0; const recs = this.readAll().filter((r) => r.ts >= since && r.ts <= until && (opts.sessionId === undefined || r.sessionId === opts.sessionId)); return summarizeStats(recs); } } export function formatStats(stats: UsageStats, label = ""): string { const head = label ? `${label}: ` : ""; const cost = stats.cost; const lines = [ `${head}${stats.records} requests`, ` tokens: in=${stats.prompt} out=${stats.completion} cached=${stats.cached} cacheWrite=${stats.cacheWrite}`, ` est. cost: $${cost.total.toFixed(6)} (in $${cost.input} / out $${cost.output} / cacheRead $${cost.cacheRead} / cacheWrite $${cost.cacheWrite})`, ` cache hit rate: ${(stats.cacheHitRate * 100).toFixed(1)}%`, ]; return lines.join("\n"); }