Files
OmniRoute/tests/unit/usage-analytics-route.test.ts
小妍儿 ✨ afb91a83bd fix(analytics): expose flat-rate estimates on cost dashboards (#11460)
Claude Code (claude / cc) is correctly classified as a flat-rate subscription, so the analytics API reports $0 — accurate as billed cost, and useless as a view of what the subscription actually consumed. Neither the Costs nor the Analytics dashboard had a token-price-equivalent view.

The fix keeps both meanings rather than picking one: ordinary analytics callers keep billed-cost semantics ($0 for flat-rate), /dashboard/costs and /dashboard/analytics opt in explicitly via includeFlatRateEstimates=true, the response reports whether estimates were included so a caller cannot mistake them for vendor billing records, and the figures on /dashboard/costs are labelled as flat-rate estimates rather than presented as spend. Omitted, false and unknown values all retain the existing behaviour.

Scope note carried from the description: this is a checkpoint on #11459, not its full closure — the issue stays open.

Verified in a combined worktree with three sibling PRs of this batch: typecheck:core clean, 134/134 focused tests (4 skipped), and i18n UI coverage PASS across all 42 locales for the 43-file locale pass.

One cross-PR interaction worth recording, since it is invisible from either side: this grows CostOverviewTab.tsx from 1282 to 1318 lines, which is fine against the tip's current 2002 cap but exceeds the 1283 that #12411 (file-size ratchet re-tightening) would freeze. Neither PR fails alone. Merged first on purpose so #12411's mechanical --update recomputes against the real post-merge LOC — the cap still only goes down.

Thanks @xiaoyaner0201 — the opt-in contract plus the "were estimates included" flag is the right shape for this.
2026-09-02 01:18:47 -03:00

791 lines
30 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-usage-analytics-route-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const ORIGINAL_API_KEY_SECRET = process.env.API_KEY_SECRET;
process.env.API_KEY_SECRET = "test-usage-analytics-secret";
const core = await import("../../src/lib/db/core.ts");
const { updatePricing } = await import("@/lib/db/settings");
const localDb = { updatePricing };
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const usageHistory = await import("../../src/lib/usage/usageHistory.ts");
const aggregateHistory = await import("../../src/lib/usage/aggregateHistory.ts");
const analyticsRoute = await import("../../src/app/api/usage/analytics/route.ts");
const clearPendingRequests = usageHistory.clearPendingRequests;
const EXPECTED_TOTAL_COST = 0.020925;
async function resetStorage() {
core.resetDbInstance();
apiKeysDb.resetApiKeyState();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
clearPendingRequests();
}
async function seedAnalyticsData() {
const db = core.getDbInstance();
const now = new Date();
for (let i = 0; i < 20; i++) {
const timestamp = new Date(now.getTime() - i * 60 * 60 * 1000).toISOString();
db.prepare(
`INSERT INTO usage_history (provider, model, connection_id, api_key_id, api_key_name, tokens_input, tokens_output, success, latency_ms, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
).run(
i % 2 === 0 ? "openai" : "anthropic",
i % 2 === 0 ? "gpt-4o" : "claude-sonnet",
"test-conn",
"test-key",
"Primary Key",
100 + i,
50 + i,
1,
200 + i * 10,
timestamp
);
}
db.prepare(
`INSERT INTO call_logs (provider, model, requested_model, connection_id, timestamp)
VALUES (?, ?, ?, ?, ?)`
).run("openai", "gpt-4o", "gpt-4o-mini", "test-conn", new Date().toISOString());
}
function makeRequest(url: string) {
return new Request(url, { method: "GET" });
}
function assertClose(actual: number, expected: number, epsilon = 0.000001) {
assert.ok(
Math.abs(actual - expected) <= epsilon,
`expected ${actual} to be within ${epsilon} of ${expected}`
);
}
test.beforeEach(async () => {
await resetStorage();
await localDb.updatePricing({
openai: { "gpt-4o": { input: 2.5, output: 10 } },
anthropic: { "claude-sonnet": { input: 3, output: 15 } },
});
});
test.after(() => {
core.resetDbInstance();
apiKeysDb.resetApiKeyState();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
if (ORIGINAL_API_KEY_SECRET === undefined) {
delete process.env.API_KEY_SECRET;
} else {
process.env.API_KEY_SECRET = ORIGINAL_API_KEY_SECRET;
}
});
test("GET /api/usage/analytics returns summary with aggregated metrics", async () => {
await seedAnalyticsData();
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
const body = await response.json();
assert.equal(response.status, 200);
assert.equal(body.summary.totalRequests, 20);
assert.equal(body.summary.uniqueModels, 2);
assert.equal(body.summary.uniqueAccounts, 1);
assert.equal(body.summary.uniqueApiKeys, 1);
assert.ok(body.summary.totalTokens > 0);
assert.ok(body.summary.avgLatencyMs > 0);
assertClose(body.summary.totalCost, EXPECTED_TOTAL_COST);
assert.ok(body.summary.streak > 0);
});
test("GET /api/usage/analytics includes dailyTrend array with cost data", async () => {
await seedAnalyticsData();
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
const body = await response.json();
assert.equal(response.status, 200);
assert.ok(Array.isArray(body.dailyTrend));
assert.ok(body.dailyTrend.length > 0);
assert.ok(body.dailyTrend.every((row) => typeof row.cost === "number"));
const dailyCostTotal = body.dailyTrend.reduce((sum, row) => sum + row.cost, 0);
assertClose(dailyCostTotal, body.summary.totalCost);
});
test("GET /api/usage/analytics includes byModel array with cost calculations", async () => {
await seedAnalyticsData();
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
const body = await response.json();
assert.equal(response.status, 200);
assert.ok(Array.isArray(body.byModel));
assert.ok(body.byModel.length > 0);
const gptEntry = body.byModel.find(
(m) => (m.model === "4o" || m.model === "gpt-4o") && m.provider === "openai"
);
assert.ok(gptEntry);
assert.ok(typeof gptEntry.cost === "number");
assert.ok(gptEntry.cost > 0);
});
test("GET /api/usage/analytics resolves Codex GPT-5.5 pricing through provider aliases", async () => {
const db = core.getDbInstance();
db.prepare(
`INSERT INTO usage_history (provider, model, connection_id, tokens_input, tokens_output, success, latency_ms, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
).run("codex", "gpt-5.5", "codex-conn", 1000, 500, 1, 250, new Date().toISOString());
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
const body = await response.json();
assert.equal(response.status, 200);
assertClose(body.summary.totalCost, 0.02);
assert.equal(body.byProvider[0].provider, "OpenAI Codex");
assertClose(body.byProvider[0].cost, 0.02);
assert.equal(body.byModel[0].model, "gpt-5.5");
assertClose(body.byModel[0].cost, 0.02);
});
test("GET /api/usage/analytics can include flat-rate provider estimates for the costs page", async () => {
const db = core.getDbInstance();
db.prepare(
`INSERT INTO usage_history (provider, model, connection_id, tokens_input, tokens_output, success, latency_ms, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
).run(
"claude",
"claude-opus-5",
"claude-code-conn",
1_000_000,
1_000_000,
1,
250,
new Date().toISOString()
);
const defaultResponse = await analyticsRoute.GET(
makeRequest("http://localhost/api/usage/analytics?presets=1d")
);
const defaultBody = await defaultResponse.json();
assert.equal(defaultResponse.status, 200);
assertClose(defaultBody.summary.totalCost, 0);
assertClose(defaultBody.byProvider[0].cost, 0);
assertClose(defaultBody.byModel[0].cost, 0);
assertClose(defaultBody.presetSummaries["1d"].totalCost, 0);
assertClose(
defaultBody.byProvider.reduce((sum: number, row: { cost: number }) => sum + row.cost, 0),
defaultBody.summary.totalCost
);
const estimatedResponse = await analyticsRoute.GET(
makeRequest("http://localhost/api/usage/analytics?presets=1d&includeFlatRateEstimates=true")
);
const estimatedBody = await estimatedResponse.json();
assert.equal(estimatedResponse.status, 200);
assert.equal(estimatedBody.includesFlatRateEstimates, true);
assertClose(estimatedBody.summary.totalCost, 30);
assert.equal(estimatedBody.byProvider[0].provider, "Claude Code");
assertClose(estimatedBody.byProvider[0].cost, 30);
assertClose(estimatedBody.byModel[0].cost, 30);
assertClose(estimatedBody.byAccount[0].cost, 30);
assertClose(estimatedBody.byServiceTier[0].cost, 30);
assertClose(estimatedBody.presetSummaries["1d"].totalCost, 30);
assertClose(
estimatedBody.byProvider.reduce((sum: number, row: { cost: number }) => sum + row.cost, 0),
estimatedBody.summary.totalCost
);
});
test("GET /api/usage/analytics preserves archived flat-rate estimates without changing billed cost", async () => {
const db = core.getDbInstance();
db.prepare(
`INSERT INTO daily_usage_summary
(provider, model, date, total_requests, total_input_tokens, total_output_tokens, total_cost)
VALUES (?, ?, ?, ?, ?, ?, ?)`
).run("claude", "claude-opus-5", "2024-01-01", 2, 1_000_000, 1_000_000, 42.5);
const billedResponse = await analyticsRoute.GET(
makeRequest("http://localhost/api/usage/analytics?range=all")
);
const billedBody = await billedResponse.json();
assert.equal(billedResponse.status, 200);
assertClose(billedBody.summary.totalCost, 0);
assertClose(billedBody.byProvider[0].cost, 0);
const estimatedResponse = await analyticsRoute.GET(
makeRequest("http://localhost/api/usage/analytics?range=all&includeFlatRateEstimates=true")
);
const estimatedBody = await estimatedResponse.json();
assert.equal(estimatedResponse.status, 200);
assertClose(estimatedBody.summary.totalCost, 42.5);
assertClose(estimatedBody.byProvider[0].cost, 42.5);
assertClose(estimatedBody.byModel[0].cost, 42.5);
});
test("rollupUsageHistoryBeforeDate stores API-equivalent cost before deleting raw usage", async () => {
const db = core.getDbInstance();
db.prepare(
`INSERT INTO usage_history
(provider, model, connection_id, tokens_input, tokens_output, tokens_cache_read,
tokens_cache_creation, tokens_reasoning, service_tier, success, latency_ms, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
).run(
"claude",
"claude-opus-5",
"archived-claude",
1_000_000,
1_000_000,
0,
0,
0,
"standard",
1,
250,
"2024-01-01T12:00:00.000Z"
);
const result = await aggregateHistory.rollupUsageHistoryBeforeDate("2024-02-01");
assert.equal(result.errors, 0);
const retryResult = await aggregateHistory.rollupUsageHistoryBeforeDate("2024-02-01");
assert.equal(retryResult.errors, 0);
const archived = db
.prepare(
`SELECT total_requests, total_input_tokens, total_output_tokens, total_cost
FROM daily_usage_summary
WHERE provider = ? AND model = ? AND date = ?`
)
.get("claude", "claude-opus-5", "2024-01-01") as {
total_requests: number;
total_input_tokens: number;
total_output_tokens: number;
total_cost: number;
};
assert.equal(archived.total_requests, 1);
assert.equal(archived.total_input_tokens, 1_000_000);
assert.equal(archived.total_output_tokens, 1_000_000);
assertClose(archived.total_cost, 30);
});
test("GET /api/usage/analytics applies Codex Fast tier multipliers and exposes tier split", async () => {
const db = core.getDbInstance();
const timestamp = new Date().toISOString();
db.prepare(
`INSERT INTO usage_history (provider, model, connection_id, tokens_input, tokens_output, success, latency_ms, service_tier, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
).run("codex", "gpt-5.5", "codex-fast", 1000, 500, 1, 250, "priority", timestamp);
db.prepare(
`INSERT INTO usage_history (provider, model, connection_id, tokens_input, tokens_output, success, latency_ms, service_tier, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
).run("codex", "gpt-5.5", "codex-standard", 1000, 500, 1, 250, "standard", timestamp);
db.prepare(
`INSERT INTO usage_history (provider, model, connection_id, tokens_input, tokens_output, success, latency_ms, service_tier, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
).run("codex", "gpt-5.5", "codex-flex", 1000, 500, 1, 250, "flex", timestamp);
const response = await analyticsRoute.GET(
makeRequest("http://localhost/api/usage/analytics?presets=1d")
);
const body = await response.json();
assert.equal(response.status, 200);
assertClose(body.summary.totalCost, 0.08);
assert.equal(body.summary.fastRequests, 1);
assert.equal(body.summary.flexRequests, 1);
assert.equal(body.summary.standardRequests, 1);
assertClose(body.summary.fastCost, 0.05);
assertClose(body.summary.flexCost, 0.01);
assertClose(body.summary.flexSavings, 0.01);
assert.equal(body.summary.flexUsageSavingsTokens, 750);
assertClose(body.summary.standardCost, 0.02);
assert.equal(body.byServiceTier.length, 3);
assert.deepEqual(
body.byServiceTier.map((tier: { serviceTier: string }) => tier.serviceTier),
["priority", "flex", "standard"]
);
const flexTier = body.byServiceTier.find(
(tier: { serviceTier: string }) => tier.serviceTier === "flex"
);
assert.equal(flexTier.label, "flex");
assertClose(flexTier.savings, 0.01);
assert.equal(flexTier.usageSavingsTokens, 750);
assertClose(body.byProvider[0].cost, 0.08);
assertClose(body.byModel[0].cost, 0.08);
assertClose(body.presetSummaries["1d"].totalCost, 0.08);
});
test("GET /api/usage/analytics does not report flex savings for non-Codex providers", async () => {
const db = core.getDbInstance();
db.prepare(
`INSERT INTO usage_history (provider, model, connection_id, tokens_input, tokens_output, success, latency_ms, service_tier, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
).run("openai", "gpt-4o", "openai-flex", 1000, 500, 1, 250, "flex", new Date().toISOString());
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
const body = await response.json();
assert.equal(response.status, 200);
assertClose(body.summary.totalCost, 0.0075);
assert.equal(body.summary.flexRequests, 1);
assertClose(body.summary.flexCost, 0.0075);
assertClose(body.summary.flexSavings, 0);
assert.equal(body.summary.flexUsageSavingsTokens, 0);
const flexTier = body.byServiceTier.find(
(tier: { serviceTier: string }) => tier.serviceTier === "flex"
);
assertClose(flexTier.savings, 0);
assert.equal(flexTier.usageSavingsTokens, 0);
});
test("GET /api/usage/analytics applies Codex GPT-5.6 Sol Fast multiplier", async () => {
await localDb.updatePricing({
codex: { "gpt-5.6-sol": { input: 5, output: 30 } },
});
const db = core.getDbInstance();
db.prepare(
`INSERT INTO usage_history (provider, model, connection_id, tokens_input, tokens_output, success, latency_ms, service_tier, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
).run(
"codex",
"gpt-5.6-sol",
"codex-fast",
1000,
500,
1,
250,
"priority",
new Date().toISOString()
);
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
const body = await response.json();
assert.equal(response.status, 200);
assertClose(body.summary.totalCost, 0.03);
assertClose(body.summary.fastCost, 0.03);
});
test("GET /api/usage/analytics maps Codex auto-review usage to GPT-5.5 pricing", async () => {
const db = core.getDbInstance();
db.prepare(
`INSERT INTO usage_history (provider, model, connection_id, tokens_input, tokens_output, success, latency_ms, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
).run("codex", "codex-auto-review", "codex-conn", 1000, 500, 1, 250, new Date().toISOString());
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
const body = await response.json();
assert.equal(response.status, 200);
assertClose(body.summary.totalCost, 0.02);
assert.equal(body.byModel[0].model, "codex-auto-review");
assertClose(body.byModel[0].cost, 0.02);
});
test("GET /api/usage/analytics ignores normal combo routing in fallback statistics", async () => {
const db = core.getDbInstance();
const timestamp = new Date().toISOString();
db.prepare(
`INSERT INTO usage_history (provider, model, connection_id, tokens_input, tokens_output, success, latency_ms, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
).run("codex", "gpt-5.5", "codex-conn", 1000, 500, 1, 250, timestamp);
db.prepare(
`INSERT INTO call_logs (id, provider, model, requested_model, combo_name, connection_id, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?)`
).run("combo-call", "codex", "gpt-5.5", "combo/dev", "dev", "codex-conn", timestamp);
db.prepare(
`INSERT INTO call_logs (id, provider, model, requested_model, connection_id, timestamp)
VALUES (?, ?, ?, ?, ?, ?)`
).run("same-model-call", "codex", "GPT-5.5", "gpt-5.5", "codex-conn", timestamp);
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
const body = await response.json();
assert.equal(response.status, 200);
assert.equal(body.summary.fallbackCount, 0);
assert.equal(body.summary.fallbackRatePct, 0);
assert.equal(body.summary.requestedModelCoveragePct, 100);
});
test("GET /api/usage/analytics filters by range parameter", async () => {
await seedAnalyticsData();
const response = await analyticsRoute.GET(
makeRequest("http://localhost/api/usage/analytics?range=1d")
);
const body = await response.json();
assert.equal(response.status, 200);
assert.equal(body.range, "1d");
});
test("GET /api/usage/analytics includes byProvider array with cost data", async () => {
await seedAnalyticsData();
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
const body = await response.json();
assert.equal(response.status, 200);
assert.ok(Array.isArray(body.byProvider));
assert.ok(body.byProvider.length > 0);
assert.ok(body.byProvider.every((row) => typeof row.cost === "number"));
const providerCostTotal = body.byProvider.reduce((sum, row) => sum + row.cost, 0);
assertClose(providerCostTotal, body.summary.totalCost);
});
test("GET /api/usage/analytics includes byAccount array with cost data", async () => {
await seedAnalyticsData();
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
const body = await response.json();
assert.equal(response.status, 200);
assert.ok(Array.isArray(body.byAccount));
assert.ok(body.byAccount.length > 0);
assert.ok(body.byAccount.every((row) => row.account === "test-conn"));
assert.ok(body.byAccount.every((row) => typeof row.cost === "number"));
assertClose(
body.byAccount.reduce((sum, row) => sum + row.cost, 0),
body.summary.totalCost
);
});
test("GET /api/usage/analytics includes cost by API key", async () => {
await seedAnalyticsData();
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
const body = await response.json();
assert.equal(response.status, 200);
assert.ok(Array.isArray(body.byApiKey));
assert.equal(body.byApiKey.length, 1);
assert.equal(body.byApiKey[0].apiKeyId, "test-key");
assert.equal(body.byApiKey[0].apiKeyName, "Primary Key");
assertClose(body.byApiKey[0].cost, body.summary.totalCost);
});
test("GET /api/usage/analytics does not double-count raw and aggregated rows", async () => {
const db = core.getDbInstance();
const today = new Date();
const todayStr = today.toISOString().split("T")[0];
// The raw/aggregated split boundary is retention.usageHistory (365 days by
// default — see cleanupUsageHistory in src/lib/db/cleanup.ts, which rolls up
// and deletes usage_history rows using that exact setting). Read it live
// instead of hardcoding 30 days so this fixture stays valid regardless of
// the configured retention window.
const { getUserDatabaseSettings } = await import("../../src/lib/db/databaseSettings.ts");
const rawRetentionDays = getUserDatabaseSettings().retention.usageHistory;
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - rawRetentionDays);
const olderDate = new Date(cutoffDate);
olderDate.setDate(olderDate.getDate() - 1);
const olderDateStr = olderDate.toISOString().split("T")[0];
db.prepare(
`INSERT INTO usage_history (provider, model, connection_id, tokens_input, tokens_output, success, latency_ms, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
).run("openai", "gpt-4o", "raw-current", 100, 50, 1, 200, today.toISOString());
const insertSummary = db.prepare(
`INSERT INTO daily_usage_summary (provider, model, date, total_requests, total_input_tokens, total_output_tokens, total_cost)
VALUES (?, ?, ?, ?, ?, ?, ?)`
);
insertSummary.run("openai", "gpt-4o", todayStr, 99, 9900, 9900, 0);
insertSummary.run("openai", "gpt-4o", olderDateStr, 1, 25, 10, 0);
const response = await analyticsRoute.GET(
makeRequest("http://localhost/api/usage/analytics?range=all")
);
const body = await response.json();
assert.equal(response.status, 200);
assert.equal(body.summary.totalRequests, 2);
assert.equal(body.summary.totalTokens, 185);
});
test("GET /api/usage/analytics omits global aggregates when filtering by API key", async () => {
const apiKey = await apiKeysDb.createApiKey("Scoped Key", "machine1234567890");
const db = core.getDbInstance();
db.prepare(
`INSERT INTO usage_history (provider, model, connection_id, api_key_id, api_key_name, tokens_input, tokens_output, success, latency_ms, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
).run(
"openai",
"gpt-4o",
"scoped-conn",
apiKey.id,
"Scoped Key",
100,
50,
1,
200,
new Date().toISOString()
);
db.prepare(
`INSERT INTO daily_usage_summary (provider, model, date, total_requests, total_input_tokens, total_output_tokens, total_cost)
VALUES (?, ?, ?, ?, ?, ?, ?)`
).run("openai", "gpt-4o", "2024-01-01", 99, 9900, 9900, 0);
const response = await analyticsRoute.GET(
makeRequest(`http://localhost/api/usage/analytics?range=all&apiKeyIds=${apiKey.id}`)
);
const body = await response.json();
assert.equal(response.status, 200);
assert.equal(body.summary.totalRequests, 1);
assert.equal(body.summary.totalTokens, 150);
assert.equal(body.byApiKey.length, 1);
assert.equal(body.byApiKey[0].apiKeyId, apiKey.id);
});
test("GET /api/usage/analytics groups renamed API key usage by stable ID", async () => {
const apiKey = await apiKeysDb.createApiKey("Averyanov", "machine1234567890");
await apiKeysDb.updateApiKeyPermissions(apiKey.id, { name: "Alexander Averyanov" });
const db = core.getDbInstance();
const now = Date.now();
const insertUsage = db.prepare(
`INSERT INTO usage_history (provider, model, connection_id, api_key_id, api_key_name, tokens_input, tokens_output, success, latency_ms, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
);
insertUsage.run(
"openai",
"gpt-4o",
"test-conn",
apiKey.id,
"Averyanov",
100,
50,
1,
200,
new Date(now - 60_000).toISOString()
);
insertUsage.run(
"openai",
"gpt-4o",
"test-conn",
apiKey.id,
"Desktop",
200,
100,
1,
250,
new Date(now).toISOString()
);
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
const body = await response.json();
assert.equal(response.status, 200);
assert.equal(body.summary.uniqueApiKeys, 1);
assert.equal(body.byApiKey.length, 1);
assert.equal(body.byApiKey[0].apiKeyId, apiKey.id);
assert.equal(body.byApiKey[0].apiKeyName, "Alexander Averyanov");
assert.deepEqual(body.byApiKey[0].historicalApiKeyNames.sort(), ["Averyanov", "Desktop"]);
assert.equal(body.byApiKey[0].requests, 2);
assert.equal(body.byApiKey[0].promptTokens, 300);
assert.equal(body.byApiKey[0].completionTokens, 150);
const filteredResponse = await analyticsRoute.GET(
makeRequest(`http://localhost/api/usage/analytics?apiKeyIds=${apiKey.id}`)
);
const filteredBody = await filteredResponse.json();
assert.equal(filteredResponse.status, 200);
assert.equal(filteredBody.summary.totalRequests, 2);
assert.equal(filteredBody.byApiKey.length, 1);
assert.equal(filteredBody.byApiKey[0].apiKeyId, apiKey.id);
});
test("GET /api/usage/analytics does not persist guessed API key attribution", async () => {
await localDb.updatePricing({
openai: { "gpt-4o": { input: 2.5, output: 10 } },
});
await apiKeysDb.createApiKey("Unrestricted Key", "machine1234567890");
const db = core.getDbInstance();
db.prepare(
`INSERT INTO usage_history (provider, model, connection_id, api_key_id, api_key_name, tokens_input, tokens_output, success, latency_ms, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
).run("openai", "gpt-4o", "legacy-conn", null, null, 100, 50, 1, 200, new Date().toISOString());
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
const body = await response.json();
assert.equal(response.status, 200);
assert.equal(body.byApiKey.length, 0);
const row = db
.prepare("SELECT api_key_id, api_key_name FROM usage_history WHERE connection_id = ?")
.get("legacy-conn") as { api_key_id: string | null; api_key_name: string | null };
assert.equal(row.api_key_id, null);
assert.equal(row.api_key_name, null);
});
test("GET /api/usage/analytics returns weeklyPattern for the costs dashboard", async () => {
await seedAnalyticsData();
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
const body = await response.json();
assert.equal(response.status, 200);
assert.ok(Array.isArray(body.weeklyPattern));
assert.equal(body.weeklyPattern.length, 7);
assert.deepEqual(
body.weeklyPattern.map((row) => row.day),
["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
);
assert.ok(body.weeklyPattern.some((row) => row.totalTokens > 0 && row.avgTokens > 0));
});
test("GET /api/usage/analytics includes activityMap for heatmap", async () => {
await seedAnalyticsData();
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
const body = await response.json();
assert.equal(response.status, 200);
assert.ok(typeof body.activityMap === "object");
assert.ok(Object.keys(body.activityMap).length > 0);
});
test("GET /api/usage/analytics returns 500 on database errors", async () => {
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
const body = await response.json();
assert.equal(response.status, 200);
assert.ok(body.summary.totalRequests === 0);
});
test("GET /api/usage/analytics does not throw Unknown named parameter on short range (needsAggregated=false)", async () => {
// Regression: shared params object leaked agg-only bindings (@sinceDate, @rawCutoffDate)
// into queries that don't reference them, causing better-sqlite3 to throw.
// A short range (1h) triggers needsAggregated=false because the entire window
// falls within the raw-data-only period.
const db = core.getDbInstance();
const now = new Date();
db.prepare(
`INSERT INTO usage_history (provider, model, connection_id, tokens_input, tokens_output, success, latency_ms, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
).run("openai", "gpt-4o", "test-conn", 100, 50, 1, 200, now.toISOString());
const response = await analyticsRoute.GET(
makeRequest("http://localhost/api/usage/analytics?range=1h")
);
const body = await response.json();
assert.equal(response.status, 200);
assert.equal(body.summary.totalRequests, 1);
});
test("GET /api/usage/analytics does not throw Unknown named parameter with apiKey filter on long range", async () => {
// Regression: Object.assign(presetParams, params) leaked all main-query bindings
// into preset queries that only reference preset-prefixed placeholders.
const apiKey = await apiKeysDb.createApiKey("Preset Key", "machine-preset1234");
const db = core.getDbInstance();
const now = new Date();
// Seed data old enough to trigger aggregated + preset path
for (let i = 0; i < 5; i++) {
const ts = new Date(now.getTime() - (35 + i) * 24 * 60 * 60 * 1000).toISOString();
db.prepare(
`INSERT INTO usage_history (provider, model, connection_id, api_key_id, api_key_name, tokens_input, tokens_output, success, latency_ms, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
).run("openai", "gpt-4o", "test-conn", apiKey.id, apiKey.name, 100, 50, 1, 200, ts);
}
const response = await analyticsRoute.GET(
makeRequest(`http://localhost/api/usage/analytics?range=60d&apiKeyId=${apiKey.id}`)
);
const body = await response.json();
assert.equal(response.status, 200);
// Core regression check: no "Unknown named parameter" error.
// The exact count depends on raw-vs-aggregated boundary; we only need to
// confirm the endpoint returns 200 without throwing.
assert.ok(typeof body.summary.totalRequests === "number");
});
test("rollupUsageHistoryBeforeDate prices each request, not the day's summed tokens", async () => {
const db = core.getDbInstance();
const costCalculator = await import("../../src/lib/usage/costCalculator.ts");
// Two requests, same provider/model/day/tier, so the rollup's
// GROUP BY provider, model, date, serviceTier collapses them into one row.
//
// The first request is cache-heavy: cache_read exceeds tokens_input. Pricing
// clamps non-cached input at zero per request (costCalculator "nonCachedInput"),
// so that surplus must NOT be allowed to absorb another request's billable
// input. Summing tokens first and pricing once lets exactly that happen.
const rows = [
{ input: 1_000, output: 0, cacheRead: 5_000 },
{ input: 9_000, output: 0, cacheRead: 0 },
];
for (const row of rows) {
db.prepare(
`INSERT INTO usage_history
(provider, model, connection_id, tokens_input, tokens_output, tokens_cache_read,
tokens_cache_creation, tokens_reasoning, service_tier, success, latency_ms, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
).run(
"openai",
"gpt-4o",
"clamp-conn",
row.input,
row.output,
row.cacheRead,
0,
0,
"standard",
1,
200,
"2024-03-01T12:00:00.000Z"
);
}
// Ground truth: price every request individually, exactly as the live
// analytics path does for raw (non-archived) usage.
let expectedCost = 0;
for (const row of rows) {
expectedCost += await costCalculator.calculateCost(
"openai",
"gpt-4o",
{
input: row.input,
output: row.output,
cacheRead: row.cacheRead,
cacheCreation: 0,
reasoning: 0,
},
{
provider: "openai",
model: "gpt-4o",
serviceTier: "standard",
flatRateAsZero: false,
}
);
}
const result = await aggregateHistory.rollupUsageHistoryBeforeDate("2024-04-01");
assert.equal(result.errors, 0);
const archived = db
.prepare(
`SELECT total_cost FROM daily_usage_summary
WHERE provider = ? AND model = ? AND date = ?`
)
.get("openai", "gpt-4o", "2024-03-01") as { total_cost: number };
// Retention must preserve the billed value. Archiving a day may not silently
// discount it just because the day's requests were grouped before pricing.
assertClose(archived.total_cost, expectedCost);
});