Skip to content
121 linesCodeBlameRaw
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 */
10import type { AgentPolicy, AgentSpendBreakdown, Limit, PersonBudgets, SpendPeriod, UsageReport, User, WorkspaceAgent } from "@g1t/contracts";
11
12import { type Pricing, type SpendScope, pricingOf, spanFor } from "./spend";
13import { billing, workspaceAgents } from "./services.server";
14
15const warn = (what: string) => (error: unknown) => {
16 console.warn(`spend: ${what} failed`, error);
17 return null;
18};
19
20const 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. */
23export 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. */
31export 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. */
37export 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. */
40export 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
53export 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. */
78export 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. */
86export type PillData = {
87 month: string;
88 me: { spentMicros: number; budgetMicros: number | null; byKind: AgentSpendBreakdown["by_kind"]; byAgent: AgentSpendBreakdown["by_agent"] } | null;
89 /**
90 * The workspace's month: everything it used at price (the figure Home and
91 * Spend show), what it was charged after its plan and credit (the figure
92 * its spend limit governs), its limit, and its agents' share.
93 */
94 workspace: { spentMicros: number | null; chargedMicros: number | null; limitMicros: number | null; agentsMicros: number | null; byAgent: AgentSpendBreakdown["by_agent"] } | null;
95};
96
97export async function loadPill(viewer: User, slug: string, mayWorkspace: boolean): Promise<PillData> {
98 const me = viewer.username.toLowerCase();
99 const [mine, people, everyone, limit, used] = await Promise.all([
100 loadBreakdown(viewer, slug, "me", "month"),
101 workspaceAgents.personBudgets(slug, viewer).then(value).catch(warn("person budgets")),
102 mayWorkspace ? loadBreakdown(viewer, slug, "workspace", "month") : Promise.resolve(null),
103 mayWorkspace ? billing.limit(slug, viewer).then(value).catch(warn("limit")) : Promise.resolve(null),
104 mayWorkspace ? loadUsage(viewer, slug, "month", new Date()) : Promise.resolve(null),
105 ]);
106 const own = people?.people.find((p) => p.username === me);
107 const budget = own ? own.monthly_micros : (people?.default_micros ?? null);
108 return {
109 month: (mine ?? everyone)?.period ?? new Date().toISOString().slice(0, 7),
110 me: mine ? { spentMicros: mine.total_micros, budgetMicros: budget, byKind: mine.by_kind, byAgent: mine.by_agent } : null,
111 workspace: mayWorkspace
112 ? {
113 spentMicros: used ? (used.free ? used.totals.costMicros : used.totals.priceMicros) : null,
114 chargedMicros: limit?.spentMicros ?? null,
115 limitMicros: limit ? (limit.spendLimitMicros ?? limit.ceilingMicros) : null,
116 agentsMicros: everyone?.total_micros ?? null,
117 byAgent: everyone?.by_agent ?? [],
118 }
119 : null,
120 };
121}