Skip to content

g1t/apps/sudo/app/lib/costs.ts

216 lines10,017 bytesCodeBlameRaw

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.

Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1/**
2 * Costs & margin: the arithmetic behind the page, apart from the SVG and
3 * the Workers runtime so it can be tested under Node. Money is in micros.
4 */
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays5import type { CostDay, CostMappingInput, CostSettings, SpendCaps } from "@g1t/contracts";
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily6
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays7import { parseDollars, usd } from "./money.ts";
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily8
9/** Buckets Cloudflare does not bill: their cost is g1t's own figure. */
10export const NOT_CLOUDFLARE = new Set(["models"]);
11
12/** A day's cost: Cloudflare's bill, or g1t's own figure where Cloudflare does not bill it. */
13export function dayCost(day: CostDay): number {
14 return NOT_CLOUDFLARE.has(day.bucket) ? day.ownCostMicros : day.cfCostMicros;
15}
16
17export type DayFigures = { day: string; revenueMicros: number; costMicros: number };
18
19/** Every day from `since` to `until`, inclusive (YYYY-MM-DD, UTC). */
20export function daysBetween(since: string, until: string): string[] {
21 const start = Date.parse(`${since}T00:00:00Z`);
22 const end = Date.parse(`${until}T00:00:00Z`);
23 if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return [];
24 const out: string[] = [];
25 for (let at = start; at <= end && out.length < 400; at += 86_400_000) out.push(new Date(at).toISOString().slice(0, 10));
26 return out;
27}
28
29/**
30 * A day per day of the range, every day there even with nothing on it.
31 * All of g1t: money in (what workspaces paid, the plan too) against every
32 * cost. One product: what customers were charged for it at price against
33 * what it cost.
34 */
35export function daySeries(days: CostDay[], since: string, until: string, bucket: string | null): DayFigures[] {
36 const totals = new Map<string, DayFigures>(daysBetween(since, until).map((day) => [day, { day, revenueMicros: 0, costMicros: 0 }]));
37 for (const row of days) {
38 if (bucket && row.bucket !== bucket) continue;
39 const figures = totals.get(row.day);
40 if (!figures) continue;
41 figures.revenueMicros += bucket ? row.valueMicros : row.cashMicros;
42 figures.costMicros += dayCost(row);
43 }
44 return [...totals.values()];
45}
46
47/** Margin as a whole percent of revenue; none when there was none. */
48export function marginPercent(revenueMicros: number, costMicros: number): number | null {
49 return revenueMicros > 0 ? ((revenueMicros - costMicros) / revenueMicros) * 100 : null;
50}
51
52/** `12.3%`, `−4.0%`, or a dash. */
53export function percentLabel(percent: number | null | undefined, { signed = false }: { signed?: boolean } = {}): string {
54 if (percent == null || !Number.isFinite(percent)) return "—";
55 const text = `${Math.abs(percent).toFixed(1)}%`;
56 if (percent < 0) return `−${text}`;
57 return signed && percent > 0 ? `+${text}` : text;
58}
59
60/** How a margin reads against the floor: under it is danger, close to it a warning. */
61export function marginTone(percent: number | null | undefined, floor: number): "danger" | "warn" | "mint" | undefined {
62 if (percent == null) return undefined;
63 if (percent < floor) return "danger";
64 if (percent < floor + 5) return "warn";
65 return "mint";
66}
67
68/** A count: `1,234` or `1.2M`. */
69export function countLabel(value: number): string {
70 if (!Number.isFinite(value)) return "—";
71 if (Math.abs(value) >= 10_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
72 return Math.round(value).toLocaleString("en-US");
73}
74
75/** What a kind of drift is called. */
76export function driftLabel(kind: string): string {
77 return { count: "Count", cost: "Cost", leak: "Leak" }[kind] ?? kind;
78}
79
80/** A cost per unit in micros, as dollars with as many places as it needs: `$0.000016`, `$0.15`. */
81export function unitDollars(micros: number): string {
82 const dollars = micros / 1_000_000;
83 if (dollars === 0) return "$0";
84 if (Math.abs(dollars) >= 1) return `$${dollars.toFixed(2)}`;
85 const places = Math.min(10, Math.max(2, 2 - Math.floor(Math.log10(Math.abs(dollars)))));
86 return `$${dollars.toFixed(places)}`;
87}
88
89/** The range shown: 7 to 90 days, 30 when not said. */
90export function parseRange(raw: string | null): number {
91 const days = Number(raw);
92 return Number.isInteger(days) && days >= 7 && days <= 90 ? days : 30;
93}
94
95/** The product to chart, if it is one the page has. */
96export function parseBucket(raw: string | null, known: string[]): string | null {
97 return raw && known.includes(raw) ? raw : null;
98}
99
100export type Parsed<T> = { ok: true; value: T } | { ok: false; error: string };
101
102function numberField(form: FormData, name: string): number {
103 const raw = String(form.get(name) ?? "").trim();
104 return raw === "" ? Number.NaN : Number(raw);
105}
106
107/** The guardrails from the settings form. */
108export function parseCostSettings(form: FormData): Parsed<CostSettings> {
109 const autoApplyPercent = numberField(form, "autoApplyPercent");
110 const noticeDays = numberField(form, "noticeDays");
111 const marginFloorPercent = numberField(form, "marginFloorPercent");
112 const alertDays = numberField(form, "alertDays");
113 const anomalyFactor = numberField(form, "anomalyFactor");
114 const minDaily = parseDollars(String(form.get("minDailyCost") ?? ""));
115 const anomalyFloor = parseDollars(String(form.get("anomalyFloor") ?? ""));
116 if (!(autoApplyPercent >= 0 && autoApplyPercent <= 100)) return { ok: false, error: "The guardrail is a percentage from 0 to 100." };
117 if (!(Number.isInteger(noticeDays) && noticeDays >= 0 && noticeDays <= 90)) return { ok: false, error: "Notice is 0 to 90 days." };
118 if (!(marginFloorPercent >= -100 && marginFloorPercent <= 100)) return { ok: false, error: "The margin floor is a percentage." };
119 if (!(Number.isInteger(alertDays) && alertDays >= 1 && alertDays <= 30)) return { ok: false, error: "Alert after 1 to 30 days." };
120 if (!(anomalyFactor > 0 && anomalyFactor <= 100)) return { ok: false, error: "The factor is a positive number." };
121 if (minDaily == null || anomalyFloor == null) return { ok: false, error: "Amounts are dollars to the cent." };
122 return {
123 ok: true,
124 value: {
125 autoApply: form.get("autoApply") === "on",
126 autoApplyPercent,
127 noticeDays,
128 marginFloorPercent,
129 alertDays,
130 minDailyCostMicros: minDaily,
131 anomalyFactor,
132 anomalyFloorMicros: anomalyFloor,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit133 cardFee: form.get("cardFee") === "on",
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily134 },
135 };
136}
137
138const NAME = /^(\*|[a-z0-9][a-z0-9_]{0,79})$/;
139
140/** A mapping from the mapping form: Cloudflare's product and meter (or `*`) to one of g1t's products. */
141export function parseMapping(form: FormData): Parsed<CostMappingInput> {
142 const value = (name: string) => String(form.get(name) ?? "").trim();
143 const product = value("product").toLowerCase();
144 const meter = value("meter").toLowerCase() || "*";
145 const remove = form.get("remove") === "1";
146 if (!NAME.test(product) || product === "*") return { ok: false, error: "Cloudflare's product, as the lines table names it." };
147 if (!NAME.test(meter)) return { ok: false, error: "A meter prefix as the lines table names it, or * for all of the product." };
148 if (remove) return { ok: true, value: { product, meter, remove: true } };
149 const bucket = value("bucket").toLowerCase();
150 if (!NAME.test(bucket) || bucket === "*") return { ok: false, error: "Which of g1t's products it is a cost of." };
151 const drift = value("driftPercent");
152 const driftPercent = drift === "" ? null : Number(drift);
153 if (driftPercent != null && !(driftPercent > 0 && driftPercent <= 1000)) return { ok: false, error: "Drift is a percentage above zero." };
154 return {
155 ok: true,
156 value: {
157 product,
158 meter,
159 bucket,
160 priceMeter: value("priceMeter") || null,
161 ownMeter: value("ownMeter") || null,
162 scaleToOwn: form.get("scaleToOwn") === "on",
163 driftPercent,
164 note: value("note").slice(0, 200),
165 },
166 };
167}
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays168
169// --- g1t's own spend (billing's budget) ---------------------------------------
170
171/**
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging172 * The red bar on every sudo page: the daily breaker open, or a 100%-discount
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays173 * account's monthly budget used up. Null when neither.
174 */
175export function spendBanner(caps: SpendCaps): string | null {
176 const parts: string[] = [];
177 if (caps.tripped) {
178 parts.push(
179 `the daily breaker is open (${usd(caps.todayMicros)} of ${usd(caps.dailyCapMicros)} today), so new hosted-model agent runs g1t pays for wait until 00:00 UTC`,
180 );
181 }
182 for (const budget of caps.comped) {
183 if (budget.ceilingMicros > 0 && budget.usedMicros >= budget.ceilingMicros) {
184 parts.push(`${budget.name} used its ${usd(budget.ceilingMicros)} monthly budget, so new work on it is refused`);
185 }
186 }
187 if (parts.length === 0) return null;
188 const text = parts.join("; and ");
189 return `${text.charAt(0).toUpperCase()}${text.slice(1)}.`;
190}
191
192/** What g1t paid this month, by bucket, with the free tier and Cloudflare's subscriptions; and the total. */
193export function spendRows(caps: SpendCaps): { rows: { key: string; title: string; micros: number; note: string }[]; totalMicros: number } {
194 const notes: Record<string, string> = {
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging195 comped: "Work on accounts with a 100% discount, at cost",
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays196 trial: "Trial credit, at cost",
197 oss: "Checks and workflows on public repositories, at cost",
198 given: "Free workspaces' overruns past their trial",
199 unpaid: "Charged, but no real money yet (test-mode payments)",
200 };
201 const rows = caps.monthBuckets.map((b) => ({ key: b.bucket, title: b.title, micros: b.micros, note: notes[b.bucket] ?? "" }));
202 rows.push({ key: "free", title: "Free tier", micros: caps.freeTierMicros, note: "Free workspaces' share of git, storage and platform, reconciled through yesterday" });
Costs: Cloudflare's subscriptions read from Cloudflare each day, the estimate only until then; sudo's costs split into Costs & margin and Bill & pricing203 rows.push({
204 key: "fixed",
205 title: "Cloudflare subscriptions",
206 micros: caps.fixedMonthlyMicros,
207 note: caps.fixedSource === "cloudflare" ? "A month, as Cloudflare lists them" : "A month, estimated (CLOUDFLARE_FIXED_MONTHLY_MICROS)",
208 });
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays209 return { rows, totalMicros: rows.reduce((sum, row) => sum + row.micros, 0) };
210}
211
212/** How far a cap is used, 0 to 100, for a meter. */
213export function capPercent(usedMicros: number, capMicros: number): number {
214 if (capMicros <= 0) return 0;
215 return Math.max(0, Math.min(100, (usedMicros / capMicros) * 100));
216}