Skip to content
114 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 workspace: { spentMicros: number | null; limitMicros: number | null; agentsMicros: number | null; byAgent: AgentSpendBreakdown["by_agent"] } | null;
90};
91
92export 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}