mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
Fix API key identity in usage analytics (#2008)
Integrated into release/v3.8.0
This commit is contained in:
committed by
GitHub
parent
7214efa86e
commit
e9d96fa3ff
@@ -184,13 +184,17 @@ export default function ApiManagerPageClient() {
|
||||
const stats: Record<string, KeyUsageStats> = {};
|
||||
|
||||
for (const key of apiKeys) {
|
||||
// Match analytics entry by key name (reliable across both systems)
|
||||
const analyticsMatch = byApiKey.find((entry: any) => entry.apiKeyName === key.name);
|
||||
const analyticsMatch = byApiKey.find(
|
||||
(entry: any) =>
|
||||
entry.apiKeyId === key.id || (!entry.apiKeyId && entry.apiKeyName === key.name)
|
||||
);
|
||||
|
||||
// The call-logs endpoint returns entries sorted by timestamp DESC,
|
||||
// so the first match is the most recent one.
|
||||
const lastUsed =
|
||||
(logs || []).find((log: any) => log.apiKeyName === key.name)?.timestamp || null;
|
||||
(logs || []).find(
|
||||
(log: any) => log.apiKeyId === key.id || (!log.apiKeyId && log.apiKeyName === key.name)
|
||||
)?.timestamp || null;
|
||||
|
||||
stats[key.id] = {
|
||||
totalRequests: analyticsMatch?.requests ?? 0,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getApiKeys } from "@/lib/db/apiKeys";
|
||||
import { getDbInstance } from "@/lib/db/core";
|
||||
|
||||
function getRangeStartIso(range: string): string | null {
|
||||
@@ -76,6 +77,16 @@ function uniqueValues(values: Array<string | null | undefined>): string[] {
|
||||
return result;
|
||||
}
|
||||
|
||||
function makeApiKeyUsageGroup(apiKeyId: string, fallbackName: string): string {
|
||||
return apiKeyId ? `id:${apiKeyId}` : `name:${fallbackName}`;
|
||||
}
|
||||
|
||||
function addApiKeyAlias(target: Set<string>, value: unknown): void {
|
||||
if (typeof value !== "string") return;
|
||||
const trimmed = value.trim();
|
||||
if (trimmed) target.add(trimmed);
|
||||
}
|
||||
|
||||
function stripCodexEffortSuffix(model: string): string {
|
||||
return model.replace(/-(?:xhigh|high|medium|low|none)$/i, "");
|
||||
}
|
||||
@@ -244,6 +255,13 @@ export async function GET(request: Request) {
|
||||
const presetsParam = searchParams.get("presets");
|
||||
|
||||
const db = getDbInstance();
|
||||
const apiKeys = await getApiKeys();
|
||||
const currentApiKeyNames = new Map<string, string>();
|
||||
for (const apiKey of apiKeys) {
|
||||
if (typeof apiKey.id === "string" && typeof apiKey.name === "string") {
|
||||
currentApiKeyNames.set(apiKey.id, apiKey.name);
|
||||
}
|
||||
}
|
||||
|
||||
const conditions = [];
|
||||
const params: Record<string, string> = {};
|
||||
@@ -296,7 +314,7 @@ export async function GET(request: Request) {
|
||||
COALESCE(SUM(tokens_input + tokens_output), 0) as totalTokens,
|
||||
COUNT(DISTINCT model) as uniqueModels,
|
||||
COUNT(DISTINCT connection_id) as uniqueAccounts,
|
||||
COUNT(DISTINCT api_key_id) as uniqueApiKeys,
|
||||
COUNT(DISTINCT COALESCE(NULLIF(api_key_id, ''), NULLIF(api_key_name, ''))) as uniqueApiKeys,
|
||||
COALESCE(SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END), 0) as successfulRequests,
|
||||
COALESCE(AVG(latency_ms), 0) as avgLatencyMs,
|
||||
COALESCE(MIN(timestamp), '') as firstRequest,
|
||||
@@ -489,8 +507,8 @@ export async function GET(request: Request) {
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
api_key_id as apiKeyId,
|
||||
COALESCE(NULLIF(api_key_name, ''), NULLIF(api_key_id, ''), 'Unknown API key') as apiKeyName,
|
||||
NULLIF(api_key_id, '') as apiKeyId,
|
||||
COALESCE(NULLIF(api_key_id, ''), NULLIF(api_key_name, ''), 'unknown') as apiKeyGroupKey,
|
||||
LOWER(provider) as provider,
|
||||
LOWER(model) as model,
|
||||
COUNT(*) as requests,
|
||||
@@ -502,11 +520,42 @@ export async function GET(request: Request) {
|
||||
COALESCE(SUM(tokens_input + tokens_output), 0) as totalTokens
|
||||
FROM usage_history
|
||||
${apiKeyWhereClause}
|
||||
GROUP BY api_key_id, api_key_name, LOWER(provider), LOWER(model)
|
||||
GROUP BY COALESCE(NULLIF(api_key_id, ''), NULLIF(api_key_name, ''), 'unknown'), NULLIF(api_key_id, ''), LOWER(provider), LOWER(model)
|
||||
`
|
||||
)
|
||||
.all(params) as Array<Record<string, unknown>>;
|
||||
|
||||
const apiKeyMetadataRows = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
NULLIF(api_key_id, '') as apiKeyId,
|
||||
NULLIF(api_key_name, '') as apiKeyName,
|
||||
COALESCE(NULLIF(api_key_id, ''), NULLIF(api_key_name, ''), 'unknown') as apiKeyGroupKey,
|
||||
MAX(timestamp) as lastUsed
|
||||
FROM usage_history
|
||||
${apiKeyWhereClause}
|
||||
GROUP BY NULLIF(api_key_id, ''), NULLIF(api_key_name, '')
|
||||
ORDER BY lastUsed DESC
|
||||
`
|
||||
)
|
||||
.all(params) as Array<Record<string, unknown>>;
|
||||
|
||||
const apiKeyMetadata = new Map<string, { latestName: string; aliases: Set<string> }>();
|
||||
for (const row of apiKeyMetadataRows) {
|
||||
const apiKeyId = toStringValue(row.apiKeyId);
|
||||
const apiKeyGroupKey = toStringValue(row.apiKeyGroupKey, "unknown");
|
||||
const groupKey = makeApiKeyUsageGroup(apiKeyId, apiKeyGroupKey);
|
||||
const existing = apiKeyMetadata.get(groupKey) || {
|
||||
latestName: "",
|
||||
aliases: new Set<string>(),
|
||||
};
|
||||
const apiKeyName = toStringValue(row.apiKeyName);
|
||||
if (!existing.latestName && apiKeyName) existing.latestName = apiKeyName;
|
||||
addApiKeyAlias(existing.aliases, apiKeyName);
|
||||
apiKeyMetadata.set(groupKey, existing);
|
||||
}
|
||||
|
||||
const weeklyRows = db
|
||||
.prepare(
|
||||
`
|
||||
@@ -742,6 +791,7 @@ export async function GET(request: Request) {
|
||||
apiKey: string;
|
||||
apiKeyId: string | null;
|
||||
apiKeyName: string;
|
||||
historicalApiKeyNames: string[];
|
||||
requests: number;
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
@@ -751,12 +801,20 @@ export async function GET(request: Request) {
|
||||
>();
|
||||
for (const row of apiKeyRows) {
|
||||
const apiKeyId = toStringValue(row.apiKeyId);
|
||||
const apiKeyName = toStringValue(row.apiKeyName, apiKeyId || "Unknown API key");
|
||||
const key = `${apiKeyId || "unknown"}::${apiKeyName}`;
|
||||
const apiKeyGroupKey = toStringValue(row.apiKeyGroupKey, "unknown");
|
||||
const key = makeApiKeyUsageGroup(apiKeyId, apiKeyGroupKey);
|
||||
const metadata = apiKeyMetadata.get(key);
|
||||
const apiKeyName =
|
||||
(apiKeyId ? currentApiKeyNames.get(apiKeyId) : undefined) ||
|
||||
metadata?.latestName ||
|
||||
apiKeyId ||
|
||||
apiKeyGroupKey ||
|
||||
"Unknown API key";
|
||||
const existing = apiKeyMap.get(key) || {
|
||||
apiKey: apiKeyId && apiKeyName !== apiKeyId ? `${apiKeyName} (${apiKeyId})` : apiKeyName,
|
||||
apiKeyId: apiKeyId || null,
|
||||
apiKeyName,
|
||||
historicalApiKeyNames: Array.from(metadata?.aliases || []),
|
||||
requests: 0,
|
||||
promptTokens: 0,
|
||||
completionTokens: 0,
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import { getDbInstance } from "../db/core";
|
||||
import { getApiKeys } from "../db/apiKeys";
|
||||
import { getPendingRequests } from "./usageHistory";
|
||||
import { getAccountDisplayName } from "@/lib/display/names";
|
||||
import { calculateCost } from "./costCalculator";
|
||||
@@ -29,6 +30,7 @@ type UsageBreakdown = UsageBucket & {
|
||||
accountName?: string;
|
||||
apiKeyId?: string | null;
|
||||
apiKeyName?: string;
|
||||
historicalApiKeyNames?: string[];
|
||||
};
|
||||
|
||||
type ActiveRequest = {
|
||||
@@ -55,6 +57,10 @@ function toStringOrEmpty(value: unknown): string {
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
function getApiKeyStatsKey(apiKeyId: string | null, apiKeyName: string | null): string {
|
||||
return apiKeyId ? `id:${apiKeyId}` : `name:${apiKeyName || "unknown"}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get aggregated usage stats.
|
||||
* Uses UNION of recent raw data and older aggregated data when aggregation is enabled.
|
||||
@@ -126,6 +132,18 @@ export async function getUsageStats() {
|
||||
toStringOrEmpty(conn.name) || toStringOrEmpty(conn.email) || connectionId;
|
||||
}
|
||||
|
||||
const currentApiKeyNames = new Map<string, string>();
|
||||
try {
|
||||
const apiKeys = await getApiKeys();
|
||||
for (const apiKey of apiKeys) {
|
||||
if (typeof apiKey.id === "string" && typeof apiKey.name === "string") {
|
||||
currentApiKeyNames.set(apiKey.id, apiKey.name);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Stats can still be computed from usage_history when api_keys is unavailable.
|
||||
}
|
||||
|
||||
const pendingRequests = getPendingRequests();
|
||||
|
||||
const stats: {
|
||||
@@ -286,26 +304,35 @@ export async function getUsageStats() {
|
||||
|
||||
// By API key
|
||||
if (apiKeyId || apiKeyName) {
|
||||
const keyName = apiKeyName || apiKeyId || "unknown";
|
||||
const keyId = apiKeyId || null;
|
||||
const apiKey = keyId ? `${keyName} (${keyId})` : keyName;
|
||||
if (!stats.byApiKey[apiKey]) {
|
||||
stats.byApiKey[apiKey] = {
|
||||
const key = getApiKeyStatsKey(apiKeyId, apiKeyName);
|
||||
const displayName =
|
||||
(apiKeyId ? currentApiKeyNames.get(apiKeyId) : undefined) ||
|
||||
apiKeyName ||
|
||||
apiKeyId ||
|
||||
"unknown";
|
||||
if (!stats.byApiKey[key]) {
|
||||
stats.byApiKey[key] = {
|
||||
requests: 0,
|
||||
promptTokens: 0,
|
||||
completionTokens: 0,
|
||||
cost: 0,
|
||||
apiKeyId: keyId,
|
||||
apiKeyName: keyName,
|
||||
apiKeyId,
|
||||
apiKeyName: displayName,
|
||||
historicalApiKeyNames: [],
|
||||
lastUsed: timestamp,
|
||||
};
|
||||
}
|
||||
stats.byApiKey[apiKey].requests++;
|
||||
stats.byApiKey[apiKey].promptTokens += promptTokens;
|
||||
stats.byApiKey[apiKey].completionTokens += completionTokens;
|
||||
stats.byApiKey[apiKey].cost += entryCost;
|
||||
if (new Date(timestamp) > new Date(stats.byApiKey[apiKey].lastUsed || timestamp)) {
|
||||
stats.byApiKey[apiKey].lastUsed = timestamp;
|
||||
const apiKeyStats = stats.byApiKey[key];
|
||||
if (apiKeyName && !apiKeyStats.historicalApiKeyNames?.includes(apiKeyName)) {
|
||||
apiKeyStats.historicalApiKeyNames?.push(apiKeyName);
|
||||
}
|
||||
apiKeyStats.apiKeyName = displayName;
|
||||
apiKeyStats.requests++;
|
||||
apiKeyStats.promptTokens += promptTokens;
|
||||
apiKeyStats.completionTokens += completionTokens;
|
||||
apiKeyStats.cost += entryCost;
|
||||
if (new Date(timestamp) > new Date(apiKeyStats.lastUsed || timestamp)) {
|
||||
apiKeyStats.lastUsed = timestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,13 @@ function shortModelName(model: string) {
|
||||
return parts[parts.length - 1] || model;
|
||||
}
|
||||
|
||||
function getApiKeyAnalyticsKey(
|
||||
apiKeyId: string | null | undefined,
|
||||
apiKeyName: string | null | undefined
|
||||
) {
|
||||
return apiKeyId ? `id:${apiKeyId}` : `name:${apiKeyName || "unknown"}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute all analytics data from usage history
|
||||
* @param {Array} history - Array of usage entries
|
||||
@@ -184,7 +191,7 @@ export async function computeAnalytics(
|
||||
if (entry.model) summary.uniqueModels.add(modelShort);
|
||||
if (entry.connectionId) summary.uniqueAccounts.add(entry.connectionId);
|
||||
if (entry.apiKeyId || entry.apiKeyName) {
|
||||
summary.uniqueApiKeys.add(entry.apiKeyId || entry.apiKeyName);
|
||||
summary.uniqueApiKeys.add(getApiKeyAnalyticsKey(entry.apiKeyId, entry.apiKeyName));
|
||||
}
|
||||
|
||||
// Daily trend
|
||||
@@ -260,12 +267,14 @@ export async function computeAnalytics(
|
||||
// By API key
|
||||
if (entry.apiKeyId || entry.apiKeyName) {
|
||||
const keyName = entry.apiKeyName || entry.apiKeyId || "unknown";
|
||||
const key = getApiKeyAnalyticsKey(entry.apiKeyId, entry.apiKeyName);
|
||||
const keyLabel = entry.apiKeyId ? `${keyName} (${entry.apiKeyId})` : keyName;
|
||||
if (!byApiKeyMap[keyLabel]) {
|
||||
byApiKeyMap[keyLabel] = {
|
||||
if (!byApiKeyMap[key]) {
|
||||
byApiKeyMap[key] = {
|
||||
apiKey: keyLabel,
|
||||
apiKeyId: entry.apiKeyId || null,
|
||||
apiKeyName: keyName,
|
||||
historicalApiKeyNames: [],
|
||||
requests: 0,
|
||||
promptTokens: 0,
|
||||
completionTokens: 0,
|
||||
@@ -273,11 +282,14 @@ export async function computeAnalytics(
|
||||
cost: 0,
|
||||
};
|
||||
}
|
||||
byApiKeyMap[keyLabel].requests++;
|
||||
byApiKeyMap[keyLabel].promptTokens += pt;
|
||||
byApiKeyMap[keyLabel].completionTokens += ct;
|
||||
byApiKeyMap[keyLabel].totalTokens += totalTkns;
|
||||
byApiKeyMap[keyLabel].cost += cost;
|
||||
if (entry.apiKeyName && !byApiKeyMap[key].historicalApiKeyNames.includes(entry.apiKeyName)) {
|
||||
byApiKeyMap[key].historicalApiKeyNames.push(entry.apiKeyName);
|
||||
}
|
||||
byApiKeyMap[key].requests++;
|
||||
byApiKeyMap[key].promptTokens += pt;
|
||||
byApiKeyMap[key].completionTokens += ct;
|
||||
byApiKeyMap[key].totalTokens += totalTkns;
|
||||
byApiKeyMap[key].cost += cost;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,16 +61,15 @@ export default function UsageAnalytics() {
|
||||
setError(null);
|
||||
|
||||
// Update available keys from unfiltered data (only when no filter is active).
|
||||
// Use apiKeyName as the stable identifier — it is always populated
|
||||
// for every OmniRoute API key regardless of the downstream provider.
|
||||
if (selectedApiKeys.length === 0 && data.byApiKey?.length > 0) {
|
||||
const seen = new Set<string>();
|
||||
const keys: { id: string; name: string }[] = [];
|
||||
for (const k of data.byApiKey) {
|
||||
const id = k.apiKeyId || k.apiKeyName || "unknown";
|
||||
const name = k.apiKeyName || k.apiKeyId || "unknown";
|
||||
if (seen.has(name)) continue;
|
||||
seen.add(name);
|
||||
keys.push({ id: name, name });
|
||||
if (seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
keys.push({ id, name });
|
||||
}
|
||||
setAvailableApiKeys(keys);
|
||||
}
|
||||
|
||||
@@ -155,16 +155,7 @@ test("GET /api/usage/analytics maps Codex auto-review usage to GPT-5.5 pricing",
|
||||
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()
|
||||
);
|
||||
).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();
|
||||
@@ -254,6 +245,65 @@ test("GET /api/usage/analytics includes cost by API key", async () => {
|
||||
assertClose(body.byApiKey[0].cost, body.summary.totalCost);
|
||||
});
|
||||
|
||||
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 } },
|
||||
|
||||
@@ -9,9 +9,11 @@ process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
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 providersDb = await import("../../src/lib/db/providers.ts");
|
||||
const usageHistory = await import("../../src/lib/usage/usageHistory.ts");
|
||||
const usageStats = await import("../../src/lib/usage/usageStats.ts");
|
||||
const legacyUsageAnalytics = await import("../../src/lib/usageAnalytics.ts");
|
||||
const callLogs = await import("../../src/lib/usage/callLogs.ts");
|
||||
const { calculateCost } = await import("../../src/lib/usage/costCalculator.ts");
|
||||
|
||||
@@ -20,6 +22,7 @@ const clearPendingRequests = usageHistory.clearPendingRequests;
|
||||
|
||||
async function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
apiKeysDb.resetApiKeyState();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
clearPendingRequests();
|
||||
@@ -31,6 +34,7 @@ test.beforeEach(async () => {
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
apiKeysDb.resetApiKeyState();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
@@ -258,7 +262,7 @@ test("getUsageStats aggregates totals, buckets, pending requests, and cost break
|
||||
assert.equal(stats.byAccount[accountKey].requests, 2);
|
||||
assert.equal(stats.byAccount[accountKey].accountName, "Primary Account");
|
||||
|
||||
assert.equal(stats.byApiKey["Service Key (api-key-1)"].requests, 2);
|
||||
assert.equal(stats.byApiKey["id:api-key-1"].requests, 2);
|
||||
assert.equal(stats.pending.byModel["pricing-model (pricing-provider)"], 1);
|
||||
assert.equal(stats.pending.byAccount[connection.id]["pricing-model (pricing-provider)"], 1);
|
||||
assert.deepEqual(stats.activeRequests, [
|
||||
@@ -275,6 +279,90 @@ test("getUsageStats aggregates totals, buckets, pending requests, and cost break
|
||||
assert.equal(recentBucketTotal, 1);
|
||||
});
|
||||
|
||||
test("getUsageStats groups renamed API key usage by stable ID", async () => {
|
||||
const db = core.getDbInstance();
|
||||
const now = new Date().toISOString();
|
||||
db.prepare(
|
||||
`INSERT INTO api_keys (id, name, key, machine_id, allowed_models, no_log, created_at, key_prefix)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run(
|
||||
"api-key-rename",
|
||||
"Current Name",
|
||||
"omni-test-key",
|
||||
"machine1234567890",
|
||||
"[]",
|
||||
0,
|
||||
now,
|
||||
"omni-test-ke"
|
||||
);
|
||||
|
||||
await usageHistory.saveRequestUsage({
|
||||
provider: "provider-a",
|
||||
model: "model-a",
|
||||
apiKeyId: "api-key-rename",
|
||||
apiKeyName: "Original Name",
|
||||
tokens: { input: 10, output: 5 },
|
||||
success: true,
|
||||
timestamp: new Date(Date.now() - 60_000).toISOString(),
|
||||
});
|
||||
await usageHistory.saveRequestUsage({
|
||||
provider: "provider-a",
|
||||
model: "model-a",
|
||||
apiKeyId: "api-key-rename",
|
||||
apiKeyName: "Renamed Alias",
|
||||
tokens: { input: 20, output: 10 },
|
||||
success: true,
|
||||
timestamp: now,
|
||||
});
|
||||
|
||||
const stats = await usageStats.getUsageStats();
|
||||
const row = stats.byApiKey["id:api-key-rename"];
|
||||
|
||||
assert.ok(row);
|
||||
assert.equal(Object.keys(stats.byApiKey).length, 1);
|
||||
assert.equal(row.apiKeyId, "api-key-rename");
|
||||
assert.equal(row.apiKeyName, "Current Name");
|
||||
assert.deepEqual(row.historicalApiKeyNames?.sort(), ["Original Name", "Renamed Alias"]);
|
||||
assert.equal(row.requests, 2);
|
||||
assert.equal(row.promptTokens, 30);
|
||||
assert.equal(row.completionTokens, 15);
|
||||
});
|
||||
|
||||
test("computeAnalytics groups renamed API key usage by stable ID", async () => {
|
||||
const analytics = await legacyUsageAnalytics.computeAnalytics(
|
||||
[
|
||||
{
|
||||
timestamp: new Date(Date.now() - 60_000).toISOString(),
|
||||
provider: "provider-a",
|
||||
model: "model-a",
|
||||
apiKeyId: "api-key-legacy",
|
||||
apiKeyName: "Original Name",
|
||||
tokens: { input: 10, output: 5 },
|
||||
},
|
||||
{
|
||||
timestamp: new Date().toISOString(),
|
||||
provider: "provider-a",
|
||||
model: "model-a",
|
||||
apiKeyId: "api-key-legacy",
|
||||
apiKeyName: "Renamed Alias",
|
||||
tokens: { input: 20, output: 10 },
|
||||
},
|
||||
],
|
||||
"all"
|
||||
);
|
||||
|
||||
assert.equal(analytics.summary.uniqueApiKeys, 1);
|
||||
assert.equal(analytics.byApiKey.length, 1);
|
||||
assert.equal(analytics.byApiKey[0].apiKeyId, "api-key-legacy");
|
||||
assert.deepEqual(analytics.byApiKey[0].historicalApiKeyNames.sort(), [
|
||||
"Original Name",
|
||||
"Renamed Alias",
|
||||
]);
|
||||
assert.equal(analytics.byApiKey[0].requests, 2);
|
||||
assert.equal(analytics.byApiKey[0].promptTokens, 30);
|
||||
assert.equal(analytics.byApiKey[0].completionTokens, 15);
|
||||
});
|
||||
|
||||
test("recent request summaries are generated from SQLite call logs", async () => {
|
||||
const connection = await providersDb.createProviderConnection({
|
||||
provider: "log-provider",
|
||||
|
||||
Reference in New Issue
Block a user