Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 1 | /** |
| 2 | * Spend's reads: each part from the service that owns it, each on its own | |
| 3 | * so one that fails leaves only its part saying so. Billing has the | |
| 4 | * workspace's money (usage at price, the spend limit, caps, the price | |
| 5 | * book); the agents service has where agent work went (by agent, person, | |
| 6 | * channel, model, kind) and the budgets below the workspace's (its agents | |
| 7 | * together, each person, each agent, each session). The shaping is in | |
| 8 | * ./spend.ts. | |
| 9 | */ | |
| 10 | import type { AgentPolicy, AgentSpendBreakdown, Limit, PersonBudgets, SpendPeriod, UsageReport, User, WorkspaceAgent } from "@g1t/contracts"; | |
| 11 | ||
| 12 | import { type Pricing, type SpendScope, pricingOf, spanFor } from "./spend"; | |
| 13 | import { billing, workspaceAgents } from "./services.server"; | |
| 14 | ||
| 15 | const warn = (what: string) => (error: unknown) => { | |
| 16 | console.warn(`spend: ${what} failed`, error); | |
| 17 | return null; | |
| 18 | }; | |
| 19 | ||
| 20 | const value = <T>(result: { ok: true; value: T } | { ok: false } | null): T | null => (result && result.ok ? result.value : null); | |
| 21 | ||
| 22 | /** Where agent work went for the scope and period: everyone's, or only what the viewer asked for. */ | |
| 23 | export function loadBreakdown(viewer: User, slug: string, scope: SpendScope, period: SpendPeriod): Promise<AgentSpendBreakdown | null> { | |
| 24 | return workspaceAgents | |
| 25 | .spend(slug, viewer, null, { period, person: scope === "me" ? viewer.username.toLowerCase() : null }) | |
| 26 | .then(value) | |
| 27 | .catch(warn("agents spend")); | |
| 28 | } | |
| 29 | ||
| 30 | /** Everything the workspace used over the period, at price, by product and day. */ | |
| 31 | export function loadUsage(viewer: User, slug: string, period: SpendPeriod, now: Date): Promise<UsageReport | null> { | |
| 32 | const { from, until } = spanFor(period, now); | |
| 33 | return billing.usageReport(slug, viewer, { from, until }).then(value).catch(warn("usage report")); | |
| 34 | } | |
| 35 | ||
| 36 | /** One agent's budget and month, as the budgets list shows it. */ | |
| 37 | export type AgentBudgetRow = Pick<WorkspaceAgent, "id" | "handle" | "display_name" | "avatar_seed" | "budget" | "spent_month_micros" | "builtin">; | |
| 38 | ||
| 39 | /** Every level of budget, widest first, with what is spent against each this month. */ | |
| 40 | export type Budgets = { | |
| 41 | /** The workspace's spend limit (billing); null when billing did not answer or is off. */ | |
| 42 | limit: Limit | null; | |
| 43 | /** The plan's caps on one run and one issue (billing). */ | |
| 44 | caps: { runMicros: number; issueMicros: number } | null; | |
| 45 | /** The workspace's agent policy: every agent together, the per-person default, a new agent's, a session's. */ | |
| 46 | policy: AgentPolicy | null; | |
| 47 | /** Every agent's spend this month, against the policy's budget. */ | |
| 48 | agentsMonthMicros: number | null; | |
| 49 | people: PersonBudgets | null; | |
| 50 | agents: AgentBudgetRow[] | null; | |
| 51 | }; | |
| 52 | ||
| 53 | export async function loadBudgets(viewer: User, slug: string, monthBreakdown: Promise<AgentSpendBreakdown | null>): Promise<Budgets> { | |
| 54 | const [limit, entitlements, policy, people, agents, month] = await Promise.all([ | |
| 55 | billing.limit(slug, viewer).then(value).catch(warn("limit")), | |
| 56 | billing.entitlements(slug).catch(warn("entitlements")), | |
| 57 | workspaceAgents.policy(slug, viewer).then(value).catch(warn("agent policy")), | |
| 58 | workspaceAgents.personBudgets(slug, viewer).then(value).catch(warn("person budgets")), | |
| 59 | workspaceAgents.list(slug, viewer).then(value).catch(warn("agents")), | |
| 60 | monthBreakdown, | |
| 61 | ]); | |
| 62 | return { | |
| 63 | limit, | |
| 64 | caps: entitlements ? { runMicros: entitlements.runCapMicros, issueMicros: entitlements.issueCapMicros } : null, | |
| 65 | policy, | |
| 66 | agentsMonthMicros: month?.total_micros ?? null, | |
| 67 | people, | |
| 68 | agents: agents | |
| 69 | ? agents | |
| 70 | .filter((a) => !a.archived_at) | |
| 71 | .map((a) => ({ id: a.id, handle: a.handle, display_name: a.display_name, avatar_seed: a.avatar_seed, budget: a.budget, spent_month_micros: a.spent_month_micros, builtin: a.builtin })) | |
| 72 | .sort((a, b) => Number(b.builtin) - Number(a.builtin) || b.spent_month_micros - a.spent_month_micros) | |
| 73 | : null, | |
| 74 | }; | |
| 75 | } | |
| 76 | ||
| 77 | /** What the price book says about pricing. */ | |
| 78 | export function loadPricing(): Promise<Pricing | null> { | |
| 79 | return billing | |
| 80 | .prices() | |
| 81 | .then(pricingOf) | |
| 82 | .catch(warn("prices")); | |
| 83 | } | |
| 84 | ||
| 85 | /** The top bar's pill: the viewer's month, and the workspace's for someone who may see it. */ | |
| 86 | export type PillData = { | |
| 87 | month: string; | |
| 88 | me: { spentMicros: number; budgetMicros: number | null; byKind: AgentSpendBreakdown["by_kind"]; byAgent: AgentSpendBreakdown["by_agent"] } | null; | |
| 89 | workspace: { spentMicros: number | null; limitMicros: number | null; agentsMicros: number | null; byAgent: AgentSpendBreakdown["by_agent"] } | null; | |
| 90 | }; | |
| 91 | ||
| 92 | export async function loadPill(viewer: User, slug: string, mayWorkspace: boolean): Promise<PillData> { | |
| 93 | const me = viewer.username.toLowerCase(); | |
| 94 | const [mine, people, everyone, limit] = await Promise.all([ | |
| 95 | loadBreakdown(viewer, slug, "me", "month"), | |
| 96 | workspaceAgents.personBudgets(slug, viewer).then(value).catch(warn("person budgets")), | |
| 97 | mayWorkspace ? loadBreakdown(viewer, slug, "workspace", "month") : Promise.resolve(null), | |
| 98 | mayWorkspace ? billing.limit(slug, viewer).then(value).catch(warn("limit")) : Promise.resolve(null), | |
| 99 | ]); | |
| 100 | const own = people?.people.find((p) => p.username === me); | |
| 101 | const budget = own ? own.monthly_micros : (people?.default_micros ?? null); | |
| 102 | return { | |
| 103 | month: (mine ?? everyone)?.period ?? new Date().toISOString().slice(0, 7), | |
| 104 | me: mine ? { spentMicros: mine.total_micros, budgetMicros: budget, byKind: mine.by_kind, byAgent: mine.by_agent } : null, | |
| 105 | workspace: mayWorkspace | |
| 106 | ? { | |
| 107 | spentMicros: limit?.spentMicros ?? null, | |
| 108 | limitMicros: limit ? (limit.spendLimitMicros ?? limit.ceilingMicros) : null, | |
| 109 | agentsMicros: everyone?.total_micros ?? null, | |
| 110 | byAgent: everyone?.by_agent ?? [], | |
| 111 | } | |
| 112 | : null, | |
| 113 | }; | |
| 114 | } |