Skip to content
2,778 linesCodeBlameRaw

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.

Topics sit in About on the files pages, under the description1//! What g1t earns on each thing it sells, measured against what
2//! Cloudflare actually charged for it.
3//!
4//! Once a day, after `costs` has read Cloudflare's bill, the reconciler
5//! puts three figures side by side for every day and each of g1t's
6//! products (a "bucket": sandboxes, deployments, git, repository storage,
7//! …):
8//!
9//! 1. **What Cloudflare charged**: the day's cost lines `cost_map` gives
10//! the bucket.
11//! 2. **What g1t's meters recorded**: the cost on the ledger's entries for
12//! it (the price book's cost at the time) and, where a mapping names
13//! one, g1t's own count of the same units (git operations).
14//! 3. **What customers were charged**: the entries' value at price, before
15//! the plan's included usage, a trial or a pool paid part of it; and of
16//! that, what workspaces paid. Month-end meters (git, storage, scans,
17//! embeddings, the actions cache) come from daily snapshots of what they
18//! had come to (`pending_days`). The plan's price is the `platform`
19//! bucket's: the plan pays for running g1t.
20//!
21//! From those: margin per product (value against cost) and for all of g1t
22//! (money in against every cost); drift (counts or costs that disagree past
23//! a mapping's threshold, and leaks: cost with no revenue, or a Cloudflare
24//! meter no one mapped); each workspace's cost, Cloudflare's figure shared
25//! out by each workspace's own meters; and price proposals when a unit's
26//! real cost has moved (`pricing`). Alerts go to staff by email and as a
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.27//! banner in sudo.
Topics sit in About on the files pages, under the description28
29use std::collections::{BTreeMap, BTreeSet};
30
31use g1t_contracts::billing::*;
32use g1t_contracts::{FailureCode, Outcome, new_id};
33use g1t_contracts::time::rfc3339;
34use g1t_kit::now_ms;
35use serde::{Deserialize, Serialize};
36use worker::wasm_bindgen::JsValue;
37use worker::{Env, Result};
38
39use crate::Billing;
40use crate::costs::{self, ARTIFACTS_OPERATIONS, DAY_MS, Rule, SOURCE_ARTIFACTS, SOURCE_BILLABLE, UNMAPPED};
41
42/// Buckets that are the cost of running g1t, paid by the plan rather than
43/// sold by the unit: never a leak for having no revenue of their own.
44pub(crate) const OVERHEAD: [&str; 1] = ["platform"];
45/// Buckets Cloudflare does not bill: their cost is g1t's own figure.
46pub(crate) const NOT_CLOUDFLARE: [&str; 1] = ["models"];
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises47/// The key an agent's planning run is reconciled under. Its ledger task
48/// is `plan`, which is also the plan's payments' key (`revenue_map`: the
49/// platform bucket), so read as `plan` its model cost went to running g1t,
50/// where Cloudflare's bill is the cost, and was lost. No `revenue_map`
51/// row: models, like every agent run.
52pub(crate) const PLANNING_KEY: &str = "planning";
Topics sit in About on the files pages, under the description53/// The days drift is judged over.
54const DRIFT_DAYS: u64 = 7;
55/// The days a workspace's cost is set against its revenue.
56const ANOMALY_DAYS: u64 = 30;
57/// The days a unit's cost is measured over.
58const MEASURE_DAYS: u64 = 30;
59/// Fewer of g1t's units than this say nothing about cost per unit.
60const MIN_UNITS: f64 = 1_000.0;
61/// An open alert is emailed again after this long.
62const REMIND_MS: u64 = 7 * DAY_MS;
63
64// ---------------------------------------------------------------------
65// The arithmetic, apart from the database so it can be tested.
66// ---------------------------------------------------------------------
67
68/// One of g1t's products on one day.
69#[derive(Clone, Debug, Default, PartialEq)]
70pub(crate) struct ProductDay {
71 pub day: String,
72 pub bucket: String,
73 /// What Cloudflare charged g1t, in millionths of a dollar.
74 pub cf_cost_micros: i64,
75 /// What g1t's meters recorded it cost (the price book's cost).
76 pub own_cost_micros: i64,
77 /// What customers were charged for it at price, before what paid.
78 pub value_micros: i64,
79 /// Of that, what workspaces paid themselves.
80 pub cash_micros: i64,
81 /// Units Cloudflare counted and units g1t counted, where a mapping
82 /// says they are the same units.
83 pub cf_quantity: f64,
84 pub own_quantity: f64,
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it85 /// Of `cost()`, what went on usage g1t gave away (the workspaces'
86 /// `WorkspaceDay::given`, added up).
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running87 pub given: Given,
Topics sit in About on the files pages, under the description88}
89
90impl ProductDay {
91 /// What it cost: Cloudflare's figure where Cloudflare bills it, else
92 /// g1t's own (models are billed by their providers, through the gateway).
93 pub fn cost(&self) -> i64 {
94 if NOT_CLOUDFLARE.contains(&self.bucket.as_str()) { self.own_cost_micros } else { self.cf_cost_micros }
95 }
96}
97
98/// A line of Cloudflare's bill, as stored.
99#[derive(Clone, Debug, Deserialize)]
100pub(crate) struct LineRow {
101 pub day: String,
102 pub source: String,
103 pub product: String,
104 pub meter: String,
105 pub quantity: f64,
106 pub cost_usd: f64,
107}
108
109/// A count of g1t's own, as stored.
110#[derive(Clone, Debug, Deserialize)]
111pub(crate) struct OwnRow {
112 pub day: String,
113 pub meter: String,
114 pub workspace: String,
115 pub quantity: f64,
116}
117
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running118/// What g1t gave away, by why: its own comped workspaces, free use (a
119/// free period, free allowances, overruns g1t covered), the trial, and the
Merge branch 'worktree-agent-a633ac0f7f66d419d'120/// open-source pool, and discounts on an account's terms (what they took
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging121/// below cost plus the margin, `ledger.discount_micros`), and credits g1t
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97122/// staff gave, promotional and goodwill, when spent (`grants`), and usage a
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)123/// testing reset wiped (`reset_costs`): g1t paid for it and nobody will;
124/// and what was charged while payments were not live (Stripe's test mode),
125/// which brought in no real money (`without_real_money`).
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97126/// The Team plan's included usage is paid for by the plan's price, so it is
127/// sold, not given; so is what a refund pays for.
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running128#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
129pub(crate) struct Given {
130 pub comped: i64,
131 pub free: i64,
132 pub trial: i64,
133 pub pool: i64,
Merge branch 'worktree-agent-a633ac0f7f66d419d'134 pub discount: i64,
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging135 pub credit_promotional: i64,
136 pub credit_goodwill: i64,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97137 pub reset: i64,
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)138 pub unpaid: i64,
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running139}
140
141impl Given {
142 pub fn total(&self) -> i64 {
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)143 self.comped + self.free + self.trial + self.pool + self.discount + self.credit() + self.reset + self.unpaid
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging144 }
145
146 /// Credits from g1t, both kinds.
147 pub fn credit(&self) -> i64 {
148 self.credit_promotional + self.credit_goodwill
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running149 }
150
151 fn add(&mut self, other: &Given) {
152 self.comped += other.comped;
153 self.free += other.free;
154 self.trial += other.trial;
155 self.pool += other.pool;
Merge branch 'worktree-agent-a633ac0f7f66d419d'156 self.discount += other.discount;
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging157 self.credit_promotional += other.credit_promotional;
158 self.credit_goodwill += other.credit_goodwill;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97159 self.reset += other.reset;
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)160 self.unpaid += other.unpaid;
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running161 }
162
163 /// The same shares of `cost` as these are of `value`, at most all of it.
164 fn of(&self, cost: i64, value: i64) -> Given {
165 let total = self.total();
166 if value <= 0 || cost <= 0 || total <= 0 {
167 return Given::default();
168 }
169 let given = cost as i128 * total.min(value) as i128 / value as i128;
170 let part = |x: i64| (given * x.max(0) as i128 / total as i128) as i64;
Merge branch 'worktree-agent-a633ac0f7f66d419d'171 Given {
172 comped: part(self.comped),
173 free: part(self.free),
174 trial: part(self.trial),
175 pool: part(self.pool),
176 discount: part(self.discount),
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging177 credit_promotional: part(self.credit_promotional),
178 credit_goodwill: part(self.credit_goodwill),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97179 reset: part(self.reset),
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)180 unpaid: part(self.unpaid),
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging181 }
182 }
183}
184
185/// Credits from g1t in the reconciliation (`grants`): usage paid for with
186/// promotional or goodwill credit is given, not money in; a refund comes
187/// off money in on the day it refunds, shared over that day's paid usage.
188/// What credit paid for that is not among `rows` (month-end meters, or
189/// what was owed from before) is a row of its own on its day.
190pub(crate) fn apply_credits(rows: &mut Vec<UsageRow>, draws: &[(String, crate::grants::Draw)], refunds: &[crate::grants::Refunded]) {
191 let mut paid: BTreeMap<(String, String, String), Given> = BTreeMap::new();
192 for (workspace, draw) in draws {
193 let given = paid
194 .entry((draw.at[..10].to_owned(), workspace.clone(), crate::grants::usage_key(draw.task.as_deref(), &draw.reference)))
195 .or_default();
196 match draw.kind {
197 CreditKind::Promotional => given.credit_promotional += draw.micros,
198 CreditKind::Goodwill => given.credit_goodwill += draw.micros,
199 // Money already paid: what it pays for is paid for.
200 CreditKind::Refund | CreditKind::Purchased => {}
201 }
202 }
203 for ((day, workspace, key), given) in paid {
204 if given.credit() == 0 {
205 continue;
Merge branch 'worktree-agent-a633ac0f7f66d419d'206 }
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging207 match rows.iter_mut().find(|r| r.day == day && r.workspace == workspace && r.key == key) {
208 Some(row) => {
209 row.cash -= given.credit();
210 row.given.add(&given);
211 }
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97212 None => rows.push(UsageRow { day, workspace, key, bucket: None, value: 0, cash: -given.credit(), cost: 0, given }),
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging213 }
214 }
215 for refund in refunds {
216 let weights: Vec<(String, f64)> = rows
217 .iter()
218 .enumerate()
219 .filter(|(_, r)| r.day == refund.day && r.workspace == refund.workspace && r.cash > 0)
220 .map(|(i, r)| (format!("{i:08}"), r.cash as f64))
221 .collect();
222 let shares = attribute(refund.micros, &weights);
223 if shares.is_empty() {
224 rows.push(UsageRow {
225 day: refund.day.clone(),
226 workspace: refund.workspace.clone(),
227 key: "other".into(),
228 cash: -refund.micros,
229 ..UsageRow::default()
230 });
231 }
232 for (index, micros) in shares {
233 if let Ok(i) = index.parse::<usize>() {
234 rows[i].cash -= micros;
235 }
236 }
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running237 }
238}
239
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)240/// Money in only where it is real. Charges made while payments were not
241/// live (Stripe's test mode) brought in nothing: a row's cash from before
242/// `live_since` (all of it while payments are not live, `None`) is taken
243/// out of cash and counted as given away (`unpaid`), so it is never money
244/// in, never margin, and never what a workspace paid.
245pub(crate) fn without_real_money(rows: &mut [UsageRow], live_since: Option<&str>) {
246 for row in rows {
247 if live_since.is_some_and(|since| row.day.as_str() >= since) || row.cash == 0 {
248 continue;
249 }
250 // What else gave it away already (a 100% discount) stays that.
251 row.given.unpaid += row.cash.min(row.value - row.given.total()).max(0);
252 row.cash = 0;
253 }
254}
255
256/// The day payments went live, from `cost_settings` (`payments_live_since`)
257/// as a value read there: None while they are not live, the day kept when
258/// they are, else `today` (the first time they are seen live).
259pub(crate) fn live_since(live: bool, kept: Option<&str>, today: &str) -> Option<String> {
260 if !live {
261 return None;
262 }
263 Some(kept.filter(|d| d.len() >= 10).map_or_else(|| today.to_owned(), |d| d[..10].to_owned()))
264}
265
Topics sit in About on the files pages, under the description266/// What a workspace was charged for one key on one day.
267#[derive(Clone, Debug, Default, PartialEq)]
268pub(crate) struct UsageRow {
269 pub day: String,
270 pub workspace: String,
271 /// A ledger task (or `builds`), a month-end source, or `plan`.
272 pub key: String,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97273 /// The bucket, where it is known already (what a testing reset kept,
274 /// `reset_costs`); else `key`'s, from `revenue_map`.
275 pub bucket: Option<String>,
Topics sit in About on the files pages, under the description276 pub value: i64,
277 pub cash: i64,
278 pub cost: i64,
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it279 /// Of `value`, what g1t gave away: all of it for g1t's own (comped)
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running280 /// workspaces and in a free period, else what the trial and the pool
281 /// paid and the overruns g1t covered.
282 pub given: Given,
Topics sit in About on the files pages, under the description283}
284
285/// One workspace's share of a product's cost on one day.
286#[derive(Clone, Debug, PartialEq)]
287pub(crate) struct WorkspaceDay {
288 pub day: String,
289 pub workspace: String,
290 pub bucket: String,
291 pub cost: i64,
Margin alerts measure what is sold, and say dollars when a percentage would mislead292 /// What the workspace paid in cash.
Topics sit in About on the files pages, under the description293 pub revenue: i64,
Margin alerts measure what is sold, and say dollars when a percentage would mislead294 /// What its usage was priced at, whoever paid for it.
295 pub value: i64,
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running296 /// Of `cost`, the part g1t gave away: all of it for a comped workspace
297 /// or one with nothing priced that day (free use), else the cost times
298 /// the shares of its usage that day that g1t paid for.
299 pub given: Given,
Topics sit in About on the files pages, under the description300}
301
302fn micros(dollars: f64) -> i64 {
303 (dollars * 1_000_000.0).round() as i64
304}
305
306/// Puts the day's bill, g1t's counts and what customers were charged side
307/// by side, a row per day and bucket, and shares each bucket's cost out
308/// to workspaces.
309pub(crate) fn fold(
310 rules: &[Rule],
311 revenue_map: &BTreeMap<String, String>,
312 lines: &[LineRow],
313 own: &[OwnRow],
314 usage: &[UsageRow],
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running315 internal: &BTreeSet<String>,
Topics sit in About on the files pages, under the description316) -> (Vec<ProductDay>, Vec<WorkspaceDay>) {
317 let mut days: BTreeMap<(String, String), ProductDay> = BTreeMap::new();
318 let entry = |day: &str, bucket: &str| -> ProductDay {
319 ProductDay { day: day.to_owned(), bucket: bucket.to_owned(), ..ProductDay::default() }
320 };
321 // Which of g1t's own meters count each bucket's units.
322 let mut own_meters: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
323 for rule in rules {
324 if let Some(meter) = &rule.own_meter {
325 own_meters.entry(rule.bucket.as_str()).or_default().insert(meter.as_str());
326 }
327 }
328 let mut events: BTreeMap<(String, String), f64> = BTreeMap::new();
329 for line in lines {
330 let rule = costs::classify(rules, &line.product, &line.meter);
331 let bucket = rule.map_or(UNMAPPED, |r| r.bucket.as_str());
332 let key = (line.day.clone(), bucket.to_owned());
333 if line.source == SOURCE_ARTIFACTS {
334 // What Artifacts counted: operations only, and only where the
335 // bill does not count them itself.
336 if ARTIFACTS_OPERATIONS.contains(&line.meter.as_str()) {
337 *events.entry(key).or_default() += line.quantity;
338 }
339 continue;
340 }
341 let row = days.entry(key.clone()).or_insert_with(|| entry(&key.0, &key.1));
342 row.cf_cost_micros += micros(line.cost_usd);
343 if line.source == SOURCE_BILLABLE && rule.is_some_and(|r| r.own_meter.is_some()) {
344 row.cf_quantity += line.quantity;
345 }
346 }
347 for (key, quantity) in events {
348 let row = days.entry(key.clone()).or_insert_with(|| entry(&key.0, &key.1));
349 if row.cf_quantity == 0.0 {
350 row.cf_quantity = quantity;
351 }
352 }
353 // g1t's own counts of the same units, by bucket and by workspace.
354 let mut own_by: BTreeMap<(String, String), Vec<(String, f64)>> = BTreeMap::new();
355 // Cloudflare's own count by workspace, where it gives one
356 // (`cloudflare_<bucket>`): the best way to share its cost.
357 let mut cf_by: BTreeMap<(String, String), Vec<(String, f64)>> = BTreeMap::new();
358 for count in own {
359 if let Some(bucket) = count.meter.strip_prefix("cloudflare_") {
360 cf_by.entry((count.day.clone(), bucket.to_owned())).or_default().push((count.workspace.clone(), count.quantity));
361 continue;
362 }
363 for (bucket, meters) in &own_meters {
364 if meters.contains(count.meter.as_str()) {
365 let key = (count.day.clone(), (*bucket).to_owned());
366 days.entry(key.clone()).or_insert_with(|| entry(&key.0, &key.1)).own_quantity += count.quantity;
367 own_by.entry(key).or_default().push((count.workspace.clone(), count.quantity));
368 }
369 }
370 }
371 // What customers were charged.
372 let bucket_of = |key: &str| revenue_map.get(key).cloned().unwrap_or_else(|| "models".to_owned());
373 let mut value_by: BTreeMap<(String, String), Vec<(String, f64)>> = BTreeMap::new();
374 let mut cost_by: BTreeMap<(String, String), Vec<(String, f64)>> = BTreeMap::new();
375 let mut revenue: BTreeMap<(String, String, String), i64> = BTreeMap::new();
Margin alerts measure what is sold, and say dollars when a percentage would mislead376 let mut valued: BTreeMap<(String, String, String), i64> = BTreeMap::new();
Topics sit in About on the files pages, under the description377 let mut active: BTreeMap<String, Vec<(String, f64)>> = BTreeMap::new();
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running378 let mut gave: BTreeMap<(String, String), (Given, i64)> = BTreeMap::new();
Topics sit in About on the files pages, under the description379 for u in usage {
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it380 let g = gave.entry((u.day.clone(), u.workspace.clone())).or_default();
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running381 g.0.add(&u.given);
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it382 g.1 += u.value;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97383 let bucket = u.bucket.clone().unwrap_or_else(|| bucket_of(&u.key));
Topics sit in About on the files pages, under the description384 let key = (u.day.clone(), bucket.clone());
385 let row = days.entry(key.clone()).or_insert_with(|| entry(&key.0, &key.1));
386 row.own_cost_micros += u.cost;
387 row.value_micros += u.value;
388 row.cash_micros += u.cash;
389 value_by.entry(key.clone()).or_default().push((u.workspace.clone(), u.value as f64));
390 cost_by.entry(key).or_default().push((u.workspace.clone(), u.cost as f64));
Margin alerts measure what is sold, and say dollars when a percentage would mislead391 *revenue.entry((u.day.clone(), u.workspace.clone(), bucket.clone())).or_default() += u.cash;
392 *valued.entry((u.day.clone(), u.workspace.clone(), bucket)).or_default() += u.value;
Topics sit in About on the files pages, under the description393 active.entry(u.day.clone()).or_default().push((u.workspace.clone(), u.value.max(u.cost) as f64));
394 }
395 // Each bucket's cost shared out: by Cloudflare's own count per
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running396 // workspace, else by g1t's own count of its units, else by what its
397 // usage cost (so free use carries its own cost), else by what it was
398 // charged; running g1t, and what no one mapped, by each workspace's
399 // share of all usage that day.
Topics sit in About on the files pages, under the description400 let mut shares: BTreeMap<(String, String, String), i64> = BTreeMap::new();
401 for ((day, bucket), row) in &days {
402 let key = (day.clone(), bucket.clone());
403 let weigh = |m: &BTreeMap<(String, String), Vec<(String, f64)>>| m.get(&key).filter(|w| w.iter().any(|(_, v)| *v > 0.0)).cloned();
404 let weights = if OVERHEAD.contains(&bucket.as_str()) || bucket == UNMAPPED {
405 active.get(day).cloned()
406 } else {
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running407 weigh(&cf_by).or_else(|| weigh(&own_by)).or_else(|| weigh(&cost_by)).or_else(|| weigh(&value_by)).or_else(|| active.get(day).cloned())
Topics sit in About on the files pages, under the description408 };
409 for (workspace, micros) in attribute(row.cost(), &weights.unwrap_or_default()) {
410 *shares.entry((day.clone(), workspace, bucket.clone())).or_default() += micros;
411 }
412 }
413 let keys: BTreeSet<(String, String, String)> = shares.keys().chain(revenue.keys()).cloned().collect();
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it414 let workspaces: Vec<WorkspaceDay> = keys
Topics sit in About on the files pages, under the description415 .into_iter()
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it416 .map(|(day, workspace, bucket)| {
417 let cost = shares.get(&(day.clone(), workspace.clone(), bucket.clone())).copied().unwrap_or(0);
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running418 // The day's shares given away apply to every bucket, so a
419 // comped workspace's part of running g1t is given too. A
420 // workspace with nothing priced that day used g1t for free.
421 let given = if internal.contains(&workspace) {
422 Given { comped: cost, ..Given::default() }
423 } else {
424 match gave.get(&(day.clone(), workspace.clone())) {
425 Some((given, value)) if *value > 0 => given.of(cost, *value),
426 _ => Given { free: cost.max(0), ..Given::default() },
427 }
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it428 };
429 WorkspaceDay {
430 cost,
431 revenue: revenue.get(&(day.clone(), workspace.clone(), bucket.clone())).copied().unwrap_or(0),
432 value: valued.get(&(day.clone(), workspace.clone(), bucket.clone())).copied().unwrap_or(0),
433 given,
434 day,
435 workspace,
436 bucket,
437 }
Topics sit in About on the files pages, under the description438 })
439 .collect();
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it440 for w in &workspaces {
441 if let Some(row) = days.get_mut(&(w.day.clone(), w.bucket.clone())) {
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running442 row.given.add(&w.given);
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it443 }
444 }
Topics sit in About on the files pages, under the description445 (days.into_values().collect(), workspaces)
446}
447
448/// A month-end source's day, from the snapshots of what it had come to:
449/// each day's figure less the day before's in the same month (the first
450/// day of a month, or the first snapshot, is its own).
451pub(crate) fn pending_deltas(snapshots: &[(String, String, String, i64, i64)]) -> Vec<UsageRow> {
452 // (day, workspace, source, cost, charge), any order.
453 let mut sorted = snapshots.to_vec();
454 sorted.sort_by(|a, b| (&a.1, &a.2, &a.0).cmp(&(&b.1, &b.2, &b.0)));
455 let mut out = Vec::new();
456 let mut previous: Option<&(String, String, String, i64, i64)> = None;
457 for snap in &sorted {
458 let (day, workspace, source, cost, charge) = snap;
459 let (before_cost, before_charge) = match previous {
460 Some(p) if p.1 == *workspace && p.2 == *source && p.0[..7] == day[..7] => (p.3, p.4),
461 _ => (0, 0),
462 };
463 let (cost, charge) = ((cost - before_cost).max(0), (charge - before_charge).max(0));
464 if cost > 0 || charge > 0 {
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97465 out.push(UsageRow { day: day.clone(), workspace: workspace.clone(), key: source.clone(), bucket: None, value: charge, cash: charge, cost, given: Given::default() });
Topics sit in About on the files pages, under the description466 }
467 previous = Some(snap);
468 }
469 out
470}
471
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises472/// A month-end meter's day on a 100%-discount workspace: all of it given
473/// (comped) and none of it money in. The snapshot holds what the month
474/// would charge before the discount, which the month's close takes off in
475/// full; counted as paid, flagon-io's cache, embeddings and scans read as
476/// money in ($0.0023 on 2026-10-08).
477pub(crate) fn comped_meter(mut u: UsageRow) -> UsageRow {
478 u.given = Given { comped: u.value, ..Given::default() };
479 u.cash = 0;
480 u
481}
482
Topics sit in About on the files pages, under the description483/// Margin as a share of what was charged, in percent; None when nothing was.
484pub(crate) fn margin_percent(revenue_micros: i64, cost_micros: i64) -> Option<f64> {
485 (revenue_micros > 0).then(|| (revenue_micros - cost_micros) as f64 * 100.0 / revenue_micros as f64)
486}
487
488/// How far `ours` is from `theirs`, in percent of theirs; None when theirs
489/// is nothing.
490pub(crate) fn delta_percent(ours: f64, theirs: f64) -> Option<f64> {
491 (theirs > 0.0).then(|| (ours - theirs) * 100.0 / theirs)
492}
493
494#[derive(Clone, Copy, Debug, PartialEq, Eq)]
495pub(crate) enum DriftKind {
496 /// g1t counted a different number of units than Cloudflare did.
497 Count,
Merge remote-tracking branch 'origin/main' into workspace-chat498 /// The price book's cost of a bucket's usage differs from what the
499 /// same usage comes to at Cloudflare's list prices, before the included
Billing, integrations, security and identity no longer serve g1t's own agents. Billing stops metering agent runs, model tokens and AI Gateway requests, drops its model catalogue and price-keeper model prices, and stops selling AI credit, which only ever paid for those; credit bought earlier still pays toward what is owed. A usage report, statement or invoice that holds an old agent or model meter shows it as "Other usage" rather than failing, and the get_ai_credit and buy_ai_credit API operations and MCP billing actions go. Integrations no longer connects model providers or routes models, and no longer assigns imported issues or alerts to g1t; a model provider already connected can still be disconnected. Security drops "Fix with g1t". Identity no longer creates agent tokens or puts agents on teams, a team's lead is a person, and run credentials are issued only for checks, the merge queue, merge checks, deployments and bumps; tokens and credentials already issued keep working until they expire. The reserved names g1t and g1t-agent stay reserved. No tables are dropped.500 /// amounts (a price may be stale).
Topics sit in About on the files pages, under the description501 Cost,
502 /// Cloudflare charged for something nothing charges customers for.
503 Leak,
504}
505
506impl DriftKind {
507 pub fn as_str(self) -> &'static str {
508 match self {
509 DriftKind::Count => "count",
510 DriftKind::Cost => "cost",
511 DriftKind::Leak => "leak",
512 }
513 }
514}
515
516#[derive(Clone, Debug, PartialEq)]
517pub(crate) struct Drift {
518 pub bucket: String,
519 pub kind: DriftKind,
520 pub ours: f64,
521 pub cloudflare: f64,
522 pub delta_percent: Option<f64>,
523}
524
525/// Drift over a window for one bucket: counts more than `threshold`
Merge remote-tracking branch 'origin/main' into workspace-chat526/// percent apart, the price book's cost of the usage that far from what the
527/// same usage comes to at Cloudflare's list prices (`list_micros`, from
528/// `list_costs`), and cost with nothing charged for it. Under
529/// `min_cost_micros` in all, cost says nothing.
530///
531/// The price book is set against the list cost, never against what
532/// Cloudflare billed: the bill is net of the included amounts and the price
533/// book's cost is of every unit, so a bucket whose usage mostly fits in
534/// them would read as a stale price when nothing changed. Without a list
535/// cost (a meter in the bucket with no list price) the price book is not
536/// checked. A leak is still what Cloudflare billed: money spent.
537pub(crate) fn drifts(bucket: &str, days: &[ProductDay], threshold: f64, counted: bool, min_cost_micros: i64, list_micros: Option<f64>) -> Vec<Drift> {
Billing, integrations, security and identity no longer serve g1t's own agents. Billing stops metering agent runs, model tokens and AI Gateway requests, drops its model catalogue and price-keeper model prices, and stops selling AI credit, which only ever paid for those; credit bought earlier still pays toward what is owed. A usage report, statement or invoice that holds an old agent or model meter shows it as "Other usage" rather than failing, and the get_ai_credit and buy_ai_credit API operations and MCP billing actions go. Integrations no longer connects model providers or routes models, and no longer assigns imported issues or alerts to g1t; a model provider already connected can still be disconnected. Security drops "Fix with g1t". Identity no longer creates agent tokens or puts agents on teams, a team's lead is a person, and run credentials are issued only for checks, the merge queue, merge checks, deployments and bumps; tokens and credentials already issued keep working until they expire. The reserved names g1t and g1t-agent stay reserved. No tables are dropped.538 // Models: g1t no longer runs agents or sells model access. What their
539 // usage cost before is the ledger's own figure, with nothing to set it
540 // against.
541 if NOT_CLOUDFLARE.contains(&bucket) {
542 return Vec::new();
543 }
Topics sit in About on the files pages, under the description544 let overhead = OVERHEAD.contains(&bucket);
545 let sum = |f: &dyn Fn(&ProductDay) -> f64| days.iter().map(f).sum::<f64>();
546 let cf_cost = sum(&|d| d.cf_cost_micros as f64);
547 let own_cost = sum(&|d| d.own_cost_micros as f64);
548 let value = sum(&|d| d.value_micros as f64);
Costs: counts are compared with Cloudflare's from the day g1t started counting, so days before a meter existed are not drift549 // Counts are compared from the first day g1t counted: before its meter
550 // was deployed there is only Cloudflare's side. A meter that never
551 // counted anything is compared over every day, so it still shows.
552 let first_counted = days.iter().filter(|d| d.own_quantity > 0.0).map(|d| d.day.as_str()).min();
553 let compared = |d: &&ProductDay| first_counted.is_none_or(|from| d.day.as_str() >= from);
554 let (cf_quantity, own_quantity) = days.iter().filter(compared).fold((0.0, 0.0), |(cf, own), d| (cf + d.cf_quantity, own + d.own_quantity));
Topics sit in About on the files pages, under the description555 let mut out = Vec::new();
556 if counted && cf_quantity > 0.0 {
557 let delta = delta_percent(own_quantity, cf_quantity);
558 if delta.is_some_and(|d| d.abs() > threshold) {
559 out.push(Drift { bucket: bucket.into(), kind: DriftKind::Count, ours: own_quantity, cloudflare: cf_quantity, delta_percent: delta });
560 }
561 }
Billing, integrations, security and identity no longer serve g1t's own agents. Billing stops metering agent runs, model tokens and AI Gateway requests, drops its model catalogue and price-keeper model prices, and stops selling AI credit, which only ever paid for those; credit bought earlier still pays toward what is owed. A usage report, statement or invoice that holds an old agent or model meter shows it as "Other usage" rather than failing, and the get_ai_credit and buy_ai_credit API operations and MCP billing actions go. Integrations no longer connects model providers or routes models, and no longer assigns imported issues or alerts to g1t; a model provider already connected can still be disconnected. Security drops "Fix with g1t". Identity no longer creates agent tokens or puts agents on teams, a team's lead is a person, and run credentials are issued only for checks, the merge queue, merge checks, deployments and bumps; tokens and credentials already issued keep working until they expire. The reserved names g1t and g1t-agent stay reserved. No tables are dropped.562 // Its usage at Cloudflare's list prices, before the included amounts.
563 let theirs = list_micros.unwrap_or(0.0);
Merge remote-tracking branch 'origin/main' into workspace-chat564 let enough = theirs.max(own_cost) >= min_cost_micros as f64;
Billing, integrations, security and identity no longer serve g1t's own agents. Billing stops metering agent runs, model tokens and AI Gateway requests, drops its model catalogue and price-keeper model prices, and stops selling AI credit, which only ever paid for those; credit bought earlier still pays toward what is owed. A usage report, statement or invoice that holds an old agent or model meter shows it as "Other usage" rather than failing, and the get_ai_credit and buy_ai_credit API operations and MCP billing actions go. Integrations no longer connects model providers or routes models, and no longer assigns imported issues or alerts to g1t; a model provider already connected can still be disconnected. Security drops "Fix with g1t". Identity no longer creates agent tokens or puts agents on teams, a team's lead is a person, and run credentials are issued only for checks, the merge queue, merge checks, deployments and bumps; tokens and credentials already issued keep working until they expire. The reserved names g1t and g1t-agent stay reserved. No tables are dropped.565 if enough && !overhead && theirs > 0.0 && own_cost > 0.0 {
Merge remote-tracking branch 'origin/main' into workspace-chat566 let delta = delta_percent(own_cost, theirs);
Topics sit in About on the files pages, under the description567 if delta.is_some_and(|d| d.abs() > threshold) {
Merge remote-tracking branch 'origin/main' into workspace-chat568 out.push(Drift { bucket: bucket.into(), kind: DriftKind::Cost, ours: own_cost, cloudflare: theirs, delta_percent: delta });
Topics sit in About on the files pages, under the description569 }
570 }
571 if !overhead && cf_cost >= min_cost_micros as f64 && value <= 0.0 {
572 out.push(Drift { bucket: bucket.into(), kind: DriftKind::Leak, ours: value, cloudflare: cf_cost, delta_percent: None });
573 }
574 out
575}
576
Merge remote-tracking branch 'origin/main' into workspace-chat577/// What each bucket's billable usage over a window comes to at
578/// Cloudflare's list prices, before the included amounts (`cycle::list_cost`
579/// line by line, in micros): what the price book's cost of the same usage
580/// is checked against. None for a bucket with usage on a meter that has no
581/// list price: its usage cannot be priced like for like, so it is not
582/// checked. Other sources (Artifacts events, AI Gateway) are left out.
583pub(crate) fn list_costs(rules: &[Rule], lines: &[LineRow]) -> BTreeMap<String, Option<f64>> {
584 let mut out: BTreeMap<String, Option<f64>> = BTreeMap::new();
585 for line in lines.iter().filter(|l| l.source == SOURCE_BILLABLE) {
586 let bucket = costs::classify(rules, &line.product, &line.meter).map_or(UNMAPPED, |r| r.bucket.as_str());
587 let entry = out.entry(bucket.to_owned()).or_insert(Some(0.0));
588 if line.quantity <= 0.0 {
589 continue;
590 }
591 *entry = match (*entry, crate::cycle::list_cost(&line.product, &line.meter, line.quantity)) {
592 (Some(sum), Some(cost)) => Some(sum + cost * 1_000_000.0),
593 _ => None,
594 };
595 }
596 out
Merge branch 'worktree-agent-a633ac0f7f66d419d'597}
598
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97599/// Where a testing reset's history starts: all of a workspace's ledger.
600pub(crate) const RESET_HISTORY_FROM: &str = "2000-01-01";
601
602/// What a testing reset wiped that g1t paid for, on one day for one
603/// bucket (`reset_costs`).
604#[derive(Clone, Debug, PartialEq)]
605pub(crate) struct Wiped {
606 pub day: String,
607 pub bucket: String,
608 pub cost: i64,
609 pub value: i64,
610}
611
612/// A workspace's usage rows, about to be wiped, as what g1t paid for: the
613/// rows with a cost, by day and bucket, valued as the reconciliation
614/// valued them (at price where nothing paid). Plan payments, credits and
615/// a workspace's own model provider cost g1t nothing and are left out.
616pub(crate) fn wiped(rows: &[UsageRow], revenue_map: &BTreeMap<String, String>, margin_percent: u32) -> Vec<Wiped> {
617 let mut by: BTreeMap<(String, String), (i64, i64)> = BTreeMap::new();
618 for u in rows.iter().filter(|u| u.cost > 0) {
619 let bucket = u.bucket.clone().unwrap_or_else(|| revenue_map.get(&u.key).cloned().unwrap_or_else(|| NOT_CLOUDFLARE[0].to_owned()));
620 let sums = by.entry((u.day.clone(), bucket)).or_default();
621 sums.0 += u.cost;
622 sums.1 += u.value.max(0);
623 }
624 by.into_iter()
625 .map(|((day, bucket), (cost, value))| Wiped {
626 day,
627 bucket,
628 cost,
629 value: if value > 0 { value } else { crate::credits::with_margin(cost, margin_percent) },
630 })
631 .collect()
632}
633
634/// What testing resets kept, each (day, workspace, bucket, cost, value),
635/// as usage rows: valued as before, nothing paid, all of it given away
636/// (why "testing resets"). A reset's own row (bucket '') is not usage.
637pub(crate) fn reset_usage(kept: &[(String, String, String, i64, i64)]) -> Vec<UsageRow> {
638 kept.iter()
639 .filter(|(_, _, bucket, cost, value)| !bucket.is_empty() && (*cost != 0 || *value != 0))
640 .map(|(day, workspace, bucket, cost, value)| UsageRow {
641 day: day.clone(),
642 workspace: workspace.clone(),
643 key: "reset".into(),
644 bucket: Some(bucket.clone()),
645 value: *value,
646 cash: 0,
647 cost: *cost,
648 given: Given { reset: *value, ..Given::default() },
649 })
650 .collect()
Merge branch 'worktree-agent-a633ac0f7f66d419d'651}
652
Topics sit in About on the files pages, under the description653/// When the last `days` in a row (each with enough cost to say something)
654/// were all under the floor: the first of them and the worst margin.
655/// Each item is a day's (day, revenue, cost).
656pub(crate) fn breach(series: &[(String, i64, i64)], floor_percent: f64, days: usize, min_cost_micros: i64) -> Option<(String, f64)> {
657 if days == 0 || series.len() < days {
658 return None;
659 }
660 let tail = &series[series.len() - days..];
661 let mut worst = f64::INFINITY;
662 for (_, revenue, cost) in tail {
663 if *cost < min_cost_micros {
664 return None;
665 }
666 let margin = margin_percent(*revenue, *cost).unwrap_or(-100.0);
667 if margin >= floor_percent {
668 return None;
669 }
670 worst = worst.min(margin);
671 }
672 Some((tail[0].0.clone(), worst))
673}
674
675/// `total` shared out in proportion to `weights`, in whole millionths that
676/// add up to it exactly (largest remainder first). Nothing to share, or no
677/// weight, shares nothing.
678pub(crate) fn attribute(total: i64, weights: &[(String, f64)]) -> Vec<(String, i64)> {
679 let mut merged: BTreeMap<String, f64> = BTreeMap::new();
680 for (key, w) in weights {
681 *merged.entry(key.clone()).or_default() += w.max(0.0);
682 }
683 let sum: f64 = merged.values().sum();
684 if total <= 0 || sum <= 0.0 {
685 return Vec::new();
686 }
687 let mut shares: Vec<(String, i64, f64)> = merged
688 .into_iter()
689 .map(|(key, w)| {
690 let exact = total as f64 * w / sum;
691 (key, exact.floor() as i64, exact - exact.floor())
692 })
693 .collect();
694 let mut left = total - shares.iter().map(|s| s.1).sum::<i64>();
695 let mut order: Vec<usize> = (0..shares.len()).collect();
696 order.sort_by(|a, b| shares[*b].2.total_cmp(&shares[*a].2).then(shares[*a].0.cmp(&shares[*b].0)));
697 for index in order {
698 if left <= 0 {
699 break;
700 }
701 shares[index].1 += 1;
702 left -= 1;
703 }
704 shares.into_iter().filter(|s| s.1 > 0).map(|(key, micros, _)| (key, micros)).collect()
705}
706
707/// Workspaces that cost g1t more than `factor` times what they paid, with
708/// at least `floor_micros` of cost: each (workspace, cost, revenue), the
709/// biggest gap first.
Models' margin read -14%: usage nothing paid for is valued at price, not $0710/// What a day's usage was worth at price. g1t's own workspaces are valued
711/// at price. So is usage nothing paid for, neither charged nor drawn from
712/// the plan, a trial, a pool or a gift (a free period): it was given away at
713/// its price, not sold for nothing. Anything paid keeps what it was paid, so
714/// a discount still shows as one.
715pub(crate) fn usage_value(internal: bool, cost: i64, paid: i64, margin_percent: u32) -> i64 {
716 if internal || (paid == 0 && cost > 0) {
717 return crate::credits::with_margin(cost, margin_percent);
718 }
719 paid
720}
721
Margin alerts measure what is sold, and say dollars when a percentage would mislead722/// What the overall alert says: the money as money, and a percentage only
723/// while there is enough coming in for one to mean something (a few cents
724/// against dollars of cost reads as -8000%).
725pub(crate) fn overall_detail(took: i64, spent: i64, days: usize, floor: f64, worst: f64) -> String {
726 if took < 1_000_000 * days as i64 {
727 return format!(
728 "All of g1t, comped workspaces left out: took in {} against {} of Cloudflare's bill over {days} days.",
729 dollars(took),
730 dollars(spent)
731 );
732 }
733 format!("All of g1t, comped workspaces left out: money in against Cloudflare's bill under {floor:.0}% for {days} days running, as low as {worst:.1}%.")
734}
735
Topics sit in About on the files pages, under the description736pub(crate) fn anomalies(rows: &[(String, i64, i64)], factor: f64, floor_micros: i64) -> Vec<(String, i64, i64)> {
737 let mut out: Vec<(String, i64, i64)> = rows
738 .iter()
739 .filter(|(_, cost, revenue)| *cost >= floor_micros && *cost as f64 > *revenue as f64 * factor)
740 .cloned()
741 .collect();
742 out.sort_by(|a, b| (b.1 - b.2).cmp(&(a.1 - a.2)).then(a.0.cmp(&b.0)));
743 out
744}
745
Merge remote-tracking branch 'origin/main' into workspace-chat746/// Cloudflare's rate for one of its units: the median over the costed
747/// days of cost over quantity, in dollars. None while nothing is costed.
748/// Each item is a day's (quantity, cost), from `rate_line`.
Topics sit in About on the files pages, under the description749pub(crate) fn billed_rate(days: &[(f64, f64)]) -> Option<f64> {
750 let mut rates: Vec<f64> = days.iter().filter(|(q, c)| *q > 0.0 && *c > 0.0).map(|(q, c)| c / q).collect();
751 if rates.is_empty() {
752 return None;
753 }
754 rates.sort_by(f64::total_cmp);
755 Some(rates[rates.len() / 2])
756}
757
Merge remote-tracking branch 'origin/main' into workspace-chat758/// A line's (quantity, cost) for `billed_rate`: at its list price for all
759/// of the quantity where the meter has one, never what the cycle billed,
760/// which is net of the included amounts (nothing until the cycle passes
761/// them, part of a day's usage on the day it does, then whole millions) and
762/// so says nothing about the price of each unit. A meter with no list
763/// price has only what Cloudflare billed; the median over the charged days
764/// leaves out the day its included amount ran out.
765pub(crate) fn rate_line(product: &str, meter: &str, quantity: f64, cost_usd: f64) -> (f64, f64) {
766 (quantity, crate::cycle::list_cost(product, meter, quantity).unwrap_or(cost_usd))
767}
768
Topics sit in About on the files pages, under the description769/// What one of g1t's units costs, from Cloudflare's rate per its own unit
770/// and how many of Cloudflare's units each of g1t's took: if Cloudflare
771/// counts three operations for every git operation g1t counts, a git
772/// operation costs three of Cloudflare's. None without enough of g1t's
773/// units to say.
774pub(crate) fn derived_unit_cost(rate_per_cf_unit: f64, cf_units: f64, own_units: f64) -> Option<f64> {
775 (own_units >= MIN_UNITS && cf_units > 0.0 && rate_per_cf_unit > 0.0).then(|| rate_per_cf_unit * cf_units / own_units)
776}
777
778/// How many units a price is per: `1,000 operations` → 1,000, `million
779/// requests` → 1,000,000, `second` → 1.
780pub(crate) fn unit_size(unit: &str) -> f64 {
781 let first = unit.split_whitespace().next().unwrap_or_default().replace(',', "");
782 match first.as_str() {
783 "million" => 1_000_000.0,
784 "thousand" => 1_000.0,
785 n => n.parse().unwrap_or(1.0),
786 }
787}
788
789fn day_before(day: &str, days: u64) -> String {
790 let ms = g1t_contracts::time::parse_rfc3339(&format!("{day}T00:00:00Z")).unwrap_or(0);
791 rfc3339(ms.saturating_sub(days * DAY_MS))[..10].to_owned()
792}
793
794/// Dollars to the cent from a dollar up, finer below: `$17.02`, `$0.063`.
795fn dollars(micros: i64) -> String {
796 if micros.abs() >= 1_000_000 {
797 let cents = (micros as f64 / 10_000.0).round() as i64;
798 format!("{}${}.{:02}", if cents < 0 { "-" } else { "" }, cents.abs() / 100, cents.abs() % 100)
799 } else {
800 crate::features::dollars(micros)
801 }
802}
803
804/// The days a plan payment is spread over.
805const PLAN_DAYS: u64 = 30;
806
807/// `micros` paid on `day` spread evenly over `days` days from it, in
808/// whole micros that add up to it (the first days take the remainder).
809pub(crate) fn spread(day: &str, micros: i64, days: u64) -> Vec<(String, i64)> {
810 if micros <= 0 || days == 0 {
811 return Vec::new();
812 }
813 let start = g1t_contracts::time::parse_rfc3339(&format!("{}T00:00:00Z", &day[..10.min(day.len())])).unwrap_or(0);
814 let each = micros / days as i64;
815 let rest = micros % days as i64;
816 (0..days)
817 .map(|n| (rfc3339(start + n * DAY_MS)[..10].to_owned(), each + i64::from((n as i64) < rest)))
818 .collect()
819}
820
821// ---------------------------------------------------------------------
822// The daily run, and what sudo reads.
823// ---------------------------------------------------------------------
824
825#[derive(Serialize)]
826struct Mail<'a> {
827 to: &'a str,
828 from: &'a str,
829 subject: &'a str,
830 text: String,
831 html: String,
832}
833
834fn escape(text: &str) -> String {
835 text.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;").replace('"', "&quot;")
836}
837
838/// Emails staff through Cloudflare Email Sending, the `EMAIL` binding.
839pub(crate) async fn email_staff(env: &Env, to: &str, subject: &str, lines: &[String]) -> Result<()> {
Merge branch 'main' into actions-toolkit-oidc-artifacts840 email_staff_page(env, to, subject, lines, ("Costs & margin", "https://sudo.g1t.sh/costs"), "g1t-billing's margin guard").await
841}
842
843/// Emails staff, linking to a page of sudo (`page`: its name and address)
844/// and saying what sent it.
845pub(crate) async fn email_staff_page(env: &Env, to: &str, subject: &str, lines: &[String], page: (&str, &str), sender: &str) -> Result<()> {
846 let (name, link) = page;
847 let text = format!("{}\n\n{name}: {link}\n\nSent by {sender} (COSTS_ALERT_EMAIL).\n", lines.join("\n\n"));
Topics sit in About on the files pages, under the description848 let mut html = String::from("<div style=\"font-family:system-ui,sans-serif;max-width:560px;margin:0 auto;padding:24px 16px;color:#16150f\">");
849 for line in lines {
850 html.push_str(&format!("<p style=\"font-size:15px;line-height:1.6\">{}</p>", escape(line)));
851 }
852 html.push_str(&format!(
Merge branch 'main' into actions-toolkit-oidc-artifacts853 "<p><a href=\"{link}\">Open {} in sudo</a></p><p style=\"font-size:13px;color:#6e6a5e\">Sent by {} (COSTS_ALERT_EMAIL).</p></div>",
854 escape(name),
855 escape(sender)
Topics sit in About on the files pages, under the description856 ));
857 let mail = Mail { to, from: "g1t <noreply@g1t.sh>", subject, text, html };
858 let binding = g1t_kit::js::binding(env, "EMAIL")?;
859 g1t_kit::js::call(&binding, "send", &[g1t_kit::js::to_js(&mail)?]).await?;
860 Ok(())
861}
862
863#[derive(Deserialize)]
864struct AlertRow {
865 id: String,
866 kind: String,
867 subject: String,
868 detail: String,
869 since: String,
870 opened_at: String,
871 emailed_at: Option<String>,
872}
873
874impl From<AlertRow> for MarginAlert {
875 fn from(r: AlertRow) -> Self {
876 MarginAlert { id: r.id, kind: r.kind, subject: r.subject, detail: r.detail, since: r.since, opened_at: r.opened_at, emailed_at: r.emailed_at }
877 }
878}
879
880#[derive(Deserialize)]
881struct MarginRow {
882 day: String,
883 bucket: String,
884 cf_cost_micros: i64,
885 own_cost_micros: i64,
886 value_micros: i64,
887 cash_micros: i64,
888 cf_quantity: f64,
889 own_quantity: f64,
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it890 #[serde(default)]
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running891 given_comped_micros: Option<i64>,
892 #[serde(default)]
893 given_free_micros: Option<i64>,
894 #[serde(default)]
895 given_trial_micros: Option<i64>,
896 #[serde(default)]
897 given_pool_micros: Option<i64>,
Merge branch 'worktree-agent-a633ac0f7f66d419d'898 #[serde(default)]
899 given_discount_micros: Option<i64>,
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging900 #[serde(default)]
901 given_credit_promotional_micros: Option<i64>,
902 #[serde(default)]
903 given_credit_goodwill_micros: Option<i64>,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97904 #[serde(default)]
905 given_reset_micros: Option<i64>,
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)906 #[serde(default)]
907 given_unpaid_micros: Option<i64>,
Topics sit in About on the files pages, under the description908}
909
910impl From<MarginRow> for ProductDay {
911 fn from(r: MarginRow) -> Self {
912 ProductDay {
913 day: r.day,
914 bucket: r.bucket,
915 cf_cost_micros: r.cf_cost_micros,
916 own_cost_micros: r.own_cost_micros,
917 value_micros: r.value_micros,
918 cash_micros: r.cash_micros,
919 cf_quantity: r.cf_quantity,
920 own_quantity: r.own_quantity,
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running921 given: Given {
922 comped: r.given_comped_micros.unwrap_or(0),
923 free: r.given_free_micros.unwrap_or(0),
924 trial: r.given_trial_micros.unwrap_or(0),
925 pool: r.given_pool_micros.unwrap_or(0),
Merge branch 'worktree-agent-a633ac0f7f66d419d'926 discount: r.given_discount_micros.unwrap_or(0),
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging927 credit_promotional: r.given_credit_promotional_micros.unwrap_or(0),
928 credit_goodwill: r.given_credit_goodwill_micros.unwrap_or(0),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97929 reset: r.given_reset_micros.unwrap_or(0),
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)930 unpaid: r.given_unpaid_micros.unwrap_or(0),
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running931 },
Topics sit in About on the files pages, under the description932 }
933 }
934}
935
936impl Billing {
937 /// The day's work: read Cloudflare's bill and g1t's own counts,
938 /// reconcile, look for drift, measure unit costs, apply prices whose
939 /// day has come, and raise or clear alerts.
940 pub(crate) async fn costs_daily(&self, env: &Env, keeper: &crate::keeper::Keeper) -> Result<CostsRun> {
941 let mut run = CostsRun::default();
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)942 // The subscriptions first: they say when the billing cycle starts,
943 // which the bill is priced by. Not a problem for the run: the last
944 // read, or the estimate, stays.
945 if keeper.can_read_bill()
946 && let Err(error) = self.read_subscriptions(keeper).await
947 {
948 worker::console_error!("Cloudflare's subscriptions were not read: {error}");
949 }
Billing, integrations, security and identity no longer serve g1t's own agents. Billing stops metering agent runs, model tokens and AI Gateway requests, drops its model catalogue and price-keeper model prices, and stops selling AI credit, which only ever paid for those; credit bought earlier still pays toward what is owed. A usage report, statement or invoice that holds an old agent or model meter shows it as "Other usage" rather than failing, and the get_ai_credit and buy_ai_credit API operations and MCP billing actions go. Integrations no longer connects model providers or routes models, and no longer assigns imported issues or alerts to g1t; a model provider already connected can still be disconnected. Security drops "Fix with g1t". Identity no longer creates agent tokens or puts agents on teams, a team's lead is a person, and run credentials are issued only for checks, the merge queue, merge checks, deployments and bumps; tokens and credentials already issued keep working until they expire. The reserved names g1t and g1t-agent stay reserved. No tables are dropped.950 let (since, until, bill_since) = match self.read_cloudflare(keeper, &mut run.problems).await? {
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)951 Some((since, until, lines, bill_since)) => {
Topics sit in About on the files pages, under the description952 run.lines = lines;
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)953 (since, until, Some(bill_since))
Topics sit in About on the files pages, under the description954 }
955 // Without the bill, still reconcile what g1t knows itself, over
956 // the same days the bill would be read for.
957 None => {
958 #[derive(Deserialize)]
959 struct Last {
960 day: Option<String>,
961 }
962 let last = self.db.prepare("SELECT MAX(day) AS day FROM margin_days").first::<Last>(None).await?.and_then(|l| l.day);
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)963 let (since, until) = costs::window(last.as_deref(), now_ms());
964 (since, until, None)
Topics sit in About on the files pages, under the description965 }
966 };
967 if let Err(error) = self.count_own(&since, &until).await {
968 run.problems.push(format!("g1t's own counts could not be read: {error}"));
969 }
970 self.snapshot_pending(&until).await?;
Models' margin read -14%: usage nothing paid for is valued at price, not $0971 // Reconciled over the whole window sudo shows, not only the days the
972 // bill was read for: it reads only what is already kept, so a change
973 // in how a day is valued reaches every day shown at the next run.
974 let window = day_before(&until, costs::BACKFILL_DAYS - 1);
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)975 let reconcile_from = [Some(window), Some(since.clone()), bill_since].into_iter().flatten().min().unwrap_or_default();
Models' margin read -14%: usage nothing paid for is valued at price, not $0976 run.days = self.reconcile_range(&reconcile_from, &until).await?;
Billing, integrations, security and identity no longer serve g1t's own agents. Billing stops metering agent runs, model tokens and AI Gateway requests, drops its model catalogue and price-keeper model prices, and stops selling AI credit, which only ever paid for those; credit bought earlier still pays toward what is owed. A usage report, statement or invoice that holds an old agent or model meter shows it as "Other usage" rather than failing, and the get_ai_credit and buy_ai_credit API operations and MCP billing actions go. Integrations no longer connects model providers or routes models, and no longer assigns imported issues or alerts to g1t; a model provider already connected can still be disconnected. Security drops "Fix with g1t". Identity no longer creates agent tokens or puts agents on teams, a team's lead is a person, and run credentials are issued only for checks, the merge queue, merge checks, deployments and bumps; tokens and credentials already issued keep working until they expire. The reserved names g1t and g1t-agent stay reserved. No tables are dropped.977 let drift = self.find_drift(&until).await?;
Topics sit in About on the files pages, under the description978 run.proposals = self.measure_units(&until).await?;
979 self.apply_due_versions().await?;
980 run.alerts = self.raise_alerts(env, &until, &drift).await?;
981 if let Some(identity) = &self.identity
982 && let Err(error) = self.tell_owners_of_rises(identity).await
983 {
984 run.problems.push(format!("owners could not be told of a price rise: {error}"));
985 }
986 for problem in &run.problems {
987 worker::console_warn!("costs: {problem}");
988 }
989 Ok(run)
990 }
991
992 /// What each month-end source had come to by the end of `day`.
993 async fn snapshot_pending(&self, day: &str) -> Result<()> {
994 self.db
995 .prepare(
996 "INSERT INTO pending_days (day, workspace, source, cost_micros, charge_micros)
997 SELECT ?1, workspace, source, COALESCE(cost_micros, 0), COALESCE(charge_micros, 0) FROM pending_usage WHERE month = ?2
998 ON CONFLICT (day, workspace, source) DO UPDATE SET cost_micros = excluded.cost_micros, charge_micros = excluded.charge_micros",
999 )
1000 .bind(&[day.into(), day[..7].into()])?
1001 .run()
1002 .await?;
1003 Ok(())
1004 }
1005
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971006 /// What customers were charged on the days, by workspace and key: every
1007 /// workspace's, or only `only`'s.
1008 async fn usage_rows(&self, since: &str, until: &str, only: Option<&str>) -> Result<Vec<UsageRow>> {
1009 let only_sql = only.unwrap_or("");
Topics sit in About on the files pages, under the description1010 #[derive(Deserialize)]
1011 struct Row {
1012 day: String,
1013 workspace: String,
1014 key: String,
1015 internal: i64,
1016 own_provider: i64,
1017 cash: Option<i64>,
1018 drawn: Option<i64>,
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running1019 trial: Option<i64>,
1020 oss: Option<i64>,
1021 covered: Option<i64>,
Merge branch 'worktree-agent-a633ac0f7f66d419d'1022 discount: Option<i64>,
Topics sit in About on the files pages, under the description1023 cost: Option<i64>,
1024 }
1025 let charged_here = crate::storage::CHARGED_HERE.iter().map(|s| format!("'{s}'")).collect::<Vec<_>>().join(", ");
1026 let end = format!("{until}T23:59:59.999Z");
1027 let rows = self
1028 .db
1029 .prepare(format!(
1030 "SELECT substr(created_at, 1, 10) AS day, workspace,
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises1031 CASE WHEN task = 'deployments' AND reference LIKE 'deploy/%' THEN 'builds'
1032 WHEN task = 'plan' THEN '{planning}' ELSE COALESCE(task, 'other') END AS key,
Topics sit in About on the files pages, under the description1033 CASE WHEN workspace IN ({internal}) THEN 1 ELSE 0 END AS internal,
1034 CASE WHEN billed_to = 'workspace' THEN 1 ELSE 0 END AS own_provider,
1035 -SUM(amount_micros) AS cash,
1036 SUM(COALESCE(credit_micros, 0) + COALESCE(trial_micros, 0) + COALESCE(oss_micros, 0) + COALESCE(given_micros, 0)) AS drawn,
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running1037 SUM(COALESCE(trial_micros, 0)) AS trial,
1038 SUM(COALESCE(oss_micros, 0)) AS oss,
1039 SUM(COALESCE(given_micros, 0)) AS covered,
Merge branch 'worktree-agent-a633ac0f7f66d419d'1040 SUM(COALESCE(discount_micros, 0)) AS discount,
Topics sit in About on the files pages, under the description1041 SUM(COALESCE(cost_micros, 0)) AS cost
1042 FROM ledger
1043 WHERE kind = 'usage' AND created_at >= ?1 AND created_at <= ?2 AND COALESCE(task, '') NOT IN ({charged_here})
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971044 AND (?3 = '' OR workspace = ?3)
Topics sit in About on the files pages, under the description1045 GROUP BY 1, 2, 3, 4, 5",
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises1046 internal = crate::sales::INTERNAL_SQL,
1047 planning = PLANNING_KEY
Topics sit in About on the files pages, under the description1048 ))
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971049 .bind(&[since.into(), end.as_str().into(), only_sql.into()])?
Topics sit in About on the files pages, under the description1050 .all()
1051 .await?
1052 .results::<Row>()?;
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it1053 let mut internal = BTreeSet::new();
Topics sit in About on the files pages, under the description1054 let mut out: Vec<UsageRow> = rows
1055 .into_iter()
1056 .map(|r| {
1057 // A workspace's own model provider was paid there: no cost
1058 // to g1t. g1t's own workspaces are valued at price.
1059 let cost = if r.own_provider == 1 { 0 } else { r.cost.unwrap_or(0) };
1060 let cash = r.cash.unwrap_or(0);
Merge branch 'worktree-agent-a633ac0f7f66d419d'1061 // A discount took its part below cost plus the margin: it is
1062 // valued at price and that part counted as given, so a
1063 // discounted sale never reads as margin lost.
1064 let discount = r.discount.unwrap_or(0).max(0);
1065 let paid = cash + r.drawn.unwrap_or(0) + discount;
Models' margin read -14%: usage nothing paid for is valued at price, not $01066 let value = usage_value(r.internal == 1, cost, paid, self.margin_percent);
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running1067 let given = if r.internal == 1 {
1068 Given { comped: value, ..Given::default() }
1069 } else if paid == 0 && cost > 0 {
1070 Given { free: value, ..Given::default() }
1071 } else {
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging1072 Given { free: r.covered.unwrap_or(0), trial: r.trial.unwrap_or(0), pool: r.oss.unwrap_or(0), discount, ..Given::default() }
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running1073 };
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it1074 if r.internal == 1 {
1075 internal.insert(r.workspace.clone());
1076 }
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971077 UsageRow { day: r.day, workspace: r.workspace, key: r.key, bucket: None, value, cash, cost, given }
Topics sit in About on the files pages, under the description1078 })
1079 .collect();
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging1080 // Credits from g1t: what promotional and goodwill credit paid for
1081 // is given, not money in; a refund gives money back on its day.
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971082 let (mut draws, mut refunds) = self.credit_effects(since, until).await?;
1083 if let Some(only) = only {
1084 draws.retain(|(workspace, _)| workspace == only);
1085 refunds.retain(|r| r.workspace == only);
1086 }
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging1087 apply_credits(&mut out, &draws, &refunds);
Topics sit in About on the files pages, under the description1088 // Month-end sources, from their daily snapshots.
1089 #[derive(Deserialize)]
1090 struct Snap {
1091 day: String,
1092 workspace: String,
1093 source: String,
1094 cost_micros: i64,
1095 charge_micros: i64,
1096 }
1097 let snaps = self
1098 .db
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971099 .prepare("SELECT day, workspace, source, cost_micros, charge_micros FROM pending_days WHERE day >= ?1 AND day <= ?2 AND (?3 = '' OR workspace = ?3)")
1100 .bind(&[day_before(since, 1).into(), until.into(), only_sql.into()])?
Topics sit in About on the files pages, under the description1101 .all()
1102 .await?
1103 .results::<Snap>()?
1104 .into_iter()
1105 .filter(|s| crate::storage::CHARGED_HERE.contains(&s.source.as_str()) || s.source == "domains")
1106 .map(|s| (s.day, s.workspace, s.source, s.cost_micros, s.charge_micros))
1107 .collect::<Vec<_>>();
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises1108 out.extend(
1109 pending_deltas(&snaps)
1110 .into_iter()
1111 .filter(|u| u.day.as_str() >= since)
1112 .map(|u| if internal.contains(&u.workspace) { comped_meter(u) } else { u }),
1113 );
Topics sit in About on the files pages, under the description1114 // The plan's price, spread over the 30 days it pays for, so a month's
1115 // payment does not read as one very good day and 29 bad ones.
1116 #[derive(Deserialize)]
1117 struct Plan {
1118 day: String,
1119 workspace: String,
1120 micros: Option<i64>,
1121 }
1122 let plans = self
1123 .db
1124 .prepare(
1125 "SELECT substr(paid_at, 1, 10) AS day, workspace, SUM(amount_micros) AS micros FROM plan_payments
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971126 WHERE paid_at >= ?1 AND paid_at <= ?2 AND (?3 = '' OR workspace = ?3) GROUP BY 1, 2",
Topics sit in About on the files pages, under the description1127 )
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971128 .bind(&[day_before(since, PLAN_DAYS - 1).into(), end.as_str().into(), only_sql.into()])?
Topics sit in About on the files pages, under the description1129 .all()
1130 .await?
1131 .results::<Plan>()?;
1132 for p in plans {
1133 for (day, micros) in spread(&p.day, p.micros.unwrap_or(0), PLAN_DAYS) {
1134 if day.as_str() >= since && day.as_str() <= until {
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971135 out.push(UsageRow { day, workspace: p.workspace.clone(), key: "plan".into(), bucket: None, value: micros, cash: micros, cost: 0, given: Given::default() });
Topics sit in About on the files pages, under the description1136 }
1137 }
1138 }
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)1139 // Charges without real money behind them are not money in.
1140 without_real_money(&mut out, self.payments_live_since().await?.as_deref());
Topics sit in About on the files pages, under the description1141 Ok(out)
1142 }
1143
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)1144 /// The day payments went live (`live_since`): kept in `cost_settings`
1145 /// the first time they are seen live, so charges from before it stay
1146 /// test money after the switch.
1147 pub(crate) async fn payments_live_since(&self) -> Result<Option<String>> {
1148 #[derive(Deserialize)]
1149 struct Row {
1150 value: String,
1151 }
1152 let live = self.stripe.as_ref().is_some_and(crate::stripe::Stripe::live);
1153 let kept = self
1154 .db
1155 .prepare("SELECT value FROM cost_settings WHERE key = 'payments_live_since'")
1156 .first::<Row>(None)
1157 .await?
1158 .map(|r| r.value)
1159 .filter(|v| v.len() >= 10);
1160 let today = rfc3339(now_ms())[..10].to_owned();
1161 let since = live_since(live, kept.as_deref(), &today);
1162 if let Some(day) = since.as_deref().filter(|_| kept.is_none()) {
1163 self.db
1164 .prepare(
1165 "INSERT INTO cost_settings (key, value, updated_at, updated_by) VALUES ('payments_live_since', ?1, ?2, 'billing')
1166 ON CONFLICT (key) DO UPDATE SET value = ?1, updated_at = ?2, updated_by = 'billing'",
1167 )
1168 .bind(&[day.into(), rfc3339(now_ms()).into()])?
1169 .run()
1170 .await?;
1171 }
1172 Ok(since)
1173 }
1174
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971175 /// Which bucket each ledger key (and month-end source) is revenue of.
1176 async fn revenue_map(&self) -> Result<BTreeMap<String, String>> {
Topics sit in About on the files pages, under the description1177 #[derive(Deserialize)]
1178 struct Map {
1179 key: String,
1180 bucket: String,
1181 }
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971182 Ok(self
Topics sit in About on the files pages, under the description1183 .db
1184 .prepare("SELECT key, bucket FROM revenue_map")
1185 .all()
1186 .await?
1187 .results::<Map>()?
1188 .into_iter()
1189 .map(|m| (m.key, m.bucket))
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971190 .collect())
1191 }
1192
1193 /// What a testing reset of `workspace` is about to wipe that g1t paid
1194 /// for, a row per day and bucket: its whole ledger and month-end
1195 /// snapshots, valued as the reconciliation values them.
1196 pub(crate) async fn wiped_by_reset(&self, workspace: &str) -> Result<Vec<Wiped>> {
1197 let today = rfc3339(now_ms())[..10].to_owned();
1198 let rows = self.usage_rows(RESET_HISTORY_FROM, &today, Some(workspace)).await?;
1199 Ok(wiped(&rows, &self.revenue_map().await?, self.margin_percent))
1200 }
1201
1202 /// What testing resets kept for the days, as usage rows.
1203 async fn reset_rows(&self, since: &str, until: &str) -> Result<Vec<UsageRow>> {
1204 #[derive(Deserialize)]
1205 struct Kept {
1206 day: String,
1207 workspace: String,
1208 bucket: String,
1209 cost: Option<i64>,
1210 value: Option<i64>,
1211 }
1212 let kept = self
1213 .db
1214 .prepare(
1215 "SELECT day, workspace, bucket, SUM(cost_micros) AS cost, SUM(value_micros) AS value FROM reset_costs
1216 WHERE day >= ?1 AND day <= ?2 AND bucket <> '' GROUP BY day, workspace, bucket",
1217 )
1218 .bind(&[since.into(), until.into()])?
1219 .all()
1220 .await?
1221 .results::<Kept>()?;
1222 Ok(reset_usage(
1223 &kept.into_iter().map(|k| (k.day, k.workspace, k.bucket, k.cost.unwrap_or(0), k.value.unwrap_or(0))).collect::<Vec<_>>(),
1224 ))
1225 }
1226
1227 /// Reconciles the days and writes `margin_days` and `workspace_costs`.
1228 async fn reconcile_range(&self, since: &str, until: &str) -> Result<u32> {
1229 let rules = self.rules().await?;
1230 let revenue_map = self.revenue_map().await?;
Topics sit in About on the files pages, under the description1231 let lines = self
1232 .db
1233 .prepare("SELECT day, source, product, meter, quantity, cost_usd FROM cost_lines WHERE day >= ?1 AND day <= ?2")
1234 .bind(&[since.into(), until.into()])?
1235 .all()
1236 .await?
1237 .results::<LineRow>()?;
1238 let own = self
1239 .db
1240 .prepare("SELECT day, meter, workspace, quantity FROM own_counts WHERE day >= ?1 AND day <= ?2")
1241 .bind(&[since.into(), until.into()])?
1242 .all()
1243 .await?
1244 .results::<OwnRow>()?;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971245 let mut usage = self.usage_rows(since, until, None).await?;
1246 // What testing resets wiped: still paid for, now given away.
1247 usage.extend(self.reset_rows(since, until).await?);
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running1248 #[derive(Deserialize)]
1249 struct Internal {
1250 workspace: String,
1251 }
1252 let internal: BTreeSet<String> = self
1253 .db
1254 .prepare(format!("WITH i(workspace) AS ({}) SELECT DISTINCT workspace FROM i", crate::sales::INTERNAL_SQL))
1255 .all()
1256 .await?
1257 .results::<Internal>()?
1258 .into_iter()
1259 .map(|i| i.workspace)
1260 .collect();
1261 let (days, workspaces) = fold(&rules, &revenue_map, &lines, &own, &usage, &internal);
Topics sit in About on the files pages, under the description1262 let now = rfc3339(now_ms());
1263 self.db
1264 .batch(vec![
1265 self.db.prepare("DELETE FROM margin_days WHERE day >= ?1 AND day <= ?2").bind(&[since.into(), until.into()])?,
1266 self.db.prepare("DELETE FROM workspace_costs WHERE day >= ?1 AND day <= ?2").bind(&[since.into(), until.into()])?,
1267 ])
1268 .await?;
1269 for chunk in days.chunks(50) {
1270 let mut statements = Vec::with_capacity(chunk.len());
1271 for d in chunk {
1272 statements.push(
1273 self.db
1274 .prepare(
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)1275 "INSERT OR REPLACE INTO margin_days (day, bucket, cf_cost_micros, own_cost_micros, value_micros, cash_micros, cf_quantity, own_quantity, given_micros, given_comped_micros, given_free_micros, given_trial_micros, given_pool_micros, given_discount_micros, given_credit_promotional_micros, given_credit_goodwill_micros, given_reset_micros, given_unpaid_micros, computed_at)
1276 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
Topics sit in About on the files pages, under the description1277 )
1278 .bind(&[
1279 d.day.as_str().into(),
1280 d.bucket.as_str().into(),
1281 (d.cf_cost_micros as f64).into(),
1282 (d.own_cost_micros as f64).into(),
1283 (d.value_micros as f64).into(),
1284 (d.cash_micros as f64).into(),
1285 d.cf_quantity.into(),
1286 d.own_quantity.into(),
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running1287 (d.given.total() as f64).into(),
1288 (d.given.comped as f64).into(),
1289 (d.given.free as f64).into(),
1290 (d.given.trial as f64).into(),
1291 (d.given.pool as f64).into(),
Merge branch 'worktree-agent-a633ac0f7f66d419d'1292 (d.given.discount as f64).into(),
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging1293 (d.given.credit_promotional as f64).into(),
1294 (d.given.credit_goodwill as f64).into(),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971295 (d.given.reset as f64).into(),
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)1296 (d.given.unpaid as f64).into(),
Topics sit in About on the files pages, under the description1297 now.as_str().into(),
1298 ])?,
1299 );
1300 }
1301 self.db.batch(statements).await?;
1302 }
1303 for chunk in workspaces.chunks(50) {
1304 let mut statements = Vec::with_capacity(chunk.len());
1305 for w in chunk {
1306 statements.push(
1307 self.db
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it1308 .prepare("INSERT OR REPLACE INTO workspace_costs (day, workspace, bucket, cost_micros, revenue_micros, value_micros, given_micros) VALUES (?, ?, ?, ?, ?, ?, ?)")
Topics sit in About on the files pages, under the description1309 .bind(&[
1310 w.day.as_str().into(),
1311 w.workspace.as_str().into(),
1312 w.bucket.as_str().into(),
1313 (w.cost as f64).into(),
1314 (w.revenue as f64).into(),
Margin alerts measure what is sold, and say dollars when a percentage would mislead1315 (w.value as f64).into(),
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running1316 (w.given.total() as f64).into(),
Topics sit in About on the files pages, under the description1317 ])?,
1318 );
1319 }
1320 self.db.batch(statements).await?;
1321 }
1322 Ok(costs::days_between(since, until).len() as u32)
1323 }
1324
1325 async fn margin_days(&self, since: &str, until: &str) -> Result<Vec<ProductDay>> {
1326 Ok(self
1327 .db
1328 .prepare("SELECT * FROM margin_days WHERE day >= ?1 AND day <= ?2 ORDER BY day, bucket")
1329 .bind(&[since.into(), until.into()])?
1330 .all()
1331 .await?
1332 .results::<MarginRow>()?
1333 .into_iter()
1334 .map(ProductDay::from)
1335 .collect())
Merge branch 'worktree-agent-a633ac0f7f66d419d'1336 }
1337
Topics sit in About on the files pages, under the description1338 /// Drift over the last week, written to `cost_drift` (replacing the
1339 /// last run's), with unmapped Cloudflare meters as leaks.
Billing, integrations, security and identity no longer serve g1t's own agents. Billing stops metering agent runs, model tokens and AI Gateway requests, drops its model catalogue and price-keeper model prices, and stops selling AI credit, which only ever paid for those; credit bought earlier still pays toward what is owed. A usage report, statement or invoice that holds an old agent or model meter shows it as "Other usage" rather than failing, and the get_ai_credit and buy_ai_credit API operations and MCP billing actions go. Integrations no longer connects model providers or routes models, and no longer assigns imported issues or alerts to g1t; a model provider already connected can still be disconnected. Security drops "Fix with g1t". Identity no longer creates agent tokens or puts agents on teams, a team's lead is a person, and run credentials are issued only for checks, the merge queue, merge checks, deployments and bumps; tokens and credentials already issued keep working until they expire. The reserved names g1t and g1t-agent stay reserved. No tables are dropped.1340 async fn find_drift(&self, until: &str) -> Result<Vec<(Drift, String)>> {
Topics sit in About on the files pages, under the description1341 let since = day_before(until, DRIFT_DAYS - 1);
1342 let settings = self.cost_settings().await?;
1343 let rules = self.rules().await?;
1344 let days = self.margin_days(&since, until).await?;
1345 let mut by: BTreeMap<String, Vec<ProductDay>> = BTreeMap::new();
1346 for d in days {
1347 by.entry(d.bucket.clone()).or_default().push(d);
1348 }
Merge remote-tracking branch 'origin/main' into workspace-chat1349 // The same days' usage at list prices, before the included amounts:
1350 // what the price book is checked against (never the bill, which is
1351 // net of them).
1352 let lines = self
1353 .db
1354 .prepare("SELECT day, source, product, meter, quantity, cost_usd FROM cost_lines WHERE source = ?1 AND day >= ?2 AND day <= ?3")
1355 .bind(&[SOURCE_BILLABLE.into(), since.as_str().into(), until.into()])?
1356 .all()
1357 .await?
1358 .results::<LineRow>()?;
1359 let list = list_costs(&rules, &lines);
Topics sit in About on the files pages, under the description1360 let mut found = Vec::new();
1361 for (bucket, days) in &by {
1362 let bucket_rules: Vec<&Rule> = rules.iter().filter(|r| &r.bucket == bucket).collect();
1363 let threshold = bucket_rules.iter().map(|r| r.drift_percent).fold(f64::INFINITY, f64::min);
1364 let threshold = if threshold.is_finite() { threshold } else { 10.0 };
1365 let counted = bucket_rules.iter().any(|r| r.own_meter.is_some());
Merge remote-tracking branch 'origin/main' into workspace-chat1366 let list_micros = list.get(bucket).copied().flatten();
1367 for drift in drifts(bucket, days, threshold, counted, settings.min_daily_cost_micros, list_micros) {
Topics sit in About on the files pages, under the description1368 let title = costs::bucket_title(bucket);
1369 let detail = match drift.kind {
1370 DriftKind::Count => format!(
1371 "{title}: g1t counted {}, Cloudflare {} over the last {DRIFT_DAYS} days ({:+.1}%). Customers are charged for what g1t counts; check what Cloudflare counts as a unit and change the repos service's operation_mapping (set_operation_mapping).",
1372 crate::features::thousands(drift.ours.max(0.0).round() as u64),
1373 crate::features::thousands(drift.cloudflare.max(0.0).round() as u64),
1374 drift.delta_percent.unwrap_or(0.0)
1375 ),
1376 DriftKind::Cost => format!(
Merge remote-tracking branch 'origin/main' into workspace-chat1377 "{title}: the last {DRIFT_DAYS} days' usage comes to {} at Cloudflare's list prices, before the included amounts; the price book's cost of the same usage is {} ({:+.1}%). A price may be stale: see the proposals.",
Topics sit in About on the files pages, under the description1378 dollars(drift.cloudflare as i64),
1379 dollars(drift.ours as i64),
1380 drift.delta_percent.unwrap_or(0.0)
1381 ),
1382 DriftKind::Leak if bucket == UNMAPPED => {
1383 format!("Cloudflare charged {} for meters no mapping claims. Map them on Costs & margin.", dollars(drift.cloudflare as i64))
1384 }
1385 DriftKind::Leak => format!(
1386 "{title}: Cloudflare charged {} over the last {DRIFT_DAYS} days and customers were charged nothing for it.",
1387 dollars(drift.cloudflare as i64)
1388 ),
1389 };
1390 found.push((drift, detail));
1391 }
1392 }
1393 let now = rfc3339(now_ms());
1394 let mut statements = vec![self.db.prepare("DELETE FROM cost_drift")];
1395 for (drift, detail) in &found {
1396 statements.push(
1397 self.db
1398 .prepare("INSERT OR REPLACE INTO cost_drift (bucket, kind, ours, cloudflare, delta_percent, detail, found_at) VALUES (?, ?, ?, ?, ?, ?, ?)")
1399 .bind(&[
1400 drift.bucket.as_str().into(),
1401 drift.kind.as_str().into(),
1402 drift.ours.into(),
1403 drift.cloudflare.into(),
1404 drift.delta_percent.map_or(JsValue::NULL, JsValue::from),
1405 detail.as_str().into(),
1406 now.as_str().into(),
1407 ])?,
1408 );
1409 }
1410 self.db.batch(statements).await?;
1411 Ok(found)
1412 }
1413
1414 /// Unit costs from the bill for mappings that scale to g1t's own count
1415 /// (git operations), proposed to the price book.
1416 async fn measure_units(&self, until: &str) -> Result<u32> {
1417 #[derive(Deserialize)]
1418 struct Scaled {
1419 product: String,
1420 meter: String,
1421 price_meter: String,
1422 own_meter: String,
1423 unit: Option<String>,
1424 }
1425 let scaled = self
1426 .db
1427 .prepare(
1428 "SELECT m.product, m.meter, m.price_meter, m.own_meter, p.unit FROM cost_map m LEFT JOIN prices p ON p.meter = m.price_meter
1429 WHERE m.scale_to_own = 1 AND m.price_meter IS NOT NULL AND m.own_meter IS NOT NULL",
1430 )
1431 .all()
1432 .await?
1433 .results::<Scaled>()?;
1434 let since = day_before(until, MEASURE_DAYS - 1);
1435 let rules = self.rules().await?;
1436 let mut proposed = 0;
1437 for s in scaled {
1438 #[derive(Deserialize)]
1439 struct Day {
1440 product: String,
1441 meter: String,
1442 quantity: f64,
1443 cost_usd: f64,
1444 }
1445 let lines = self
1446 .db
1447 .prepare("SELECT product, meter, quantity, cost_usd FROM cost_lines WHERE source = ?1 AND product = ?2 AND day >= ?3 AND day <= ?4")
1448 .bind(&[SOURCE_BILLABLE.into(), s.product.as_str().into(), since.as_str().into(), until.into()])?
1449 .all()
1450 .await?
1451 .results::<Day>()?;
1452 // Only the lines this very mapping claims.
1453 let mine: Vec<(f64, f64)> = lines
1454 .iter()
1455 .filter(|l| costs::classify(&rules, &l.product, &l.meter).is_some_and(|r| r.product == s.product && r.meter == s.meter))
Merge remote-tracking branch 'origin/main' into workspace-chat1456 .map(|l| rate_line(&l.product, &l.meter, l.quantity, l.cost_usd))
Topics sit in About on the files pages, under the description1457 .collect();
1458 let Some(rate) = billed_rate(&mine) else { continue };
1459 let cf_units: f64 = mine.iter().map(|(q, _)| q).sum();
1460 #[derive(Deserialize)]
1461 struct Own {
1462 total: Option<f64>,
1463 }
1464 let own_units = self
1465 .db
1466 .prepare("SELECT SUM(quantity) AS total FROM own_counts WHERE meter = ?1 AND day >= ?2 AND day <= ?3")
1467 .bind(&[s.own_meter.as_str().into(), since.as_str().into(), until.into()])?
1468 .first::<Own>(None)
1469 .await?
1470 .and_then(|o| o.total)
1471 .unwrap_or(0.0);
1472 let Some(per_unit) = derived_unit_cost(rate, cf_units, own_units) else { continue };
1473 let size = unit_size(s.unit.as_deref().unwrap_or("1"));
1474 let measured = per_unit * size * 1_000_000.0;
1475 let reason = format!(
1476 "Cloudflare billed ${:.4} per 1,000 of its units and counted {:.2} of them for each one g1t counted over the last {MEASURE_DAYS} days ({} against {})",
1477 rate * 1000.0,
1478 cf_units / own_units,
1479 crate::features::thousands(cf_units.round() as u64),
1480 crate::features::thousands(own_units.round() as u64)
1481 );
1482 if self.propose(&s.price_meter, measured, &reason, "reconciler").await?.is_some() {
1483 proposed += 1;
1484 }
1485 }
1486 Ok(proposed)
1487 }
1488
1489 /// Opens, updates and closes margin alerts, and emails staff about new
1490 /// ones (and open ones each week).
1491 async fn raise_alerts(&self, env: &Env, until: &str, drift: &[(Drift, String)]) -> Result<u32> {
1492 let settings = self.cost_settings().await?;
1493 let since = day_before(until, u64::from(settings.alert_days.max(1)) - 1);
1494 let days = self.margin_days(&since, until).await?;
1495 let mut conditions: Vec<(String, String, String, String)> = Vec::new();
1496 // Each product under the floor.
1497 let mut by: BTreeMap<String, Vec<(String, i64, i64)>> = BTreeMap::new();
1498 let mut all: BTreeMap<String, (i64, i64)> = BTreeMap::new();
1499 for d in &days {
1500 let overall = all.entry(d.day.clone()).or_default();
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it1501 // What g1t gave away (comped workspaces, free periods, the
1502 // trial and the pools) is a budget it chose to spend, watched on
1503 // its own (budget.rs): not part of whether what is sold pays.
Topics sit in About on the files pages, under the description1504 overall.0 += d.cash_micros;
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running1505 overall.1 += (d.cost() - d.given.total()).max(0);
Topics sit in About on the files pages, under the description1506 if !OVERHEAD.contains(&d.bucket.as_str()) && d.bucket != UNMAPPED {
1507 by.entry(d.bucket.clone()).or_default().push((d.day.clone(), d.value_micros, d.cost()));
1508 }
1509 }
1510 let floor = settings.margin_floor_percent;
1511 let n = settings.alert_days as usize;
1512 for (bucket, series) in &by {
1513 if let Some((from, worst)) = breach(series, floor, n, settings.min_daily_cost_micros) {
1514 conditions.push((
1515 "margin".into(),
1516 bucket.clone(),
1517 format!("{}: margin under {floor:.0}% for {n} days running, as low as {worst:.1}%.", costs::bucket_title(bucket)),
1518 from,
1519 ));
Margin alerts measure what is sold, and say dollars when a percentage would mislead1520 }
1521 }
Topics sit in About on the files pages, under the description1522 let series: Vec<(String, i64, i64)> = all.into_iter().map(|(day, (revenue, cost))| (day, revenue, cost)).collect();
1523 if let Some((from, worst)) = breach(&series, floor, n, settings.min_daily_cost_micros) {
Margin alerts measure what is sold, and say dollars when a percentage would mislead1524 let tail = &series[series.len().saturating_sub(n)..];
1525 let (took, spent) = tail.iter().fold((0i64, 0i64), |(r, c), (_, revenue, cost)| (r + revenue, c + cost));
1526 conditions.push(("overall".into(), "g1t".into(), overall_detail(took, spent, n, floor, worst), from));
Topics sit in About on the files pages, under the description1527 }
1528 for (d, detail) in drift {
1529 let kind = if d.kind == DriftKind::Leak { "leak" } else { "drift" };
1530 conditions.push((kind.into(), format!("{}:{}", d.bucket, d.kind.as_str()), detail.clone(), until.to_owned()));
1531 }
1532 // Workspaces costing more than they pay.
1533 for (workspace, cost, revenue) in self.workspace_anomalies(until, &settings).await? {
1534 conditions.push((
1535 "workspace".into(),
1536 workspace.clone(),
Margin alerts measure what is sold, and say dollars when a percentage would mislead1537 format!(
1538 "{workspace} cost g1t {} on Cloudflare over {ANOMALY_DAYS} days, and its usage was priced at {}: its prices are below cost.",
1539 dollars(cost),
1540 dollars(revenue)
1541 ),
Topics sit in About on the files pages, under the description1542 day_before(until, ANOMALY_DAYS - 1),
1543 ));
1544 }
1545
1546 let open = self
1547 .db
1548 .prepare("SELECT * FROM margin_alerts WHERE resolved_at IS NULL")
1549 .all()
1550 .await?
1551 .results::<AlertRow>()?;
1552 let now = now_ms();
1553 let stamp = rfc3339(now);
1554 let mut to_email: Vec<String> = Vec::new();
1555 let mut kept: BTreeSet<String> = BTreeSet::new();
1556 for (kind, subject, detail, from) in &conditions {
1557 match open.iter().find(|a| &a.kind == kind && &a.subject == subject) {
1558 Some(alert) => {
1559 kept.insert(alert.id.clone());
1560 self.db
1561 .prepare("UPDATE margin_alerts SET detail = ? WHERE id = ?")
1562 .bind(&[detail.as_str().into(), alert.id.as_str().into()])?
1563 .run()
1564 .await?;
1565 let stale = alert
1566 .emailed_at
1567 .as_deref()
1568 .and_then(g1t_contracts::time::parse_rfc3339)
1569 .is_none_or(|at| now.saturating_sub(at) >= REMIND_MS);
1570 if stale && kind != "workspace" {
1571 to_email.push(format!("Still open: {detail}"));
1572 kept.insert(format!("email:{}", alert.id));
1573 }
1574 }
1575 None => {
1576 let id = new_id("mal", now);
1577 self.db
1578 .prepare("INSERT INTO margin_alerts (id, kind, subject, detail, since, opened_at) VALUES (?, ?, ?, ?, ?, ?)")
1579 .bind(&[id.as_str().into(), kind.as_str().into(), subject.as_str().into(), detail.as_str().into(), from.as_str().into(), stamp.as_str().into()])?
1580 .run()
1581 .await?;
1582 kept.insert(id.clone());
Margin alerts measure what is sold, and say dollars when a percentage would mislead1583 // A workspace's is for Reach out, not the inbox.
1584 if kind != "workspace" {
1585 to_email.push(detail.clone());
1586 }
Topics sit in About on the files pages, under the description1587 kept.insert(format!("email:{id}"));
1588 }
1589 }
1590 }
1591 for alert in &open {
1592 if !kept.contains(&alert.id) {
1593 self.db
1594 .prepare("UPDATE margin_alerts SET resolved_at = ? WHERE id = ?")
1595 .bind(&[stamp.as_str().into(), alert.id.as_str().into()])?
1596 .run()
1597 .await?;
1598 }
1599 }
1600 let to = env.var("COSTS_ALERT_EMAIL").map(|v| v.to_string()).unwrap_or_default();
1601 if !to_email.is_empty() && !to.trim().is_empty() {
1602 let subject = format!("[g1t costs] {} margin alert{}", to_email.len(), if to_email.len() == 1 { "" } else { "s" });
1603 match email_staff(env, to.trim(), &subject, &to_email).await {
1604 Ok(()) => {
1605 for marker in kept.iter().filter_map(|k| k.strip_prefix("email:")) {
1606 self.db
1607 .prepare("UPDATE margin_alerts SET emailed_at = ? WHERE id = ?")
1608 .bind(&[stamp.as_str().into(), marker.into()])?
1609 .run()
1610 .await?;
1611 }
1612 }
1613 Err(error) => worker::console_error!("could not email the margin alerts: {error}"),
1614 }
1615 }
1616 Ok(conditions.len() as u32)
1617 }
1618
1619 /// Workspaces costing g1t more than they pay over 30 days, not g1t's own.
Margin alerts measure what is sold, and say dollars when a percentage would mislead1620 /// Each day's cost shared out to comped workspaces.
Topics sit in About on the files pages, under the description1621 async fn workspace_anomalies(&self, until: &str, settings: &CostSettings) -> Result<Vec<(String, i64, i64)>> {
1622 #[derive(Deserialize)]
1623 struct Row {
1624 workspace: String,
1625 cost: Option<i64>,
1626 revenue: Option<i64>,
1627 }
1628 let rows = self
1629 .db
1630 .prepare(format!(
Margin alerts measure what is sold, and say dollars when a percentage would mislead1631 // Against what its usage was priced at, not the cash it
1632 // paid: a trial or a gift paying for usage is not a price
1633 // below cost.
The workspace cost alert compares only days that carry their value, not the days before it was kept1634 // Days from before value_micros was kept have none: only days
1635 // since the first one that does are compared.
Margin alerts measure what is sold, and say dollars when a percentage would mislead1636 "SELECT workspace, SUM(cost_micros) AS cost, SUM(value_micros) AS revenue FROM workspace_costs
The workspace cost alert compares only days that carry their value, not the days before it was kept1637 WHERE day >= ?1 AND day <= ?2 AND workspace NOT IN ({})
1638 AND day >= (SELECT MIN(day) FROM workspace_costs WHERE value_micros > 0)
1639 GROUP BY workspace",
Topics sit in About on the files pages, under the description1640 crate::sales::INTERNAL_SQL
1641 ))
1642 .bind(&[day_before(until, ANOMALY_DAYS - 1).into(), until.into()])?
1643 .all()
1644 .await?
1645 .results::<Row>()?;
1646 let rows: Vec<(String, i64, i64)> = rows.into_iter().map(|r| (r.workspace, r.cost.unwrap_or(0), r.revenue.unwrap_or(0))).collect();
1647 Ok(anomalies(&rows, settings.anomaly_factor, settings.anomaly_floor_micros))
1648 }
1649
1650 /// For Reach out: workspaces with an open cost-over-revenue alert,
1651 /// each with its detail and cost.
1652 pub(crate) async fn costing_more_than_they_pay(&self) -> Result<Vec<(String, String, i64)>> {
1653 let alerts = self
1654 .db
1655 .prepare("SELECT * FROM margin_alerts WHERE resolved_at IS NULL AND kind = 'workspace' ORDER BY opened_at DESC LIMIT 50")
1656 .all()
1657 .await?
1658 .results::<AlertRow>()?;
1659 let mut out = Vec::new();
1660 for alert in alerts {
1661 #[derive(Deserialize)]
1662 struct Cost {
1663 cost: Option<i64>,
1664 }
1665 let cost = self
1666 .db
1667 .prepare("SELECT SUM(cost_micros) AS cost FROM workspace_costs WHERE workspace = ? AND day >= ?")
1668 .bind(&[alert.subject.as_str().into(), alert.since.as_str().into()])?
1669 .first::<Cost>(None)
1670 .await?
1671 .and_then(|c| c.cost)
1672 .unwrap_or(0);
1673 out.push((alert.subject, alert.detail, cost));
1674 }
1675 Ok(out)
1676 }
1677
1678 /// `admin_cost_alerts`: what sudo's banner says.
1679 pub(crate) async fn admin_cost_alerts(&self, _: AdminCostAlertsArgs) -> Result<Vec<MarginAlert>> {
1680 Ok(self
1681 .db
1682 .prepare("SELECT * FROM margin_alerts WHERE resolved_at IS NULL ORDER BY opened_at DESC LIMIT 50")
1683 .all()
1684 .await?
1685 .results::<AlertRow>()?
1686 .into_iter()
1687 .map(MarginAlert::from)
1688 .collect())
1689 }
1690
1691 /// `admin_run_costs`: the daily run, now.
1692 pub(crate) async fn admin_run_costs(&self, env: &Env, a: AdminRunCostsArgs) -> Result<Outcome<CostsRun>> {
1693 let keeper = crate::keeper::Keeper::from_env(env);
1694 let run = self.costs_daily(env, &keeper).await?;
1695 if !a.by.is_empty() {
1696 self.audit(
1697 "costs",
1698 "costs_run",
1699 &format!("{} lines, {} days, {} proposals, {} alerts", run.lines, run.days, run.proposals, run.alerts),
1700 &a.by,
1701 )
1702 .await?;
1703 }
1704 Ok(Outcome::Ok(run))
1705 }
1706
1707 /// `admin_set_cost_mapping`.
1708 pub(crate) async fn admin_set_cost_mapping(&self, a: AdminSetCostMappingArgs) -> Result<Outcome<CostMapping>> {
1709 let product = costs::slug(&a.product);
1710 let meter = if a.meter.trim() == "*" { "*".to_owned() } else { costs::slug(&a.meter) };
1711 if product.is_empty() || meter.is_empty() {
1712 return Ok(Outcome::fail(FailureCode::Invalid, "Name Cloudflare's product and a meter (or * for all of it)."));
1713 }
1714 let now = rfc3339(now_ms());
1715 if a.remove {
1716 self.db
1717 .prepare("DELETE FROM cost_map WHERE product = ? AND meter = ?")
1718 .bind(&[product.as_str().into(), meter.as_str().into()])?
1719 .run()
1720 .await?;
1721 self.audit("costs", "cost_mapping_removed", &format!("{product}/{meter}"), &a.by).await?;
1722 return Ok(Outcome::Ok(CostMapping {
1723 product,
1724 meter,
1725 bucket: String::new(),
1726 price_meter: None,
1727 own_meter: None,
1728 scale_to_own: false,
1729 drift_percent: 0.0,
1730 note: String::new(),
1731 updated_at: now,
1732 updated_by: a.by,
1733 }));
1734 }
1735 let bucket = costs::slug(&a.bucket);
1736 if bucket.is_empty() {
1737 return Ok(Outcome::fail(FailureCode::Invalid, "Say which of g1t's products it is a cost of."));
1738 }
1739 let clean = |v: Option<String>| v.map(|v| v.trim().to_owned()).filter(|v| !v.is_empty());
1740 let (price_meter, own_meter) = (clean(a.price_meter), clean(a.own_meter));
1741 let drift = a.drift_percent.filter(|d| d.is_finite() && *d > 0.0).unwrap_or(10.0);
1742 self.db
1743 .prepare(
1744 "INSERT INTO cost_map (product, meter, bucket, price_meter, own_meter, scale_to_own, drift_percent, note, updated_at, updated_by)
1745 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
1746 ON CONFLICT (product, meter) DO UPDATE SET bucket = ?3, price_meter = ?4, own_meter = ?5, scale_to_own = ?6,
1747 drift_percent = ?7, note = ?8, updated_at = ?9, updated_by = ?10",
1748 )
1749 .bind(&[
1750 product.as_str().into(),
1751 meter.as_str().into(),
1752 bucket.as_str().into(),
1753 crate::optional(price_meter.as_deref()),
1754 crate::optional(own_meter.as_deref()),
1755 i32::from(a.scale_to_own).into(),
1756 drift.into(),
1757 a.note.trim().into(),
1758 now.as_str().into(),
1759 a.by.as_str().into(),
1760 ])?
1761 .run()
1762 .await?;
1763 self.audit("costs", "cost_mapping", &format!("{product}/{meter} → {bucket}"), &a.by).await?;
1764 Ok(Outcome::Ok(CostMapping {
1765 product,
1766 meter,
1767 bucket,
1768 price_meter,
1769 own_meter,
1770 scale_to_own: a.scale_to_own,
1771 drift_percent: drift,
1772 note: a.note.trim().to_owned(),
1773 updated_at: now,
1774 updated_by: a.by,
1775 }))
1776 }
1777
1778 /// `admin_costs`: the Costs & margin page.
1779 pub(crate) async fn admin_costs(&self, a: AdminCostsArgs, configured: bool) -> Result<CostsReport> {
1780 let until = rfc3339(now_ms())[..10].to_owned();
1781 let span = u64::from(a.days.unwrap_or(30).clamp(7, 90));
1782 let since = day_before(&until, span - 1);
1783 let days = self.margin_days(&since, &until).await?;
1784 let rules = self.rules().await?;
1785
1786 let mut products: BTreeMap<String, ProductMargin> = BTreeMap::new();
1787 let mut overall = OverallMargin::default();
1788 for d in &days {
1789 let p = products.entry(d.bucket.clone()).or_insert_with(|| ProductMargin {
1790 bucket: d.bucket.clone(),
1791 title: costs::bucket_title(&d.bucket),
1792 cost_source: if NOT_CLOUDFLARE.contains(&d.bucket.as_str()) { "ledger" } else { "cloudflare" }.into(),
1793 overhead: OVERHEAD.contains(&d.bucket.as_str()),
1794 ..ProductMargin::default()
1795 });
1796 p.cf_cost_micros += d.cf_cost_micros;
1797 p.own_cost_micros += d.own_cost_micros;
1798 p.value_micros += d.value_micros;
1799 p.cost_micros += d.cost();
1800 overall.cost_micros += d.cost();
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running1801 overall.given_micros += d.given.total();
1802 if NOT_CLOUDFLARE.contains(&d.bucket.as_str()) {
1803 overall.models_cost_micros += d.cost();
1804 } else {
1805 overall.cloudflare_cost_micros += d.cost();
1806 }
1807 overall.given_comped_micros += d.given.comped;
1808 overall.given_free_micros += d.given.free;
1809 overall.given_trial_micros += d.given.trial;
1810 overall.given_pool_micros += d.given.pool;
Merge branch 'worktree-agent-a633ac0f7f66d419d'1811 overall.given_discount_micros += d.given.discount;
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging1812 overall.given_credit_promotional_micros += d.given.credit_promotional;
1813 overall.given_credit_goodwill_micros += d.given.credit_goodwill;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971814 overall.given_reset_micros += d.given.reset;
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)1815 overall.given_unpaid_micros += d.given.unpaid;
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running1816 let sold = (d.cost() - d.given.total()).max(0);
1817 if OVERHEAD.contains(&d.bucket.as_str()) {
Topics sit in About on the files pages, under the description1818 overall.plans_micros += d.cash_micros;
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running1819 overall.running_cost_micros += sold;
1820 } else if d.bucket == UNMAPPED {
1821 overall.usage_micros += d.cash_micros;
1822 overall.unmapped_cost_micros += sold;
Topics sit in About on the files pages, under the description1823 } else {
1824 overall.usage_micros += d.cash_micros;
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running1825 overall.usage_cost_micros += sold;
Topics sit in About on the files pages, under the description1826 }
1827 }
1828 for p in products.values_mut() {
1829 p.margin_micros = p.value_micros - p.cost_micros;
1830 p.margin_percent = margin_percent(p.value_micros, p.cost_micros);
1831 }
1832 let revenue = overall.usage_micros + overall.plans_micros;
1833 overall.margin_micros = revenue - overall.cost_micros;
1834 overall.margin_percent = margin_percent(revenue, overall.cost_micros);
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it1835 let sold = (overall.cost_micros - overall.given_micros).max(0);
1836 overall.sold_margin_micros = revenue - sold;
1837 overall.sold_margin_percent = margin_percent(revenue, sold);
Costs: the plan's included usage counts as paid for the usage it covered, out of what plans leave for running g1t; the run button shows it is running with CSS alone (sudo ships no JavaScript)1838 // The plan's included usage was paid for by the plan's price: it is
1839 // money in for the usage it covered, and out of what the plans
1840 // leave for running g1t.
1841 #[derive(Deserialize)]
1842 struct Included {
1843 micros: Option<i64>,
1844 }
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)1845 // Only what a plan paid for with real money (`without_real_money`).
1846 let live_since = self.payments_live_since().await?;
1847 overall.included_micros = match &live_since {
1848 None => 0,
1849 Some(live) => self
1850 .db
1851 .prepare(format!(
1852 "SELECT SUM(COALESCE(credit_micros, 0)) AS micros FROM ledger
1853 WHERE kind = 'usage' AND created_at >= ?1 AND created_at <= ?2 AND workspace NOT IN ({})",
1854 crate::sales::INTERNAL_SQL
1855 ))
1856 .bind(&[std::cmp::max(since.clone(), live.clone()).into(), format!("{until}T23:59:59.999Z").into()])?
1857 .first::<Included>(None)
1858 .await?
1859 .and_then(|r| r.micros)
1860 .unwrap_or(0),
1861 };
Costs: the plan's included usage counts as paid for the usage it covered, out of what plans leave for running g1t; the run button shows it is running with CSS alone (sudo ships no JavaScript)1862 let usage_in = overall.usage_micros + overall.included_micros;
1863 overall.usage_margin_micros = usage_in - overall.usage_cost_micros;
1864 overall.usage_margin_percent = margin_percent(usage_in, overall.usage_cost_micros);
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging1865 // Credits from g1t over the range: given, spent, and refunds' money
1866 // given back.
1867 overall.credits_given_micros = self
1868 .db
1869 .prepare("SELECT SUM(amount_micros) AS micros FROM credit_grants WHERE created_at >= ?1 AND created_at <= ?2")
1870 .bind(&[since.as_str().into(), format!("{until}T23:59:59.999Z").into()])?
1871 .first::<Included>(None)
1872 .await?
1873 .and_then(|r| r.micros)
1874 .unwrap_or(0);
1875 let (draws, refunds) = self.credit_effects(&since, &until).await?;
1876 overall.credits_used_micros = draws.iter().map(|(_, d)| d.micros).sum();
1877 overall.credits_refunded_micros = refunds.iter().map(|r| r.micros).sum();
Merge Stripe Tax, the card fee on card payments, and one free workspace per person1878 // Tax and card fees came in with payments but are neither cash nor
1879 // revenue: balances and plan payments are credited without them
1880 // (tax.rs), so cash above never holds them. Shown apart.
1881 (overall.tax_collected_micros, overall.card_fees_micros) = self.extras_between(&since, &until).await?;
Topics sit in About on the files pages, under the description1882 let mut products: Vec<ProductMargin> = products.into_values().collect();
1883 products.sort_by_key(|p| std::cmp::Reverse(p.cost_micros.max(p.value_micros)));
1884
1885 #[derive(Deserialize)]
1886 struct DriftRow {
1887 bucket: String,
1888 kind: String,
1889 ours: f64,
1890 cloudflare: f64,
1891 delta_percent: Option<f64>,
1892 detail: String,
1893 found_at: String,
1894 }
1895 let drift = self
1896 .db
1897 .prepare("SELECT * FROM cost_drift ORDER BY kind, bucket")
1898 .all()
1899 .await?
1900 .results::<DriftRow>()?
1901 .into_iter()
1902 .map(|r| CostDrift {
1903 title: costs::bucket_title(&r.bucket),
1904 bucket: r.bucket,
1905 kind: r.kind,
1906 ours: r.ours,
1907 cloudflare: r.cloudflare,
1908 delta_percent: r.delta_percent,
1909 detail: r.detail,
1910 found_at: r.found_at,
1911 })
1912 .collect();
1913
1914 #[derive(Deserialize)]
1915 struct Top {
1916 workspace: String,
1917 cost: Option<i64>,
1918 revenue: Option<i64>,
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it1919 given: Option<i64>,
Topics sit in About on the files pages, under the description1920 internal: i64,
1921 }
1922 let top_workspaces = self
1923 .db
1924 .prepare(format!(
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it1925 "SELECT workspace, SUM(cost_micros) AS cost, SUM(revenue_micros) AS revenue, SUM(given_micros) AS given,
Topics sit in About on the files pages, under the description1926 CASE WHEN workspace IN ({}) THEN 1 ELSE 0 END AS internal
1927 FROM workspace_costs WHERE day >= ?1 AND day <= ?2 GROUP BY workspace ORDER BY cost DESC LIMIT 15",
1928 crate::sales::INTERNAL_SQL
1929 ))
1930 .bind(&[since.as_str().into(), until.as_str().into()])?
1931 .all()
1932 .await?
1933 .results::<Top>()?
1934 .into_iter()
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it1935 .map(|t| WorkspaceCost { workspace: t.workspace, cost_micros: t.cost.unwrap_or(0), revenue_micros: t.revenue.unwrap_or(0), given_micros: t.given.unwrap_or(0), internal: t.internal == 1 })
Topics sit in About on the files pages, under the description1936 .collect();
1937
1938 #[derive(Deserialize)]
1939 struct Summary {
1940 source: String,
1941 product: String,
1942 meter: String,
1943 raw_name: String,
1944 unit: String,
1945 quantity: f64,
1946 cost_usd: f64,
1947 }
1948 let lines = self
1949 .db
1950 .prepare(
1951 "SELECT source, product, meter, MAX(raw_name) AS raw_name, MAX(unit) AS unit, SUM(quantity) AS quantity, SUM(cost_usd) AS cost_usd
1952 FROM cost_lines WHERE day >= ?1 AND day <= ?2 GROUP BY source, product, meter ORDER BY cost_usd DESC, product, meter LIMIT 200",
1953 )
1954 .bind(&[since.as_str().into(), until.as_str().into()])?
1955 .all()
1956 .await?
1957 .results::<Summary>()?
1958 .into_iter()
1959 .map(|l| CostLineSummary {
1960 bucket: costs::classify(&rules, &l.product, &l.meter).map(|r| r.bucket.clone()),
1961 product: l.product,
1962 meter: l.meter,
1963 raw_name: l.raw_name,
1964 unit: l.unit,
1965 source: l.source,
1966 quantity: l.quantity,
1967 cost_micros: micros(l.cost_usd),
1968 })
1969 .collect();
1970
1971 #[derive(Deserialize)]
1972 struct MapRow {
1973 product: String,
1974 meter: String,
1975 bucket: String,
1976 price_meter: Option<String>,
1977 own_meter: Option<String>,
1978 scale_to_own: i64,
1979 drift_percent: f64,
1980 note: String,
1981 updated_at: String,
1982 updated_by: String,
1983 }
1984 let mappings = self
1985 .db
1986 .prepare("SELECT * FROM cost_map ORDER BY product, meter")
1987 .all()
1988 .await?
1989 .results::<MapRow>()?
1990 .into_iter()
1991 .map(|m| CostMapping {
1992 product: m.product,
1993 meter: m.meter,
1994 bucket: m.bucket,
1995 price_meter: m.price_meter,
1996 own_meter: m.own_meter,
1997 scale_to_own: m.scale_to_own == 1,
1998 drift_percent: m.drift_percent,
1999 note: m.note,
2000 updated_at: m.updated_at,
2001 updated_by: m.updated_by,
2002 })
2003 .collect();
2004
2005 #[derive(Deserialize)]
2006 struct Fetched {
2007 at: Option<String>,
2008 }
2009 let fetched_at = self.db.prepare("SELECT MAX(fetched_at) AS at FROM cost_lines").first::<Fetched>(None).await?.and_then(|f| f.at);
2010
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)2011 // Cloudflare's subscriptions over the range, day by day as each
2012 // day's share of its billing cycle; and the cycle itself.
2013 let anchor = self.cycle_anchor().await?;
2014 let fixed = self.fixed_monthly(self.caps.fixed_monthly).await?;
2015 overall.subscriptions_micros = crate::cycle::accrued(fixed.monthly_micros, &since, &until, anchor);
2016 let cycle = self.cloudflare_cycle(&until, anchor, fixed.monthly_micros).await?;
2017 let bill_read = self.bill_read().await?;
2018 // What AI Gateway priced g1t's own provider traffic at, beside the
2019 // ledger's model cost (Cloudflare-billed requests are Cloudflare's).
2020 overall.gateway_cost_micros = self
2021 .db
2022 .prepare("SELECT SUM(cost_usd) AS cost FROM cost_lines WHERE source = ?1 AND day >= ?2 AND day <= ?3 AND substr(meter, 1, 11) <> ?4")
2023 .bind(&[costs::SOURCE_GATEWAY.into(), since.as_str().into(), until.as_str().into(), costs::GATEWAY_WHOLESALE.into()])?
2024 .first::<CostSum>(None)
2025 .await?
2026 .and_then(|c| c.cost)
2027 .map_or(0, micros);
2028 // The workspaces' shares of the cost, and what no one's usage carried.
2029 #[derive(Deserialize)]
2030 struct Shared {
2031 micros: Option<i64>,
2032 }
2033 let shared = self
2034 .db
2035 .prepare("SELECT SUM(cost_micros) AS micros FROM workspace_costs WHERE day >= ?1 AND day <= ?2")
2036 .bind(&[since.as_str().into(), until.as_str().into()])?
2037 .first::<Shared>(None)
2038 .await?
2039 .and_then(|s| s.micros)
2040 .unwrap_or(0);
2041 let unattributed_micros = unattributed(overall.cost_micros, shared);
2042
Topics sit in About on the files pages, under the description2043 Ok(CostsReport {
2044 configured,
2045 fetched_at,
2046 days: days
2047 .iter()
2048 .map(|d| CostDay {
2049 day: d.day.clone(),
2050 bucket: d.bucket.clone(),
2051 cf_cost_micros: d.cf_cost_micros,
2052 own_cost_micros: d.own_cost_micros,
2053 value_micros: d.value_micros,
2054 cash_micros: d.cash_micros,
2055 })
2056 .collect(),
2057 since,
2058 until,
2059 products,
2060 overall,
2061 drift,
2062 alerts: self.admin_cost_alerts(AdminCostAlertsArgs {}).await?,
2063 proposals: self.proposals().await?,
2064 versions: self.versions().await?,
2065 top_workspaces,
2066 lines,
2067 mappings,
2068 settings: self.cost_settings().await?,
2069 caps: self.spend_caps().await?,
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)2070 cycle,
2071 bill_read,
2072 unattributed_micros,
Topics sit in About on the files pages, under the description2073 })
2074 }
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)2075
2076 /// Cloudflare's billing cycle that `today` is in: its usage cost so far,
2077 /// by meter, with the included amounts, and where it is heading. None
2078 /// while nothing of it has been read.
2079 async fn cloudflare_cycle(&self, today: &str, anchor: u32, monthly_micros: i64) -> Result<Option<CloudflareCycle>> {
2080 #[derive(Deserialize)]
2081 struct Row {
2082 product: String,
2083 meter: String,
2084 raw_name: String,
2085 unit: String,
2086 quantity: f64,
2087 billable_quantity: Option<f64>,
2088 cost_usd: f64,
2089 basis: Option<String>,
2090 }
2091 let cycle = crate::cycle::cycle_of(today, anchor);
2092 let rows = self
2093 .db
2094 .prepare(
2095 "SELECT product, meter, MAX(raw_name) AS raw_name, MAX(unit) AS unit, SUM(quantity) AS quantity,
2096 SUM(billable_quantity) AS billable_quantity, SUM(cost_usd) AS cost_usd, MAX(basis) AS basis
2097 FROM cost_lines WHERE source = ?1 AND day >= ?2 AND day <= ?3 GROUP BY product, meter",
2098 )
2099 .bind(&[SOURCE_BILLABLE.into(), cycle.start.as_str().into(), today.into()])?
2100 .all()
2101 .await?
2102 .results::<Row>()?;
2103 if rows.is_empty() {
2104 return Ok(None);
2105 }
2106 let elapsed = cycle.days_elapsed(today);
2107 let meters = rows
2108 .into_iter()
2109 .map(|r| {
2110 let list = crate::cycle::list_price(&r.product, &r.meter);
2111 CycleMeter {
2112 included: list.map(|p| if p.daily { p.included * elapsed as f64 } else { p.included }),
2113 billable_quantity: r.billable_quantity.unwrap_or(0.0),
2114 cost_micros: micros(r.cost_usd),
2115 basis: r.basis.filter(|b| !b.is_empty()).unwrap_or_else(|| crate::cycle::BASIS_NONE.to_owned()),
2116 product: r.product,
2117 meter: r.meter,
2118 raw_name: r.raw_name,
2119 unit: r.unit,
2120 quantity: r.quantity,
2121 }
2122 })
2123 .collect();
2124 Ok(Some(cycle_report(&cycle, elapsed, meters, monthly_micros)))
2125 }
2126
2127 /// The last read of billable usage (`cost_reads`).
2128 async fn bill_read(&self) -> Result<Option<BillRead>> {
2129 #[derive(Deserialize)]
2130 struct Row {
2131 read_at: String,
2132 since: String,
2133 until: String,
2134 rows: u32,
2135 pages: u32,
2136 consumed_rows: u32,
2137 pricing_only_rows: u32,
2138 costed_rows: u32,
2139 }
2140 Ok(self
2141 .db
2142 .prepare("SELECT * FROM cost_reads WHERE source = ?1")
2143 .bind(&[SOURCE_BILLABLE.into()])?
2144 .first::<Row>(None)
2145 .await?
2146 .map(|r| BillRead {
2147 read_at: r.read_at,
2148 since: r.since,
2149 until: r.until,
2150 rows: r.rows,
2151 pages: r.pages,
2152 consumed_rows: r.consumed_rows,
2153 pricing_only_rows: r.pricing_only_rows,
2154 costed_rows: r.costed_rows,
2155 }))
2156 }
Topics sit in About on the files pages, under the description2157}
2158
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)2159#[derive(Deserialize)]
2160struct CostSum {
2161 cost: Option<f64>,
2162}
2163
2164/// Of `total` cost, what the workspaces' shares did not carry: running g1t
2165/// on days no workspace used anything. Never below zero.
2166pub(crate) fn unattributed(total: i64, shared: i64) -> i64 {
2167 (total - shared).max(0)
2168}
2169
2170/// The cycle as sudo shows it: the meters, most costly first, their total,
2171/// the average day and Cloudflare's projection.
2172pub(crate) fn cycle_report(cycle: &crate::cycle::Cycle, elapsed: u32, mut meters: Vec<CycleMeter>, monthly_micros: i64) -> CloudflareCycle {
2173 meters.sort_by(|a, b| b.cost_micros.cmp(&a.cost_micros).then(b.quantity.total_cmp(&a.quantity)).then(a.meter.cmp(&b.meter)));
2174 let usage: i64 = meters.iter().map(|m| m.cost_micros).sum();
2175 let days = cycle.days();
2176 CloudflareCycle {
2177 start: cycle.start.clone(),
2178 end: cycle.end.clone(),
2179 days,
2180 days_elapsed: elapsed,
2181 usage_micros: usage,
2182 projected_micros: crate::cycle::project(usage, elapsed, days),
2183 average_daily_micros: if elapsed > 0 { usage / elapsed as i64 } else { usage },
2184 subscriptions_micros: monthly_micros,
2185 meters,
2186 }
2187}
2188
Topics sit in About on the files pages, under the description2189#[cfg(test)]
2190mod tests {
2191 use super::*;
2192
Margin alerts measure what is sold, and say dollars when a percentage would mislead2193 #[test]
Models' margin read -14%: usage nothing paid for is valued at price, not $02194 fn usage_nothing_paid_for_is_valued_at_price_and_paid_usage_at_what_was_paid() {
2195 // A free period: charged nothing, drawn from nothing.
2196 assert_eq!(usage_value(false, 1_000_000, 0, 20), 1_200_000);
2197 // Charged, or drawn from a trial: what was paid.
2198 assert_eq!(usage_value(false, 1_000_000, 1_200_000, 20), 1_200_000);
2199 assert_eq!(usage_value(false, 1_000_000, 900_000, 20), 900_000);
2200 // g1t's own: at price.
2201 assert_eq!(usage_value(true, 1_000_000, 0, 20), 1_200_000);
2202 // No cost, nothing paid: nothing.
2203 assert_eq!(usage_value(false, 0, 0, 20), 0);
2204 }
2205
2206 #[test]
Margin alerts measure what is sold, and say dollars when a percentage would mislead2207 fn the_overall_alert_says_dollars_while_little_comes_in() {
2208 let small = overall_detail(90_000, 7_500_000, 3, 10.0, -8239.7);
2209 assert!(small.contains("took in $0.09 against $7.50"), "{small}");
2210 assert!(!small.contains('%'), "{small}");
2211 let real = overall_detail(30_000_000, 40_000_000, 3, 10.0, -33.3);
2212 assert!(real.contains("as low as -33.3%"), "{real}");
2213 }
2214
Topics sit in About on the files pages, under the description2215 fn rule(product: &str, meter: &str, bucket: &str, own: Option<&str>) -> Rule {
2216 Rule { product: product.into(), meter: meter.into(), bucket: bucket.into(), price_meter: None, own_meter: own.map(Into::into), drift_percent: 10.0 }
2217 }
2218
2219 fn rules() -> Vec<Rule> {
2220 vec![
2221 rule("containers", "*", "sandboxes", None),
2222 rule("workers", "*", "platform", None),
2223 rule("artifacts", "*", "git", Some("git_operations")),
2224 rule("artifacts", "events_", "git", Some("git_operations")),
2225 ]
2226 }
2227
2228 fn revenue_map() -> BTreeMap<String, String> {
2229 [("sandbox", "sandboxes"), ("git", "git"), ("plan", "platform")].iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
2230 }
2231
2232 fn line(day: &str, source: &str, product: &str, meter: &str, quantity: f64, cost: f64) -> LineRow {
2233 LineRow { day: day.into(), source: source.into(), product: product.into(), meter: meter.into(), quantity, cost_usd: cost }
2234 }
2235
2236 fn usage(day: &str, workspace: &str, key: &str, value: i64, cash: i64, cost: i64) -> UsageRow {
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb972237 UsageRow { day: day.into(), workspace: workspace.into(), key: key.into(), bucket: None, value, cash, cost, given: Given::default() }
Topics sit in About on the files pages, under the description2238 }
2239
2240 #[test]
2241 fn a_day_puts_the_bill_g1ts_counts_and_charges_side_by_side() {
2242 let lines = vec![
2243 line("2026-10-15", SOURCE_BILLABLE, "containers", "container_memory", 1000.0, 2.00),
2244 line("2026-10-15", SOURCE_BILLABLE, "artifacts", "artifacts_operations", 30_000.0, 3.00),
2245 // Artifacts' own events: not used while the bill has a count.
2246 line("2026-10-15", SOURCE_ARTIFACTS, "artifacts", "events_pull", 29_000.0, 0.0),
2247 line("2026-10-15", SOURCE_BILLABLE, "workers", "workers_cpu_ms", 1.0, 0.50),
2248 line("2026-10-15", SOURCE_BILLABLE, "browser_rendering", "browser_hours", 2.0, 0.25),
2249 ];
2250 let own = vec![
2251 OwnRow { day: "2026-10-15".into(), meter: "git_operations".into(), workspace: "acme".into(), quantity: 7_500.0 },
2252 OwnRow { day: "2026-10-15".into(), meter: "git_operations".into(), workspace: "beta".into(), quantity: 2_500.0 },
2253 ];
2254 let usage = vec![
2255 usage("2026-10-15", "acme", "sandbox", 2_400_000, 1_000_000, 2_000_000),
2256 usage("2026-10-15", "beta", "sandbox", 1_200_000, 1_200_000, 1_000_000),
2257 usage("2026-10-15", "acme", "git", 600_000, 600_000, 500_000),
2258 usage("2026-10-15", "acme", "implement", 120_000, 120_000, 100_000),
2259 usage("2026-10-15", "beta", "plan", 20_000_000, 20_000_000, 0),
2260 ];
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running2261 let (days, workspaces) = fold(&rules(), &revenue_map(), &lines, &own, &usage, &BTreeSet::new());
Topics sit in About on the files pages, under the description2262 let get = |bucket: &str| days.iter().find(|d| d.bucket == bucket).unwrap();
2263 let sandboxes = get("sandboxes");
2264 assert_eq!((sandboxes.cf_cost_micros, sandboxes.own_cost_micros, sandboxes.value_micros, sandboxes.cash_micros), (2_000_000, 3_000_000, 3_600_000, 2_200_000));
2265 let git = get("git");
2266 assert_eq!(git.cf_cost_micros, 3_000_000);
2267 assert_eq!((git.cf_quantity, git.own_quantity), (30_000.0, 10_000.0));
2268 assert_eq!(get("platform").value_micros, 20_000_000);
2269 // Not mapped: a leak until someone maps it.
2270 assert_eq!(get(UNMAPPED).cf_cost_micros, 250_000);
2271 // Models: no Cloudflare line, their cost is g1t's own.
2272 assert_eq!(get("models").cost(), 100_000);
2273 // Git's cost shared by g1t's own counts (Cloudflare gave none per
2274 // workspace here): three quarters to acme.
2275 let share = |ws: &str, bucket: &str| workspaces.iter().find(|w| w.workspace == ws && w.bucket == bucket).map(|w| (w.cost, w.revenue));
2276 assert_eq!(share("acme", "git"), Some((2_250_000, 600_000)));
2277 assert_eq!(share("beta", "git"), Some((750_000, 0)));
2278 // Every bucket's cost is shared out exactly.
2279 for d in &days {
2280 let shared: i64 = workspaces.iter().filter(|w| w.bucket == d.bucket).map(|w| w.cost).sum();
2281 assert_eq!(shared, d.cost(), "{}", d.bucket);
2282 }
2283 }
2284
2285 #[test]
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises2286 fn a_planning_run_is_model_cost_not_running_g1t() {
2287 // flagon-io's planning run on 2026-10-07 cost $0.0748 of model
2288 // calls; read under the ledger's task, `plan`, it went to running
2289 // g1t, where Cloudflare's bill is the cost, and the model cost was
2290 // lost from the statement and the drift.
2291 let usage = vec![
2292 usage("2026-10-07", "flagon-io", PLANNING_KEY, 89_741, 0, 74_784),
2293 usage("2026-10-07", "acme", "plan", 666_666, 666_666, 0),
2294 ];
2295 let (days, _) = fold(&rules(), &revenue_map(), &[], &[], &usage, &BTreeSet::new());
2296 let get = |bucket: &str| days.iter().find(|d| d.bucket == bucket).unwrap();
2297 assert_eq!((get("models").cost(), get("models").value_micros), (74_784, 89_741));
2298 assert_eq!((get("platform").own_cost_micros, get("platform").cash_micros), (0, 666_666));
2299 assert!(!revenue_map().contains_key(PLANNING_KEY));
2300 }
2301
2302 #[test]
2303 fn a_comped_workspaces_month_end_meters_are_given_never_money_in() {
2304 let snap = |day: &str, cost: i64, charge: i64| (day.to_string(), "flagon-io".to_string(), "cache".to_string(), cost, charge);
2305 let rows: Vec<UsageRow> = pending_deltas(&[snap("2026-10-07", 1, 2), snap("2026-10-08", 473, 568)]).into_iter().map(comped_meter).collect();
2306 assert_eq!(rows.iter().map(|r| (r.cash, r.value, r.given.comped)).collect::<Vec<_>>(), vec![(0, 2, 2), (0, 566, 566)]);
2307 let internal: BTreeSet<String> = ["flagon-io".to_string()].into();
2308 let (days, workspaces) = fold(&rules(), &revenue_map(), &[], &[], &rows, &internal);
2309 assert!(days.iter().all(|d| d.cash_micros == 0));
2310 assert!(workspaces.iter().all(|w| w.revenue == 0));
2311 }
2312
2313 #[test]
Topics sit in About on the files pages, under the description2314 fn artifacts_events_count_when_the_bill_does_not() {
2315 let lines = vec![
2316 line("2026-10-05", SOURCE_ARTIFACTS, "artifacts", "events_pull", 120.0, 0.0),
2317 line("2026-10-05", SOURCE_ARTIFACTS, "artifacts", "events_push", 30.0, 0.0),
2318 line("2026-10-05", SOURCE_ARTIFACTS, "artifacts", "events_ratelimited", 9.0, 0.0),
2319 ];
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running2320 let (days, _) = fold(&rules(), &revenue_map(), &lines, &[], &[], &BTreeSet::new());
Topics sit in About on the files pages, under the description2321 assert_eq!(days[0].cf_quantity, 150.0);
2322 assert_eq!(days[0].cf_cost_micros, 0);
2323 }
2324
2325 #[test]
2326 fn month_end_meters_are_told_by_the_day_from_snapshots() {
2327 let snap = |day: &str, cost: i64, charge: i64| (day.to_string(), "acme".to_string(), "git".to_string(), cost, charge);
2328 let rows = pending_deltas(&[snap("2026-10-30", 100, 120), snap("2026-10-31", 250, 300), snap("2026-11-01", 40, 48), snap("2026-11-02", 40, 48)]);
2329 assert_eq!(
2330 rows.iter().map(|r| (r.day.as_str(), r.cost, r.value)).collect::<Vec<_>>(),
2331 vec![("2026-10-30", 100, 120), ("2026-10-31", 150, 180), ("2026-11-01", 40, 48)]
2332 );
2333 }
2334
2335 #[test]
2336 fn a_plan_payment_is_spread_over_the_month_it_pays_for() {
2337 let days = spread("2026-10-01T00:00:00.000Z", 20_000_000, 30);
2338 assert_eq!(days.len(), 30);
2339 assert_eq!(days[0], ("2026-10-01".to_string(), 666_667));
2340 assert_eq!(days[29], ("2026-10-30".to_string(), 666_666));
2341 assert_eq!(days.iter().map(|d| d.1).sum::<i64>(), 20_000_000);
2342 assert!(spread("2026-10-01", 0, 30).is_empty());
2343 assert_eq!(dollars(17_024_000), "$17.02");
2344 assert_eq!(dollars(-27_668_620), "-$27.67");
2345 assert_eq!(dollars(63_000), "$0.063");
2346 }
2347
2348 #[test]
2349 fn margins_and_deltas() {
2350 assert_eq!(margin_percent(1_200_000, 1_000_000).map(|m| (m * 100.0).round() / 100.0), Some(16.67));
2351 assert_eq!(margin_percent(0, 5), None);
2352 assert_eq!(delta_percent(110.0, 100.0), Some(10.0));
2353 assert_eq!(delta_percent(1.0, 0.0), None);
2354 }
2355
2356 fn day(bucket: &str, cf: i64, own: i64, value: i64, cfq: f64, ownq: f64) -> ProductDay {
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running2357 ProductDay { day: "2026-10-15".into(), bucket: bucket.into(), cf_cost_micros: cf, own_cost_micros: own, value_micros: value, cash_micros: value, cf_quantity: cfq, own_quantity: ownq, given: Given::default() }
Topics sit in About on the files pages, under the description2358 }
2359
2360 #[test]
2361 fn counts_more_than_the_threshold_apart_are_drift() {
2362 // Cloudflare counted 30,000 operations where g1t counted 10,000:
2363 // binding reads, perhaps. -66.7%.
Merge remote-tracking branch 'origin/main' into workspace-chat2364 let drift = drifts("git", &[day("git", 3_000_000, 1_500_000, 1_800_000, 30_000.0, 10_000.0)], 10.0, true, 100_000, Some(3_000_000.0));
Topics sit in About on the files pages, under the description2365 assert_eq!(drift.iter().map(|d| d.kind).collect::<Vec<_>>(), vec![DriftKind::Count, DriftKind::Cost]);
2366 assert!((drift[0].delta_percent.unwrap() + 66.666).abs() < 0.01);
2367 // 9% apart: within 10%.
Merge remote-tracking branch 'origin/main' into workspace-chat2368 assert!(drifts("git", &[day("git", 1_000_000, 1_000_000, 1_200_000, 10_000.0, 10_900.0)], 10.0, true, 100_000, Some(1_000_000.0)).is_empty());
Topics sit in About on the files pages, under the description2369 // Uncounted products have no count drift.
Merge remote-tracking branch 'origin/main' into workspace-chat2370 assert!(drifts("sandboxes", &[day("sandboxes", 1_000_000, 1_050_000, 1_200_000, 5.0, 0.0)], 10.0, false, 100_000, Some(1_000_000.0)).is_empty());
Topics sit in About on the files pages, under the description2371 }
2372
2373 #[test]
2374 fn cost_with_no_revenue_is_a_leak_but_not_for_running_g1t() {
Merge remote-tracking branch 'origin/main' into workspace-chat2375 let leak = drifts("actions_cache", &[day("actions_cache", 400_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000, None);
Topics sit in About on the files pages, under the description2376 assert_eq!(leak.len(), 1);
2377 assert_eq!(leak[0].kind, DriftKind::Leak);
Merge remote-tracking branch 'origin/main' into workspace-chat2378 assert!(drifts("platform", &[day("platform", 5_000_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000, None).is_empty());
Topics sit in About on the files pages, under the description2379 // Pennies say nothing.
Merge remote-tracking branch 'origin/main' into workspace-chat2380 assert!(drifts("actions_cache", &[day("actions_cache", 50_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000, None).is_empty());
2381 assert!(drifts(UNMAPPED, &[day(UNMAPPED, 250_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000, None)[0].kind == DriftKind::Leak);
2382 }
2383
2384 /// A week of sandboxes mostly inside the cycle's included amounts:
2385 /// Cloudflare billed $0.0922 (the memory past 25 GiB-hours), and the
2386 /// same usage is $1.70 at list prices.
2387 fn sandbox_week() -> (Vec<Rule>, Vec<LineRow>) {
2388 let mut rules = rules();
2389 rules.push(rule("durable_objects", "durable_objects_compute_duration", "sandboxes", None));
2390 let lines = vec![
2391 line("2026-10-07", SOURCE_BILLABLE, "containers", "container_memory_per_gib_second", 126_870.0, 0.092_175),
2392 line("2026-10-07", SOURCE_BILLABLE, "containers", "container_vcpu", 12_000.0, 0.0),
2393 line("2026-10-07", SOURCE_BILLABLE, "containers", "container_disk_per_gb_second", 253_740.0, 0.0),
2394 line("2026-10-07", SOURCE_BILLABLE, "durable_objects", "durable_objects_compute_duration", 90_000.0, 0.0),
2395 // Not billable usage: no part of the list cost.
2396 line("2026-10-07", SOURCE_ARTIFACTS, "artifacts", "events_push", 40.0, 0.0),
2397 ];
2398 (rules, lines)
2399 }
2400
2401 #[test]
2402 fn usage_inside_the_included_amounts_is_not_a_stale_price() {
2403 let (rules, lines) = sandbox_week();
2404 let list = list_costs(&rules, &lines);
2405 let sandboxes = list["sandboxes"].unwrap();
2406 assert!((sandboxes - 1_699_936.8).abs() < 1.0, "{sandboxes}");
2407 // The price book's cost of the same usage is $1.69: within 1% of
2408 // the list, though Cloudflare billed $0.0922 after the included
2409 // amounts. Before, that read as +1733.6%.
2410 let week = day("sandboxes", 92_175, 1_690_000, 2_028_000, 0.0, 0.0);
2411 assert!(drifts("sandboxes", std::slice::from_ref(&week), 10.0, false, 100_000, Some(sandboxes)).is_empty());
2412 let net = drifts("sandboxes", &[week], 10.0, false, 100_000, Some(92_175.0));
2413 assert_eq!(net[0].kind, DriftKind::Cost, "billed against the price book was the false alarm");
2414 }
2415
2416 #[test]
2417 fn a_stale_price_is_still_drift() {
2418 let (rules, lines) = sandbox_week();
2419 let sandboxes = list_costs(&rules, &lines)["sandboxes"].unwrap();
2420 // The price book still costs the same usage at $1.20: a list price
2421 // rose and the book did not follow.
2422 let found = drifts("sandboxes", &[day("sandboxes", 92_175, 1_200_000, 1_440_000, 0.0, 0.0)], 10.0, false, 100_000, Some(sandboxes));
2423 assert_eq!(found.len(), 1);
2424 assert_eq!((found[0].kind, found[0].ours, found[0].cloudflare.round()), (DriftKind::Cost, 1_200_000.0, 1_699_937.0));
2425 assert!((found[0].delta_percent.unwrap() + 29.4).abs() < 0.1);
2426 // Nothing billed at all (the whole week inside the included
2427 // amounts) is checked all the same.
2428 assert_eq!(drifts("sandboxes", &[day("sandboxes", 0, 1_200_000, 1_440_000, 0.0, 0.0)], 10.0, false, 100_000, Some(sandboxes)).len(), 1);
2429 }
2430
2431 #[test]
2432 fn a_bucket_with_a_meter_with_no_list_price_is_not_checked() {
2433 let (rules, mut lines) = sandbox_week();
2434 lines.push(line("2026-10-07", SOURCE_BILLABLE, "artifacts", "storage", 3.0, 0.4));
2435 lines.push(line("2026-10-07", SOURCE_BILLABLE, "artifacts", "operations", 0.0, 0.0));
2436 let list = list_costs(&rules, &lines);
2437 assert_eq!(list["git"], None);
2438 assert!(list["sandboxes"].is_some());
2439 // No list cost: no cost drift, but a leak is still what was billed.
2440 assert!(drifts("git", &[day("git", 3_000_000, 1_000_000, 1_200_000, 0.0, 0.0)], 10.0, false, 100_000, None).is_empty());
2441 assert_eq!(drifts("git", &[day("git", 3_000_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000, None)[0].kind, DriftKind::Leak);
2442 }
2443
2444 #[test]
2445 fn a_unit_rate_is_the_list_price_where_there_is_one() {
2446 // Billed $0.20 for 11.16M CPU ms (9.16M past the included 30M, in
2447 // whole millions): $0.02 a million at list, whatever was billed.
2448 let (q, c) = rate_line("workers", "workers_cpu_ms", 11_160_000.0, 0.20);
2449 assert!((billed_rate(&[(q, c)]).unwrap() * 1e6 - 0.02).abs() < 1e-12);
2450 // No list price: what Cloudflare billed.
2451 assert_eq!(rate_line("artifacts", "operations", 1_000.0, 0.5), (1_000.0, 0.5));
Topics sit in About on the files pages, under the description2452 }
2453
2454 #[test]
2455 fn a_margin_alert_needs_n_days_in_a_row_under_the_floor() {
2456 let s = |d: &str, revenue: i64, cost: i64| (d.to_string(), revenue, cost);
2457 // 5%, 0%, -20%: three days under 10%.
2458 let series = vec![s("10-13", 1_200_000, 1_000_000), s("10-14", 1_050_000, 1_000_000), s("10-15", 1_000_000, 1_000_000), s("10-16", 1_000_000, 1_200_000)];
2459 let (from, worst) = breach(&series, 10.0, 3, 100_000).unwrap();
2460 assert_eq!(from, "10-14");
2461 assert!((worst + 20.0).abs() < 1e-9);
2462 // A good day in the window clears it.
2463 let mended = vec![s("10-14", 1_050_000, 1_000_000), s("10-15", 1_300_000, 1_000_000), s("10-16", 1_000_000, 1_200_000)];
2464 assert!(breach(&mended, 10.0, 3, 100_000).is_none());
2465 // Cost with no revenue at all is the worst margin there is.
2466 assert_eq!(breach(&[s("10-16", 0, 500_000)], 10.0, 1, 100_000).unwrap().1, -100.0);
2467 // Too little cost to judge.
2468 assert!(breach(&[s("10-16", 0, 5_000)], 10.0, 1, 100_000).is_none());
2469 assert!(breach(&series, 10.0, 9, 100_000).is_none());
2470 }
2471
2472 #[test]
2473 fn shared_costs_add_up_to_the_bill() {
2474 let w = |k: &str, v: f64| (k.to_string(), v);
2475 assert_eq!(attribute(100, &[w("a", 1.0), w("b", 1.0), w("c", 1.0)]), vec![("a".into(), 34), ("b".into(), 33), ("c".into(), 33)]);
2476 assert_eq!(attribute(10, &[w("a", 3.0), w("b", 1.0), w("a", 0.0)]), vec![("a".into(), 8), ("b".into(), 2)]);
2477 assert!(attribute(10, &[w("a", 0.0)]).is_empty());
2478 assert!(attribute(0, &[w("a", 1.0)]).is_empty());
2479 }
2480
2481 #[test]
Costs: counts are compared with Cloudflare's from the day g1t started counting, so days before a meter existed are not drift2482 fn counts_are_compared_from_the_day_g1t_started_counting() {
2483 let on = |day: &str, cf: f64, own: f64| ProductDay { day: day.into(), bucket: "git".into(), cf_quantity: cf, own_quantity: own, ..ProductDay::default() };
2484 // Five days of Cloudflare's count before g1t's meter, then two that match.
2485 let days = vec![on("2026-10-01", 500.0, 0.0), on("2026-10-05", 300.0, 0.0), on("2026-10-06", 210.0, 231.0), on("2026-10-07", 450.0, 458.0)];
Merge remote-tracking branch 'origin/main' into workspace-chat2486 assert!(drifts("git", &days, 10.0, true, 0, None).iter().all(|d| d.kind != DriftKind::Count));
Costs: counts are compared with Cloudflare's from the day g1t started counting, so days before a meter existed are not drift2487 // A real gap on the days both counted still shows.
2488 let days = vec![on("2026-10-01", 500.0, 0.0), on("2026-10-06", 400.0, 231.0), on("2026-10-07", 600.0, 300.0)];
Merge remote-tracking branch 'origin/main' into workspace-chat2489 let found = drifts("git", &days, 10.0, true, 0, None);
Costs: counts are compared with Cloudflare's from the day g1t started counting, so days before a meter existed are not drift2490 let count = found.iter().find(|d| d.kind == DriftKind::Count).unwrap();
2491 assert_eq!((count.ours, count.cloudflare), (531.0, 1000.0));
2492 // A meter that never counted is compared over every day.
2493 let days = vec![on("2026-10-06", 400.0, 0.0)];
Merge remote-tracking branch 'origin/main' into workspace-chat2494 assert!(drifts("git", &days, 10.0, true, 0, None).iter().any(|d| d.kind == DriftKind::Count));
Costs: counts are compared with Cloudflare's from the day g1t started counting, so days before a meter existed are not drift2495 }
2496
2497 #[test]
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it2498 fn what_g1t_gives_away_is_kept_apart_from_what_it_sells() {
2499 let map = BTreeMap::new();
2500 // A comped workspace (all of it given), one in its trial (half paid
2501 // by the trial) and one paying in cash, all on models.
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running2502 let comped = usage("2026-10-15", "flagon", "agent", 1_200_000, 0, 1_000_000);
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it2503 let mut trial = usage("2026-10-15", "acme", "agent", 1_200_000, 600_000, 1_000_000);
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running2504 trial.given = Given { trial: 600_000, ..Given::default() };
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it2505 let paying = usage("2026-10-15", "beta", "agent", 1_200_000, 1_200_000, 1_000_000);
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running2506 // Nothing priced that day: free use.
2507 let free = usage("2026-10-15", "gamma", "agent", 0, 0, 1_000_000);
2508 let internal = BTreeSet::from(["flagon".to_string()]);
2509 let (days, workspaces) = fold(&[], &map, &[], &[], &[comped, trial, paying, free], &internal);
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it2510 let models = days.iter().find(|d| d.bucket == "models").unwrap();
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running2511 assert_eq!(models.cost(), 4_000_000);
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging2512 assert_eq!(models.given, Given { comped: 1_000_000, free: 1_000_000, trial: 500_000, ..Given::default() });
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running2513 let given = |w: &str| workspaces.iter().find(|x| x.workspace == w).unwrap().given.total();
2514 assert_eq!((given("flagon"), given("acme"), given("beta"), given("gamma")), (1_000_000, 500_000, 0, 1_000_000));
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it2515 }
2516
2517 #[test]
Merge branch 'worktree-agent-a633ac0f7f66d419d'2518 fn a_discounted_sale_keeps_its_margin_and_counts_the_discount_as_given() {
2519 // $1 of model cost at 20%, sold to an account with 30% off: charged
2520 // $0.84, and $0.36 below cost plus the margin given (as usage_rows
2521 // reads the ledger: value at price, the discount part given).
2522 let mut sale = usage("2026-10-15", "acme", "agent", 1_200_000, 840_000, 1_000_000);
2523 sale.given = Given { discount: 360_000, ..Given::default() };
2524 let (days, _) = fold(&[], &BTreeMap::new(), &[], &[], &[sale], &BTreeSet::new());
2525 let models = days.iter().find(|d| d.bucket == "models").unwrap();
2526 assert_eq!(models.value_micros, 1_200_000);
2527 assert_eq!(models.given, Given { discount: 300_000, ..Given::default() });
2528 // What was sold (cost less given) still makes the margin.
2529 let sold = models.cost() - models.given.total();
2530 assert_eq!(margin_percent(models.cash_micros, sold).map(|m| m.round()), Some(17.0));
2531 }
2532
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging2533 fn draw(kind: CreditKind, reference: &str, task: Option<&str>, at: &str, micros: i64) -> (String, crate::grants::Draw) {
2534 let draw = crate::grants::Draw { grant: "crd_a".into(), kind, reference: reference.into(), task: task.map(Into::into), at: at.into(), micros };
2535 ("acme".to_owned(), draw)
2536 }
2537
2538 #[test]
2539 fn usage_paid_for_with_credit_is_given_not_money_in() {
2540 // $1.20 of usage on $1 of cost, all of it paid with promotional credit.
2541 let mut rows = vec![usage("2026-10-15", "acme", "implement", 1_200_000, 1_200_000, 1_000_000)];
2542 apply_credits(&mut rows, &[draw(CreditKind::Promotional, "run_1", Some("implement"), "2026-10-15T10:00:00Z", 1_200_000)], &[]);
2543 assert_eq!(rows[0].cash, 0);
2544 assert_eq!(rows[0].given, Given { credit_promotional: 1_200_000, ..Given::default() });
2545 let (days, workspaces) = fold(&[], &BTreeMap::new(), &[], &[], &rows, &BTreeSet::new());
2546 let models = days.iter().find(|d| d.bucket == "models").unwrap();
2547 // Valued at its price, none of it money in, all of its cost given:
2548 // the margin on what was sold is untouched by it.
2549 assert_eq!((models.value_micros, models.cash_micros), (1_200_000, 0));
2550 assert_eq!(models.given, Given { credit_promotional: 1_000_000, ..Given::default() });
2551 assert_eq!(models.cost() - models.given.total(), 0);
2552 assert_eq!(workspaces[0].given.total(), 1_000_000);
2553 // Half paid with goodwill credit: half the cost given, half sold.
2554 let mut rows = vec![usage("2026-10-15", "acme", "implement", 1_200_000, 1_200_000, 1_000_000)];
2555 apply_credits(&mut rows, &[draw(CreditKind::Goodwill, "run_1", Some("implement"), "2026-10-15T10:00:00Z", 600_000)], &[]);
2556 let (days, _) = fold(&[], &BTreeMap::new(), &[], &[], &rows, &BTreeSet::new());
2557 let models = days.iter().find(|d| d.bucket == "models").unwrap();
2558 assert_eq!(models.cash_micros, 600_000);
2559 assert_eq!(models.given, Given { credit_goodwill: 500_000, ..Given::default() });
2560 let sold = models.cost() - models.given.total();
2561 assert_eq!(margin_percent(models.cash_micros, sold).map(|m| m.round()), Some(17.0));
2562 }
2563
2564 #[test]
2565 fn what_a_refund_pays_for_is_paid_for_and_the_refund_comes_off_its_day() {
2566 // A refund's credit pays for usage: still money in, nothing given.
2567 let mut rows = vec![usage("2026-10-15", "acme", "implement", 1_200_000, 1_200_000, 1_000_000)];
2568 apply_credits(&mut rows, &[draw(CreditKind::Refund, "run_9", Some("implement"), "2026-10-15T10:00:00Z", 1_200_000)], &[]);
2569 assert_eq!((rows[0].cash, rows[0].given), (1_200_000, Given::default()));
2570 // The $3 refunded for Oct 2 comes off that day's money in, shared
2571 // over what was paid that day.
2572 let mut rows = vec![
2573 usage("2026-10-02", "acme", "implement", 4_000_000, 4_000_000, 3_000_000),
2574 usage("2026-10-02", "acme", "sandbox", 2_000_000, 2_000_000, 1_500_000),
2575 usage("2026-10-02", "beta", "implement", 9_000_000, 9_000_000, 7_000_000),
2576 ];
2577 let refund = crate::grants::Refunded { workspace: "acme".into(), day: "2026-10-02".into(), micros: 3_000_000 };
2578 apply_credits(&mut rows, &[], std::slice::from_ref(&refund));
2579 assert_eq!((rows[0].cash, rows[1].cash, rows[2].cash), (2_000_000, 1_000_000, 9_000_000));
2580 assert!(rows.iter().all(|r| r.given == Given::default()));
2581 // Nothing paid that day: a line of its own, money in less than nothing.
2582 let mut rows = vec![];
2583 apply_credits(&mut rows, &[], &[refund]);
2584 assert_eq!((rows[0].key.as_str(), rows[0].cash, rows[0].value), ("other", -3_000_000, 0));
2585 }
2586
2587 #[test]
2588 fn credit_spent_on_month_end_meters_is_a_line_of_its_own() {
2589 // Storage is reconciled from snapshots, not its ledger line: what
2590 // credit paid of it is its own row on the day it was charged.
2591 let mut rows = vec![usage("2026-10-01", "acme", "implement", 1_000, 1_000, 800)];
2592 apply_credits(&mut rows, &[draw(CreditKind::Goodwill, "storage/2026-09", Some("storage"), "2026-10-01T00:05:00Z", 2_000_000)], &[]);
2593 assert_eq!(rows.len(), 2);
2594 assert_eq!((rows[1].key.as_str(), rows[1].cash, rows[1].value), ("storage", -2_000_000, 0));
2595 assert_eq!(rows[1].given.credit_goodwill, 2_000_000);
2596 assert_eq!(rows[0].cash, 1_000);
Merge branch 'worktree-agent-a633ac0f7f66d419d'2597 }
2598
2599 #[test]
Topics sit in About on the files pages, under the description2600 fn a_workspace_that_costs_more_than_it_pays_is_flagged() {
2601 let rows = vec![("acme".to_string(), 5_000_000, 1_000_000), ("beta".to_string(), 900_000, 0), ("gamma".to_string(), 2_000_000, 3_000_000)];
2602 let found = anomalies(&rows, 1.0, 1_000_000);
2603 assert_eq!(found, vec![("acme".to_string(), 5_000_000, 1_000_000)]);
2604 // At twice its revenue as the threshold, $5 against $3 is fine.
2605 assert!(anomalies(&[("acme".to_string(), 5_000_000, 3_000_000)], 2.0, 1_000_000).is_empty());
2606 }
2607
2608 #[test]
2609 fn a_git_operation_costs_what_cloudflare_counts_for_it() {
2610 // $0.15 per 1,000 of Cloudflare's operations, on the charged days.
2611 let rate = billed_rate(&[(10_000.0, 0.0), (20_000.0, 3.0), (30_000.0, 4.5), (5_000.0, 0.75)]).unwrap();
2612 assert!((rate - 0.000_15).abs() < 1e-12);
2613 // Cloudflare counted 3 for every 1 g1t did: binding reads count.
2614 let per_op = derived_unit_cost(rate, 300_000.0, 100_000.0).unwrap();
2615 let per_thousand_micros = per_op * unit_size("1,000 operations") * 1e6;
2616 assert!((per_thousand_micros - 450_000.0).abs() < 1e-6, "{per_thousand_micros}");
2617 // Too few of g1t's units to say.
2618 assert!(derived_unit_cost(rate, 3_000.0, 500.0).is_none());
2619 assert!(billed_rate(&[(10_000.0, 0.0)]).is_none());
2620 assert_eq!(unit_size("million requests"), 1e6);
2621 assert_eq!(unit_size("second"), 1.0);
2622 }
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb972623
2624 /// The case that started it: syntaqx's ~$8.62 of model usage was wiped
2625 /// by a testing reset, AI Gateway still priced all $11.11, and the
2626 /// ledger had $2.49 left.
2627 fn gateway_and_ledger(kept: bool) -> (Vec<ProductDay>, Vec<WorkspaceDay>) {
2628 let rules = vec![rule("ai_gateway_requests", "*", "models", None), rule("containers", "*", "sandboxes", None)];
2629 let lines = vec![
2630 line("2026-10-05", costs::SOURCE_GATEWAY, "ai_gateway_requests", "anthropic_claude_opus_5_5", 1.0, 11.11),
2631 line("2026-10-05", SOURCE_BILLABLE, "containers", "container_memory", 10.0, 0.30),
2632 ];
2633 let mut usage = vec![usage("2026-10-05", "acme", "implement", 2_988_000, 2_988_000, 2_490_000), usage("2026-10-05", "acme", "sandbox", 120_000, 120_000, 100_000)];
2634 if kept {
2635 // What the reset kept (reset_costs), read back for the day.
2636 usage.extend(reset_usage(&[
2637 ("2026-10-05".into(), "syntaqx".into(), "models".into(), 8_620_000, 10_344_000),
2638 ("2026-10-05".into(), "syntaqx".into(), "sandboxes".into(), 100_000, 120_000),
2639 // The reset's own row is not usage.
2640 ("2026-10-07".into(), "syntaqx".into(), String::new(), 0, 0),
2641 ]));
2642 }
2643 fold(&rules, &revenue_map(), &lines, &[], &usage, &BTreeSet::new())
2644 }
2645
2646 #[test]
Billing, integrations, security and identity no longer serve g1t's own agents. Billing stops metering agent runs, model tokens and AI Gateway requests, drops its model catalogue and price-keeper model prices, and stops selling AI credit, which only ever paid for those; credit bought earlier still pays toward what is owed. A usage report, statement or invoice that holds an old agent or model meter shows it as "Other usage" rather than failing, and the get_ai_credit and buy_ai_credit API operations and MCP billing actions go. Integrations no longer connects model providers or routes models, and no longer assigns imported issues or alerts to g1t; a model provider already connected can still be disconnected. Security drops "Fix with g1t". Identity no longer creates agent tokens or puts agents on teams, a team's lead is a person, and run credentials are issued only for checks, the merge queue, merge checks, deployments and bumps; tokens and credentials already issued keep working until they expire. The reserved names g1t and g1t-agent stay reserved. No tables are dropped.2647 fn what_a_reset_kept_is_on_the_ledgers_side_of_model_cost() {
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb972648 let models = |days: &[ProductDay]| days.iter().find(|d| d.bucket == "models").cloned().unwrap();
Billing, integrations, security and identity no longer serve g1t's own agents. Billing stops metering agent runs, model tokens and AI Gateway requests, drops its model catalogue and price-keeper model prices, and stops selling AI credit, which only ever paid for those; credit bought earlier still pays toward what is owed. A usage report, statement or invoice that holds an old agent or model meter shows it as "Other usage" rather than failing, and the get_ai_credit and buy_ai_credit API operations and MCP billing actions go. Integrations no longer connects model providers or routes models, and no longer assigns imported issues or alerts to g1t; a model provider already connected can still be disconnected. Security drops "Fix with g1t". Identity no longer creates agent tokens or puts agents on teams, a team's lead is a person, and run credentials are issued only for checks, the merge queue, merge checks, deployments and bumps; tokens and credentials already issued keep working until they expire. The reserved names g1t and g1t-agent stay reserved. No tables are dropped.2649 // Without it: the ledger's $2.49 of model cost.
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb972650 let (days, _) = gateway_and_ledger(false);
Billing, integrations, security and identity no longer serve g1t's own agents. Billing stops metering agent runs, model tokens and AI Gateway requests, drops its model catalogue and price-keeper model prices, and stops selling AI credit, which only ever paid for those; credit bought earlier still pays toward what is owed. A usage report, statement or invoice that holds an old agent or model meter shows it as "Other usage" rather than failing, and the get_ai_credit and buy_ai_credit API operations and MCP billing actions go. Integrations no longer connects model providers or routes models, and no longer assigns imported issues or alerts to g1t; a model provider already connected can still be disconnected. Security drops "Fix with g1t". Identity no longer creates agent tokens or puts agents on teams, a team's lead is a person, and run credentials are issued only for checks, the merge queue, merge checks, deployments and bumps; tokens and credentials already issued keep working until they expire. The reserved names g1t and g1t-agent stay reserved. No tables are dropped.2651 assert_eq!(models(&days).own_cost_micros, 2_490_000);
2652 // With it: the ledger's model cost and the reset's add up to what
2653 // AI Gateway priced.
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb972654 let (days, _) = gateway_and_ledger(true);
2655 let m = models(&days);
2656 assert_eq!(m.own_cost_micros, 11_110_000);
Billing, integrations, security and identity no longer serve g1t's own agents. Billing stops metering agent runs, model tokens and AI Gateway requests, drops its model catalogue and price-keeper model prices, and stops selling AI credit, which only ever paid for those; credit bought earlier still pays toward what is owed. A usage report, statement or invoice that holds an old agent or model meter shows it as "Other usage" rather than failing, and the get_ai_credit and buy_ai_credit API operations and MCP billing actions go. Integrations no longer connects model providers or routes models, and no longer assigns imported issues or alerts to g1t; a model provider already connected can still be disconnected. Security drops "Fix with g1t". Identity no longer creates agent tokens or puts agents on teams, a team's lead is a person, and run credentials are issued only for checks, the merge queue, merge checks, deployments and bumps; tokens and credentials already issued keep working until they expire. The reserved names g1t and g1t-agent stay reserved. No tables are dropped.2657 // Models are no longer compared with anything: no drift either way.
Merge remote-tracking branch 'origin/main' into workspace-chat2658 assert!(drifts("models", &[m], 10.0, false, 100_000, None).is_empty());
Billing, integrations, security and identity no longer serve g1t's own agents. Billing stops metering agent runs, model tokens and AI Gateway requests, drops its model catalogue and price-keeper model prices, and stops selling AI credit, which only ever paid for those; credit bought earlier still pays toward what is owed. A usage report, statement or invoice that holds an old agent or model meter shows it as "Other usage" rather than failing, and the get_ai_credit and buy_ai_credit API operations and MCP billing actions go. Integrations no longer connects model providers or routes models, and no longer assigns imported issues or alerts to g1t; a model provider already connected can still be disconnected. Security drops "Fix with g1t". Identity no longer creates agent tokens or puts agents on teams, a team's lead is a person, and run credentials are issued only for checks, the merge queue, merge checks, deployments and bumps; tokens and credentials already issued keep working until they expire. The reserved names g1t and g1t-agent stay reserved. No tables are dropped.2659 assert!(drifts("models", &[models(&gateway_and_ledger(false).0)], 10.0, false, 100_000, None).is_empty());
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb972660 // The reset's own row makes no bucket of its own.
2661 assert!(!days.iter().any(|d| d.bucket.is_empty()));
2662 }
2663
2664 #[test]
2665 fn what_a_reset_kept_is_given_away_as_testing_resets() {
2666 let (days, workspaces) = gateway_and_ledger(true);
2667 let models = days.iter().find(|d| d.bucket == "models").unwrap();
2668 // All of syntaqx's model cost is given, none of it money in.
2669 assert_eq!(models.given, Given { reset: 8_620_000, ..Given::default() });
2670 assert_eq!(models.cash_micros, 2_988_000);
2671 // Cloudflare's sandbox cost is shared by what each workspace's usage
2672 // cost: syntaqx's half is given too.
2673 let sandboxes = days.iter().find(|d| d.bucket == "sandboxes").unwrap();
2674 assert_eq!((sandboxes.cost(), sandboxes.given.reset), (300_000, 150_000));
2675 // Who g1t paid: syntaqx is still on it, all of its cost given.
2676 let syntaqx: Vec<&WorkspaceDay> = workspaces.iter().filter(|w| w.workspace == "syntaqx").collect();
2677 assert_eq!(syntaqx.iter().map(|w| w.cost).sum::<i64>(), 8_770_000);
2678 assert!(syntaqx.iter().all(|w| w.given.reset == w.cost && w.given.total() == w.cost && w.revenue == 0));
2679 // The statement reads it back from margin_days by why.
2680 let row = MarginRow {
2681 day: models.day.clone(),
2682 bucket: models.bucket.clone(),
2683 cf_cost_micros: models.cf_cost_micros,
2684 own_cost_micros: models.own_cost_micros,
2685 value_micros: models.value_micros,
2686 cash_micros: models.cash_micros,
2687 cf_quantity: 0.0,
2688 own_quantity: 0.0,
2689 given_comped_micros: Some(0),
2690 given_free_micros: Some(0),
2691 given_trial_micros: Some(0),
2692 given_pool_micros: Some(0),
2693 given_discount_micros: Some(0),
2694 given_credit_promotional_micros: Some(0),
2695 given_credit_goodwill_micros: Some(0),
2696 given_reset_micros: Some(models.given.reset),
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)2697 given_unpaid_micros: Some(0),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb972698 };
2699 assert_eq!(ProductDay::from(row).given, models.given);
2700 }
2701
2702 #[test]
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)2703 fn test_mode_charges_are_never_money_in() {
2704 // A $12 sandbox charge and the $20 plan, both while payments were in
2705 // Stripe's test mode: valued as before, given as unpaid, no cash.
2706 let mut rows = vec![
2707 usage("2026-10-06", "acme", "sandbox", 12_000_000, 12_000_000, 10_000_000),
2708 UsageRow { day: "2026-10-06".into(), workspace: "acme".into(), key: "plan".into(), value: 666_667, cash: 666_667, ..UsageRow::default() },
2709 ];
2710 without_real_money(&mut rows, None);
2711 assert!(rows.iter().all(|r| r.cash == 0));
2712 // A comped workspace's charge is given once, as comped.
2713 let mut comped = vec![UsageRow { given: Given { comped: 500, ..Given::default() }, ..usage("2026-10-06", "flagon-io", "sandbox", 500, 500, 400) }];
2714 without_real_money(&mut comped, None);
2715 assert_eq!((comped[0].cash, comped[0].given.total()), (0, 500));
2716 assert_eq!(rows[0].given.unpaid, 12_000_000);
2717 assert_eq!(rows[0].value, 12_000_000);
2718 assert_eq!(rows[1].given.unpaid, 666_667);
2719 let (days, workspaces) = fold(&rules(), &revenue_map(), &[], &[], &rows, &BTreeSet::new());
2720 assert_eq!(days.iter().map(|d| d.cash_micros).sum::<i64>(), 0);
2721 let sandboxes = days.iter().find(|d| d.bucket == "sandboxes").unwrap();
2722 assert_eq!(sandboxes.given.unpaid, sandboxes.cost());
2723 assert!(workspaces.iter().all(|w| w.revenue == 0));
2724 // Once live: from that day on, it is money.
2725 let mut rows = vec![
2726 usage("2026-10-06", "acme", "sandbox", 1_000_000, 1_000_000, 800_000),
2727 usage("2026-10-07", "acme", "sandbox", 1_000_000, 1_000_000, 800_000),
2728 ];
2729 without_real_money(&mut rows, Some("2026-10-07"));
2730 assert_eq!((rows[0].cash, rows[0].given.unpaid), (0, 1_000_000));
2731 assert_eq!((rows[1].cash, rows[1].given.unpaid), (1_000_000, 0));
2732 // When payments went live is kept from the first time it is seen.
2733 assert_eq!(live_since(false, Some("2026-10-07"), "2026-10-09"), None);
2734 assert_eq!(live_since(true, None, "2026-10-09").as_deref(), Some("2026-10-09"));
2735 assert_eq!(live_since(true, Some("2026-10-07"), "2026-10-09").as_deref(), Some("2026-10-07"));
2736 }
2737
2738 #[test]
2739 fn workspaces_and_running_g1t_add_up_to_the_bill() {
2740 // Running g1t on a day with usage is shared; on a day with none it
2741 // is no one's, and the report says so.
2742 let lines = vec![
2743 line("2026-10-06", SOURCE_BILLABLE, "workers", "workers_cpu_ms", 1.0, 0.10),
2744 line("2026-10-07", SOURCE_BILLABLE, "workers", "workers_cpu_ms", 1.0, 0.20),
2745 line("2026-10-07", SOURCE_BILLABLE, "containers", "container_memory_per_gib_second", 1.0, 0.09),
2746 ];
2747 let usage = vec![usage("2026-10-07", "acme", "sandbox", 100, 100, 80), usage("2026-10-07", "beta", "sandbox", 300, 300, 240)];
2748 let (days, workspaces) = fold(&rules(), &revenue_map(), &lines, &[], &usage, &BTreeSet::new());
2749 let total: i64 = days.iter().map(ProductDay::cost).sum();
2750 let shared: i64 = workspaces.iter().map(|w| w.cost).sum();
2751 assert_eq!(total, 390_000);
2752 assert_eq!(unattributed(total, shared), 100_000);
2753 assert_eq!(shared + unattributed(total, shared), total);
2754 }
2755
2756 #[test]
2757 fn the_cycle_report_projects_as_cloudflare_does() {
2758 let cycle = crate::cycle::cycle_of("2026-10-09", 28);
2759 let meter = |meter: &str, cost: i64, quantity: f64| CycleMeter { meter: meter.into(), cost_micros: cost, quantity, ..CycleMeter::default() };
2760 let report = cycle_report(&cycle, 12, vec![meter("container_memory", 92_175, 126_870.0), meter("workers_cpu_ms", 200_000, 39_160_000.0), meter("d1_rows_read", 0, 61_820_000.0)], 30_000_000);
2761 assert_eq!((report.start.as_str(), report.end.as_str(), report.days, report.days_elapsed), ("2026-09-28", "2026-10-27", 30, 12));
2762 assert_eq!(report.usage_micros, 292_175);
2763 assert_eq!(report.projected_micros, 730_438);
2764 assert_eq!(report.average_daily_micros, 24_347);
2765 assert_eq!(report.meters[0].meter, "workers_cpu_ms");
2766 assert_eq!(report.meters[2].meter, "d1_rows_read");
2767 }
2768
2769 #[test]
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb972770 fn reconciling_again_gives_the_same_answer() {
2771 assert_eq!(gateway_and_ledger(true), gateway_and_ledger(true));
2772 // A reset's kept rows are read back exactly as kept: running it
2773 // again cannot count them twice.
2774 let kept = [("2026-10-05".to_string(), "syntaqx".to_string(), "models".to_string(), 8_620_000, 10_344_000)];
2775 assert_eq!(reset_usage(&kept), reset_usage(&kept));
2776 assert_eq!(reset_usage(&kept).len(), 1);
2777 }
Topics sit in About on the files pages, under the description2778}

This file's history is long; its oldest lines are credited to the oldest commit read.