mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 21:02:12 +03:00
Stop usage analytics reads from backfilling missing API key attribution into historical rows so legacy records remain unchanged and surface as unknown instead of guessed ownership. Also require management authentication on admin concurrency and compression analytics endpoints, and preserve image payloads during lite compression unless a model is explicitly marked as non-vision.
246 lines
8.9 KiB
TypeScript
246 lines
8.9 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 localDb = await import("../../src/lib/localDb.ts");
|
|
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
|
|
const usageHistory = await import("../../src/lib/usage/usageHistory.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 });
|
|
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 });
|
|
|
|
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.provider === "openai");
|
|
assert.ok(gptEntry);
|
|
assert.ok(typeof gptEntry.cost === "number");
|
|
assert.ok(gptEntry.cost > 0);
|
|
});
|
|
|
|
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.equal(body.byAccount[0].account, "test-conn");
|
|
assert.equal(typeof body.byAccount[0].cost, "number");
|
|
assertClose(body.byAccount[0].cost, 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 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.ok(body.byApiKey.some((row) => row.apiKeyName === "Unknown API key"));
|
|
|
|
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);
|
|
});
|