import { Database } from "bun:sqlite";
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import * as os from "node:os";
import * as path from "node:path";
import { getBundledModel } from "@oh-my-pi/pi-ai";
import { getAgentDir, getStatsDbPath, setAgentDir, TempDir } from "@oh-my-pi/pi-utils";
import { closeDb, getRecentRequests, initDb, insertMessageStats } from "../src/db";
import type { MessageStats } from "../src/types";
const originalConfigDir = process.env.PI_CONFIG_DIR;
const originalAgentDir = getAgentDir();
let tempDir: TempDir | null = null;
beforeEach(() => {
tempDir = TempDir.createSync("@pi-stats-db-");
const configDir = path.relative(os.homedir(), tempDir.join("config"));
process.env.PI_CONFIG_DIR = configDir;
setAgentDir(path.join(os.homedir(), configDir, "agent"));
});
afterEach(() => {
closeDb();
if (originalConfigDir === undefined) {
delete process.env.PI_CONFIG_DIR;
} else {
process.env.PI_CONFIG_DIR = originalConfigDir;
}
setAgentDir(originalAgentDir);
tempDir?.removeSync();
tempDir = null;
});
function createCodexGptStats(entryId: string): MessageStats {
return {
sessionFile: "/tmp/session.jsonl",
entryId,
folder: "/tmp/project",
model: "gpt-5.4",
provider: "openai-codex",
api: "openai-codex-responses",
timestamp: Date.now(),
duration: 1000,
ttft: 100,
stopReason: "stop",
errorMessage: null,
usage: {
input: 1000,
output: 500,
cacheRead: 200,
cacheWrite: 0,
totalTokens: 1700,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
};
}
function expectedCodexGptCost() {
const cost = getBundledModel("openai-codex", "gpt-5.4").cost;
const input = (cost.input / 1_000_000) * 1000;
const output = (cost.output / 1_000_000) * 500;
const cacheRead = (cost.cacheRead / 1_000_000) * 200;
return {
input,
output,
cacheRead,
total: input + output + cacheRead,
};
}
describe("stats GPT cost correction", () => {
it("stores catalog-derived cost when OpenAI Codex session usage has zero cost", async () => {
await initDb();
insertMessageStats([createCodexGptStats("inserted")]);
const expected = expectedCodexGptCost();
const request = getRecentRequests(1)[0];
expect(expected.total).toBeGreaterThan(0);
expect(request?.usage.cost.input).toBeCloseTo(expected.input, 8);
expect(request?.usage.cost.output).toBeCloseTo(expected.output, 8);
expect(request?.usage.cost.cacheRead).toBeCloseTo(expected.cacheRead, 8);
expect(request?.usage.cost.total).toBeCloseTo(expected.total, 8);
});
it("backfills existing zero-cost OpenAI Codex GPT rows on database init", async () => {
await initDb();
closeDb();
const database = new Database(getStatsDbPath());
database
.prepare(`
INSERT INTO messages (
session_file, entry_id, folder, model, provider, api, timestamp,
duration, ttft, stop_reason, error_message,
input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, total_tokens, premium_requests,
cost_input, cost_output, cost_cache_read, cost_cache_write, cost_total
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`)
.run(
"/tmp/session.jsonl",
"backfilled",
"/tmp/project",
"gpt-5.4",
"openai-codex",
"openai-codex-responses",
Date.now(),
1000,
100,
"stop",
null,
1000,
500,
200,
0,
1700,
0,
0,
0,
0,
0,
0,
);
database.close();
await initDb();
const request = getRecentRequests(1)[0];
expect(request?.usage.cost.total).toBeCloseTo(expectedCodexGptCost().total, 8);
});
});