mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
- byProvider now resolves the internal provider id to its configured display name via getProviderById() (fallback: raw id for providers not in the static registry). Fixes the Usage page showing "codex" instead of "OpenAI Codex". - byModel's in-memory dedup key now uses the normalized model name instead of the raw one, so the same logical model recorded under a bare and a provider-prefixed spelling (e.g. "glm-5.2" vs "z-ai/glm-5.2") merges into a single aggregated row instead of appearing twice with the same displayed name. - Introduces a local UsageRows type alias in route.ts to shrink the repeated "as Array<Record<string, unknown>>" casts back under the frozen file-size baseline once the file was touched.
This commit is contained in:
committed by
GitHub
parent
63c85ea76d
commit
eb92e626d2
1
changelog.d/fixes/7534-usage-provider-display-name.md
Normal file
1
changelog.d/fixes/7534-usage-provider-display-name.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(api): Usage page "by provider" table now shows the configured provider display name (e.g. "OpenAI Codex") instead of the raw internal provider id (e.g. "codex") (#7534)
|
||||
@@ -0,0 +1 @@
|
||||
- fix(api): Usage page "model usage" table no longer lists the same logical model twice when it was recorded under both a bare and a provider-prefixed spelling (e.g. `glm-5.2` and `z-ai/glm-5.2`) — the in-memory dedup key now uses the normalized model name (#7535)
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getProviderById } from "@/shared/constants/providers";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { getApiKeys } from "@/lib/db/apiKeys";
|
||||
import { getUserDatabaseSettings } from "@/lib/db/databaseSettings";
|
||||
@@ -54,6 +55,7 @@ function getRangeStartIso(range: string): string | null {
|
||||
const WEEKDAY_LABELS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||
|
||||
type PricingByProvider = Record<string, Record<string, Record<string, unknown>>>;
|
||||
type UsageRows = Array<Record<string, unknown>>;
|
||||
type ComputeCostFromPricing = (
|
||||
pricing: Record<string, unknown> | null | undefined,
|
||||
tokens: Record<string, number | undefined> | null | undefined,
|
||||
@@ -413,9 +415,8 @@ export async function GET(request: Request) {
|
||||
|
||||
const summaryRow = getUsageSummary(unifiedSource, unifiedParams) as Record<string, unknown>;
|
||||
|
||||
const dailyRows = getDailyUsage(unifiedSource, unifiedParams) as Array<Record<string, unknown>>;
|
||||
|
||||
const dailyCostRows = getDailyCostRows(unifiedSource, unifiedParams) as Array<Record<string, unknown>>;
|
||||
const dailyRows = getDailyUsage(unifiedSource, unifiedParams) as UsageRows;
|
||||
const dailyCostRows = getDailyCostRows(unifiedSource, unifiedParams) as UsageRows;
|
||||
|
||||
const heatmapStart = new Date();
|
||||
heatmapStart.setUTCDate(heatmapStart.getUTCDate() - 364);
|
||||
@@ -437,30 +438,30 @@ export async function GET(request: Request) {
|
||||
});
|
||||
}
|
||||
|
||||
const heatmapRows = getHeatmapRows(heatmapConditions, heatmapParams) as Array<Record<string, unknown>>;
|
||||
const heatmapRows = getHeatmapRows(heatmapConditions, heatmapParams) as UsageRows;
|
||||
|
||||
const modelRows = getModelUsageRows(unifiedSource, unifiedParams) as Array<Record<string, unknown>>;
|
||||
const modelRows = getModelUsageRows(unifiedSource, unifiedParams) as UsageRows;
|
||||
|
||||
const providerCostRows = getProviderCostRows(unifiedSource, unifiedParams) as Array<Record<string, unknown>>;
|
||||
const providerCostRows = getProviderCostRows(unifiedSource, unifiedParams) as UsageRows;
|
||||
|
||||
const providerRows = getProviderUsageRows(unifiedSource, unifiedParams) as Array<Record<string, unknown>>;
|
||||
const providerRows = getProviderUsageRows(unifiedSource, unifiedParams) as UsageRows;
|
||||
|
||||
const accountCostWhereClause = whereClause
|
||||
.replace(/timestamp/g, "usage_history.timestamp")
|
||||
.replace(/api_key_/g, "usage_history.api_key_");
|
||||
const accountCostRows = getAccountCostRows(accountCostWhereClause, params) as Array<Record<string, unknown>>;
|
||||
const accountCostRows = getAccountCostRows(accountCostWhereClause, params) as UsageRows;
|
||||
|
||||
const accountRows = getAccountUsageRows(accountCostWhereClause, params) as Array<Record<string, unknown>>;
|
||||
const accountRows = getAccountUsageRows(accountCostWhereClause, params) as UsageRows;
|
||||
|
||||
const apiKeyWhereClause = appendWhereCondition(
|
||||
whereClause,
|
||||
"(api_key_id IS NOT NULL AND api_key_id != '') OR (api_key_name IS NOT NULL AND api_key_name != '')"
|
||||
);
|
||||
const apiKeyRows = getApiKeyUsageRows(apiKeyWhereClause, params) as Array<Record<string, unknown>>;
|
||||
const apiKeyRows = getApiKeyUsageRows(apiKeyWhereClause, params) as UsageRows;
|
||||
|
||||
const serviceTierRows = getServiceTierUsageRows(unifiedSource, unifiedParams) as Array<Record<string, unknown>>;
|
||||
const serviceTierRows = getServiceTierUsageRows(unifiedSource, unifiedParams) as UsageRows;
|
||||
|
||||
const apiKeyMetadataRows = getApiKeyMetadataRows(apiKeyWhereClause, params) as Array<Record<string, unknown>>;
|
||||
const apiKeyMetadataRows = getApiKeyMetadataRows(apiKeyWhereClause, params) as UsageRows;
|
||||
|
||||
const apiKeyMetadata = new Map<string, { latestName: string; aliases: Set<string> }>();
|
||||
for (const row of apiKeyMetadataRows) {
|
||||
@@ -477,7 +478,7 @@ export async function GET(request: Request) {
|
||||
apiKeyMetadata.set(groupKey, existing);
|
||||
}
|
||||
|
||||
const weeklyRows = getWeeklyPatternRows(unifiedSource, unifiedParams) as Array<Record<string, unknown>>;
|
||||
const weeklyRows = getWeeklyPatternRows(unifiedSource, unifiedParams) as UsageRows;
|
||||
|
||||
const fallbackRow = getFallbackStats(whereClause, params) as Record<string, unknown>;
|
||||
|
||||
@@ -590,7 +591,7 @@ export async function GET(request: Request) {
|
||||
normalizeModelName,
|
||||
computeCostFromPricing
|
||||
);
|
||||
const key = `${provider}::${model}`;
|
||||
const key = `${provider}::${short}`;
|
||||
const existing = modelMap.get(key) || {
|
||||
model: short,
|
||||
provider,
|
||||
@@ -662,7 +663,7 @@ export async function GET(request: Request) {
|
||||
}
|
||||
|
||||
const byProvider = providerRows.map((row) => ({
|
||||
provider: row.provider,
|
||||
provider: getProviderById(toStringValue(row.provider))?.name ?? toStringValue(row.provider),
|
||||
requests: Number(row.requests),
|
||||
promptTokens: Number(row.promptTokens),
|
||||
completionTokens: Number(row.completionTokens),
|
||||
@@ -897,16 +898,15 @@ export async function GET(request: Request) {
|
||||
}
|
||||
|
||||
const presetSinceIso = getRangeStartIso(presetRange);
|
||||
const { unifiedSource: presetUnifiedSource, unifiedParams: presetParams } =
|
||||
buildPresetUnifiedSource({
|
||||
sinceIso: presetSinceIso ?? null,
|
||||
untilIso: null,
|
||||
rawCutoffDate,
|
||||
apiKeyWhere,
|
||||
apiKeyParams: apiKeyParamEntries,
|
||||
});
|
||||
const { unifiedSource: pSrc, unifiedParams: pParams } = buildPresetUnifiedSource({
|
||||
sinceIso: presetSinceIso ?? null,
|
||||
untilIso: null,
|
||||
rawCutoffDate,
|
||||
apiKeyWhere,
|
||||
apiKeyParams: apiKeyParamEntries,
|
||||
});
|
||||
|
||||
const presetModelRows = getPresetCostModelRows(presetUnifiedSource, presetParams) as Array<Record<string, unknown>>;
|
||||
const presetModelRows = getPresetCostModelRows(pSrc, pParams) as UsageRows;
|
||||
|
||||
let presetTotalCost = 0;
|
||||
for (const row of presetModelRows) {
|
||||
|
||||
82
tests/unit/usage-analytics-model-dedup-7535.test.ts
Normal file
82
tests/unit/usage-analytics-model-dedup-7535.test.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
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-model-dedup-")
|
||||
);
|
||||
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-model-dedup-secret";
|
||||
|
||||
const core = await import("../../src/lib/db/core.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 { normalizeModelName } = await import("../../src/lib/usage/costCalculator.ts");
|
||||
|
||||
function makeRequest(url: string) {
|
||||
return new Request(url, { method: "GET" });
|
||||
}
|
||||
|
||||
test.beforeEach(() => {
|
||||
core.resetDbInstance();
|
||||
apiKeysDb.resetApiKeyState();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
usageHistory.clearPendingRequests();
|
||||
});
|
||||
|
||||
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("#7535: byModel must not list the same logical model twice under one raw/one prefixed id", async () => {
|
||||
const db = core.getDbInstance();
|
||||
const now = new Date();
|
||||
|
||||
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("zai", "glm-5.2", "test-conn", "test-key", "Primary Key", 100, 50, 1, 200, now.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(
|
||||
"zai",
|
||||
"z-ai/glm-5.2",
|
||||
"test-conn",
|
||||
"test-key",
|
||||
"Primary Key",
|
||||
80,
|
||||
40,
|
||||
1,
|
||||
150,
|
||||
new Date(now.getTime() - 60_000).toISOString()
|
||||
);
|
||||
|
||||
assert.equal(normalizeModelName("glm-5.2"), "glm-5.2");
|
||||
assert.equal(normalizeModelName("z-ai/glm-5.2"), "glm-5.2");
|
||||
|
||||
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const glmEntries = body.byModel.filter((row: { model: string }) => row.model === "glm-5.2");
|
||||
assert.equal(
|
||||
glmEntries.length,
|
||||
1,
|
||||
`expected exactly one "glm-5.2" row in byModel, got ${glmEntries.length}: ${JSON.stringify(glmEntries)} (#7535)`
|
||||
);
|
||||
assert.equal(glmEntries[0].requests, 2, "the two raw spellings should merge into one aggregated row");
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
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-provider-name-")
|
||||
);
|
||||
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-provider-name-secret";
|
||||
|
||||
const core = await import("../../src/lib/db/core.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 providers = await import("../../src/shared/constants/providers.ts");
|
||||
|
||||
function makeRequest(url: string) {
|
||||
return new Request(url, { method: "GET" });
|
||||
}
|
||||
|
||||
test.beforeEach(() => {
|
||||
core.resetDbInstance();
|
||||
apiKeysDb.resetApiKeyState();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
usageHistory.clearPendingRequests();
|
||||
});
|
||||
|
||||
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("#7534: byProvider exposes the configured display name, not the raw internal provider id", async () => {
|
||||
const db = core.getDbInstance();
|
||||
const now = new Date().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("codex", "gpt-5.5", "test-conn", "test-key", "Primary Key", 100, 50, 1, 200, now);
|
||||
|
||||
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
|
||||
const body = await response.json();
|
||||
|
||||
const expectedDisplayName = providers.getProviderById("codex")?.name;
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(
|
||||
body.byProvider[0].provider,
|
||||
expectedDisplayName,
|
||||
`expected byProvider[0].provider to be the display name "${expectedDisplayName}", ` +
|
||||
`but got "${body.byProvider[0].provider}" (#7534)`
|
||||
);
|
||||
});
|
||||
|
||||
test("#7534: byProvider falls back to the raw id for providers not in the static registry", async () => {
|
||||
const db = core.getDbInstance();
|
||||
const now = new Date().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-compatible-custom",
|
||||
"some-model",
|
||||
"test-conn",
|
||||
"test-key",
|
||||
"Primary Key",
|
||||
100,
|
||||
50,
|
||||
1,
|
||||
200,
|
||||
now
|
||||
);
|
||||
|
||||
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.byProvider[0].provider, "openai-compatible-custom");
|
||||
});
|
||||
@@ -144,7 +144,7 @@ test("GET /api/usage/analytics resolves Codex GPT-5.5 pricing through provider a
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assertClose(body.summary.totalCost, 0.02);
|
||||
assert.equal(body.byProvider[0].provider, "codex");
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user