Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs-site/src/content/docs/guides/web-dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,9 @@ For a custom usage interval, the server must confirm the exact requested start a
If an older running proxy does not support those bounds, the dashboard and CLI reject its report;
upgrade and restart that proxy before retrying. Resetting a manual model price affects only that
model, preserving other rates saved independently.
They are not billing receipts or evidence of an actual charge; subscription usage or provider credits
The **Usage** Models and Providers tables show the estimated priced portion for each row; any
requests without a matching price or usable usage are counted as excluded beside that amount.
These are not billing receipts or evidence of an actual charge; subscription usage or provider credits
may apply instead.

Provider model rows may include **unresolved requested model usage**: the saved route sent the
Expand Down
Binary file added docs/pr-assets/usage-api-list-price.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,7 @@ export const de: Record<TKey, string> = {
"usage.cost.total": "API-Listenpreis-Äquivalent (dieser Zeitraum)",
"usage.cost.disclaimer": "Kein Abrechnungsbeleg. Stattdessen können Abonnementnutzung oder Anbieter-Guthaben gelten.",
"usage.cost.unpricedNote": "{count} Anfragen ohne Preis oder Nutzung ausgeschlossen",
"usage.cost.excluded": "({count} Anfragen ausgeschlossen)",
"logs.detail.section.basic": "Grundinformationen",
"logs.detail.route.section": "Route-Entscheidung",
"logs.detail.route.kind": "Route-Typ",
Expand Down Expand Up @@ -896,6 +897,7 @@ export const de: Record<TKey, string> = {
"usage.col.measured": "Gemessen",
"usage.col.reported": "Gemeldet",
"usage.col.tokens": "Tokens",
"usage.col.apiListPrice": "API-Listenpreis",
"usage.col.share": "Anteil",
"usage.heatmap.less": "Weniger",
"usage.heatmap.more": "Mehr",
Expand Down
2 changes: 2 additions & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,7 @@ export const en = {
"usage.cost.total": "API list-price equivalent (this range)",
"usage.cost.disclaimer": "Not a billing receipt. Subscription usage or provider credits may apply instead.",
"usage.cost.unpricedNote": "{count} requests excluded (no price or usage)",
"usage.cost.excluded": "({count} requests excluded)",
"logs.detail.section.basic": "Basic information",
"logs.detail.route.section": "Route decision",
"logs.detail.route.kind": "Route kind",
Expand Down Expand Up @@ -947,6 +948,7 @@ export const en = {
"usage.col.measured": "Measured",
"usage.col.reported": "Reported",
"usage.col.tokens": "Tokens",
"usage.col.apiListPrice": "API list-price",
"usage.col.share": "Share",
"usage.heatmap.less": "Less",
"usage.heatmap.more": "More",
Expand Down
2 changes: 2 additions & 0 deletions gui/src/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,7 @@ export const fr: Record<TKey, string> = {
"usage.cost.total": "Équivalent au tarif catalogue de l’API (cette période)",
"usage.cost.disclaimer": "Ceci n’est pas un reçu de facturation. L’utilisation d’un abonnement ou les crédits du fournisseur peuvent s’appliquer à la place.",
"usage.cost.unpricedNote": "{count} requêtes exclues (aucun tarif ni donnée d’utilisation)",
"usage.cost.excluded": "({count} requêtes exclues)",
"logs.detail.section.basic": "Informations générales",
"logs.detail.route.section": "Décision de routage",
"logs.detail.route.kind": "Type de route",
Expand Down Expand Up @@ -924,6 +925,7 @@ export const fr: Record<TKey, string> = {
"usage.col.measured": "Mesurées",
"usage.col.reported": "Communiquées",
"usage.col.tokens": "Jetons",
"usage.col.apiListPrice": "Tarif catalogue API",
"usage.col.share": "Part",
"usage.heatmap.less": "Moins",
"usage.heatmap.more": "Plus",
Expand Down
2 changes: 2 additions & 0 deletions gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,7 @@ export const ja: Record<TKey, string> = {
"usage.cost.total": "API 定価相当額(この期間)",
"usage.cost.disclaimer": "請求明細ではありません。サブスクリプション利用量やプロバイダークレジットが代わりに適用される場合があります。",
"usage.cost.unpricedNote": "{count} 件のリクエストを除外(価格または使用量なし)",
"usage.cost.excluded": "({count} 件のリクエストを除外)",
"logs.detail.section.basic": "基本情報",
"logs.detail.route.section": "ルート決定",
"logs.detail.route.kind": "ルート種別",
Expand Down Expand Up @@ -862,6 +863,7 @@ export const ja: Record<TKey, string> = {
"usage.col.measured": "計測",
"usage.col.reported": "報告",
"usage.col.tokens": "トークン",
"usage.col.apiListPrice": "API 定価",
"usage.col.share": "割合",
"usage.heatmap.less": "少ない",
"usage.heatmap.more": "多い",
Expand Down
2 changes: 2 additions & 0 deletions gui/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,7 @@ export const ko: Record<TKey, string> = {
"usage.cost.total": "API 정가 환산치 (이 기간)",
"usage.cost.disclaimer": "결제 영수증이 아닙니다. 구독 사용량 또는 프로바이더 크레딧이 대신 적용될 수 있습니다.",
"usage.cost.unpricedNote": "비용 산정 불가 {count}건 제외",
"usage.cost.excluded": "({count}건 요청 제외)",
"logs.detail.section.basic": "기본 정보",
"logs.detail.route.section": "라우팅 결정",
"logs.detail.route.kind": "라우팅 종류",
Expand Down Expand Up @@ -930,6 +931,7 @@ export const ko: Record<TKey, string> = {
"usage.col.measured": "측정됨",
"usage.col.reported": "측정됨",
"usage.col.tokens": "토큰",
"usage.col.apiListPrice": "API 정가",
"usage.col.share": "비율",
"usage.heatmap.less": "적음",
"usage.heatmap.more": "많음",
Expand Down
2 changes: 2 additions & 0 deletions gui/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,7 @@ export const ru: Record<TKey, string> = {
"usage.cost.total": "Эквивалент стоимости по прайс-листу API (за этот период)",
"usage.cost.disclaimer": "Не является счётом. Расходы могут покрываться подпиской или кредитами провайдера.",
"usage.cost.unpricedNote": "Исключено {count} запросов (нет цены или данных использования)",
"usage.cost.excluded": "({count} запросов исключено)",
"logs.detail.section.basic": "Основная информация",
"logs.detail.route.section": "Решение о маршруте",
"logs.detail.route.kind": "Тип маршрута",
Expand Down Expand Up @@ -917,6 +918,7 @@ export const ru: Record<TKey, string> = {
"usage.col.measured": "Измерено",
"usage.col.reported": "Сообщено",
"usage.col.tokens": "Токены",
"usage.col.apiListPrice": "Прайс-лист API",
"usage.col.share": "Доля",
"usage.heatmap.less": "Меньше",
"usage.heatmap.more": "Больше",
Expand Down
2 changes: 2 additions & 0 deletions gui/src/i18n/tr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,7 @@ export const tr: Record<TKey, string> = {
"usage.cost.total": "API liste fiyatı eşdeğeri",
"usage.cost.disclaimer": "Fatura makbuzu değildir.",
"usage.cost.unpricedNote": "{count} istek hariç tutuldu",
"usage.cost.excluded": "({count} istek hariç tutuldu)",
"logs.detail.section.basic": "Temel bilgiler",
"logs.detail.route.section": "Yönlendirme kararı",
"logs.detail.route.kind": "Yönlendirme türü",
Expand Down Expand Up @@ -936,6 +937,7 @@ export const tr: Record<TKey, string> = {
"usage.col.measured": "Ölçülen",
"usage.col.reported": "Bildirilen",
"usage.col.tokens": "Jetonlar",
"usage.col.apiListPrice": "API liste fiyatı",
"usage.col.share": "Pay",
"usage.heatmap.less": "Daha az",
"usage.heatmap.more": "Daha fazla",
Expand Down
2 changes: 2 additions & 0 deletions gui/src/i18n/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,7 @@ export const zhTW: Record<TKey, string> = {
"usage.cost.total": "API 標價折算(當前範圍)",
"usage.cost.disclaimer": "這不是帳單或扣費憑證。實際可能計入訂閱用量或消耗服務商額度。",
"usage.cost.unpricedNote": "已排除 {count} 個無法計費的請求",
"usage.cost.excluded": "(已排除 {count} 個請求)",
"logs.detail.section.basic": "基本資訊",
"logs.detail.section.performance": "效能",
"logs.detail.section.cost": "API 標價折算",
Expand Down Expand Up @@ -737,6 +738,7 @@ export const zhTW: Record<TKey, string> = {
"usage.col.measured": "已計量",
"usage.col.reported": "已上報",
"usage.col.tokens": "Token 數",
"usage.col.apiListPrice": "API 標價",
"usage.col.share": "佔比",
"usage.heatmap.less": "少",
"usage.heatmap.more": "多",
Expand Down
2 changes: 2 additions & 0 deletions gui/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,7 @@ export const zh: Record<TKey, string> = {
"usage.cost.total": "API 标价折算(当前范围)",
"usage.cost.disclaimer": "这不是账单或扣费凭证。实际可能计入订阅用量或消耗服务商额度。",
"usage.cost.unpricedNote": "已排除 {count} 个无法计费的请求",
"usage.cost.excluded": "(已排除 {count} 个请求)",
"logs.detail.section.basic": "基本信息",
"logs.detail.route.section": "路由决策",
"logs.detail.route.kind": "路由类型",
Expand Down Expand Up @@ -911,6 +912,7 @@ export const zh: Record<TKey, string> = {
"usage.col.measured": "已计量",
"usage.col.reported": "已上报",
"usage.col.tokens": "Token 数",
"usage.col.apiListPrice": "API 标价",
"usage.col.share": "占比",
"usage.heatmap.less": "少",
"usage.heatmap.more": "多",
Expand Down
38 changes: 38 additions & 0 deletions gui/src/pages/Usage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ interface UsageModel {
totalTokens: number;
inputTokens: number;
outputTokens: number;
/** API list-price estimate for the priced portion of this row. */
estimatedCostUsd?: number;
pricedRequests?: number;
/** Requests excluded from the estimate because price or usable usage is unavailable. */
unpricedRequests?: number;
shareRatio: number;
}

Expand All @@ -73,6 +78,11 @@ interface UsageProvider {
reportedRequests: number;
estimatedRequests: number;
totalTokens: number;
/** API list-price estimate for the priced portion of this row. */
estimatedCostUsd?: number;
pricedRequests?: number;
/** Requests excluded from the estimate because price or usable usage is unavailable. */
unpricedRequests?: number;
shareRatio: number;
}

Expand Down Expand Up @@ -105,6 +115,30 @@ function formatPct(ratio: number): string {
return `${Math.round(ratio * 100)}%`;
}

type UsageCostRow = Pick<UsageModel, "estimatedCostUsd" | "pricedRequests" | "unpricedRequests">;

/**
* Newer proxies return the per-row coverage fields even when every request is
* unpriced. Older proxies have none of them, so keep their cells unavailable
* rather than making an unknown amount look like a free request.
*/
function UsageListPrice({ row, locale, t }: { row: UsageCostRow; locale: Locale; t: TFn }) {
const hasPriceData = row.estimatedCostUsd !== undefined
|| row.pricedRequests !== undefined
|| row.unpricedRequests !== undefined;
if (!hasPriceData) return <span className="muted">—</span>;

const excludedRequests = row.unpricedRequests ?? 0;
return (
<>
<span className="mono">{formatUsdEstimate(row.estimatedCostUsd ?? 0, locale)}</span>
{excludedRequests > 0 && (
<span className="muted text-caption"> {t("usage.cost.excluded", { count: excludedRequests })}</span>
)}
</>
);
}

// Stable per-model bar color: hash the provider/model id to a hue so the same model keeps its color
// across days and renders. Saturation/lightness are fixed for a cohesive palette on the dark chart.
function modelColor(model: string, provider: string): string {
Expand Down Expand Up @@ -550,6 +584,7 @@ function UsageModelsTable({
<th className="num">{t("usage.col.requests")}</th>
<th className="num">{t("usage.col.measured")}</th>
<th className="num">{t("usage.col.tokens")}</th>
<th className="num" title={t("usage.cost.disclaimer")}>{t("usage.col.apiListPrice")}</th>
<th>{t("usage.col.share")}</th>
</tr>
</thead>
Expand All @@ -561,6 +596,7 @@ function UsageModelsTable({
<td className="num">{model.requests}</td>
<td className="num">{model.measuredRequests}</td>
<td className="num mono">{formatTokens(model.totalTokens, locale)}</td>
<td className="num"><UsageListPrice row={model} locale={locale} t={t} /></td>
<td><div className="usage-bar"><div className="usage-bar-fill" style={{ width: `${Math.round(model.shareRatio * 100)}%` }} /></div></td>
</tr>
))}
Expand Down Expand Up @@ -611,6 +647,7 @@ function UsageProvidersTable({
<th className="num">{t("usage.col.requests")}</th>
<th className="num">{t("usage.col.measured")}</th>
<th className="num">{t("usage.col.tokens")}</th>
<th className="num" title={t("usage.cost.disclaimer")}>{t("usage.col.apiListPrice")}</th>
<th>{t("usage.col.share")}</th>
</tr>
</thead>
Expand All @@ -621,6 +658,7 @@ function UsageProvidersTable({
<td className="num">{provider.requests}</td>
<td className="num">{provider.measuredRequests}</td>
<td className="num mono">{formatTokens(provider.totalTokens, locale)}</td>
<td className="num"><UsageListPrice row={provider} locale={locale} t={t} /></td>
<td><div className="usage-bar"><div className="usage-bar-fill" style={{ width: `${Math.round(provider.shareRatio * 100)}%` }} /></div></td>
</tr>
))}
Expand Down
88 changes: 88 additions & 0 deletions gui/tests/usage-layout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,94 @@ test("usage workspace i18n keys exist in every locale", async () => {
expect(dict).toContain('"usage.scope.machine":');
expect(dict).toContain('"usage.scope.hub":');
expect(dict).toContain('"usage.hubOffline":');
expect(dict).toContain('"usage.col.apiListPrice":');
expect(dict).toContain('"usage.cost.excluded":');
}
});

test("Usage breakdown tables show priced totals and excluded requests without calling them free", async () => {
const globalKeys = ["document", "window", "navigator", "localStorage", "IS_REACT_ACT_ENVIRONMENT", "ResizeObserver"] as const;
const previous = Object.fromEntries(globalKeys.map(key => [key, Reflect.get(globalThis, key)]));
const originalFetch = globalThis.fetch;
const testWindow = new Window({ url: "http://localhost/" });
Object.defineProperties(globalThis, {
document: { configurable: true, value: testWindow.document },
window: { configurable: true, value: testWindow },
navigator: { configurable: true, value: testWindow.navigator },
localStorage: { configurable: true, value: testWindow.localStorage },
});
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
class ResizeObserverStub {
constructor(_callback: ResizeObserverCallback) {}
observe() {}
unobserve() {}
disconnect() {}
}
Object.defineProperty(globalThis, "ResizeObserver", { configurable: true, value: ResizeObserverStub });
Object.defineProperty(testWindow, "ResizeObserver", { configurable: true, value: ResizeObserverStub });
clearClientResourceStoresForTests();
globalThis.fetch = (async () => Response.json({
range: "30d",
surface: "all",
since: null,
generatedAt: Date.now(),
summary: {
requests: 4,
measuredRequests: 4,
reportedRequests: 4,
unreportedRequests: 0,
unsupportedRequests: 0,
estimatedRequests: 0,
inputTokens: 100,
outputTokens: 50,
cachedInputTokens: 0,
reasoningOutputTokens: 0,
totalTokens: 150,
coverageRatio: 1,
},
days: [],
models: [
{ provider: "priced", model: "priced-model", requests: 1, measuredRequests: 1, reportedRequests: 1, estimatedRequests: 0, totalTokens: 100, inputTokens: 50, outputTokens: 50, estimatedCostUsd: 1.25, pricedRequests: 1, unpricedRequests: 0, shareRatio: 2 / 3 },
{ provider: "unpriced", model: "unpriced-model", requests: 3, measuredRequests: 3, reportedRequests: 3, estimatedRequests: 0, totalTokens: 50, inputTokens: 50, outputTokens: 0, pricedRequests: 0, unpricedRequests: 3, shareRatio: 1 / 3 },
],
providers: [
{ provider: "priced", requests: 1, measuredRequests: 1, reportedRequests: 1, estimatedRequests: 0, totalTokens: 100, estimatedCostUsd: 1.25, pricedRequests: 1, unpricedRequests: 0, shareRatio: 2 / 3 },
{ provider: "unpriced", requests: 3, measuredRequests: 3, reportedRequests: 3, estimatedRequests: 0, totalTokens: 50, pricedRequests: 0, unpricedRequests: 3, shareRatio: 1 / 3 },
],
historyTruncated: false,
truncatedPrefixBytes: 0,
entriesTruncated: false,
entriesDropped: 0,
})) as typeof fetch;

const container = document.createElement("div");
document.body.append(container);
const { createRoot } = await import("react-dom/client");
const root = createRoot(container);
try {
await act(async () => {
root.render(createElement(LanguageProvider, null, createElement(Usage, { apiBase: "http://usage-list-price-test" })));
});
const deadline = Date.now() + 1_000;
while (!(container.textContent ?? "").includes("(3 requests excluded)")) {
if (Date.now() >= deadline) throw new Error("Usage list-price cells did not render");
await act(async () => {
await new Promise<void>(resolve => testWindow.setTimeout(resolve, 10));
});
}

expect(container.textContent).toContain("~$1.2500");
expect(container.textContent).toContain("~$0.0000 (3 requests excluded)");
expect([...container.querySelectorAll("th")].filter(cell => cell.textContent === "API list-price")).toHaveLength(2);
} finally {
await act(async () => { root.unmount(); });
container.remove();
globalThis.fetch = originalFetch;
clearClientResourceStoresForTests();
testWindow.close();
for (const key of globalKeys) {
Object.defineProperty(globalThis, key, { configurable: true, value: previous[key] });
}
}
});

Expand Down
Loading