mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 14:52:09 +03:00
feat: enhance cost formatting and add Codex GPT-5.5 pricing support (#1944)
Integrated into release/v3.7.9
This commit is contained in:
@@ -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() {
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<MetricCard
|
||||
label={t("spendToday")}
|
||||
value={currencyFormatter.format(presetCosts["1d"] || 0)}
|
||||
value={formatCurrencyCost(locale, presetCosts["1d"] || 0)}
|
||||
loading={summaryLoading}
|
||||
color="text-emerald-400"
|
||||
/>
|
||||
<MetricCard
|
||||
label={t("spend7d")}
|
||||
value={currencyFormatter.format(presetCosts["7d"] || 0)}
|
||||
value={formatCurrencyCost(locale, presetCosts["7d"] || 0)}
|
||||
loading={summaryLoading}
|
||||
color="text-sky-400"
|
||||
/>
|
||||
<MetricCard
|
||||
label={t("spend30d")}
|
||||
value={currencyFormatter.format(presetCosts["30d"] || 0)}
|
||||
value={formatCurrencyCost(locale, presetCosts["30d"] || 0)}
|
||||
loading={summaryLoading}
|
||||
color="text-violet-400"
|
||||
/>
|
||||
<MetricCard
|
||||
label={t("selectedWindow")}
|
||||
value={currencyFormatter.format(summary.totalCost || 0)}
|
||||
value={formatCurrencyCost(locale, summary.totalCost || 0)}
|
||||
subValue={selectedRangeLabel}
|
||||
color="text-amber-400"
|
||||
/>
|
||||
@@ -438,7 +459,7 @@ export default function CostOverviewTab() {
|
||||
/>
|
||||
<CompactMetric
|
||||
label={t("avgCostPerRequest")}
|
||||
value={currencyFormatter.format(avgCostPerRequest)}
|
||||
value={formatCurrencyCost(locale, avgCostPerRequest)}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -64,6 +64,44 @@ function findKeyInsensitive(obj: Record<string, any> | undefined | null, key: st
|
||||
return obj[key.toLowerCase()];
|
||||
}
|
||||
|
||||
function uniqueValues(values: Array<string | null | undefined>): string[] {
|
||||
const seen = new Set<string>();
|
||||
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<string, string>,
|
||||
@@ -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<string, unknown> | null | undefined) => {
|
||||
if (!prov || typeof prov !== "object") return null;
|
||||
return (
|
||||
findKeyInsensitive(prov as Record<string, unknown>, lowerModel) ||
|
||||
findKeyInsensitive(prov as Record<string, unknown>, normalizedModel) ||
|
||||
findKeyInsensitive(prov as Record<string, unknown>, shortModel) ||
|
||||
findKeyInsensitive(prov as Record<string, unknown>, hyphenModel) ||
|
||||
findKeyInsensitive(prov as Record<string, unknown>, hyphenNormalized) ||
|
||||
null
|
||||
);
|
||||
for (const candidate of modelCandidates) {
|
||||
const pricing = findKeyInsensitive(prov as Record<string, unknown>, 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)
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user