Skip to content
363 linesCodeBlameRaw
1/**
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 */
5import type { BillRead, CloudflareCycle, CostDay, CostMappingInput, CostSettings, CycleMeter, OverallMargin, PauseLevel, PlatformGuard, SpendCaps } from "@g1t/contracts";
6
7import { parseDollars, usd } from "./money.ts";
8
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/**
48 * The margin on the price a markup gives, in percent: cost plus 20% is a
49 * 16.7% margin, since the 20% is of the cost and the margin of the price.
50 */
51export function marginOnPrice(markupPercent: number): number {
52 return markupPercent > -100 ? (markupPercent / (100 + markupPercent)) * 100 : 0;
53}
54
55/**
56 * Who g1t paid over the range: Cloudflare's usage bill, its subscriptions
57 * (a month's, over the range) and the model providers. The same total as
58 * the statement's All in.
59 */
60export function whoPaid(
61 overall: { costMicros: number; cloudflareCostMicros?: number; modelsCostMicros?: number },
62 subscriptionsMicros: number,
63): { totalMicros: number; cloudflareMicros: number; subscriptionsMicros: number; modelsMicros: number } {
64 const modelsMicros = overall.modelsCostMicros ?? 0;
65 const cloudflareMicros = overall.cloudflareCostMicros ?? overall.costMicros - modelsMicros;
66 return { totalMicros: overall.costMicros + subscriptionsMicros, cloudflareMicros, subscriptionsMicros, modelsMicros };
67}
68
69/**
70 * Cloudflare's subscriptions over the range: billing's figure (each day its
71 * billing cycle's share, the same accrual as the month view), or from a
72 * billing that does not send one, a month's over 30 days.
73 */
74export function subscriptionsOver(monthlyMicros: number, days: number, overall?: Pick<OverallMargin, "subscriptionsMicros">): number {
75 if (typeof overall?.subscriptionsMicros === "number") return overall.subscriptionsMicros;
76 return Math.round((monthlyMicros * days) / 30);
77}
78
79/** What was given away, by why, for the statement: only the ones with any. */
80export function givenParts(o: OverallMargin): [string, number][] {
81 return (
82 [
83 ["100% discounts", o.givenCompedMicros ?? 0],
84 ["free use", o.givenFreeMicros ?? 0],
85 ["trial", o.givenTrialMicros ?? 0],
86 ["open-source pool", o.givenPoolMicros ?? 0],
87 ["partial discounts", o.givenDiscountMicros ?? 0],
88 ["promotional credit", o.givenCreditPromotionalMicros ?? 0],
89 ["goodwill credit", o.givenCreditGoodwillMicros ?? 0],
90 ["testing resets", o.givenResetMicros ?? 0],
91 ["charged without real money", o.givenUnpaidMicros ?? 0],
92 ] as [string, number][]
93 ).filter(([, micros]) => micros > 0);
94}
95
96/** How a cycle meter's cost was arrived at, in words. */
97export function basisLabel(basis: string): string {
98 if (basis === "cloudflare") return "Cloudflare's cost";
99 if (basis === "list") return "List price past the included amount";
100 return "No list price: counted at $0";
101}
102
103/** A cycle's headline: cost so far, the projection and the average day, as Cloudflare's Billable usage page puts them. */
104export function cycleHeadline(cycle: CloudflareCycle): { title: string; detail: string } {
105 return {
106 title: `${cycle.start} to ${cycle.end}, day ${cycle.daysElapsed} of ${cycle.days}`,
107 detail: `${usd(cycle.usageMicros, { cents: true })} so far, ${usd(cycle.averageDailyMicros, { cents: true })} a day; projected ${usd(cycle.projectedMicros, { cents: true })} for the cycle, and ${usd(cycle.subscriptionsMicros, { cents: true })} of subscriptions`,
108 };
109}
110
111/** Meters with no list price that were used: costed at $0 until one is added. */
112export function unpricedMeters(meters: CycleMeter[]): CycleMeter[] {
113 return meters.filter((m) => m.basis === "none" && m.quantity > 0);
114}
115
116/** What the last read of the bill got, in a sentence, and whether it looks incomplete. */
117export function billReadNote(read: BillRead): { text: string; warn: boolean } {
118 const parts = [`${read.rows.toLocaleString("en-US")} rows in ${read.pages} ${read.pages === 1 ? "page" : "pages"} for ${read.since} to ${read.until}`];
119 if (read.pricingOnlyRows > 0) parts.push(`${read.pricingOnlyRows} with only a pricing quantity, which can be in blocks`);
120 parts.push(read.costedRows > 0 ? `${read.costedRows} with Cloudflare's own cost` : "none with a cost of Cloudflare's, so the list prices apply");
121 return { text: `${parts.join("; ")}.`, warn: read.rows === 0 || read.pricingOnlyRows > 0 };
122}
123
124/**
125 * A price version's cost and price as the table shows them. A rate g1t
126 * sets has no cost behind it; a weight is a multiplier, not money.
127 */
128export function versionCells(v: { costMicros: number; priceMicros: number; basis?: string }): { cost: string; price: string; note: string | null } {
129 if (v.basis === "weight") {
130 const weight = Number((v.costMicros / 1_000_000).toFixed(6));
131 return { cost: "—", price: `×${weight}`, note: "A weight on the agent rate's tokens, not money" };
132 }
133 if (v.basis === "rate") return { cost: "—", price: unitDollars(v.priceMicros), note: "g1t's own rate: no cost behind it" };
134 return { cost: unitDollars(v.costMicros), price: unitDollars(v.priceMicros), note: null };
135}
136
137/**
138 * What became of a proposal, in words, for its line: who decided it, and
139 * when one never took effect because a later measurement replaced it.
140 */
141export function proposalOutcome(p: { status: string; decidedBy: string | null; effectiveAt: string | null }): string | null {
142 const by = p.decidedBy ? `${p.status === "superseded" ? "applied" : p.status} by ${p.decidedBy}` : null;
143 const replaced = "replaced by a later measurement before it took effect; nothing was charged at it";
144 if (p.status === "superseded") return by ? `${by}, then ${replaced}` : "replaced by a later measurement";
145 if ((p.status === "applied" || p.status === "approved") && !p.effectiveAt) return by ? `${by}, then ${replaced}` : replaced;
146 return by;
147}
148
149/** Margin as a whole percent of revenue; none when there was none. */
150export function marginPercent(revenueMicros: number, costMicros: number): number | null {
151 return revenueMicros > 0 ? ((revenueMicros - costMicros) / revenueMicros) * 100 : null;
152}
153
154/** `12.3%`, `−4.0%`, or a dash. */
155export function percentLabel(percent: number | null | undefined, { signed = false }: { signed?: boolean } = {}): string {
156 if (percent == null || !Number.isFinite(percent)) return "—";
157 const text = `${Math.abs(percent).toFixed(1)}%`;
158 if (percent < 0) return `−${text}`;
159 return signed && percent > 0 ? `+${text}` : text;
160}
161
162/** How a margin reads against the floor: under it is danger, close to it a warning. */
163export function marginTone(percent: number | null | undefined, floor: number): "danger" | "warn" | "mint" | undefined {
164 if (percent == null) return undefined;
165 if (percent < floor) return "danger";
166 if (percent < floor + 5) return "warn";
167 return "mint";
168}
169
170/** A count: `1,234` or `1.2M`. */
171export function countLabel(value: number): string {
172 if (!Number.isFinite(value)) return "—";
173 if (Math.abs(value) >= 10_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
174 return Math.round(value).toLocaleString("en-US");
175}
176
177/** What a kind of drift is called. */
178export function driftLabel(kind: string): string {
179 return { count: "Count", cost: "Cost", leak: "Leak" }[kind] ?? kind;
180}
181
182/** A cost per unit in micros, as dollars with as many places as it needs: `$0.000016`, `$0.15`. */
183export function unitDollars(micros: number): string {
184 const dollars = micros / 1_000_000;
185 if (dollars === 0) return "$0";
186 if (Math.abs(dollars) >= 1) return `$${dollars.toFixed(2)}`;
187 const places = Math.min(10, Math.max(2, 2 - Math.floor(Math.log10(Math.abs(dollars)))));
188 return `$${dollars.toFixed(places)}`;
189}
190
191/** The range shown: 7 to 90 days, 30 when not said. */
192export function parseRange(raw: string | null): number {
193 const days = Number(raw);
194 return Number.isInteger(days) && days >= 7 && days <= 90 ? days : 30;
195}
196
197/** The product to chart, if it is one the page has. */
198export function parseBucket(raw: string | null, known: string[]): string | null {
199 return raw && known.includes(raw) ? raw : null;
200}
201
202export type Parsed<T> = { ok: true; value: T } | { ok: false; error: string };
203
204function numberField(form: FormData, name: string): number {
205 const raw = String(form.get(name) ?? "").trim();
206 return raw === "" ? Number.NaN : Number(raw);
207}
208
209/** The guardrails from the settings form. */
210export function parseCostSettings(form: FormData): Parsed<CostSettings> {
211 const autoApplyPercent = numberField(form, "autoApplyPercent");
212 const noticeDays = numberField(form, "noticeDays");
213 const marginFloorPercent = numberField(form, "marginFloorPercent");
214 const alertDays = numberField(form, "alertDays");
215 const anomalyFactor = numberField(form, "anomalyFactor");
216 const minDaily = parseDollars(String(form.get("minDailyCost") ?? ""));
217 const anomalyFloor = parseDollars(String(form.get("anomalyFloor") ?? ""));
218 if (!(autoApplyPercent >= 0 && autoApplyPercent <= 100)) return { ok: false, error: "The guardrail is a percentage from 0 to 100." };
219 if (!(Number.isInteger(noticeDays) && noticeDays >= 0 && noticeDays <= 90)) return { ok: false, error: "Notice is 0 to 90 days." };
220 if (!(marginFloorPercent >= -100 && marginFloorPercent <= 100)) return { ok: false, error: "The margin floor is a percentage." };
221 if (!(Number.isInteger(alertDays) && alertDays >= 1 && alertDays <= 30)) return { ok: false, error: "Alert after 1 to 30 days." };
222 if (!(anomalyFactor > 0 && anomalyFactor <= 100)) return { ok: false, error: "The factor is a positive number." };
223 if (minDaily == null || anomalyFloor == null) return { ok: false, error: "Amounts are dollars to the cent." };
224 return {
225 ok: true,
226 value: {
227 autoApply: form.get("autoApply") === "on",
228 autoApplyPercent,
229 noticeDays,
230 marginFloorPercent,
231 alertDays,
232 minDailyCostMicros: minDaily,
233 anomalyFactor,
234 anomalyFloorMicros: anomalyFloor,
235 cardFee: form.get("cardFee") === "on",
236 },
237 };
238}
239
240const NAME = /^(\*|[a-z0-9][a-z0-9_]{0,79})$/;
241
242/** A mapping from the mapping form: Cloudflare's product and meter (or `*`) to one of g1t's products. */
243export function parseMapping(form: FormData): Parsed<CostMappingInput> {
244 const value = (name: string) => String(form.get(name) ?? "").trim();
245 const product = value("product").toLowerCase();
246 const meter = value("meter").toLowerCase() || "*";
247 const remove = form.get("remove") === "1";
248 if (!NAME.test(product) || product === "*") return { ok: false, error: "Cloudflare's product, as the lines table names it." };
249 if (!NAME.test(meter)) return { ok: false, error: "A meter prefix as the lines table names it, or * for all of the product." };
250 if (remove) return { ok: true, value: { product, meter, remove: true } };
251 const bucket = value("bucket").toLowerCase();
252 if (!NAME.test(bucket) || bucket === "*") return { ok: false, error: "Which of g1t's products it is a cost of." };
253 const drift = value("driftPercent");
254 const driftPercent = drift === "" ? null : Number(drift);
255 if (driftPercent != null && !(driftPercent > 0 && driftPercent <= 1000)) return { ok: false, error: "Drift is a percentage above zero." };
256 return {
257 ok: true,
258 value: {
259 product,
260 meter,
261 bucket,
262 priceMeter: value("priceMeter") || null,
263 ownMeter: value("ownMeter") || null,
264 scaleToOwn: form.get("scaleToOwn") === "on",
265 driftPercent,
266 note: value("note").slice(0, 200),
267 },
268 };
269}
270
271// --- g1t's own spend (billing's budget) ---------------------------------------
272
273/**
274 * The red bar on every sudo page: the daily breaker open, or a 100%-discount
275 * account's monthly budget used up. Null when neither.
276 */
277export function spendBanner(caps: SpendCaps): string | null {
278 const parts: string[] = [];
279 if (caps.tripped) {
280 parts.push(
281 `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`,
282 );
283 }
284 for (const budget of caps.comped) {
285 if (budget.ceilingMicros > 0 && budget.usedMicros >= budget.ceilingMicros) {
286 parts.push(`${budget.name} used its ${usd(budget.ceilingMicros)} monthly budget, so new work on it is refused`);
287 }
288 }
289 if (parts.length === 0) return null;
290 const text = parts.join("; and ");
291 return `${text.charAt(0).toUpperCase()}${text.slice(1)}.`;
292}
293
294/** What g1t paid this month, by bucket, with the free tier and Cloudflare's subscriptions; and the total. */
295export function spendRows(caps: SpendCaps): { rows: { key: string; title: string; micros: number; note: string }[]; totalMicros: number } {
296 const notes: Record<string, string> = {
297 comped: "Work on accounts with a 100% discount: what it cost g1t, not its price",
298 trial: "Trial credit, at what it cost g1t",
299 oss: "Checks and workflows on public repositories, at what they cost g1t",
300 given: "Free workspaces' overruns past their trial, at what they cost g1t",
301 unpaid: "Usage charged while payments are in Stripe's test mode: what it cost g1t, not what was charged",
302 };
303 const rows = caps.monthBuckets.map((b) => ({ key: b.bucket, title: b.title, micros: b.micros, note: notes[b.bucket] ?? "" }));
304 rows.push({ key: "free", title: "Free tier", micros: caps.freeTierMicros, note: "Free workspaces' share of git, storage and platform, as last reconciled" });
305 const monthly = usd(caps.fixedMonthlyMicros, { cents: true });
306 const accrued = typeof caps.fixedMonthMicros === "number";
307 rows.push({
308 key: "fixed",
309 title: "Cloudflare subscriptions",
310 micros: accrued ? (caps.fixedMonthMicros as number) : caps.fixedMonthlyMicros,
311 note: accrued
312 ? `This month's days so far, each its billing cycle's share of ${monthly} a month${caps.fixedSource === "cloudflare" ? ", as Cloudflare lists them" : ", estimated (CLOUDFLARE_FIXED_MONTHLY_MICROS)"}`
313 : caps.fixedSource === "cloudflare"
314 ? "The whole month, as Cloudflare lists them"
315 : "The whole month, estimated (CLOUDFLARE_FIXED_MONTHLY_MICROS)",
316 });
317 return { rows, totalMicros: rows.reduce((sum, row) => sum + row.micros, 0) };
318}
319
320/** How far a cap is used, 0 to 100, for a meter. */
321export function capPercent(usedMicros: number, capMicros: number): number {
322 if (capMicros <= 0) return 0;
323 return Math.max(0, Math.min(100, (usedMicros / capMicros) * 100));
324}
325
326// --- Platform pause and usage watch (billing's platform.rs) -----------------
327
328/** What each level of the platform pause stops, for the page and the banner. */
329export const PAUSE_LEVELS: { level: PauseLevel; title: string; stops: string }[] = [
330 { level: "compute", title: "Compute", stops: "New agent runs, checks, workflow jobs and deploy builds, for every workspace. Runs already going finish." },
331 { level: "schedules", title: "Schedules", stops: "Actions' cron-triggered runs, and the runner's sweep that starts queued agents." },
332 { level: "indexing", title: "Indexing", stops: "Context embeddings and backfills, and search's backfills (they go on from where they were when resumed)." },
333 { level: "renders", title: "Renders", stops: "Social card images: a cache miss gets the brand card or the static logo." },
334];
335
336/** Whether a form's level is one of the four. */
337export function parsePauseLevel(value: unknown): PauseLevel | null {
338 const level = String(value ?? "");
339 return PAUSE_LEVELS.some((l) => l.level === level) ? (level as PauseLevel) : null;
340}
341
342/** The red bar on every sudo page while any level is paused. Null when none is. */
343export function pauseBanner(guard: PlatformGuard): string | null {
344 const paused = guard.levels.filter((l) => l.paused);
345 if (paused.length === 0) return null;
346 const names = paused.map((l) => (l.auto ? `${l.level} (by the usage watcher)` : l.level));
347 const list = names.length === 1 ? names[0] : `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
348 return `Paused across g1t: ${list}.`;
349}
350
351/** `1.2M`, `240k`, `2.0B`: a count in a few characters. */
352export function count(value: number): string {
353 const n = Math.abs(value);
354 if (n >= 1e9) return `${(value / 1e9).toFixed(1)}B`;
355 if (n >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
356 if (n >= 1e3) return `${Math.round(value / 1e3)}k`;
357 return `${Math.round(value)}`;
358}
359
360/** An hour's value as a share of its threshold, rounded; null with no threshold. */
361export function thresholdShare(value: number, threshold: number): number | null {
362 return threshold > 0 ? Math.round((value / threshold) * 100) : null;
363}