diff --git a/src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx b/src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx
index a0a983ce44..d02a86b9d4 100644
--- a/src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx
+++ b/src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx
@@ -111,6 +111,27 @@ function createCurrencyFormatter(locale: string) {
});
}
+function formatCurrencyCost(locale: string, value: number): string {
+ const numericValue = Number(value || 0);
+ if (!Number.isFinite(numericValue) || numericValue === 0) {
+ return new Intl.NumberFormat(locale, {
+ style: "currency",
+ currency: "USD",
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ }).format(0);
+ }
+
+ const absValue = Math.abs(numericValue);
+ const fractionDigits = absValue < 0.01 ? 6 : absValue < 1 ? 4 : 2;
+ return new Intl.NumberFormat(locale, {
+ style: "currency",
+ currency: "USD",
+ minimumFractionDigits: fractionDigits,
+ maximumFractionDigits: fractionDigits,
+ }).format(numericValue);
+}
+
function csvCell(value: string | number): string {
const text = String(value);
return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
@@ -398,25 +419,25 @@ export default function CostOverviewTab() {
@@ -438,7 +459,7 @@ export default function CostOverviewTab() {
/>
diff --git a/src/app/api/usage/analytics/route.ts b/src/app/api/usage/analytics/route.ts
index e89d9351e2..d284e1c876 100644
--- a/src/app/api/usage/analytics/route.ts
+++ b/src/app/api/usage/analytics/route.ts
@@ -64,6 +64,44 @@ function findKeyInsensitive(obj: Record | undefined | null, key: st
return obj[key.toLowerCase()];
}
+function uniqueValues(values: Array): string[] {
+ const seen = new Set();
+ const result: string[] = [];
+ for (const value of values) {
+ const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
+ if (!normalized || seen.has(normalized)) continue;
+ seen.add(normalized);
+ result.push(normalized);
+ }
+ return result;
+}
+
+function stripCodexEffortSuffix(model: string): string {
+ return model.replace(/-(?:xhigh|high|medium|low|none)$/i, "");
+}
+
+function getPricingModelCandidates(
+ model: string,
+ normalizeModelName: (model: string) => string
+): string[] {
+ const normalizedModel = normalizeModelName(model);
+ const lowerModel = model.toLowerCase();
+ const lowerNormalized = normalizedModel.toLowerCase();
+ const hyphenModel = lowerModel.replace(/\./g, "-");
+ const hyphenNormalized = lowerNormalized.replace(/\./g, "-");
+ const effortBaseModel = stripCodexEffortSuffix(lowerNormalized);
+
+ return uniqueValues([
+ lowerModel,
+ lowerNormalized,
+ hyphenModel,
+ hyphenNormalized,
+ effortBaseModel,
+ effortBaseModel.replace(/\./g, "-"),
+ lowerNormalized === "codex-auto-review" ? "gpt-5.5" : null,
+ ]);
+}
+
function resolveModelPricing(
pricingByProvider: PricingByProvider,
providerAliasMap: Record,
@@ -91,22 +129,15 @@ function resolveModelPricing(
if (pLower === "antigravity") providerPricing = findKeyInsensitive(pricingByProvider, "ag");
}
- const normalizedModel = normalizeModelName(model).toLowerCase();
- const shortModel = normalizedModel; // normalizeModelName behaves exactly like shortModelName
- const hyphenModel = model.toLowerCase().replace(/\./g, "-");
- const hyphenNormalized = normalizedModel.replace(/\./g, "-");
- const lowerModel = model.toLowerCase();
+ const modelCandidates = getPricingModelCandidates(model, normalizeModelName);
const tryFind = (prov: Record | null | undefined) => {
if (!prov || typeof prov !== "object") return null;
- return (
- findKeyInsensitive(prov as Record, lowerModel) ||
- findKeyInsensitive(prov as Record, normalizedModel) ||
- findKeyInsensitive(prov as Record, shortModel) ||
- findKeyInsensitive(prov as Record, hyphenModel) ||
- findKeyInsensitive(prov as Record, hyphenNormalized) ||
- null
- );
+ for (const candidate of modelCandidates) {
+ const pricing = findKeyInsensitive(prov as Record, candidate);
+ if (pricing) return pricing;
+ }
+ return null;
};
let pricing = providerPricing ? tryFind(providerPricing) : null;
@@ -478,10 +509,20 @@ export async function GET(request: Request) {
COUNT(*) as total,
SUM(CASE WHEN requested_model IS NOT NULL AND requested_model != '' THEN 1 ELSE 0 END) as with_requested,
SUM(CASE
- WHEN requested_model IS NOT NULL
+ WHEN (combo_name IS NULL OR combo_name = '')
+ AND requested_model IS NOT NULL
AND requested_model != ''
AND model IS NOT NULL
- AND requested_model != model
+ AND model != ''
+ THEN 1 ELSE 0 END
+ ) as fallback_eligible,
+ SUM(CASE
+ WHEN (combo_name IS NULL OR combo_name = '')
+ AND requested_model IS NOT NULL
+ AND requested_model != ''
+ AND model IS NOT NULL
+ AND model != ''
+ AND LOWER(requested_model) != LOWER(model)
THEN 1 ELSE 0 END
) as fallbacks
FROM call_logs
@@ -515,10 +556,11 @@ export async function GET(request: Request) {
lastRequest: summaryRow?.lastRequest || "",
fallbackCount: Number(fallbackRow?.fallbacks || 0),
fallbackRatePct:
- Number(fallbackRow?.with_requested || 0) > 0
+ Number(fallbackRow?.fallback_eligible || 0) > 0
? Number(
(
- (Number(fallbackRow?.fallbacks || 0) / Number(fallbackRow?.with_requested || 1)) *
+ (Number(fallbackRow?.fallbacks || 0) /
+ Number(fallbackRow?.fallback_eligible || 1)) *
100
).toFixed(2)
)
diff --git a/src/shared/constants/pricing.ts b/src/shared/constants/pricing.ts
index bbeaeeeeab..66baacfc2e 100644
--- a/src/shared/constants/pricing.ts
+++ b/src/shared/constants/pricing.ts
@@ -175,6 +175,7 @@ export const DEFAULT_PRICING = {
// OpenAI Codex (cx)
cx: {
+ "codex-auto-review": GPT_5_5_PRICING,
// GPT 5.5
"gpt-5.5": GPT_5_5_PRICING,
"gpt5.5": GPT_5_5_PRICING,
diff --git a/tests/unit/usage-analytics-route.test.ts b/tests/unit/usage-analytics-route.test.ts
index 97127672d7..66935d5819 100644
--- a/tests/unit/usage-analytics-route.test.ts
+++ b/tests/unit/usage-analytics-route.test.ts
@@ -132,6 +132,74 @@ test("GET /api/usage/analytics includes byModel array with cost calculations", a
assert.ok(gptEntry.cost > 0);
});
+test("GET /api/usage/analytics resolves Codex GPT-5.5 pricing through provider aliases", async () => {
+ const db = core.getDbInstance();
+ db.prepare(
+ `INSERT INTO usage_history (provider, model, connection_id, tokens_input, tokens_output, success, latency_ms, timestamp)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
+ ).run("codex", "gpt-5.5", "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();
+
+ assert.equal(response.status, 200);
+ assertClose(body.summary.totalCost, 0.02);
+ assert.equal(body.byProvider[0].provider, "codex");
+ assertClose(body.byProvider[0].cost, 0.02);
+ assert.equal(body.byModel[0].model, "gpt-5.5");
+ assertClose(body.byModel[0].cost, 0.02);
+});
+
+test("GET /api/usage/analytics maps Codex auto-review usage to GPT-5.5 pricing", async () => {
+ const db = core.getDbInstance();
+ 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()
+ );
+
+ const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
+ const body = await response.json();
+
+ assert.equal(response.status, 200);
+ assertClose(body.summary.totalCost, 0.02);
+ assert.equal(body.byModel[0].model, "codex-auto-review");
+ assertClose(body.byModel[0].cost, 0.02);
+});
+
+test("GET /api/usage/analytics ignores normal combo routing in fallback statistics", async () => {
+ const db = core.getDbInstance();
+ const timestamp = new Date().toISOString();
+ db.prepare(
+ `INSERT INTO usage_history (provider, model, connection_id, tokens_input, tokens_output, success, latency_ms, timestamp)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
+ ).run("codex", "gpt-5.5", "codex-conn", 1000, 500, 1, 250, timestamp);
+ db.prepare(
+ `INSERT INTO call_logs (id, provider, model, requested_model, combo_name, connection_id, timestamp)
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
+ ).run("combo-call", "codex", "gpt-5.5", "combo/dev", "dev", "codex-conn", timestamp);
+ db.prepare(
+ `INSERT INTO call_logs (id, provider, model, requested_model, connection_id, timestamp)
+ VALUES (?, ?, ?, ?, ?, ?)`
+ ).run("same-model-call", "codex", "GPT-5.5", "gpt-5.5", "codex-conn", timestamp);
+
+ 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.fallbackCount, 0);
+ assert.equal(body.summary.fallbackRatePct, 0);
+ assert.equal(body.summary.requestedModelCoveragePct, 100);
+});
+
test("GET /api/usage/analytics filters by range parameter", async () => {
await seedAnalyticsData();