diff --git a/src/lib/usage/migrations.ts b/src/lib/usage/migrations.ts index 9fd9e1b053..620b8f4f2b 100644 --- a/src/lib/usage/migrations.ts +++ b/src/lib/usage/migrations.ts @@ -16,6 +16,7 @@ import { getLegacyDotDataDir, isSamePath } from "../dataPaths"; import { getAppLogFilePath } from "../logEnv"; import { protectPayloadForLog } from "../logPayloads"; import { sanitizePII } from "../piiSanitizer"; +import { resolveProviderId } from "@/shared/constants/providers"; import { writeCallArtifact, type CallLogArtifact } from "./callLogArtifacts"; import { resolveImportedUsageAccountIdentity, @@ -333,7 +334,7 @@ export function migrateUsageJsonToSqlite() { : resolveOrphanedUsageAccountIdentity(entry.provider, connectionId); const identity = resolveImportedUsageAccountIdentity(entry, fallbackIdentity); insert.run({ - provider: entry.provider || null, + provider: entry.provider ? resolveProviderId(entry.provider) : null, model: entry.model || null, connectionId, accountKey: identity.accountKey, diff --git a/src/lib/usage/usageHistory.ts b/src/lib/usage/usageHistory.ts index a6fb9a7d3a..0a58123bc9 100644 --- a/src/lib/usage/usageHistory.ts +++ b/src/lib/usage/usageHistory.ts @@ -8,6 +8,7 @@ */ import { getDbInstance } from "../db/core"; +import { resolveProviderId } from "@/shared/constants/providers"; import { protectPayloadForLog } from "../logPayloads"; import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/errorSanitization.ts"; import { @@ -728,7 +729,7 @@ export async function saveRequestUsage(entry: UsageEntry) { ) .get( timestamp, - entry.provider || null, + (entry.provider ? resolveProviderId(entry.provider) : null), entry.model || null, entry.connectionId || null, entry.apiKeyId || null, @@ -756,7 +757,7 @@ export async function saveRequestUsage(entry: UsageEntry) { VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` ).run( - entry.provider || null, + (entry.provider ? resolveProviderId(entry.provider) : null), entry.model || null, entry.connectionId || null, accountIdentity.accountKey, diff --git a/tests/unit/usage-history-provider-alias-13459.test.ts b/tests/unit/usage-history-provider-alias-13459.test.ts new file mode 100644 index 0000000000..a7a1e5866b --- /dev/null +++ b/tests/unit/usage-history-provider-alias-13459.test.ts @@ -0,0 +1,37 @@ +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"; + +// #13459: usage_history rows written under a provider alias ("af") and under the +// canonical id ("api-airforce") split one provider into two analytics buckets. + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-usage-alias-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const usageHistory = await import("../../src/lib/usage/usageHistory.ts"); + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); + +test("#13459 saveRequestUsage stores the canonical provider id for an alias", async () => { + for (const provider of ["af", "api-airforce"]) { + await usageHistory.saveRequestUsage({ + provider, + model: "gpt-4o-mini", + tokens: { input: 10, output: 5 }, + success: true, + latencyMs: 100, + timestamp: new Date().toISOString(), + }); + } + const rows = core + .getDbInstance() + .prepare("SELECT provider, COUNT(*) AS n FROM usage_history GROUP BY provider") + .all() as Array<{ provider: string; n: number }>; + assert.deepEqual(rows, [{ provider: "api-airforce", n: 2 }]); +});