From 1cbb77c30b5fbf2d0d10b87dd66c36aa54d2c9a8 Mon Sep 17 00:00:00 2001 From: Koosha Paridehpour <42529354+KooshaPari@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:05:21 -0700 Subject: [PATCH] fix(analytics): normalize provider aliases to canonical id in usage_history (#13545) `usage_history` rows are written with the canonical provider id (`resolveProviderId`), both from live `saveRequestUsage` and from the legacy JSON import. Traffic logged under an alias (`af`) and under the id (`api-airforce`) no longer splits one provider into two analytics buckets (#13459). Maintainer addition: `tests/unit/usage-history-provider-alias-13459.test.ts` saves one row under the alias and one under the id and asserts a single `api-airforce` bucket (fails on the release tip, passes with the fix). Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green. Thanks @KooshaPari! --- src/lib/usage/migrations.ts | 3 +- src/lib/usage/usageHistory.ts | 5 ++- ...usage-history-provider-alias-13459.test.ts | 37 +++++++++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 tests/unit/usage-history-provider-alias-13459.test.ts 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 }]); +});