@shanepadgett/tau-agent 0.39.2 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extensions/cost-report/README.md +32 -0
- package/extensions/cost-report/analyze.ts +423 -0
- package/extensions/cost-report/html.ts +540 -0
- package/extensions/cost-report/index.ts +181 -0
- package/extensions/cost-report/panel.ts +64 -0
- package/extensions/cost-report/range.ts +145 -0
- package/extensions/cost-report/types.ts +93 -0
- package/extensions/tau-help/help.md +4 -0
- package/package.json +2 -2
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import { daysInMonth } from "./range.ts";
|
|
2
|
+
import type { CostReport, DayCost } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
function escapeHtml(value: string): string {
|
|
5
|
+
return value
|
|
6
|
+
.replaceAll("&", "&")
|
|
7
|
+
.replaceAll("<", "<")
|
|
8
|
+
.replaceAll(">", ">")
|
|
9
|
+
.replaceAll('"', """)
|
|
10
|
+
.replaceAll("'", "'");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function money(value: number): string {
|
|
14
|
+
return new Intl.NumberFormat(undefined, {
|
|
15
|
+
style: "currency",
|
|
16
|
+
currency: "USD",
|
|
17
|
+
minimumFractionDigits: 2,
|
|
18
|
+
maximumFractionDigits: 2,
|
|
19
|
+
}).format(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function tokens(value: number): string {
|
|
23
|
+
if (value < 1000) return String(Math.round(value));
|
|
24
|
+
if (value < 10_000) return `${(value / 1000).toFixed(1)}K`;
|
|
25
|
+
if (value < 1_000_000) return `${Math.round(value / 1000)}K`;
|
|
26
|
+
if (value < 10_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
|
|
27
|
+
return `${Math.round(value / 1_000_000)}M`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function percent(part: number, whole: number): string {
|
|
31
|
+
if (whole <= 0) return "0%";
|
|
32
|
+
return `${Math.round((part / whole) * 100)}%`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function heatClass(amount: number): string {
|
|
36
|
+
if (amount <= 0) return "bg-stone-200 dark:bg-stone-800";
|
|
37
|
+
if (amount < 10) return "bg-green-600";
|
|
38
|
+
if (amount < 18) return "bg-lime-500";
|
|
39
|
+
if (amount < 28) return "bg-yellow-400";
|
|
40
|
+
if (amount < 38) return "bg-yellow-500";
|
|
41
|
+
if (amount < 45) return "bg-amber-500";
|
|
42
|
+
if (amount < 50) return "bg-orange-500";
|
|
43
|
+
if (amount < 100) return "bg-red-600";
|
|
44
|
+
return "bg-red-700";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function formatGenerated(ms: number): string {
|
|
48
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
49
|
+
dateStyle: "medium",
|
|
50
|
+
timeStyle: "short",
|
|
51
|
+
}).format(new Date(ms));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function dayButton(day: DayCost, fill: boolean): string {
|
|
55
|
+
const tip = `${escapeHtml(day.label)} · ${money(day.cost)}`;
|
|
56
|
+
const fire = day.cost > 100;
|
|
57
|
+
const weekend = day.weekend ? " is-weekend" : "";
|
|
58
|
+
const sizeClass = fill ? "w-full" : "flex-1";
|
|
59
|
+
const aria = [day.label + " · " + money(day.cost), day.weekend ? "weekend" : "", fire ? "high spend" : ""]
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.join(", ");
|
|
62
|
+
return `<button type="button" class="day-cell relative h-full min-w-0 ${sizeClass} rounded-sm border border-stone-900/10 dark:border-white/10 ${heatClass(day.cost)}${weekend}" role="listitem" aria-label="${escapeHtml(aria)}">${
|
|
63
|
+
fire ? `<span class="day-fire" aria-hidden="true">🔥</span>` : ""
|
|
64
|
+
}<span class="day-tip" aria-hidden="true">${tip}</span></button>`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function shareBar(share: number): string {
|
|
68
|
+
const width = Math.max(0, Math.min(100, Math.round(share * 100)));
|
|
69
|
+
return `<span class="mt-1 block h-1 rounded-sm bg-stone-200 dark:bg-stone-700" aria-hidden="true"><span class="block h-full rounded-sm bg-stone-500 dark:bg-stone-400" style="width:${width}%"></span></span>`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function renderDayRow(days: DayCost[]): string {
|
|
73
|
+
return `<div class="flex h-10 w-full gap-1" role="list" aria-label="Spend by day">${days.map((day) => dayButton(day, false)).join("")}</div>`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function renderYtd(days: DayCost[], year: number): string {
|
|
77
|
+
const byMonth = new Map<number, DayCost[]>();
|
|
78
|
+
for (const day of days) {
|
|
79
|
+
const month = new Date(day.timestampMs).getMonth();
|
|
80
|
+
const list = byMonth.get(month) ?? [];
|
|
81
|
+
list.push(day);
|
|
82
|
+
byMonth.set(month, list);
|
|
83
|
+
}
|
|
84
|
+
const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
85
|
+
const rows: string[] = [];
|
|
86
|
+
const maxMonth = Math.max(0, ...byMonth.keys());
|
|
87
|
+
for (let month = 0; month <= maxMonth; month += 1) {
|
|
88
|
+
const monthDays = byMonth.get(month) ?? [];
|
|
89
|
+
const byDate = new Map(monthDays.map((day) => [new Date(day.timestampMs).getDate(), day]));
|
|
90
|
+
const cells: string[] = [];
|
|
91
|
+
const length = daysInMonth(year, month);
|
|
92
|
+
for (let date = 1; date <= 31; date += 1) {
|
|
93
|
+
if (date <= length) {
|
|
94
|
+
const existing = byDate.get(date);
|
|
95
|
+
if (existing) {
|
|
96
|
+
cells.push(dayButton(existing, true));
|
|
97
|
+
} else {
|
|
98
|
+
const ts = new Date(year, month, date).getTime();
|
|
99
|
+
cells.push(
|
|
100
|
+
dayButton(
|
|
101
|
+
{
|
|
102
|
+
dateKey: `${year}-${String(month + 1).padStart(2, "0")}-${String(date).padStart(2, "0")}`,
|
|
103
|
+
label: `${date} ${monthNames[month]}`,
|
|
104
|
+
timestampMs: ts,
|
|
105
|
+
cost: 0,
|
|
106
|
+
weekend: new Date(ts).getDay() === 0 || new Date(ts).getDay() === 6,
|
|
107
|
+
},
|
|
108
|
+
true,
|
|
109
|
+
),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
cells.push(`<span class="min-w-0" aria-hidden="true"></span>`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
rows.push(`<div class="grid grid-cols-[2.5rem_1fr] items-center gap-2" role="listitem">
|
|
117
|
+
<div class="text-xs text-stone-600 dark:text-stone-400">${monthNames[month]}</div>
|
|
118
|
+
<div class="day-grid-31 h-8 w-full" role="list" aria-label="${monthNames[month]} spend">${cells.join("")}</div>
|
|
119
|
+
</div>`);
|
|
120
|
+
}
|
|
121
|
+
return `<div class="space-y-1" role="list" aria-label="Year to date spend by month">${rows.join("")}</div>`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function renderCostReportHtml(report: CostReport): string {
|
|
125
|
+
const scopeLabel = report.scope === "project" ? "Current project" : "All sessions";
|
|
126
|
+
const scopeClass = report.scope === "project" ? "scope-project" : "scope-all";
|
|
127
|
+
const totalShare = report.totalCost;
|
|
128
|
+
const tokenShareBase = report.totalTokens;
|
|
129
|
+
const subagentShareBase = report.subagentCost;
|
|
130
|
+
|
|
131
|
+
const modelRows = report.models
|
|
132
|
+
.map((model) => {
|
|
133
|
+
const share = tokenShareBase > 0 ? model.tokens / tokenShareBase : 0;
|
|
134
|
+
return `<tr data-cost="${model.cost}" data-share="${model.tokens}">
|
|
135
|
+
<td class="px-3 py-2">${escapeHtml(model.provider)} / ${escapeHtml(model.model)}</td>
|
|
136
|
+
<td class="px-3 py-2 text-right">${money(model.cost)}</td>
|
|
137
|
+
<td class="px-3 py-2 text-right">${tokens(model.tokens)}</td>
|
|
138
|
+
<td class="px-3 py-2 text-right">${percent(model.tokens, tokenShareBase)}</td>
|
|
139
|
+
<td class="px-3 py-2">${shareBar(share)}</td>
|
|
140
|
+
<td class="px-3 py-2 text-right">${model.sessions}</td>
|
|
141
|
+
</tr>`;
|
|
142
|
+
})
|
|
143
|
+
.join("");
|
|
144
|
+
|
|
145
|
+
const subagentRows =
|
|
146
|
+
report.subagents.length === 0
|
|
147
|
+
? `<tr><td class="px-3 py-2 text-stone-600 dark:text-stone-400" colspan="5">No subagent spend in this window.</td></tr>`
|
|
148
|
+
: report.subagents
|
|
149
|
+
.map((agent) => {
|
|
150
|
+
const share = subagentShareBase > 0 ? agent.cost / subagentShareBase : 0;
|
|
151
|
+
return `<tr>
|
|
152
|
+
<td class="px-3 py-2">${escapeHtml(agent.agent)}</td>
|
|
153
|
+
<td class="px-3 py-2 text-right">${money(agent.cost)}</td>
|
|
154
|
+
<td class="px-3 py-2 text-right">${percent(agent.cost, subagentShareBase)}</td>
|
|
155
|
+
<td class="px-3 py-2">${shareBar(share)}</td>
|
|
156
|
+
<td class="px-3 py-2 text-right">${agent.calls}</td>
|
|
157
|
+
</tr>`;
|
|
158
|
+
})
|
|
159
|
+
.join("");
|
|
160
|
+
|
|
161
|
+
const projectRows = report.projects
|
|
162
|
+
.map((project) => {
|
|
163
|
+
const share = totalShare > 0 ? project.cost / totalShare : 0;
|
|
164
|
+
return `<tr data-project-key="${escapeHtml(project.key)}">
|
|
165
|
+
<td class="px-3 py-2">${escapeHtml(project.label)}</td>
|
|
166
|
+
<td class="px-3 py-2 text-right">${money(project.cost)}</td>
|
|
167
|
+
<td class="px-3 py-2 text-right">${percent(project.cost, totalShare)}</td>
|
|
168
|
+
<td class="px-3 py-2">${shareBar(share)}</td>
|
|
169
|
+
<td class="px-3 py-2 text-right">${project.sessions}</td>
|
|
170
|
+
</tr>`;
|
|
171
|
+
})
|
|
172
|
+
.join("");
|
|
173
|
+
|
|
174
|
+
const sessionRows = report.sessions
|
|
175
|
+
.map((session) => {
|
|
176
|
+
const models = session.models.map((model) => `${escapeHtml(model.label)} (${money(model.cost)})`).join(", ");
|
|
177
|
+
return `<tr data-project-key="${escapeHtml(session.projectKey)}">
|
|
178
|
+
<td class="px-3 py-2">
|
|
179
|
+
<span class="font-medium">${escapeHtml(session.name)}</span>
|
|
180
|
+
<span class="mt-0.5 block text-xs text-stone-600 dark:text-stone-400">${escapeHtml(
|
|
181
|
+
formatGenerated(session.startedAtMs),
|
|
182
|
+
)}</span>
|
|
183
|
+
</td>
|
|
184
|
+
<td class="project-col px-3 py-2">${escapeHtml(session.projectLabel)}</td>
|
|
185
|
+
<td class="px-3 py-2 font-mono text-xs text-stone-600 dark:text-stone-400">${models || "—"}</td>
|
|
186
|
+
<td class="px-3 py-2 text-right tabular">${tokens(session.tokens)}</td>
|
|
187
|
+
<td class="px-3 py-2 text-right tabular">${money(session.cost)}</td>
|
|
188
|
+
</tr>`;
|
|
189
|
+
})
|
|
190
|
+
.join("");
|
|
191
|
+
|
|
192
|
+
const projectOptions = [
|
|
193
|
+
`<option value="">All projects</option>`,
|
|
194
|
+
...report.projects.map(
|
|
195
|
+
(project) => `<option value="${escapeHtml(project.key)}">${escapeHtml(project.label)}</option>`,
|
|
196
|
+
),
|
|
197
|
+
].join("");
|
|
198
|
+
|
|
199
|
+
const dailyBody =
|
|
200
|
+
report.range.layout === "ytd"
|
|
201
|
+
? renderYtd(report.days, report.range.year ?? new Date(report.generatedAtMs).getFullYear())
|
|
202
|
+
: renderDayRow(report.days);
|
|
203
|
+
|
|
204
|
+
const dayCountLabel =
|
|
205
|
+
report.range.layout === "ytd" ? `${report.days.length} days · by month` : `${report.days.length} days in range`;
|
|
206
|
+
|
|
207
|
+
return `<!doctype html>
|
|
208
|
+
<html lang="en" class="h-full">
|
|
209
|
+
<head>
|
|
210
|
+
<meta charset="utf-8" />
|
|
211
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
212
|
+
<title>Cost report · ${escapeHtml(report.range.label)} · ${escapeHtml(scopeLabel)}</title>
|
|
213
|
+
<script>
|
|
214
|
+
(() => {
|
|
215
|
+
const key = "tau-cost-report-theme";
|
|
216
|
+
const stored = localStorage.getItem(key);
|
|
217
|
+
const dark =
|
|
218
|
+
stored === "dark" ||
|
|
219
|
+
(stored !== "light" && window.matchMedia("(prefers-color-scheme: dark)").matches);
|
|
220
|
+
document.documentElement.classList.toggle("dark", dark);
|
|
221
|
+
})();
|
|
222
|
+
</script>
|
|
223
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
224
|
+
<script>
|
|
225
|
+
tailwind.config = {
|
|
226
|
+
darkMode: "class",
|
|
227
|
+
theme: {
|
|
228
|
+
extend: {
|
|
229
|
+
fontFamily: {
|
|
230
|
+
sans: ["ui-sans-serif", "system-ui", "-apple-system", "Segoe UI", "sans-serif"],
|
|
231
|
+
mono: ["ui-monospace", "SF Mono", "Menlo", "Consolas", "monospace"],
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
</script>
|
|
237
|
+
<style type="text/tailwindcss">
|
|
238
|
+
@layer utilities {
|
|
239
|
+
.tabular { font-variant-numeric: tabular-nums; }
|
|
240
|
+
}
|
|
241
|
+
</style>
|
|
242
|
+
<style>
|
|
243
|
+
body.scope-project .all-only { display: none; }
|
|
244
|
+
body.scope-project .project-col { display: none; }
|
|
245
|
+
.day-cell { container-type: size; }
|
|
246
|
+
.day-cell .day-tip {
|
|
247
|
+
position: absolute;
|
|
248
|
+
bottom: calc(100% + 6px);
|
|
249
|
+
left: 50%;
|
|
250
|
+
z-index: 10;
|
|
251
|
+
transform: translateX(-50%);
|
|
252
|
+
white-space: nowrap;
|
|
253
|
+
padding: 0.3rem 0.5rem;
|
|
254
|
+
border-radius: 0.25rem;
|
|
255
|
+
border: 1px solid rgb(214 211 209);
|
|
256
|
+
background: rgb(255 255 255);
|
|
257
|
+
color: rgb(28 25 23);
|
|
258
|
+
font-size: 0.75rem;
|
|
259
|
+
line-height: 1.25;
|
|
260
|
+
pointer-events: none;
|
|
261
|
+
opacity: 0;
|
|
262
|
+
visibility: hidden;
|
|
263
|
+
}
|
|
264
|
+
.dark .day-cell .day-tip {
|
|
265
|
+
border-color: rgb(68 64 60);
|
|
266
|
+
background: rgb(28 25 23);
|
|
267
|
+
color: rgb(245 245 244);
|
|
268
|
+
}
|
|
269
|
+
.day-cell:hover .day-tip,
|
|
270
|
+
.day-cell:focus-visible .day-tip {
|
|
271
|
+
opacity: 1;
|
|
272
|
+
visibility: visible;
|
|
273
|
+
}
|
|
274
|
+
.day-cell .day-fire {
|
|
275
|
+
position: absolute;
|
|
276
|
+
inset: 0.25rem;
|
|
277
|
+
display: grid;
|
|
278
|
+
place-items: center;
|
|
279
|
+
font-size: 80cqmin;
|
|
280
|
+
line-height: 1;
|
|
281
|
+
pointer-events: none;
|
|
282
|
+
}
|
|
283
|
+
.day-cell.is-weekend::before {
|
|
284
|
+
content: "";
|
|
285
|
+
position: absolute;
|
|
286
|
+
inset: 0;
|
|
287
|
+
border-radius: inherit;
|
|
288
|
+
background: repeating-linear-gradient(-45deg, transparent 0 3px, rgb(0 0 0 / 0.14) 3px 5px);
|
|
289
|
+
pointer-events: none;
|
|
290
|
+
}
|
|
291
|
+
.dark .day-cell.is-weekend::before {
|
|
292
|
+
background: repeating-linear-gradient(-45deg, transparent 0 3px, rgb(255 255 255 / 0.16) 3px 5px);
|
|
293
|
+
}
|
|
294
|
+
.day-grid-31 {
|
|
295
|
+
display: grid;
|
|
296
|
+
grid-template-columns: repeat(31, minmax(0, 1fr));
|
|
297
|
+
gap: 0.25rem;
|
|
298
|
+
}
|
|
299
|
+
th.sortable {
|
|
300
|
+
cursor: pointer;
|
|
301
|
+
user-select: none;
|
|
302
|
+
}
|
|
303
|
+
th.sortable:hover {
|
|
304
|
+
color: rgb(28 25 23);
|
|
305
|
+
}
|
|
306
|
+
.dark th.sortable:hover {
|
|
307
|
+
color: rgb(245 245 244);
|
|
308
|
+
}
|
|
309
|
+
th.sortable[aria-sort="descending"]::after {
|
|
310
|
+
content: " ↓";
|
|
311
|
+
}
|
|
312
|
+
th.sortable[aria-sort="ascending"]::after {
|
|
313
|
+
content: " ↑";
|
|
314
|
+
}
|
|
315
|
+
</style>
|
|
316
|
+
</head>
|
|
317
|
+
<body class="${scopeClass} min-h-full bg-stone-100 text-stone-900 antialiased dark:bg-stone-950 dark:text-stone-100">
|
|
318
|
+
<main class="mx-auto w-full max-w-[90rem] px-4 py-8 sm:px-6 sm:py-10 lg:px-8">
|
|
319
|
+
<header class="mb-6 flex items-start justify-between gap-4 border-b border-stone-300 pb-4 dark:border-stone-700">
|
|
320
|
+
<div>
|
|
321
|
+
<h1 class="text-xl font-semibold tracking-tight">Cost report</h1>
|
|
322
|
+
<p class="mt-1 text-sm text-stone-600 dark:text-stone-400">
|
|
323
|
+
<span>${escapeHtml(report.range.label)}</span>
|
|
324
|
+
<span class="before:mx-2 before:text-stone-400 before:content-['·'] dark:before:text-stone-600">${escapeHtml(scopeLabel)}</span>
|
|
325
|
+
<span class="before:mx-2 before:text-stone-400 before:content-['·'] dark:before:text-stone-600">Generated ${escapeHtml(formatGenerated(report.generatedAtMs))}</span>
|
|
326
|
+
</p>
|
|
327
|
+
</div>
|
|
328
|
+
<button type="button" id="theme-toggle" class="shrink-0 rounded border border-stone-300 bg-white px-2.5 py-1 text-sm text-stone-800 dark:border-stone-600 dark:bg-stone-900 dark:text-stone-100" aria-label="Switch to dark mode">Dark</button>
|
|
329
|
+
</header>
|
|
330
|
+
|
|
331
|
+
<div class="mb-8 grid grid-cols-2 gap-3 sm:grid-cols-4">
|
|
332
|
+
<div class="rounded border border-stone-300 bg-white px-4 py-4 text-center dark:border-stone-700 dark:bg-stone-900">
|
|
333
|
+
<div class="text-3xl font-semibold tracking-tight tabular sm:text-4xl">${money(report.totalCost)}</div>
|
|
334
|
+
<div class="mt-1 text-sm text-stone-600 dark:text-stone-400">Total spend</div>
|
|
335
|
+
</div>
|
|
336
|
+
<div class="rounded border border-stone-300 bg-white px-4 py-4 text-center dark:border-stone-700 dark:bg-stone-900">
|
|
337
|
+
<div class="text-3xl font-semibold tracking-tight tabular sm:text-4xl">${report.sessionCount}</div>
|
|
338
|
+
<div class="mt-1 text-sm text-stone-600 dark:text-stone-400">Sessions</div>
|
|
339
|
+
</div>
|
|
340
|
+
<div class="all-only rounded border border-stone-300 bg-white px-4 py-4 text-center dark:border-stone-700 dark:bg-stone-900">
|
|
341
|
+
<div class="text-3xl font-semibold tracking-tight tabular sm:text-4xl">${report.projectCount}</div>
|
|
342
|
+
<div class="mt-1 text-sm text-stone-600 dark:text-stone-400">Projects</div>
|
|
343
|
+
</div>
|
|
344
|
+
<div class="rounded border border-stone-300 bg-white px-4 py-4 text-center dark:border-stone-700 dark:bg-stone-900">
|
|
345
|
+
<div class="text-3xl font-semibold tracking-tight tabular sm:text-4xl">${tokens(report.totalTokens)}</div>
|
|
346
|
+
<div class="mt-1 text-sm text-stone-600 dark:text-stone-400">Tokens</div>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
<section class="mb-8" aria-labelledby="daily-heading">
|
|
351
|
+
<div class="mb-3 flex items-baseline justify-between gap-3">
|
|
352
|
+
<h2 id="daily-heading" class="text-sm font-semibold">Daily spend</h2>
|
|
353
|
+
<span class="text-sm text-stone-600 dark:text-stone-400">${escapeHtml(dayCountLabel)}</span>
|
|
354
|
+
</div>
|
|
355
|
+
${dailyBody}
|
|
356
|
+
<div class="mt-3 flex flex-wrap items-center gap-2 text-xs text-stone-600 dark:text-stone-400" aria-hidden="true">
|
|
357
|
+
<span>$0</span>
|
|
358
|
+
<span class="flex gap-px">
|
|
359
|
+
<span class="size-3 rounded-sm border border-stone-900/10 bg-stone-200 dark:border-white/10 dark:bg-stone-800"></span>
|
|
360
|
+
<span class="size-3 rounded-sm border border-stone-900/10 bg-green-600"></span>
|
|
361
|
+
<span class="size-3 rounded-sm border border-stone-900/10 bg-yellow-500"></span>
|
|
362
|
+
<span class="size-3 rounded-sm border border-stone-900/10 bg-red-600"></span>
|
|
363
|
+
</span>
|
|
364
|
+
<span>$50+</span>
|
|
365
|
+
<span>· 🔥 over $100</span>
|
|
366
|
+
<span>· diagonal = weekend</span>
|
|
367
|
+
</div>
|
|
368
|
+
</section>
|
|
369
|
+
|
|
370
|
+
<div class="mb-8 grid gap-8 xl:grid-cols-2 xl:items-start">
|
|
371
|
+
<section aria-labelledby="models-heading">
|
|
372
|
+
<h2 id="models-heading" class="mb-3 text-sm font-semibold">Models</h2>
|
|
373
|
+
<div class="overflow-x-auto rounded border border-stone-300 bg-white dark:border-stone-700 dark:bg-stone-900">
|
|
374
|
+
<table id="models-table" class="w-full text-sm tabular">
|
|
375
|
+
<thead>
|
|
376
|
+
<tr class="text-left text-xs font-medium text-stone-600 dark:text-stone-400">
|
|
377
|
+
<th scope="col" class="px-3 py-2 font-medium">Model</th>
|
|
378
|
+
<th scope="col" class="sortable px-3 py-2 text-right font-medium" data-sort="cost" aria-sort="none">Cost</th>
|
|
379
|
+
<th scope="col" class="px-3 py-2 text-right font-medium">Tokens</th>
|
|
380
|
+
<th scope="col" class="sortable px-3 py-2 text-right font-medium" data-sort="share" aria-sort="descending">Share (tok)</th>
|
|
381
|
+
<th scope="col" class="min-w-[7rem] px-3 py-2 font-medium"></th>
|
|
382
|
+
<th scope="col" class="px-3 py-2 text-right font-medium">Sessions</th>
|
|
383
|
+
</tr>
|
|
384
|
+
</thead>
|
|
385
|
+
<tbody class="divide-y divide-stone-200 dark:divide-stone-800">${modelRows || `<tr><td class="px-3 py-2 text-stone-600 dark:text-stone-400" colspan="6">No model spend.</td></tr>`}</tbody>
|
|
386
|
+
</table>
|
|
387
|
+
</div>
|
|
388
|
+
</section>
|
|
389
|
+
|
|
390
|
+
<section aria-labelledby="subagents-heading">
|
|
391
|
+
<h2 id="subagents-heading" class="mb-3 text-sm font-semibold">Subagents</h2>
|
|
392
|
+
<div class="overflow-x-auto rounded border border-stone-300 bg-white dark:border-stone-700 dark:bg-stone-900">
|
|
393
|
+
<table class="w-full text-sm tabular">
|
|
394
|
+
<thead>
|
|
395
|
+
<tr class="text-left text-xs font-medium text-stone-600 dark:text-stone-400">
|
|
396
|
+
<th scope="col" class="px-3 py-2 font-medium">Agent</th>
|
|
397
|
+
<th scope="col" class="px-3 py-2 text-right font-medium">Cost</th>
|
|
398
|
+
<th scope="col" class="px-3 py-2 text-right font-medium">Share</th>
|
|
399
|
+
<th scope="col" class="min-w-[7rem] px-3 py-2 font-medium"></th>
|
|
400
|
+
<th scope="col" class="px-3 py-2 text-right font-medium">Calls</th>
|
|
401
|
+
</tr>
|
|
402
|
+
</thead>
|
|
403
|
+
<tbody class="divide-y divide-stone-200 dark:divide-stone-800">${subagentRows}</tbody>
|
|
404
|
+
</table>
|
|
405
|
+
</div>
|
|
406
|
+
</section>
|
|
407
|
+
</div>
|
|
408
|
+
|
|
409
|
+
<section class="all-only mb-8" aria-labelledby="projects-heading">
|
|
410
|
+
<h2 id="projects-heading" class="mb-3 text-sm font-semibold">Projects</h2>
|
|
411
|
+
<div class="overflow-x-auto rounded border border-stone-300 bg-white dark:border-stone-700 dark:bg-stone-900">
|
|
412
|
+
<table class="w-full text-sm tabular">
|
|
413
|
+
<thead>
|
|
414
|
+
<tr class="text-left text-xs font-medium text-stone-600 dark:text-stone-400">
|
|
415
|
+
<th scope="col" class="px-3 py-2 font-medium">Project</th>
|
|
416
|
+
<th scope="col" class="px-3 py-2 text-right font-medium">Cost</th>
|
|
417
|
+
<th scope="col" class="px-3 py-2 text-right font-medium">Share</th>
|
|
418
|
+
<th scope="col" class="min-w-[7rem] px-3 py-2 font-medium"></th>
|
|
419
|
+
<th scope="col" class="px-3 py-2 text-right font-medium">Sessions</th>
|
|
420
|
+
</tr>
|
|
421
|
+
</thead>
|
|
422
|
+
<tbody class="divide-y divide-stone-200 dark:divide-stone-800">${projectRows}</tbody>
|
|
423
|
+
</table>
|
|
424
|
+
</div>
|
|
425
|
+
</section>
|
|
426
|
+
|
|
427
|
+
<section class="mb-8" aria-labelledby="sessions-heading">
|
|
428
|
+
<div class="mb-3 flex items-baseline justify-between gap-3">
|
|
429
|
+
<h2 id="sessions-heading" class="text-sm font-semibold">Sessions</h2>
|
|
430
|
+
<div class="all-only flex items-center gap-2">
|
|
431
|
+
<label for="project-filter" class="text-sm text-stone-600 dark:text-stone-400">Project</label>
|
|
432
|
+
<select id="project-filter" class="rounded border border-stone-300 bg-white px-2 py-1 text-sm text-stone-900 dark:border-stone-600 dark:bg-stone-900 dark:text-stone-100">${projectOptions}</select>
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
<div class="overflow-x-auto rounded border border-stone-300 bg-white dark:border-stone-700 dark:bg-stone-900">
|
|
436
|
+
<table class="w-full text-sm">
|
|
437
|
+
<thead>
|
|
438
|
+
<tr class="text-left text-xs font-medium text-stone-600 dark:text-stone-400">
|
|
439
|
+
<th scope="col" class="px-3 py-2 font-medium">Session</th>
|
|
440
|
+
<th scope="col" class="project-col px-3 py-2 font-medium">Project</th>
|
|
441
|
+
<th scope="col" class="px-3 py-2 font-medium">Models</th>
|
|
442
|
+
<th scope="col" class="px-3 py-2 text-right font-medium">Tokens</th>
|
|
443
|
+
<th scope="col" class="px-3 py-2 text-right font-medium">Cost</th>
|
|
444
|
+
</tr>
|
|
445
|
+
</thead>
|
|
446
|
+
<tbody id="sessions-body" class="divide-y divide-stone-200 dark:divide-stone-800">${sessionRows || `<tr><td class="px-3 py-2 text-stone-600 dark:text-stone-400" colspan="5">No sessions.</td></tr>`}</tbody>
|
|
447
|
+
</table>
|
|
448
|
+
</div>
|
|
449
|
+
</section>
|
|
450
|
+
|
|
451
|
+
<footer class="mt-10 border-t border-stone-300 pt-4 text-xs text-stone-600 dark:border-stone-700 dark:text-stone-400">
|
|
452
|
+
<p>
|
|
453
|
+
Costs are estimates from stored session usage, not provider invoices. Branched turns count. Subagent
|
|
454
|
+
totals come from parent tool results. Direct ${money(report.directCost)} · Subagents ${money(report.subagentCost)}.
|
|
455
|
+
</p>
|
|
456
|
+
</footer>
|
|
457
|
+
</main>
|
|
458
|
+
<script>
|
|
459
|
+
(() => {
|
|
460
|
+
const key = "tau-cost-report-theme";
|
|
461
|
+
const root = document.documentElement;
|
|
462
|
+
const button = document.getElementById("theme-toggle");
|
|
463
|
+
if (button) {
|
|
464
|
+
const sync = () => {
|
|
465
|
+
const dark = root.classList.contains("dark");
|
|
466
|
+
button.textContent = dark ? "Light" : "Dark";
|
|
467
|
+
button.setAttribute("aria-label", dark ? "Switch to light mode" : "Switch to dark mode");
|
|
468
|
+
};
|
|
469
|
+
button.addEventListener("click", () => {
|
|
470
|
+
const dark = !root.classList.contains("dark");
|
|
471
|
+
root.classList.toggle("dark", dark);
|
|
472
|
+
localStorage.setItem(key, dark ? "dark" : "light");
|
|
473
|
+
sync();
|
|
474
|
+
});
|
|
475
|
+
sync();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const filter = document.getElementById("project-filter");
|
|
479
|
+
const body = document.getElementById("sessions-body");
|
|
480
|
+
if (filter && body) {
|
|
481
|
+
filter.addEventListener("change", () => {
|
|
482
|
+
const value = filter.value;
|
|
483
|
+
for (const row of body.querySelectorAll("tr[data-project-key]")) {
|
|
484
|
+
const key = row.getAttribute("data-project-key") || "";
|
|
485
|
+
row.hidden = Boolean(value) && key !== value;
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const modelsTable = document.getElementById("models-table");
|
|
491
|
+
if (modelsTable) {
|
|
492
|
+
const tbody = modelsTable.querySelector("tbody");
|
|
493
|
+
const headers = modelsTable.querySelectorAll("th.sortable");
|
|
494
|
+
if (tbody && headers.length > 0) {
|
|
495
|
+
let active = "share";
|
|
496
|
+
let direction = "desc";
|
|
497
|
+
const sortRows = () => {
|
|
498
|
+
const rows = [...tbody.querySelectorAll("tr[data-cost]")];
|
|
499
|
+
rows.sort((a, b) => {
|
|
500
|
+
const attr = "data-" + active;
|
|
501
|
+
const av = Number(a.getAttribute(attr)) || 0;
|
|
502
|
+
const bv = Number(b.getAttribute(attr)) || 0;
|
|
503
|
+
return direction === "desc" ? bv - av : av - bv;
|
|
504
|
+
});
|
|
505
|
+
for (const row of rows) tbody.appendChild(row);
|
|
506
|
+
for (const header of headers) {
|
|
507
|
+
const key = header.getAttribute("data-sort") || "";
|
|
508
|
+
header.setAttribute(
|
|
509
|
+
"aria-sort",
|
|
510
|
+
key === active ? (direction === "desc" ? "descending" : "ascending") : "none",
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
for (const header of headers) {
|
|
515
|
+
header.tabIndex = 0;
|
|
516
|
+
const activate = () => {
|
|
517
|
+
const key = header.getAttribute("data-sort") || "";
|
|
518
|
+
if (key === active) {
|
|
519
|
+
direction = direction === "desc" ? "asc" : "desc";
|
|
520
|
+
} else {
|
|
521
|
+
active = key;
|
|
522
|
+
direction = "desc";
|
|
523
|
+
}
|
|
524
|
+
sortRows();
|
|
525
|
+
};
|
|
526
|
+
header.addEventListener("click", activate);
|
|
527
|
+
header.addEventListener("keydown", (event) => {
|
|
528
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
529
|
+
event.preventDefault();
|
|
530
|
+
activate();
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
})();
|
|
537
|
+
</script>
|
|
538
|
+
</body>
|
|
539
|
+
</html>`;
|
|
540
|
+
}
|