Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
865 linesCodeBlameRaw
1//! Usage other services meter through the month, charged once it is over:
2//! security scans, the actions cache, and two billing measures itself each
3//! day: private repository storage and git operations. `context` (search
4//! embeddings) is no longer reported, since the context service was
5//! retired; it stays a source so usage noted before is still charged and
6//! closed.
7//!
8//! Each reports what it cost g1t so far this month (`note_pending`), so the
9//! workspace's limit counts it as it happens. When the month is over,
10//! billing charges it once: at cost plus the margin, on the account's
11//! terms, after the plan's included usage and the trial credit (see
12//! `credits`), dated the month's last second so it falls in that month's
13//! statement and invoice.
14//!
15//! The forge is free for every workspace up to the same amounts, on the
16//! plan or not; past them, a workspace on the plan pays at cost plus the
17//! margin and is never refused or slowed, and a free workspace is never
18//! charged but is held to them.
19//!
20//! **Git operations.** Cloudflare Artifacts charges g1t $0.15 per 1,000
21//! operations (clones, fetches, pushes) from 2026-10-14. The repos service
22//! counts those through g1t's git endpoints. Every workspace has
23//! `GIT_OPERATIONS_INCLUDED` (50,000) a month free; past it, the plan pays
24//! at cost plus the margin, and a free workspace is slowed down instead
25//! (the repos service's `GIT_OPERATIONS_FREE_CAP`, the same number).
26//!
27//! **Actions cache.** The actions service reports what each workspace's
28//! `actions/cache` entries held each day (source `cache`), at what R2
29//! charges g1t to store them ($0.015 a GB-month). Only the plan is charged
30//! for it, from the first byte; a free workspace is never charged, and its
31//! repositories are held to the cache's quota like everyone's.
32//!
33//! **Storage.** The git store does not report a repository's size, so the
34//! repos service counts the packs pushed through g1t's git endpoints (see
35//! `g1t_contracts::repos::StorageArgs`): a lower bound. Each day billing
36//! records what each workspace's private repositories hold and what is
37//! free that day (`FREE_PRIVATE_STORAGE_BYTES`, 1 GB, for everyone). Like
38//! Cloudflare's own storage billing, a month's GB-months are the days'
39//! amounts past the free one, added up and divided by the month's days
40//! (28 to 31), and only the
41//! plan is charged for them. A free workspace is never charged for
42//! storage: pushes to its private repositories stop once they hold its
43//! free amount. Public repositories are never charged.
44
45use g1t_contracts::billing::PlanKind;
46use g1t_contracts::new_id;
47use g1t_contracts::repos::{GitOperationsArgs, StorageArgs, WorkspaceGitOperations, WorkspaceStorage};
48use g1t_contracts::time::rfc3339;
49use g1t_kit::now_ms;
50use serde::Deserialize;
51use worker::Result;
52
53use crate::credits::{self, Drawn, Eligible};
54use crate::{Billing, optional};
55
56/// Sources billing charges itself when the month is over: those billing
57/// measures (storage, git operations, package storage) and those other
58/// services note through the month (`note_pending`: security scans, search
59/// embeddings, the actions cache, observability data).
60pub(crate) const CHARGED_HERE: [&str; 7] = ["security", "context", "storage", "git", "cache", "package_storage", "telemetry"];
61
62/// A `pending_usage` column less what the workspace had of it when its
63/// test-mode data was detached (`pending_baselines`, migration 0060),
64/// never below nothing. Reads `pending_usage` unaliased.
65macro_rules! net_of_baseline {
66 ($column:literal) => {
67 concat!(
68 "MAX(0, COALESCE(pending_usage.", $column, ", 0) - COALESCE((SELECT b.", $column,
69 " FROM pending_baselines b WHERE b.workspace = pending_usage.workspace AND b.source = pending_usage.source",
70 " AND b.month = pending_usage.month), 0))"
71 )
72 };
73}
74
75/// What a pending row cost g1t since the detach (all of it without one):
76/// what everything that values or charges pending usage reads.
77pub(crate) const NET_COST: &str = net_of_baseline!("cost_micros");
78/// What a pending row will be charged since the detach, before terms.
79pub(crate) const NET_CHARGE: &str = net_of_baseline!("charge_micros");
80
81/// A month's figure for a source, as `note_pending` writes it. Never into
82/// a month closed for the workspace, and the update leaves a row already
83/// charged as it was: a late report of a month that is over never
84/// rewrites what was charged.
85pub(crate) const SET_PENDING: &str = "INSERT INTO pending_usage (workspace, source, month, charge_micros, cost_micros, updated_at, detail)
86 SELECT ?1, ?2, ?3, ?4, ?5, ?6, ?7
87 WHERE NOT EXISTS (SELECT 1 FROM month_closes WHERE month_closes.workspace = ?1 AND month_closes.month = ?3)
88 ON CONFLICT (workspace, source, month) DO UPDATE SET
89 charge_micros = ?4, cost_micros = ?5, updated_at = ?6, detail = COALESCE(?7, detail)
90 WHERE pending_usage.charged_at IS NULL";
91
92/// Whether a workspace had the plan in a month by what it paid or drew:
93/// a plan payment in the month that a test-mode detach has not reversed
94/// (its `_test_detach` row nets it to nothing), or the plan's included
95/// usage drawn in it. `?1` workspace, `?2` the month's first day, `?3` the
96/// next month's, `?4` the month.
97pub(crate) const PLAN_EVIDENCE: &str = "SELECT (EXISTS (SELECT 1 FROM plan_payments p WHERE p.workspace = ?1 AND p.paid_at >= ?2 AND p.paid_at < ?3
98 AND p.amount_micros > 0 AND substr(p.invoice_id, -12) <> '_test_detach'
99 AND p.amount_micros + COALESCE((SELECT SUM(q.amount_micros) FROM plan_payments q
100 WHERE q.invoice_id = p.invoice_id || '_test_detach'), 0) > 0)
101 OR EXISTS (SELECT 1 FROM allowance_use WHERE kind = 'plan_credit' AND scope = ?1 AND month = ?4 AND used > 0)) AS had";
102
103/// Sources only the plan is charged for: a free workspace's are not kept.
104pub(crate) const PLAN_ONLY: [&str; 1] = ["cache"];
105
106/// Sources g1t pays for itself on a free workspace: security scans and
107/// (from before the context service was retired) search embeddings. No security feature is held back for the plan, and
108/// a free workspace never runs up a bill, so on a workspace without the
109/// plan they are recorded at cost, covered by g1t, and charged nothing
110/// (not even from the trial). On the plan they are charged like any usage.
111pub(crate) const COVERED_FOR_FREE: [&str; 2] = ["security", "context"];
112
113/// What pays for a month-end source's charge: the plan's included usage
114/// and the trial, and for a covered source on a free workspace, g1t.
115pub(crate) fn month_end_eligible(source: &str, plan: bool) -> Eligible {
116 let covered = !plan && COVERED_FOR_FREE.contains(&source);
117 Eligible { trial: !covered, repo: None, cover_rest: covered }
118}
119
120/// What Artifacts charges g1t, when the price book cannot be read: $0.50
121/// a GB-month of storage, and $0.15 per 1,000 git operations.
122pub(crate) const STORAGE_MICROS_PER_GB_MONTH: i64 = 500_000;
123pub(crate) const GIT_MICROS_PER_THOUSAND: i64 = 150_000;
124
125/// When Cloudflare starts charging for Artifacts operations: none before
126/// count.
127pub(crate) const GIT_BILLING_STARTS: &str = "2026-10-14T00";
128
129/// What git operations past the free amount cost g1t, at
130/// `micros_per_thousand`: nothing up to it.
131pub(crate) fn git_cost(operations: u64, included: u64, micros_per_thousand: f64) -> i64 {
132 let past = operations.saturating_sub(included);
133 (past as f64 * micros_per_thousand / 1000.0).ceil() as i64
134}
135
136/// How long after a month is over its last git operations are given to
137/// land before its final count: the repos service adds each to its hourly
138/// counts after the answer has gone back.
139pub(crate) const FINAL_MEASURE_AFTER_MS: u64 = 60 * 60 * 1000;
140
141/// How long a month's charge and close wait for its final count of git
142/// operations before going ahead without it (logged): an outage of the
143/// repos service never holds every workspace's invoice for good.
144pub(crate) const FINAL_MEASURE_WAIT_MS: u64 = 3 * 24 * 60 * 60 * 1000;
145
146/// How long ago `month` ended, at `now_ms`; none while it is not over.
147fn since_month_end(now_ms: u64, month: &str) -> Option<u64> {
148 let end = g1t_contracts::time::parse_rfc3339(&credits::next_month_start(month))?;
149 now_ms.checked_sub(end)
150}
151
152/// Whether the final count of `month`'s git operations may be taken at
153/// `now_ms`: the month is over, and its last hour has had time to land.
154pub(crate) fn final_measure_due(now_ms: u64, month: &str) -> bool {
155 since_month_end(now_ms, month).is_some_and(|ms| ms >= FINAL_MEASURE_AFTER_MS)
156}
157
158/// Whether `month`'s metered usage may be charged and the month closed:
159/// its final count of git operations was taken (`measured`), or it has
160/// waited for it long enough.
161pub(crate) fn month_final(measured: bool, now_ms: u64, month: &str) -> bool {
162 since_month_end(now_ms, month).is_some_and(|ms| measured || ms >= FINAL_MEASURE_WAIT_MS)
163}
164
165/// The first hour of `month` to count git operations from.
166pub(crate) fn git_since(month: &str) -> String {
167 let start = format!("{month}-01T00");
168 if start.as_str() < GIT_BILLING_STARTS { GIT_BILLING_STARTS.to_owned() } else { start }
169}
170
171/// A gigabyte, as Cloudflare bills storage.
172pub(crate) const GB: f64 = 1_000_000_000.0;
173
174/// GB-months from a month's daily measures, each `(private, free)` bytes:
175/// what was past the free amount each day, over the month's `month_days`.
176pub(crate) fn storage_gb_months(days: &[(i64, i64)], month_days: u32) -> f64 {
177 days.iter().map(|(private, free)| (private - free).max(0) as f64).sum::<f64>() / GB / f64::from(month_days.max(1))
178}
179
180/// GB-months of package storage past the free amounts from a month's
181/// daily measures, each `(public, private, public_free, private_free)`:
182/// public and private past their own amounts, over the month's
183/// `month_days`.
184pub(crate) fn package_gb_months(days: &[(i64, i64, i64, i64)], month_days: u32) -> f64 {
185 days.iter()
186 .map(|(public, private, public_free, private_free)| ((public - public_free).max(0) + (private - private_free).max(0)) as f64)
187 .sum::<f64>()
188 / GB
189 / f64::from(month_days.max(1))
190}
191
192/// What R2 charges g1t a GB-month, when the price book cannot be read.
193pub(crate) const PACKAGE_MICROS_PER_GB_MONTH: i64 = 15_000;
194
195/// What `gb_months` cost g1t at `micros_per_gb_month`, rounded up.
196pub(crate) fn storage_cost(gb_months: f64, micros_per_gb_month: f64) -> i64 {
197 (gb_months * micros_per_gb_month).ceil() as i64
198}
199
200/// What a source is called on the statement.
201pub(crate) fn title(source: &str) -> &'static str {
202 match source {
203 "security" => "Security scans",
204 "context" => "Search embeddings",
205 "storage" => "Private repository storage past the free amount",
206 "package_storage" => "Package storage past the free amounts",
207 "git" => "Git operations past the free amount",
208 "cache" => "Actions cache storage",
209 "domains" => "Custom domains",
210 "telemetry" => "Observability data past the free amount",
211 _ => "Metered usage",
212 }
213}
214
215/// Whether a batch's first statement, an insert `RETURNING id`, wrote its
216/// row: none comes back when `ON CONFLICT … DO NOTHING` skipped it.
217pub(crate) fn posted(results: &[worker::D1Result]) -> Result<bool> {
218 #[derive(Deserialize)]
219 struct Row {
220 #[allow(dead_code)]
221 id: String,
222 }
223 match results.first() {
224 Some(result) => Ok(!result.results::<Row>()?.is_empty()),
225 None => Ok(false),
226 }
227}
228
229/// A usage entry to put on the ledger.
230pub(crate) struct UsageLine<'a> {
231 pub workspace: &'a str,
232 /// What the workspace is charged, after terms and what paid for it.
233 pub charged: i64,
234 pub description: &'a str,
235 pub repo: Option<&'a str>,
236 pub task: &'a str,
237 pub cost: i64,
238 pub reference: &'a str,
239 pub created_at: &'a str,
240 pub drawn: Drawn,
241}
242
243impl Billing {
244 /// Puts a usage entry on the ledger and takes it off the balance, as
245 /// one write. False when an entry with its reference is already there
246 /// (another delivery of the same usage wrote it first): nothing is
247 /// written, and the caller gives back what it drew (`undraw`).
248 pub(crate) async fn post_usage(&self, line: UsageLine<'_>) -> Result<bool> {
249 self.post_usage_with(line, vec![]).await
250 }
251
252 /// `post_usage`, with `also` written in the same batch, whether or not
253 /// the entry was new.
254 pub(crate) async fn post_usage_with(&self, line: UsageLine<'_>, also: Vec<worker::D1PreparedStatement>) -> Result<bool> {
255 // The price versions it was charged at (pricing.rs).
256 let mut versions = Vec::new();
257 for meter in crate::pricing::meters_of(line.task) {
258 versions.extend(self.version_now(meter).await?);
259 }
260 let price_version = versions.join(",");
261 let id = new_id("led", now_ms());
262 // The ledger's unique reference is the guard: the entry goes in
263 // only if no other has it, and the balance moves only if it did.
264 let mut statements = vec![
265 self.db
266 .prepare(
267 "INSERT INTO ledger
268 (id, workspace, kind, amount_micros, description, repo, task, cost_micros, reference,
269 created_at, billed_to, credit_micros, trial_micros, oss_micros, given_micros, price_version)
270 VALUES (?, ?, 'usage', ?, ?, ?, ?, ?, ?, ?, 'g1t', ?, ?, ?, ?, ?)
271 ON CONFLICT (reference) DO NOTHING RETURNING id",
272 )
273 .bind(&[
274 id.as_str().into(),
275 line.workspace.into(),
276 (-(line.charged as f64)).into(),
277 line.description.into(),
278 optional(line.repo),
279 line.task.into(),
280 (line.cost as f64).into(),
281 line.reference.into(),
282 line.created_at.into(),
283 (line.drawn.credit as f64).into(),
284 (line.drawn.trial as f64).into(),
285 (line.drawn.oss as f64).into(),
286 // What g1t covered itself (a free workspace's scans and
287 // embeddings): given, never margin lost.
288 (line.drawn.given as f64).into(),
289 optional(Some(price_version.as_str()).filter(|v| !v.is_empty())),
290 ])?,
291 self.balance_if_posted(line.workspace, line.charged, &id)?,
292 ];
293 statements.extend(also);
294 let results = self.db.batch(statements).await?;
295 posted(&results)
296 }
297
298 /// Takes `charged` off the workspace's balance, only if the ledger entry
299 /// `id` was written in the same batch.
300 pub(crate) fn balance_if_posted(&self, workspace: &str, charged: i64, id: &str) -> Result<worker::D1PreparedStatement> {
301 self.db
302 .prepare(
303 "INSERT INTO accounts (workspace, balance_micros, created_at)
304 SELECT ?1, ?2, ?3 WHERE EXISTS (SELECT 1 FROM ledger WHERE id = ?4)
305 ON CONFLICT (workspace) DO UPDATE SET balance_micros = balance_micros + ?2",
306 )
307 .bind(&[workspace.into(), (-(charged as f64)).into(), rfc3339(now_ms()).into(), id.into()])
308 }
309
310 /// Writes down what a source cost g1t so far in `month`, what it will
311 /// be charged, and how much of it there was (`detail`, for the Billing
312 /// page), replacing the last figure.
313 pub(crate) async fn set_pending(
314 &self,
315 workspace: &str,
316 source: &str,
317 month: &str,
318 cost_micros: i64,
319 detail: Option<&str>,
320 ) -> Result<()> {
321 // A covered source on a free workspace is never charged, so it does
322 // not count toward the workspace's limit either.
323 let covered = COVERED_FOR_FREE.contains(&source) && !self.has_plan(&workspace.to_lowercase()).await?;
324 let charge = if covered { 0 } else { credits::with_margin(cost_micros, self.margin_percent) };
325 self.db
326 .prepare(SET_PENDING)
327 .bind(&[
328 workspace.to_lowercase().into(),
329 source.into(),
330 month.into(),
331 (charge as f64).into(),
332 (cost_micros.max(0) as f64).into(),
333 rfc3339(now_ms()).into(),
334 optional(detail),
335 ])?
336 .run()
337 .await?;
338 Ok(())
339 }
340
341 /// Charges every month that is over for the usage billing charges
342 /// itself, once each.
343 pub(crate) async fn charge_pending(&self) -> Result<()> {
344 if !self.charging {
345 return Ok(());
346 }
347 let now = rfc3339(now_ms());
348 let current = credits::month_of(&now);
349 #[derive(Deserialize)]
350 struct Row {
351 workspace: String,
352 source: String,
353 month: String,
354 cost_micros: Option<i64>,
355 }
356 let marks = CHARGED_HERE.iter().map(|s| format!("'{s}'")).collect::<Vec<_>>().join(", ");
357 // Last month waits for its final count of git operations: the
358 // months before it are due.
359 let due_before = if self.last_month_final().await? { current.clone() } else { crate::limits::previous_month(&current) };
360 let due = self
361 .db
362 // Less what was metered before a test-mode detach: not charged.
363 .prepare(format!(
364 "SELECT workspace, source, month, {NET_COST} AS cost_micros FROM pending_usage
365 WHERE month < ? AND charged_at IS NULL AND source IN ({marks}) ORDER BY month LIMIT 50"
366 ))
367 .bind(&[due_before.as_str().into()])?
368 .all()
369 .await?
370 .results::<Row>()?;
371 // Each row on its own: one that fails (and gives back what it drew)
372 // is logged and tried again next tick, and never holds back the
373 // rest, whose months could otherwise not close.
374 for row in due {
375 let charged = async {
376 // Marked charged in the same batch as its ledger entry, never
377 // before: a failure in between leaves it to the next tick, and
378 // the ledger's unique reference keeps two crons from charging
379 // it twice.
380 let mark = || {
381 self.db
382 .prepare(
383 "UPDATE pending_usage SET charged_at = ?1
384 WHERE workspace = ?2 AND source = ?3 AND month = ?4 AND charged_at IS NULL",
385 )
386 .bind(&[now.as_str().into(), row.workspace.as_str().into(), row.source.as_str().into(), row.month.as_str().into()])
387 };
388 let cost = row.cost_micros.unwrap_or(0);
389 if cost <= 0 {
390 mark()?.run().await?;
391 return Ok(());
392 }
393 let base = credits::with_margin(cost, self.margin_percent);
394 let (charge, terms_note, discount) = self.charged(&row.workspace, base).await?;
395 // The plan as it was in that month, not as it is now: a plan
396 // started since does not pay for it, and one ended since still
397 // does.
398 let plan = self.had_plan_in(&row.workspace, &row.month).await?;
399 let eligible = month_end_eligible(&row.source, plan);
400 // Read before the draw: nothing between the draw and the
401 // entry may fail without giving it back.
402 let detail = if row.source == "storage" {
403 let gb_months = self.gb_months(&row.workspace, &row.month).await?;
404 format!(": {gb_months:.2} GB-months")
405 } else {
406 String::new()
407 };
408 let marking = mark()?;
409 let drawn = self.draw_as(&row.workspace, charge, &row.month, &eligible, plan).await?;
410 let description = format!("{} in {}{detail}{terms_note}{}", title(&row.source), row.month, drawn.note());
411 let reference = format!("{}/{}/{}", row.source, row.workspace, row.month);
412 let created_at = credits::month_end(&row.month);
413 let line = UsageLine {
414 workspace: &row.workspace,
415 charged: charge - drawn.total(),
416 description: &description,
417 repo: None,
418 task: &row.source,
419 cost,
420 reference: &reference,
421 created_at: &created_at,
422 drawn,
423 };
424 let posted = match self.post_usage_with(line, vec![marking]).await {
425 Ok(posted) => posted,
426 Err(error) => {
427 // Nothing was written: what was drawn goes back.
428 self.undraw(&row.workspace, &row.month, &drawn, None).await?;
429 return Err(error);
430 }
431 };
432 if !posted {
433 // Charged already (another tick won): give back the draw.
434 self.undraw(&row.workspace, &row.month, &drawn, None).await?;
435 return Ok(());
436 }
437 self.record_discount(&reference, discount).await?;
438 Ok::<(), worker::Error>(())
439 };
440 if let Err(error) = charged.await {
441 worker::console_error!("{}: charging {} for {} failed; tried again next tick: {error}", row.workspace, row.source, row.month);
442 }
443 }
444 Ok(())
445 }
446
447 /// Whether the workspace had the plan in `month`: g1t's own,
448 /// enterprises and a plan staff gave by their terms; otherwise a plan
449 /// invoice paid in the month and not reversed by a test-mode detach,
450 /// the plan's included usage drawn in it, or a plan on now that
451 /// started before the month ended (`PLAN_EVIDENCE`).
452 pub(crate) async fn had_plan_in(&self, workspace: &str, month: &str) -> Result<bool> {
453 let account = self.account_of(workspace).await?;
454 let now = self.plan_kind_for(workspace, &account).await?;
455 if matches!(now, PlanKind::Internal | PlanKind::Enterprise) || account.allowances.plan || !self.charging {
456 return Ok(now != PlanKind::Free);
457 }
458 let next = credits::next_month_start(month);
459 if now == PlanKind::Paid
460 && let Some((started_at, _)) = self.plan_cycle(workspace).await?
461 && started_at.as_str() < next.as_str()
462 {
463 return Ok(true);
464 }
465 #[derive(Deserialize)]
466 struct Evidence {
467 had: Option<i64>,
468 }
469 Ok(self
470 .db
471 .prepare(PLAN_EVIDENCE)
472 .bind(&[workspace.into(), format!("{month}-01").into(), next.as_str().into(), month.into()])?
473 .first::<Evidence>(None)
474 .await?
475 .and_then(|e| e.had)
476 .unwrap_or(0)
477 == 1)
478 }
479
480 /// A workspace's private storage past the free amount in `month`.
481 async fn gb_months(&self, workspace: &str, month: &str) -> Result<f64> {
482 #[derive(Deserialize)]
483 struct Day {
484 private_bytes: i64,
485 free_bytes: i64,
486 }
487 let days = self
488 .db
489 .prepare("SELECT private_bytes, free_bytes FROM storage_days WHERE workspace = ? AND substr(day, 1, 7) = ?")
490 .bind(&[workspace.into(), month.into()])?
491 .all()
492 .await?
493 .results::<Day>()?;
494 Ok(storage_gb_months(&days.iter().map(|d| (d.private_bytes, d.free_bytes)).collect::<Vec<_>>(), credits::days_in(month)))
495 }
496
497 /// Once a day: what each workspace's private repositories hold, and
498 /// what this month's storage past the free amount comes to so far.
499 pub(crate) async fn measure_storage(&self) -> Result<()> {
500 let Some(repos) = &self.repos else { return Ok(()) };
501 let list: Vec<WorkspaceStorage> = g1t_kit::call(repos, "storage", &StorageArgs {}).await?;
502 let now = rfc3339(now_ms());
503 let (day, month) = (&now[..10], credits::month_of(&now));
504 let price = self.price("private_storage").await?.map_or(STORAGE_MICROS_PER_GB_MONTH as f64, |(cost, _)| cost);
505 for workspace in list {
506 let slug = workspace.namespace.to_lowercase();
507 let plan = self.has_plan(&slug).await?;
508 // The same free amount for everyone: the plan pays past it.
509 let free = self.plans.free_storage_bytes;
510 self.db
511 .prepare(
512 "INSERT INTO storage_days (workspace, day, private_bytes, free_bytes) VALUES (?1, ?2, ?3, ?4)
513 ON CONFLICT (workspace, day) DO UPDATE SET private_bytes = ?3, free_bytes = ?4",
514 )
515 .bind(&[slug.as_str().into(), day.into(), (workspace.private_bytes as f64).into(), (free as f64).into()])?
516 .run()
517 .await?;
518 // Only the plan pays for storage past its amount. A free
519 // workspace is never charged: the repos service stops its pushes
520 // to private repositories once it is full (see git_ops.rs there).
521 let gb_months = if plan { self.gb_months(&slug, &month).await? } else { 0.0 };
522 if gb_months > 0.0 {
523 let detail = format!("{gb_months:.2} GB-months past the free {}", crate::features::bytes(free));
524 self.set_pending(&slug, "storage", &month, storage_cost(gb_months, price), Some(&detail)).await?;
525 }
526 }
527 Ok(())
528 }
529
530 /// Once a day: what each workspace's packages hold, public and private,
531 /// from the packages service, and for the plan what this month's
532 /// storage past the free amounts comes to so far. A free workspace is
533 /// never charged: the packages service refuses its pushes past them.
534 pub(crate) async fn measure_packages(&self) -> Result<()> {
535 #[derive(Deserialize)]
536 struct Held {
537 workspace: String,
538 public_bytes: i64,
539 private_bytes: i64,
540 }
541 #[derive(Deserialize)]
542 struct Day {
543 public_bytes: i64,
544 private_bytes: i64,
545 public_free_bytes: i64,
546 private_free_bytes: i64,
547 }
548 let Some(packages) = &self.packages else { return Ok(()) };
549 let list: Vec<Held> = g1t_kit::call(packages, "storage_all", &serde_json::json!({})).await?;
550 let now = rfc3339(now_ms());
551 let (day, month) = (&now[..10], credits::month_of(&now));
552 let price = self.price("package_storage").await?.map_or(PACKAGE_MICROS_PER_GB_MONTH as f64, |(cost, _)| cost);
553 let (public_free, private_free) = (self.plans.public_package_free_bytes, self.plans.private_package_free_bytes);
554 for held in list {
555 let slug = held.workspace.to_lowercase();
556 self.db
557 .prepare(
558 "INSERT INTO package_storage_days (workspace, day, public_bytes, private_bytes, public_free_bytes, private_free_bytes)
559 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
560 ON CONFLICT (workspace, day) DO UPDATE SET public_bytes = ?3, private_bytes = ?4, public_free_bytes = ?5, private_free_bytes = ?6",
561 )
562 .bind(&[
563 slug.as_str().into(),
564 day.into(),
565 (held.public_bytes as f64).into(),
566 (held.private_bytes as f64).into(),
567 (public_free as f64).into(),
568 (private_free as f64).into(),
569 ])?
570 .run()
571 .await?;
572 if !self.has_plan(&slug).await? {
573 continue;
574 }
575 let days = self
576 .db
577 .prepare(
578 "SELECT public_bytes, private_bytes, public_free_bytes, private_free_bytes FROM package_storage_days
579 WHERE workspace = ? AND substr(day, 1, 7) = ?",
580 )
581 .bind(&[slug.as_str().into(), month.as_str().into()])?
582 .all()
583 .await?
584 .results::<Day>()?;
585 let gb_months = package_gb_months(
586 &days.iter().map(|d| (d.public_bytes, d.private_bytes, d.public_free_bytes, d.private_free_bytes)).collect::<Vec<_>>(),
587 credits::days_in(&month),
588 );
589 if gb_months > 0.0 {
590 let detail = format!(
591 "{gb_months:.2} GB-months past the free {} public and {} private",
592 crate::features::bytes(public_free),
593 crate::features::bytes(private_free)
594 );
595 self.set_pending(&slug, "package_storage", &month, storage_cost(gb_months, price), Some(&detail)).await?;
596 }
597 }
598 Ok(())
599 }
600
601 /// Git operations this month for each workspace, from the repos
602 /// service: what is past the free amount goes to the month's pending
603 /// usage for workspaces on the plan, which are never slowed or refused
604 /// for them. Free workspaces are never charged for them.
605 pub(crate) async fn measure_git(&self) -> Result<()> {
606 let Some(repos) = &self.repos else { return Ok(()) };
607 let month = credits::month_of(&rfc3339(now_ms()));
608 let list: Vec<WorkspaceGitOperations> =
609 g1t_kit::call(repos, "git_operations", &GitOperationsArgs { since: Some(git_since(&month)), month: month.clone(), namespace: None }).await?;
610 let price = self.price("git_operations").await?.map_or(GIT_MICROS_PER_THOUSAND as f64, |(cost, _)| cost);
611 for workspace in list {
612 let slug = workspace.namespace.to_lowercase();
613 if self.plan_kind(&slug).await? == PlanKind::Free {
614 continue;
615 }
616 let cost = git_cost(workspace.operations, self.plans.git_included, price);
617 if cost > 0 {
618 let detail = format!(
619 "{} operations, {} of them free",
620 crate::features::thousands(workspace.operations),
621 crate::features::thousands(self.plans.git_included)
622 );
623 self.set_pending(&slug, "git", &month, cost, Some(&detail)).await?;
624 }
625 }
626 Ok(())
627 }
628
629 /// Once last month is over and its last hour has landed: its final
630 /// count of git operations, for workspaces that had the plan in it,
631 /// then the month marked measured (`measured_months`), which lets its
632 /// usage be charged and the month close. The daily measure ran hours
633 /// before the month ended; charging on it would leave out the
634 /// operations after it. Called every tick until it is done.
635 pub(crate) async fn measure_last_month_git(&self) -> Result<()> {
636 let Some(repos) = &self.repos else { return Ok(()) };
637 let now = now_ms();
638 let previous = crate::limits::previous_month(&credits::month_of(&rfc3339(now)));
639 if !final_measure_due(now, &previous) || self.measured("git", &previous).await? {
640 return Ok(());
641 }
642 let list: Vec<WorkspaceGitOperations> = g1t_kit::call(
643 repos,
644 "git_operations",
645 &GitOperationsArgs { since: Some(git_since(&previous)), month: previous.clone(), namespace: None },
646 )
647 .await?;
648 let price = self.price("git_operations").await?.map_or(GIT_MICROS_PER_THOUSAND as f64, |(cost, _)| cost);
649 for workspace in list {
650 let slug = workspace.namespace.to_lowercase();
651 // The plan as it was that month, as `charge_pending` charges it.
652 if !self.had_plan_in(&slug, &previous).await? {
653 continue;
654 }
655 let cost = git_cost(workspace.operations, self.plans.git_included, price);
656 if cost > 0 {
657 let detail = format!(
658 "{} operations, {} of them free",
659 crate::features::thousands(workspace.operations),
660 crate::features::thousands(self.plans.git_included)
661 );
662 self.set_pending(&slug, "git", &previous, cost, Some(&detail)).await?;
663 }
664 }
665 self.db
666 .prepare("INSERT OR IGNORE INTO measured_months (source, month, measured_at) VALUES ('git', ?1, ?2)")
667 .bind(&[previous.as_str().into(), rfc3339(now).into()])?
668 .run()
669 .await?;
670 Ok(())
671 }
672
673 /// Whether `source`'s final count of `month` was taken.
674 async fn measured(&self, source: &str, month: &str) -> Result<bool> {
675 #[derive(Deserialize)]
676 struct Row {
677 #[allow(dead_code)]
678 month: String,
679 }
680 Ok(self
681 .db
682 .prepare("SELECT month FROM measured_months WHERE source = ? AND month = ?")
683 .bind(&[source.into(), month.into()])?
684 .first::<Row>(None)
685 .await?
686 .is_some())
687 }
688
689 /// Whether last month's metered usage may be charged and the month
690 /// closed (`month_final`). Without a repos service there are no git
691 /// operations to wait for.
692 pub(crate) async fn last_month_final(&self) -> Result<bool> {
693 if self.repos.is_none() {
694 return Ok(true);
695 }
696 let now = now_ms();
697 let previous = crate::limits::previous_month(&credits::month_of(&rfc3339(now)));
698 let measured = self.measured("git", &previous).await?;
699 let ready = month_final(measured, now, &previous);
700 if ready && !measured {
701 worker::console_error!("closing {previous} without its final count of git operations: it was never taken");
702 }
703 Ok(ready)
704 }
705
706 /// The workspace's git operations this month, as last measured.
707 pub(crate) async fn git_operations_this_month(&self, workspace: &str) -> Result<u64> {
708 let Some(repos) = &self.repos else { return Ok(0) };
709 let month = credits::month_of(&rfc3339(now_ms()));
710 let list: Result<Vec<WorkspaceGitOperations>> =
711 g1t_kit::call(repos, "git_operations", &GitOperationsArgs { since: Some(format!("{month}-01T00")), month, namespace: Some(workspace.to_lowercase()) }).await;
712 Ok(list
713 .ok()
714 .and_then(|list| list.into_iter().find(|w| w.namespace.eq_ignore_ascii_case(workspace)))
715 .map_or(0, |w| w.operations))
716 }
717}
718
719#[cfg(test)]
720mod tests {
721 use super::*;
722
723 #[test]
724 fn scans_and_embeddings_on_a_free_workspace_are_covered_by_g1t() {
725 // No plan: g1t pays, and the trial is left alone.
726 let free = month_end_eligible("security", false);
727 assert!(free.cover_rest && !free.trial);
728 assert!(month_end_eligible("context", false).cover_rest);
729 // On the plan: charged like any usage, from its included usage first.
730 let plan = month_end_eligible("security", true);
731 assert!(!plan.cover_rest && plan.trial);
732 // Storage, git and the cache are never kept for a free workspace;
733 // were one at the close, it would not be covered either.
734 assert!(!month_end_eligible("storage", false).cover_rest);
735 assert!(!month_end_eligible("git", false).cover_rest);
736 // A covered source is still a source billing charges at the close.
737 assert!(COVERED_FOR_FREE.iter().all(|s| CHARGED_HERE.contains(s)));
738 }
739
740 #[test]
741 fn storage_past_the_free_amount_is_counted_by_the_day() {
742 // 3 GB private with 1 GB free, every day of a 30-day month: 2 GB-months.
743 let month = vec![(3_000_000_000, 1_000_000_000); 30];
744 assert!((storage_gb_months(&month, 30) - 2.0).abs() < 1e-9);
745 // Under the free amount: nothing.
746 assert_eq!(storage_gb_months(&[(500_000_000, 1_000_000_000); 30], 30), 0.0);
747 // The plan has the same 1 GB free: 11 GB on it all month is 10
748 // GB-months, $5.00 to g1t, $6.00 charged, from its included usage.
749 let days = vec![(11_000_000_000, 1_000_000_000); 30];
750 assert!((storage_gb_months(&days, 30) - 10.0).abs() < 1e-9);
751 assert_eq!(credits::with_margin(storage_cost(storage_gb_months(&days, 30), STORAGE_MICROS_PER_GB_MONTH as f64), 20), 6_000_000);
752 // Ten days of 4 GB past it: a third of 4 GB-months.
753 let days = vec![(5_000_000_000, 1_000_000_000); 10];
754 assert!((storage_gb_months(&days, 30) - 4.0 / 3.0).abs() < 1e-9);
755 }
756
757 #[test]
758 fn a_month_of_storage_is_its_own_days_not_thirty() {
759 // 2 GB past the free amount every day of a 31-day month is 2
760 // GB-months, not 2.07; every day of February, 2 too, not 1.87.
761 let october = vec![(3_000_000_000, 1_000_000_000); 31];
762 assert!((storage_gb_months(&october, credits::days_in("2026-10")) - 2.0).abs() < 1e-9);
763 let february = vec![(3_000_000_000, 1_000_000_000); 28];
764 assert!((storage_gb_months(&february, credits::days_in("2026-02")) - 2.0).abs() < 1e-9);
765 assert_eq!(credits::days_in("2028-02"), 29);
766 let gb = GB as i64;
767 let packages = vec![(11 * gb, 0, 10 * gb, 0); 31];
768 assert!((package_gb_months(&packages, 31) - 1.0).abs() < 1e-9);
769 }
770
771 #[test]
772 fn package_storage_counts_public_and_private_past_their_own_free_amounts() {
773 let gb = GB as i64;
774 // 12 GB public with 10 free, 0.5 GB private with 0.5 free: 2 GB past, for 15 days.
775 let days = vec![(12 * gb, gb / 2, 10 * gb, gb / 2); 15];
776 assert!((package_gb_months(&days, 30) - 1.0).abs() < 1e-9);
777 // Under both amounts: nothing.
778 assert_eq!(package_gb_months(&[(gb, gb / 4, 10 * gb, gb / 2)], 30), 0.0);
779 // At R2's rate, a GB-month is $0.015.
780 assert_eq!(storage_cost(1.0, PACKAGE_MICROS_PER_GB_MONTH as f64), 15_000);
781 }
782
783 #[test]
784 fn storage_is_priced_at_cloudflares_rate_plus_the_margin() {
785 // $0.50 a GB-month to g1t: 2 GB-months cost $1.00, charged $1.20.
786 let cost = storage_cost(2.0, 500_000.0);
787 assert_eq!(cost, 1_000_000);
788 assert_eq!(credits::with_margin(cost, 20), 1_200_000);
789 // A fraction of a millionth rounds up.
790 assert_eq!(storage_cost(0.000_000_001, 500_000.0), 1);
791 }
792
793 #[test]
794 fn embeddings_and_scans_are_charged_at_cost_plus_the_margin() {
795 // 10 million tokens at $0.067 a million: $0.67, charged $0.804.
796 assert_eq!(credits::with_margin(670_000, 20), 804_000);
797 assert_eq!(title("context"), "Search embeddings");
798 assert_eq!(title("security"), "Security scans");
799 assert!(CHARGED_HERE.contains(&"storage") && !CHARGED_HERE.contains(&"deployments"));
800 assert!(CHARGED_HERE.contains(&"git"));
801 // Package storage and observability data are charged when the month
802 // closes, like storage, and each is named on the statement.
803 for source in ["package_storage", "telemetry"] {
804 assert!(CHARGED_HERE.contains(&source), "{source}");
805 assert_ne!(title(source), "Metered usage", "{source}");
806 assert!(!crate::pricing::meters_of(source).is_empty(), "{source}");
807 assert!(crate::pricing::meters_of(source).iter().all(|m| crate::pricing::MONTHLY.contains(m)), "{source}");
808 assert!(!COVERED_FOR_FREE.contains(&source) && !PLAN_ONLY.contains(&source), "{source}");
809 }
810 assert_eq!(title("telemetry"), "Observability data past the free amount");
811 }
812
813 #[test]
814 fn git_operations_are_charged_past_the_free_amount_at_cloudflares_price() {
815 // $0.15 per 1,000 to g1t; 50,000 a month free for everyone.
816 let per_thousand = GIT_MICROS_PER_THOUSAND as f64;
817 let free = crate::credits::Config::default().git_included;
818 assert_eq!(free, 50_000);
819 assert_eq!(git_cost(49_000, free, per_thousand), 0);
820 assert_eq!(git_cost(50_000, free, per_thousand), 0);
821 // A workspace on the plan pushes on past it, and pays: 70,000
822 // operations are 20,000 past it, $3.00 to g1t, $3.60 charged.
823 assert_eq!(git_cost(70_000, free, per_thousand), 3_000_000);
824 assert_eq!(credits::with_margin(git_cost(70_000, free, per_thousand), 20), 3_600_000);
825 // A million in a month: no cap, $142.50 to g1t.
826 assert_eq!(git_cost(1_000_000, free, per_thousand), 142_500_000);
827 // One past it: a fraction of a cent, rounded up to a millionth.
828 assert_eq!(git_cost(50_001, free, per_thousand), 150);
829 assert_eq!(title("git"), "Git operations past the free amount");
830 }
831
832 #[test]
833 fn a_month_is_charged_and_closed_only_after_its_final_count_of_git_operations() {
834 let at = |text: &str| g1t_contracts::time::parse_rfc3339(text).unwrap();
835 // The quarter-hour just after October ends: the daily measure ran at
836 // 04:17 on the 31st, so its count leaves out nearly 20 hours of
837 // operations. Neither due to measure yet, nor final.
838 let midnight = at("2026-11-01T00:00:00Z");
839 assert!(!final_measure_due(midnight, "2026-10"));
840 assert!(!month_final(false, midnight, "2026-10"));
841 // An hour on, the last hour has landed: the final count is taken,
842 // and once it is, the month is charged and closed.
843 let hour = at("2026-11-01T01:00:00Z");
844 assert!(final_measure_due(hour, "2026-10"));
845 assert!(!month_final(false, hour, "2026-10"));
846 assert!(month_final(true, hour, "2026-10"));
847 // A month not over is never final, measured or not.
848 assert!(!final_measure_due(at("2026-10-31T23:59:59Z"), "2026-10"));
849 assert!(!month_final(true, at("2026-10-31T23:59:59Z"), "2026-10"));
850 // Three days without it (the repos service down), the close goes
851 // ahead regardless.
852 assert!(!month_final(false, at("2026-11-03T23:59:59Z"), "2026-10"));
853 assert!(month_final(false, at("2026-11-04T00:00:00Z"), "2026-10"));
854 // December's ends with the year.
855 assert!(final_measure_due(at("2027-01-01T01:00:00Z"), "2026-12"));
856 assert!(!final_measure_due(at("2027-01-01T00:59:59Z"), "2026-12"));
857 }
858
859 #[test]
860 fn git_operations_count_from_when_cloudflare_starts_charging() {
861 assert_eq!(git_since("2026-10"), "2026-10-14T00");
862 assert_eq!(git_since("2026-11"), "2026-11-01T00");
863 assert_eq!(git_since("2026-09"), "2026-10-14T00");
864 }
865}