Skip to content
Merged
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
1 change: 1 addition & 0 deletions dashboard/src/content/copy.csv
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ dashboard.projects.limit_top_6,ui,ProjectUsagePanel,ProjectUsagePanel,limit_top_
dashboard.projects.limit_top_10,ui,ProjectUsagePanel,ProjectUsagePanel,limit_top_10,TOP 10,,active
dashboard.projects.empty,ui,ProjectUsagePanel,ProjectUsagePanel,empty,No public repositories,,active
dashboard.projects.unattributed,ui,ProjectUsagePanel,ProjectUsagePanel,unattributed,Not counted per repo:,Prefix for the list of sources with usage but no project attribution,active
dashboard.projects.unpriced,ui,ProjectUsagePanel,ProjectUsagePanel,unpriced,no model recorded,Shown instead of a cost when every row for this repo predates per-model project attribution,active
dashboard.widgets.title,dashboard,DashboardPage,WidgetOnboardingCard,title,Desktop Widgets,,active
dashboard.widgets.hint,dashboard,DashboardPage,WidgetOnboardingCard,hint,"Right-click your desktop → Edit Widgets → search ""TokenTracker""",,active
dashboard.widgets.dismiss_aria,dashboard,DashboardPage,WidgetOnboardingCard,dismiss_aria,Dismiss widget intro,,active
Expand Down
24 changes: 22 additions & 2 deletions dashboard/src/ui/dashboard/components/DataDetails.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,19 @@ export function DataDetails({
const n = Number(raw);
const tokenLabel = Number.isFinite(n) ? n.toLocaleString() : "—";
const projectLabel = entry?.project_key || ref.split("/").pop() || "—";
// Cost is per-model, and project rows only started carrying a model
// recently. Rows written before that price at 0 and are counted as
// unattributed rather than folded in — so a repo that is entirely
// pre-model shows no cost line at all instead of a confident $0.
const cost = Number(entry?.total_cost_usd);
const unpriced = Number(entry?.unattributed_tokens);
const hasCost = Number.isFinite(cost) && cost > 0;
const hasUnpriced = Number.isFinite(unpriced) && unpriced > 0;
const costLabel = hasCost
? `$${cost < 0.01 ? cost.toFixed(4) : cost.toFixed(2)}${hasUnpriced ? "+" : ""}`
: hasUnpriced
? copy("dashboard.projects.unpriced")
: "";
return (
<div
key={key}
Expand All @@ -162,8 +175,15 @@ export function DataDetails({
{projectLabel}
</div>
</div>
<div className="oai-text-body-sm font-medium text-oai-black dark:text-oai-white tabular-nums">
{tokenLabel}
<div className="text-right">
<div className="oai-text-body-sm font-medium text-oai-black dark:text-oai-white tabular-nums">
{tokenLabel}
</div>
{costLabel ? (
<div className="oai-text-caption text-oai-gray-500 dark:text-oai-gray-400 tabular-nums">
{costLabel}
</div>
) : null}
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,49 @@ describe("DataDetails — sources with no per-repo attribution", () => {
expect(screen.queryByText("dashboard.projects.unattributed")).toBeNull();
});
});

describe("DataDetails — per-repo cost", () => {
function projectsTab(entry) {
renderDetails({ projectEntries: [entry] });
fireEvent.click(screen.getByText("Project Usage"));
}

it("shows the cost when the rows carry a model", () => {
projectsTab({
project_key: "acme/api",
total_tokens: "1000000",
total_cost_usd: "3.000000",
unattributed_tokens: "0",
});
expect(screen.getByText("$3.00")).toBeTruthy();
});

it("marks a repo whose rows all predate per-model attribution", () => {
// Pricing those at zero and showing "$0.00" would read as "this cost
// nothing" rather than "we recorded this before we recorded models".
projectsTab({
project_key: "acme/legacy",
total_tokens: "5000",
total_cost_usd: "0.000000",
unattributed_tokens: "5000",
});
expect(screen.queryByText("$0.00")).toBeNull();
expect(screen.getByText("dashboard.projects.unpriced")).toBeTruthy();
});

it("flags a partly-priced repo rather than implying the figure is complete", () => {
projectsTab({
project_key: "acme/mixed",
total_tokens: "1000700",
total_cost_usd: "3.000000",
unattributed_tokens: "700",
});
expect(screen.getByText("$3.00+")).toBeTruthy();
});

it("shows no cost line at all when the server sent none", () => {
// Older server, newer dashboard: absent is not zero.
projectsTab({ project_key: "acme/api", total_tokens: "1000" });
expect(screen.queryByText(/^\$/)).toBeNull();
});
});
64 changes: 32 additions & 32 deletions openwiki-facts/source-facts.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,87 +44,87 @@
"methods": [
"POST"
],
"evidence": "src/lib/local-api.js:1139",
"evidence": "src/lib/local-api.js:1166",
"mutation": true
},
{
"path": "/functions/tokentracker-wrapped",
"methods": [
"GET"
],
"evidence": "src/lib/local-api.js:1173",
"evidence": "src/lib/local-api.js:1200",
"mutation": false
},
{
"path": "/functions/tokentracker-usage-summary",
"methods": [
"GET"
],
"evidence": "src/lib/local-api.js:1184",
"evidence": "src/lib/local-api.js:1211",
"mutation": false
},
{
"path": "/functions/tokentracker-usage-daily",
"methods": [
"GET"
],
"evidence": "src/lib/local-api.js:1269",
"evidence": "src/lib/local-api.js:1296",
"mutation": false
},
{
"path": "/functions/tokentracker-usage-heatmap",
"methods": [
"GET"
],
"evidence": "src/lib/local-api.js:1280",
"evidence": "src/lib/local-api.js:1307",
"mutation": false
},
{
"path": "/functions/tokentracker-usage-model-breakdown",
"methods": [
"GET"
],
"evidence": "src/lib/local-api.js:1348",
"evidence": "src/lib/local-api.js:1375",
"mutation": false
},
{
"path": "/functions/tokentracker-usage-category-breakdown",
"methods": [
"GET"
],
"evidence": "src/lib/local-api.js:1427",
"evidence": "src/lib/local-api.js:1454",
"mutation": false
},
{
"path": "/functions/tokentracker-project-usage-summary",
"methods": [
"GET"
],
"evidence": "src/lib/local-api.js:1473",
"evidence": "src/lib/local-api.js:1500",
"mutation": false
},
{
"path": "/functions/tokentracker-user-status",
"methods": [
"GET"
],
"evidence": "src/lib/local-api.js:1486",
"evidence": "src/lib/local-api.js:1513",
"mutation": false
},
{
"path": "/functions/tokentracker-usage-hourly",
"methods": [
"GET"
],
"evidence": "src/lib/local-api.js:1496",
"evidence": "src/lib/local-api.js:1523",
"mutation": false
},
{
"path": "/functions/tokentracker-usage-monthly",
"methods": [
"GET"
],
"evidence": "src/lib/local-api.js:1506",
"evidence": "src/lib/local-api.js:1533",
"mutation": false
},
{
Expand All @@ -133,15 +133,15 @@
"GET",
"POST"
],
"evidence": "src/lib/local-api.js:1540",
"evidence": "src/lib/local-api.js:1567",
"mutation": true
},
{
"path": "/functions/tokentracker-usage-limits",
"methods": [
"GET"
],
"evidence": "src/lib/local-api.js:1690",
"evidence": "src/lib/local-api.js:1717",
"mutation": false
}
]
Expand Down Expand Up @@ -214,79 +214,79 @@
},
{
"name": "parseOpencodeDbIncremental",
"evidence": "src/lib/rollout.js:2556"
"evidence": "src/lib/rollout.js:2611"
},
{
"name": "parseCursorApiIncremental",
"evidence": "src/lib/rollout.js:2737"
"evidence": "src/lib/rollout.js:2795"
},
{
"name": "parseKiroIncremental",
"evidence": "src/lib/rollout.js:2995"
"evidence": "src/lib/rollout.js:3053"
},
{
"name": "parseHermesIncremental",
"evidence": "src/lib/rollout.js:3250"
"evidence": "src/lib/rollout.js:3308"
},
{
"name": "parseKiroCliIncremental",
"evidence": "src/lib/rollout.js:3869"
"evidence": "src/lib/rollout.js:3927"
},
{
"name": "parseKimiIncremental",
"evidence": "src/lib/rollout.js:4499"
"evidence": "src/lib/rollout.js:4557"
},
{
"name": "parseKimiCodeIncremental",
"evidence": "src/lib/rollout.js:4690"
"evidence": "src/lib/rollout.js:4748"
},
{
"name": "parseCodebuddyIncremental",
"evidence": "src/lib/rollout.js:4914"
"evidence": "src/lib/rollout.js:4972"
},
{
"name": "parseRoocodeIncremental",
"evidence": "src/lib/rollout.js:5417"
"evidence": "src/lib/rollout.js:5475"
},
{
"name": "parseZedIncremental",
"evidence": "src/lib/rollout.js:5734"
"evidence": "src/lib/rollout.js:5792"
},
{
"name": "parseGooseIncremental",
"evidence": "src/lib/rollout.js:6051"
"evidence": "src/lib/rollout.js:6109"
},
{
"name": "parseDroidIncremental",
"evidence": "src/lib/rollout.js:6428"
"evidence": "src/lib/rollout.js:6486"
},
{
"name": "parseKilocodeIncremental",
"evidence": "src/lib/rollout.js:6675"
"evidence": "src/lib/rollout.js:6733"
},
{
"name": "parseOmpIncremental",
"evidence": "src/lib/rollout.js:6811"
"evidence": "src/lib/rollout.js:6869"
},
{
"name": "parsePiIncremental",
"evidence": "src/lib/rollout.js:7064"
"evidence": "src/lib/rollout.js:7122"
},
{
"name": "parseCraftIncremental",
"evidence": "src/lib/rollout.js:7348"
"evidence": "src/lib/rollout.js:7406"
},
{
"name": "parseCopilotIncremental",
"evidence": "src/lib/rollout.js:7698"
"evidence": "src/lib/rollout.js:7756"
},
{
"name": "parseGrokBuildIncremental",
"evidence": "src/lib/rollout.js:8164"
"evidence": "src/lib/rollout.js:8222"
},
{
"name": "parseAntigravityIncremental",
"evidence": "src/lib/rollout.js:8336"
"evidence": "src/lib/rollout.js:8394"
}
]
}
Expand Down
33 changes: 30 additions & 3 deletions src/lib/local-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,12 @@ function readProjectQueueData(projectQueuePath) {
for (const line of lines) {
try {
const row = JSON.parse(line);
const key = `${row.project_key || ""}|${row.source || ""}|${row.hour_start || ""}`;
// Model is part of the identity now. A legacy row has no `model` and gets
// the same "unknown" slot the state migration gives its stranded bucket,
// so the two share one key instead of both surviving and double-counting.
const key =
`${row.project_key || ""}|${row.source || ""}` +
`|${row.model || "unknown"}|${row.hour_start || ""}`;
seen.set(key, row);
} catch {
// skip malformed
Expand Down Expand Up @@ -849,6 +854,14 @@ function aggregateBySource(queuePath, inWindow, matchesSource) {
return bySrc;
}

// Per-repo cost, which the data model could not express until project rows
// carried a model. computeRowCost is the same function the rest of the product
// prices with, so a repo total and a model total cannot disagree.
//
// A row with no model prices at 0 and is counted as UNATTRIBUTED rather than
// folded silently into the total — that is the #94 tier doing exactly what it
// was added for. "Recorded before we recorded models" renders honestly instead
// of as a confident $0.
function aggregateByProject(rows) {
const byProject = new Map();
for (const row of rows) {
Expand All @@ -859,11 +872,20 @@ function aggregateByProject(rows) {
project_ref: row.project_ref || key,
total_tokens: 0,
billable_total_tokens: 0,
total_cost_usd: 0,
unattributed_tokens: 0,
});
}
const agg = byProject.get(key);
agg.total_tokens += Number(row.total_tokens || 0);
agg.billable_total_tokens += Number(row.total_tokens || 0);
const tokens = Number(row.total_tokens || 0);
agg.total_tokens += tokens;
agg.billable_total_tokens += tokens;
const model = typeof row.model === "string" ? row.model.trim() : "";
if (!model || model === "unknown") {
agg.unattributed_tokens += tokens;
} else {
agg.total_cost_usd += computeRowCost(row);
}
if (!agg.project_ref && row.project_ref) agg.project_ref = row.project_ref;
}
return byProject;
Expand All @@ -878,6 +900,11 @@ function rankProjectEntries(byKey) {
...e,
total_tokens: String(e.total_tokens),
billable_total_tokens: String(e.billable_total_tokens),
// Strings for the same reason the token counts are: the panel formats
// them, and a float in JSON invites a rounding difference between the two
// sides of the wire.
total_cost_usd: (e.total_cost_usd ?? 0).toFixed(6),
unattributed_tokens: String(e.unattributed_tokens ?? 0),
}));
}

Expand Down
Loading