|
- {isModelCostUnknown(model) ? "" : formatPercent(model.costShare)}
+ {share === null ? "" : formatPercent(share)}
|
{formatTokens(model.totalTokens)} |
diff --git a/apps/web/src/components/usage/usageBreakdown.test.ts b/apps/web/src/components/usage/usageBreakdown.test.ts
index ff0a611c7d45..a835ad4f953a 100644
--- a/apps/web/src/components/usage/usageBreakdown.test.ts
+++ b/apps/web/src/components/usage/usageBreakdown.test.ts
@@ -1,7 +1,12 @@
import type { ModelTotals } from "@t3tools/shared/usageMerge";
import { describe, expect, it } from "vite-plus/test";
-import { cacheHitRate, costPerMillionTokens, sortModelsByTokens } from "./usageBreakdown";
+import {
+ cacheHitRate,
+ costPerMillionTokens,
+ modelShare,
+ sortModelsByTokens,
+} from "./usageBreakdown";
const model = (
name: string,
@@ -24,6 +29,7 @@ const model = (
unpricedRecords: 0,
unpricedTokens: 0,
costShare: 0,
+ tokenShare: 0,
...overrides,
});
@@ -44,6 +50,26 @@ describe("sortModelsByTokens", () => {
});
});
+describe("modelShare", () => {
+ it("follows the selected metric", () => {
+ const priced = model("priced", 100, 9, { costShare: 0.9, tokenShare: 0.25 });
+
+ expect(modelShare(priced, "cost")).toBe(0.9);
+ expect(modelShare(priced, "tokens")).toBe(0.25);
+ });
+
+ it("has no cost share for an unknown cost but keeps its token share", () => {
+ const unpriced = model("unpriced", 300, 0, {
+ unpricedRecords: 1,
+ unpricedTokens: 300,
+ tokenShare: 0.75,
+ });
+
+ expect(modelShare(unpriced, "cost")).toBeNull();
+ expect(modelShare(unpriced, "tokens")).toBe(0.75);
+ });
+});
+
describe("model rates", () => {
it("counts cache writes as misses and leaves unpriced tokens out of $/1M", () => {
const mixed = model("mixed", 4_000_000, 6, {
diff --git a/apps/web/src/components/usage/usageBreakdown.ts b/apps/web/src/components/usage/usageBreakdown.ts
index 0249eb26aff2..ab449af9fbbb 100644
--- a/apps/web/src/components/usage/usageBreakdown.ts
+++ b/apps/web/src/components/usage/usageBreakdown.ts
@@ -14,6 +14,15 @@ export function sortModelsByTokens(models: readonly ModelTotals[]) {
);
}
+/**
+ * A model's share of the selected metric, or `null` for a cost share of an
+ * unknown cost. An unpriced model still has a real token share.
+ */
+export function modelShare(model: ModelTotals, metric: "cost" | "tokens"): number | null {
+ if (metric === "tokens") return model.tokenShare;
+ return isModelCostUnknown(model) ? null : model.costShare;
+}
+
/**
* Share of a model's input read from cache, or `null` without input. Cache
* writes count as misses: that input was processed in full.
diff --git a/packages/shared/src/usageMerge.test.ts b/packages/shared/src/usageMerge.test.ts
index eb50f2fb19b3..8208e5b7ed1c 100644
--- a/packages/shared/src/usageMerge.test.ts
+++ b/packages/shared/src/usageMerge.test.ts
@@ -514,6 +514,44 @@ describe("mergeUsage", () => {
expect(merged.costQuality.cacheSavingsUsd).toBe(4);
});
+ it("derives model token shares independently of their cost shares", () => {
+ const merged = mergeUsage(
+ [
+ environment(
+ "env-a",
+ summary(
+ [
+ bucket({ costUsd: 90 }),
+ bucket({
+ provider: "codex",
+ model: "gpt-5.6-sol",
+ costUsd: 10,
+ totals: {
+ uncachedInputTokens: 3 * 1160,
+ cachedInputTokens: 0,
+ cacheCreationTokens: 0,
+ outputTokens: 0,
+ reasoningTokens: 0,
+ },
+ }),
+ ],
+ [
+ { provider: "claude", hostId: "mac", homePath: "/a/.claude" },
+ { provider: "codex", hostId: "mac", homePath: "/a/.codex" },
+ ],
+ ),
+ ),
+ ],
+ USAGE_CONTRACT_VERSION,
+ );
+
+ const byModel = Object.fromEntries(merged.models.map((model) => [model.model, model]));
+ expect(byModel["claude-fable-5"]?.costShare).toBeCloseTo(0.9, 5);
+ expect(byModel["claude-fable-5"]?.tokenShare).toBeCloseTo(0.25, 5);
+ expect(byModel["gpt-5.6-sol"]?.costShare).toBeCloseTo(0.1, 5);
+ expect(byModel["gpt-5.6-sol"]?.tokenShare).toBeCloseTo(0.75, 5);
+ });
+
it("marks a model with no known rates as unpriced rather than free", () => {
const merged = mergeUsage(
[
diff --git a/packages/shared/src/usageMerge.ts b/packages/shared/src/usageMerge.ts
index 18c2c9f11c49..07718c099bef 100644
--- a/packages/shared/src/usageMerge.ts
+++ b/packages/shared/src/usageMerge.ts
@@ -51,6 +51,7 @@ export interface ModelTotals {
*/
readonly unpricedTokens: number;
readonly costShare: number;
+ readonly tokenShare: number;
}
/**
@@ -583,6 +584,7 @@ export function mergeUsage(
unpricedRecords: totals.unpricedRecords,
unpricedTokens: totals.unpricedTokens,
costShare: costUsd === 0 ? 0 : totals.costUsd / costUsd,
+ tokenShare: totalTokens === 0 ? 0 : totals.totalTokens / totalTokens,
}))
.sort((a, b) => b.costUsd - a.costUsd || b.totalTokens - a.totalTokens);