A session's page and Sessions say what it cost at list price
With sessions reporting their usage, the people looking at one want to know what it cost without opening a bill: its page and the sessions list say "≈ $4.12 at list price", quietly beside when it ran, with "some usage priced as unknown" when part of it was for a model g1t has no price for. WorkSession carries costUsd and costPartial, as the work service sends them. Part of #157
- G1t-Session:
- ses_01m573jm0ze1q9tram0b4dcva1
5 files+56−30/5 viewed
| 24 | 24 | import { ItemIcon, PullIcon } from "./work-icons"; | |
| 25 | 25 | import { cn } from "../lib/cn"; | |
| 26 | 26 | import { HARNESS_NAMES, SESSION_STATES } from "../lib/coordination"; | |
| 27 | − | import { KIND_NAMES, type SessionsFilters, narrowed, repoName, runsIn, sessionHref, sessionsQuery, workspaceSessionTitle } from "../lib/sessions"; | |
| 27 | + | import { KIND_NAMES, type SessionsFilters, narrowed, repoName, runsIn, sessionCost, sessionHref, sessionsQuery, workspaceSessionTitle } from "../lib/sessions"; | |
| 28 | 28 | import { itemPath, sessionsPath } from "../lib/work-items"; | |
| 29 | 29 | ||
| 30 | 30 | const BRANDS: Partial<Record<Harness, { path: string; title: string }>> = { | |
| ⋯ | |||
| 121 | 121 | ); | |
| 122 | 122 | } | |
| 123 | 123 | ||
| 124 | + | /** What a harness session cost, quietly, when it reported usage. */ | |
| 125 | + | function Cost({ session }: { session: WorkspaceSession }) { | |
| 126 | + | const cost = sessionCost(session.session); | |
| 127 | + | if (!cost) return null; | |
| 128 | + | return ( | |
| 129 | + | <Hint label="Estimated from the tokens its transcript recorded, at Anthropic's list price. Its subagents count on their own sessions."> | |
| 130 | + | <span className="text-xs text-faint tabular-nums">{cost}</span> | |
| 131 | + | </Hint> | |
| 132 | + | ); | |
| 133 | + | } | |
| 134 | + | ||
| 124 | 135 | /** | |
| 125 | 136 | * A live session: its state, what it is called, who runs it and in what, | |
| 126 | 137 | * where, the work item and pull request it is on, its subagents and when | |
| ⋯ | |||
| 153 | 164 | <Where session={session} workspace={workspace} /> | |
| 154 | 165 | </div> | |
| 155 | 166 | {session.step && <p className="line-clamp-1 text-xs text-muted">{session.step}</p>} | |
| 167 | + | <Cost session={session} /> | |
| 156 | 168 | {(session.item != null || session.pull != null || session.subagents > 0) && ( | |
| 157 | 169 | <div className="flex flex-wrap items-center gap-2"> | |
| 158 | 170 | {session.item != null && <ItemChip workspace={workspace} number={session.item} title={session.itemTitle} />} | |
| ⋯ | |||
| 194 | 206 | </span> | |
| 195 | 207 | <Where session={session} workspace={workspace} /> | |
| 196 | 208 | {session.subagents > 0 && <span>{session.subagents} subagents</span>} | |
| 209 | + | <Cost session={session} /> | |
| 197 | 210 | </div> | |
| 198 | 211 | </div> | |
| 199 | 212 | <span className="shrink-0 text-xs text-faint"> | |
| 3 | 3 | ||
| 4 | 4 | import type { WorkSession, WorkspaceSession } from "@g1t/contracts"; | |
| 5 | 5 | ||
| 6 | − | import { narrowed, peopleIn, repoName, runsIn, sessionHref, sessionsFilters, sessionsQuery, splitLive, workspaceSessionTitle } from "./sessions.ts"; | |
| 6 | + | import { narrowed, peopleIn, repoName, runsIn, sessionCost, sessionHref, sessionsFilters, sessionsQuery, splitLive, workspaceSessionTitle } from "./sessions.ts"; | |
| 7 | 7 | ||
| 8 | 8 | function listed(id: string, more: Partial<WorkspaceSession> = {}): WorkspaceSession { | |
| 9 | 9 | return { | |
| ⋯ | |||
| 86 | 86 | assert.equal(repoName("acme/web", "Acme"), "web"); | |
| 87 | 87 | assert.equal(repoName("other/web", "acme"), "other/web"); | |
| 88 | 88 | }); | |
| 89 | + | ||
| 90 | + | test("a session's cost reads as an estimate at list price, and says when some usage is priced as unknown", () => { | |
| 91 | + | assert.equal(sessionCost({ costUsd: 4.1234 }), "≈ $4.12 at list price"); | |
| 92 | + | assert.equal(sessionCost({ costUsd: 1234.5, costPartial: true }), "≈ $1,234.50 at list price, some usage priced as unknown"); | |
| 93 | + | assert.equal(sessionCost({ costUsd: 0.004 }), "≈ under $0.01 at list price"); | |
| 94 | + | assert.equal(sessionCost({ costUsd: 0 }), "≈ $0.00 at list price"); | |
| 95 | + | assert.equal(sessionCost({ costUsd: null, costPartial: true }), "Usage priced as unknown"); | |
| 96 | + | assert.equal(sessionCost({ costUsd: null }), null); | |
| 97 | + | assert.equal(sessionCost({}), null); | |
| 98 | + | assert.equal(sessionCost(null), null); | |
| 99 | + | }); | |
| 3 | 3 | * and where it opens, the filters Sessions keeps in its address, and live | |
| 4 | 4 | * ones apart from the rest. Pure, so it is tested under Node. | |
| 5 | 5 | */ | |
| 6 | − | import type { Harness, SessionKind, WorkspaceSession } from "@g1t/contracts"; | |
| 6 | + | import type { Harness, SessionKind, WorkSession, WorkspaceSession } from "@g1t/contracts"; | |
| 7 | 7 | ||
| 8 | 8 | import { HARNESS_NAMES, sessionTitle } from "./coordination.ts"; | |
| 9 | 9 | import { sessionPath } from "./work-items.ts"; | |
| ⋯ | |||
| 106 | 106 | return [...counts].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([name]) => name); | |
| 107 | 107 | } | |
| 108 | 108 | ||
| 109 | + | /** | |
| 110 | + | * What a session cost, in a few words: "≈ $4.12 at list price", with | |
| 111 | + | * "some usage priced as unknown" when some of it was for models g1t has no | |
| 112 | + | * price for. Null when it reported no usage. | |
| 113 | + | */ | |
| 114 | + | export function sessionCost(session: Pick<WorkSession, "costUsd" | "costPartial"> | null | undefined): string | null { | |
| 115 | + | if (!session) return null; | |
| 116 | + | const cost = session.costUsd; | |
| 117 | + | if (cost == null || !Number.isFinite(cost)) return session.costPartial ? "Usage priced as unknown" : null; | |
| 118 | + | const amount = cost > 0 && cost < 0.01 ? "under $0.01" : `$${cost.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; | |
| 119 | + | return `≈ ${amount} at list price${session.costPartial ? ", some usage priced as unknown" : ""}`; | |
| 120 | + | } | |
| 121 | + | ||
| 109 | 122 | /** `acme/web` as the workspace `acme` names it: `web`. */ | |
| 110 | 123 | export function repoName(path: string, workspace: string): string { | |
| 111 | 124 | const [owner, name] = path.split("/"); | |
| 35 | 35 | viaLabel, | |
| 36 | 36 | } from "../../../../lib/coordination"; | |
| 37 | 37 | import { notFound } from "../../../../lib/not-found.server"; | |
| 38 | + | import { sessionCost } from "../../../../lib/sessions"; | |
| 38 | 39 | import { repoPages } from "../../../../lib/repo-live"; | |
| 39 | 40 | import { agentSessions, work } from "../../../../lib/services.server"; | |
| 40 | 41 | import { assertSameOrigin, getViewer, requireUser } from "../../../../lib/session.server"; | |
| ⋯ | |||
| 341 | 342 | <SessionStateBadge state={session.state} /> | |
| 342 | 343 | <TimeAgo at={session.startedAt} /> | |
| 343 | 344 | <span suppressHydrationWarning>· {duration(session, now)}</span> | |
| 345 | + | {sessionCost(session) && <span className="tabular-nums">· {sessionCost(session)}</span>} | |
| 344 | 346 | </span> | |
| 345 | 347 | </div> | |
| 346 | 348 | <CollapsibleContent className="mt-3 space-y-4 pl-6"> | |
| ⋯ | |||
| 390 | 392 | // Its own subagents: those whose parent is in this tree (children lists every descendant). | |
| 391 | 393 | const tree = sessionTree([session, ...children]).find((node) => node.session.id === session.id)?.children ?? []; | |
| 392 | 394 | const managed = session.harness === MANAGED || root?.harness === MANAGED; | |
| 395 | + | const cost = sessionCost(session); | |
| 393 | 396 | ||
| 394 | 397 | // The skills, summed by name across the tree. | |
| 395 | 398 | const skillUses = new Map<string, number>(); | |
| ⋯ | |||
| 479 | 482 | · {session.state === "ended" ? "ended" : "last heard from"} <TimeAgo at={session.endedAt ?? session.lastSeenAt} /> | |
| 480 | 483 | {session.endReason && ` (${endReasonText(session.endReason)})`} | |
| 481 | 484 | </span> | |
| 485 | + | {cost && ( | |
| 486 | + | <Hint label="Estimated from the tokens its transcript recorded, at Anthropic's list price. Its subagents count on their own sessions."> | |
| 487 | + | <span className="tabular-nums">· {cost}</span> | |
| 488 | + | </Hint> | |
| 489 | + | )} | |
| 482 | 490 | {session.branch && ( | |
| 483 | 491 | <Link to={`${base}/tree/${session.branch}`} className="inline-flex items-center gap-1 font-mono text-xs hover:text-fg"> | |
| 484 | 492 | · <GitBranch size={12} /> | |
| 1449 | 1449 | titleSource?: TitleSource | null; | |
| 1450 | 1450 | /** When its title was last settled. */ | |
| 1451 | 1451 | titleSettledAt?: string | null; | |
| 1452 | + | /** | |
| 1453 | + | * What it cost, estimated at Anthropic's list price from the token usage | |
| 1454 | + | * its harness reported, in US dollars; none until it reports usage for a | |
| 1455 | + | * model with a known price. Only the workspace's members see it. | |
| 1456 | + | */ | |
| 1457 | + | costUsd?: number | null; | |
| 1458 | + | /** Some of its usage was for models priced as unknown, which `costUsd` leaves out. */ | |
| 1459 | + | costPartial?: boolean; | |
| 1452 | 1460 | }; | |
| 1453 | 1461 | ||
| 1454 | 1462 | /** |
Comments
Sign in to comment.