| 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 | } |