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.
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 1 | //! The Usage page: a workspace's usage over a range of days, at price, by |
| 2 | //! product family, meter, project and day, with what paid for it. | |
| 3 | //! | |
| 4 | //! Every figure comes from the ledger's own lines, measured as the | |
| 5 | //! statement measures them (`statement::PRICE_SQL`: what was charged, plus | |
| 6 | //! what included usage, the trial, a pool or g1t paid, plus what a discount | |
| 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. | 7 | //! took off), so the Usage page and Billing show the same numbers. Usage metered through the month and charged |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 8 | //! when it closes (storage, git operations, scans, embeddings, domains, |
| 9 | //! app traffic) is added from `pending_usage`, at cost plus the margin, and | |
| 10 | //! said to be pending. | |
| 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. | 11 | //! |
| 12 | //! Lines from before g1t stopped running agents and selling model access | |
| 13 | //! (agent runs, the agent rate, AI Gateway requests), and any line on a | |
| 14 | //! task this page does not know, are kept on one meter, Other, in a | |
| 15 | //! product of their own: nothing charged is ever left out. | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 16 | |
| 17 | use std::collections::{BTreeMap, BTreeSet}; | |
| 18 | ||
| 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. | 19 | use futures_util::future::{try_join, try_join3}; |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 20 | use g1t_contracts::billing::{ |
| 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. | 21 | Allowance, MeterLine, PlanKind, ProductUsage, ProjectUsage, UsageDay, UsageReport, UsageReportArgs, UsageTotals, PRODUCTS, |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 22 | }; |
| 23 | use g1t_contracts::time::{parse_rfc3339, rfc3339}; | |
| 24 | use g1t_contracts::{FailureCode, Outcome}; | |
| 25 | use g1t_kit::now_ms; | |
| 26 | use serde::Deserialize; | |
| 27 | use worker::Result; | |
| 28 | ||
| Spend adds up on one ledger. Charged this month has one definition, price less discount, included usage and credit, shared by the plan card, the spend limit, Spend and the top bar; the limit had counted usage not yet closed at full price before the discount, so a comped workspace read as charged a cent. Every agent line on the ledger names the agent and who asked, repository runs by g1t included, so Spent is the sum of its products, Agents is the agent product, and by agent adds up to it; the billing API returns by_agent and by_person. The usage and billing guide says how spend is counted. | 29 | use crate::charged::{charged, pending_split}; |
| 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. | 30 | use crate::statement::PRICE_SQL; |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 31 | use crate::{Billing, members_only}; |
| 32 | ||
| 33 | /// The longest range the page reads at once. | |
| 34 | pub(crate) const MAX_DAYS: u64 = 400; | |
| 35 | const DAY_MS: u64 = 86_400_000; | |
| 36 | ||
| 37 | /// Which meter a ledger line is on. One SQL expression, so every query | |
| 38 | /// agrees with `METERS`. | |
| 39 | pub(crate) const METER_KEY_SQL: &str = "CASE | |
| 40 | WHEN task = 'sandbox' THEN 'sandbox' | |
| 41 | WHEN task = 'self_hosted' THEN 'self_hosted' | |
| 42 | WHEN task = 'deployments' AND reference LIKE 'deploy/%' THEN 'builds' | |
| 43 | WHEN task = 'deployments' THEN 'requests' | |
| 44 | WHEN task = 'domains' THEN 'domains' | |
| 45 | WHEN task = 'git' THEN 'git' | |
| 46 | WHEN task = 'storage' THEN 'storage' | |
| 47 | WHEN task = 'cache' THEN 'cache' | |
| 48 | WHEN task = 'package_storage' THEN 'packages' | |
| 49 | WHEN task = 'security' THEN 'security' | |
| 50 | WHEN task = 'context' THEN 'context' | |
| 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. | 51 | ELSE 'other' END"; |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 52 | |
| 53 | /// Every meter: key, name, product family and the unit of its quantity. | |
| 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. | 54 | pub(crate) const METERS: [(&str, &str, &str, &str); 12] = [ |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 55 | ("sandbox", "Sandbox time", "sandboxes", "seconds"), |
| 56 | ("self_hosted", "Self-hosted runner time", "sandboxes", "seconds"), | |
| 57 | ("builds", "Builds", "deployments", "entries"), | |
| 58 | ("requests", "Requests & CPU", "deployments", "entries"), | |
| 59 | ("domains", "Custom domains", "deployments", "entries"), | |
| 60 | ("git", "Git operations", "git_storage", "operations"), | |
| 61 | ("storage", "Private storage", "git_storage", "bytes"), | |
| 62 | ("cache", "Actions cache", "git_storage", "entries"), | |
| 63 | ("packages", "Package storage", "packages", "entries"), | |
| 64 | ("security", "Security scans", "security", "entries"), | |
| 65 | ("context", "Search embeddings", "search", "entries"), | |
| 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. | 66 | // Kept apart so nothing is ever lost: a line whose meter is unknown, |
| 67 | // and agents' and models' usage from before they were retired. | |
| 68 | ("other", "Other", OTHER, "entries"), | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 69 | ]; |
| 70 | ||
| 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. | 71 | /// The product of the Other meter: not one of `PRODUCTS`, listed last and |
| 72 | /// only when it has something. | |
| 73 | pub(crate) const OTHER: &str = "other"; | |
| 74 | ||
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 75 | /// The meter month-end usage noted so far (`pending_usage.source`) is on. |
| 76 | pub(crate) fn meter_of_pending(source: &str) -> &'static str { | |
| 77 | match source { | |
| 78 | "deployments" => "requests", | |
| 79 | "domains" => "domains", | |
| 80 | "storage" => "storage", | |
| 81 | "git" => "git", | |
| 82 | "cache" => "cache", | |
| 83 | "package_storage" => "packages", | |
| 84 | "security" => "security", | |
| 85 | "context" => "context", | |
| 86 | _ => "other", | |
| 87 | } | |
| 88 | } | |
| 89 | ||
| 90 | /// Every day from `from` to `until`, both included, or why not. | |
| 91 | pub(crate) fn days_between(from: &str, until: &str) -> std::result::Result<Vec<String>, &'static str> { | |
| 92 | let (Some(start), Some(end)) = (parse_rfc3339(&format!("{from}T00:00:00Z")), parse_rfc3339(&format!("{until}T00:00:00Z"))) else { | |
| 93 | return Err("Give the range as days, YYYY-MM-DD."); | |
| 94 | }; | |
| 95 | if end < start { | |
| 96 | return Err("The range ends before it starts."); | |
| 97 | } | |
| 98 | if (end - start) / DAY_MS + 1 > MAX_DAYS { | |
| 99 | return Err("Ask for at most 400 days at a time."); | |
| 100 | } | |
| 101 | Ok((0..=(end - start) / DAY_MS).map(|n| rfc3339(start + n * DAY_MS)[..10].to_owned()).collect()) | |
| 102 | } | |
| 103 | ||
| 104 | /// One group of ledger lines: a day, a meter and a project. | |
| 105 | #[derive(Clone, Debug, Default, Deserialize, PartialEq)] | |
| 106 | pub(crate) struct Cell { | |
| 107 | pub day: String, | |
| 108 | pub meter: String, | |
| 109 | #[serde(default)] | |
| 110 | pub project: String, | |
| 111 | #[serde(default)] | |
| 112 | pub price: Option<f64>, | |
| 113 | #[serde(default)] | |
| 114 | pub quantity: Option<f64>, | |
| 115 | #[serde(default)] | |
| 116 | pub entries: Option<f64>, | |
| 117 | #[serde(default)] | |
| 118 | pub discount: Option<f64>, | |
| 119 | #[serde(default)] | |
| 120 | pub covered: Option<f64>, | |
| 121 | #[serde(default)] | |
| 122 | pub cost: Option<f64>, | |
| 123 | } | |
| 124 | ||
| Spend adds up on one ledger. Charged this month has one definition, price less discount, included usage and credit, shared by the plan card, the spend limit, Spend and the top bar; the limit had counted usage not yet closed at full price before the discount, so a comped workspace read as charged a cent. Every agent line on the ledger names the agent and who asked, repository runs by g1t included, so Spent is the sum of its products, Agents is the agent product, and by agent adds up to it; the billing API returns by_agent and by_person. The usage and billing guide says how spend is counted. | 125 | /// What is metered this month and charged when it closes, by meter: at |
| 126 | /// price, with what g1t covers of it and what the discount takes off, as | |
| 127 | /// the close will enter it (`charged::pending_split`). | |
| 128 | #[derive(Clone, Debug, Default, PartialEq)] | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 129 | pub(crate) struct Pending { |
| 130 | pub meter: &'static str, | |
| 131 | pub micros: i64, | |
| Spend adds up on one ledger. Charged this month has one definition, price less discount, included usage and credit, shared by the plan card, the spend limit, Spend and the top bar; the limit had counted usage not yet closed at full price before the discount, so a comped workspace read as charged a cent. Every agent line on the ledger names the agent and who asked, repository runs by g1t included, so Spent is the sum of its products, Agents is the agent product, and by agent adds up to it; the billing API returns by_agent and by_person. The usage and billing guide says how spend is counted. | 132 | pub covered: i64, |
| 133 | pub discount: i64, | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 134 | } |
| 135 | ||
| Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means. | 136 | /// Whether a range of days reaches into the current month, so the month's |
| 137 | /// usage not yet closed (`pending_usage`, which has no day of its own) is | |
| 138 | /// counted in it. `today` is billing's own UTC day, so the top bar, Spend, | |
| 139 | /// Home and Usage, which all ask for the first of the month to today, get | |
| 140 | /// the same answer whatever `until` they send: a range need not reach | |
| 141 | /// month end to include what the month has metered so far. | |
| 142 | pub(crate) fn reaches_this_month(from: &str, until: &str, today: &str) -> bool { | |
| 143 | from <= today && until >= &format!("{}-01", &today[..7])[..] | |
| Spend adds up on one ledger. Charged this month has one definition, price less discount, included usage and credit, shared by the plan card, the spend limit, Spend and the top bar; the limit had counted usage not yet closed at full price before the discount, so a comped workspace read as charged a cent. Every agent line on the ledger names the agent and who asked, repository runs by g1t included, so Spent is the sum of its products, Agents is the agent product, and by agent adds up to it; the billing API returns by_agent and by_person. The usage and billing guide says how spend is counted. | 144 | } |
| 145 | ||
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 146 | /// Shapes the groups into the page: products in order, each with its |
| 147 | /// meters, each meter with every day of the range and its projects. Only | |
| 148 | /// the products and projects asked for (all, when none are). | |
| 149 | pub(crate) fn shape( | |
| 150 | days: &[String], | |
| 151 | cells: &[Cell], | |
| 152 | pending: &[Pending], | |
| 153 | products: &[String], | |
| 154 | projects: &[String], | |
| 155 | ) -> (Vec<ProductUsage>, Vec<UsageDay>, UsageTotals, Vec<String>) { | |
| 156 | let wanted_product = |product: &str| products.is_empty() || products.iter().any(|p| p == product); | |
| 157 | let wanted_project = |project: &str| projects.is_empty() || projects.iter().any(|p| p == project); | |
| 158 | let index: BTreeMap<&str, usize> = days.iter().enumerate().map(|(i, d)| (d.as_str(), i)).collect(); | |
| 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. | 159 | let product_of = |meter: &str| METERS.iter().find(|m| m.0 == meter).map_or(OTHER, |m| m.2); |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 160 | let mut all_projects = BTreeSet::new(); |
| 161 | let mut lines: Vec<MeterLine> = METERS | |
| 162 | .iter() | |
| 163 | .map(|(key, label, product, unit)| MeterLine { | |
| 164 | key: (*key).to_owned(), | |
| 165 | label: (*label).to_owned(), | |
| 166 | product: (*product).to_owned(), | |
| 167 | unit: (*unit).to_owned(), | |
| 168 | quantity: 0.0, | |
| 169 | micros: 0, | |
| 170 | pending_micros: 0, | |
| 171 | daily: vec![0; days.len()], | |
| 172 | allowance: None, | |
| 173 | by_project: vec![], | |
| Merge branch 'model-routing' | 174 | note: None, |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 175 | }) |
| 176 | .collect(); | |
| 177 | let mut by_day: BTreeMap<(String, String), i64> = BTreeMap::new(); | |
| 178 | let mut totals = UsageTotals::default(); | |
| 179 | let mut projects_of: BTreeMap<(String, String), (i64, f64)> = BTreeMap::new(); | |
| 180 | for cell in cells { | |
| 181 | if !cell.project.is_empty() { | |
| 182 | all_projects.insert(cell.project.clone()); | |
| 183 | } | |
| 184 | let product = product_of(&cell.meter); | |
| 185 | if !wanted_product(product) || !wanted_project(&cell.project) { | |
| 186 | continue; | |
| 187 | } | |
| 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. | 188 | // A meter this page does not know is Other's: never dropped. |
| 189 | let meter = if METERS.iter().any(|m| m.0 == cell.meter) { cell.meter.as_str() } else { "other" }; | |
| 190 | let Some(line) = lines.iter_mut().find(|l| l.key == meter) else { continue }; | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 191 | let price = cell.price.unwrap_or(0.0).round() as i64; |
| 192 | line.micros += price; | |
| 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. | 193 | line.quantity += if line.unit == "entries" { cell.entries.unwrap_or(0.0) } else { cell.quantity.unwrap_or(0.0) }; |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 194 | if let Some(i) = index.get(cell.day.as_str()) { |
| 195 | line.daily[*i] += price; | |
| 196 | } | |
| 197 | *by_day.entry((cell.day.clone(), product.to_owned())).or_default() += price; | |
| 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. | 198 | let part = projects_of.entry((meter.to_owned(), cell.project.clone())).or_default(); |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 199 | part.0 += price; |
| 200 | part.1 += if line.unit == "entries" { cell.entries.unwrap_or(0.0) } else { cell.quantity.unwrap_or(0.0) }; | |
| 201 | totals.price_micros += price; | |
| 202 | totals.discount_micros += cell.discount.unwrap_or(0.0).round() as i64; | |
| 203 | totals.included_micros += cell.covered.unwrap_or(0.0).round() as i64; | |
| 204 | totals.cost_micros += cell.cost.unwrap_or(0.0).round() as i64; | |
| 205 | } | |
| 206 | // Pending usage is no one project's: only when all projects are shown. | |
| 207 | if projects.is_empty() { | |
| 208 | for p in pending { | |
| 209 | let product = product_of(p.meter); | |
| 210 | if !wanted_product(product) { | |
| 211 | continue; | |
| 212 | } | |
| 213 | if let Some(line) = lines.iter_mut().find(|l| l.key == p.meter) { | |
| 214 | line.micros += p.micros; | |
| 215 | line.pending_micros += p.micros; | |
| 216 | } | |
| 217 | totals.price_micros += p.micros; | |
| 218 | totals.pending_micros += p.micros; | |
| Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means. | 219 | totals.pending_charged_micros += p.micros - p.covered - p.discount; |
| Spend adds up on one ledger. Charged this month has one definition, price less discount, included usage and credit, shared by the plan card, the spend limit, Spend and the top bar; the limit had counted usage not yet closed at full price before the discount, so a comped workspace read as charged a cent. Every agent line on the ledger names the agent and who asked, repository runs by g1t included, so Spent is the sum of its products, Agents is the agent product, and by agent adds up to it; the billing API returns by_agent and by_person. The usage and billing guide says how spend is counted. | 220 | totals.included_micros += p.covered; |
| 221 | totals.discount_micros += p.discount; | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 222 | } |
| 223 | } | |
| 224 | for ((meter, project), (micros, quantity)) in projects_of { | |
| 225 | if let Some(line) = lines.iter_mut().find(|l| l.key == meter) { | |
| 226 | line.by_project.push(ProjectUsage { project, micros, quantity }); | |
| 227 | } | |
| 228 | } | |
| 229 | for line in &mut lines { | |
| 230 | line.by_project.sort_by(|a, b| b.micros.cmp(&a.micros).then(a.project.cmp(&b.project))); | |
| 231 | } | |
| 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. | 232 | // The products that still have meters, in order, then Other when it |
| 233 | // has something. | |
| 234 | let other_used = lines.iter().any(|l| l.product == OTHER && (l.micros != 0 || l.quantity != 0.0)); | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 235 | let products_out = PRODUCTS |
| 236 | .iter() | |
| 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. | 237 | .copied() |
| 238 | .filter(|(key, _)| METERS.iter().any(|m| m.2 == *key)) | |
| 239 | .chain(other_used.then_some((OTHER, "Other"))) | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 240 | .filter(|(key, _)| wanted_product(key)) |
| 241 | .map(|(key, label)| { | |
| 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. | 242 | let meters: Vec<MeterLine> = lines.iter().filter(|l| l.product == key).cloned().collect(); |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 243 | ProductUsage { |
| 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. | 244 | key: key.to_owned(), |
| 245 | label: label.to_owned(), | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 246 | micros: meters.iter().map(|m| m.micros).sum(), |
| 247 | meters, | |
| 248 | } | |
| 249 | }) | |
| 250 | .collect(); | |
| 251 | let days_out = by_day.into_iter().filter(|(_, micros)| *micros != 0).map(|((day, product), micros)| UsageDay { day, product, micros }).collect(); | |
| 252 | (products_out, days_out, totals, all_projects.into_iter().collect()) | |
| 253 | } | |
| 254 | ||
| 255 | impl Billing { | |
| 256 | /// `usage_report`: members only. | |
| 257 | pub(crate) async fn usage_report(&self, a: UsageReportArgs) -> Result<Outcome<UsageReport>> { | |
| 258 | let workspace = a.workspace.to_lowercase(); | |
| 259 | if !a.viewer.is_some_and(|viewer| viewer.is_member(&workspace)) { | |
| 260 | return Ok(members_only()); | |
| 261 | } | |
| 262 | let days = match days_between(&a.from, &a.until) { | |
| 263 | Ok(days) => days, | |
| 264 | Err(why) => return Ok(Outcome::fail(FailureCode::Invalid, why)), | |
| 265 | }; | |
| 266 | let from = days[0].clone(); | |
| 267 | let until = days[days.len() - 1].clone(); | |
| 268 | // Lines entered on the last day count, whatever the hour. | |
| 269 | let end = rfc3339(parse_rfc3339(&format!("{until}T00:00:00Z")).unwrap_or(0) + DAY_MS); | |
| 270 | let measure = if self.free { "COALESCE(cost_micros, 0)" } else { PRICE_SQL }; | |
| 271 | let now = rfc3339(now_ms()); | |
| 272 | let month = now[..7].to_owned(); | |
| Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means. | 273 | let this_month = reaches_this_month(&from, &until, &now[..10]); |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 274 | |
| 275 | let cells = async { | |
| 276 | self.db | |
| 277 | .prepare(format!( | |
| 278 | "SELECT substr(created_at, 1, 10) AS day, {meter} AS meter, COALESCE(repo, '') AS project, | |
| 279 | SUM({measure}) AS price, SUM(COALESCE(quantity, 0)) AS quantity, COUNT(*) AS entries, | |
| 280 | SUM(COALESCE(discount_micros, 0)) AS discount, | |
| 281 | SUM(COALESCE(credit_micros, 0) + COALESCE(trial_micros, 0) + COALESCE(oss_micros, 0) + COALESCE(given_micros, 0)) AS covered, | |
| 282 | SUM(COALESCE(cost_micros, 0)) AS cost | |
| 283 | FROM ledger WHERE workspace = ?1 AND kind = 'usage' AND created_at >= ?2 AND created_at < ?3 | |
| 284 | GROUP BY 1, 2, 3 LIMIT 20000", | |
| 285 | meter = METER_KEY_SQL | |
| 286 | )) | |
| 287 | .bind(&[workspace.as_str().into(), from.as_str().into(), end.as_str().into()])? | |
| 288 | .all() | |
| 289 | .await? | |
| 290 | .results::<Cell>() | |
| 291 | }; | |
| 292 | let pending = async { | |
| 293 | if !this_month { | |
| 294 | return Ok(vec![]); | |
| 295 | } | |
| Spend adds up on one ledger. Charged this month has one definition, price less discount, included usage and credit, shared by the plan card, the spend limit, Spend and the top bar; the limit had counted usage not yet closed at full price before the discount, so a comped workspace read as charged a cent. Every agent line on the ledger names the agent and who asked, repository runs by g1t included, so Spent is the sum of its products, Agents is the agent product, and by agent adds up to it; the billing API returns by_agent and by_person. The usage and billing guide says how spend is counted. | 296 | self.pending_this_month(&[workspace.as_str().into()], &month).await |
| 297 | }; | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 298 | let credits = self.credit_paid_between(&workspace, &from, &end); |
| 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. | 299 | let ((cells, pending, (credits_paid, credits)), account) = |
| 300 | try_join(try_join3(cells, pending, credits), self.account_of(&workspace)).await?; | |
| Spend adds up on one ledger. Charged this month has one definition, price less discount, included usage and credit, shared by the plan card, the spend limit, Spend and the top bar; the limit had counted usage not yet closed at full price before the discount, so a comped workspace read as charged a cent. Every agent line on the ledger names the agent and who asked, repository runs by g1t included, so Spent is the sum of its products, Agents is the agent product, and by agent adds up to it; the billing API returns by_agent and by_person. The usage and billing guide says how spend is counted. | 301 | let percent = account.terms.percent_off(); |
| 302 | // What is metered so far is entered on the account's terms when the | |
| 303 | // month closes: its discount, and what g1t covers, come off it now. | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 304 | let pending: Vec<Pending> = pending |
| 305 | .into_iter() | |
| Spend adds up on one ledger. Charged this month has one definition, price less discount, included usage and credit, shared by the plan card, the spend limit, Spend and the top bar; the limit had counted usage not yet closed at full price before the discount, so a comped workspace read as charged a cent. Every agent line on the ledger names the agent and who asked, repository runs by g1t included, so Spent is the sum of its products, Agents is the agent product, and by agent adds up to it; the billing API returns by_agent and by_person. The usage and billing guide says how spend is counted. | 306 | .map(|p| { |
| 307 | let split = pending_split(p.cost(), p.charge(), self.margin_percent, percent); | |
| 308 | Pending { meter: meter_of_pending(&p.source), micros: split.price, covered: split.covered, discount: split.discount } | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 309 | }) |
| 310 | .filter(|p| p.micros > 0) | |
| 311 | .collect(); | |
| 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. | 312 | let (mut products, days_out, mut totals, all_projects) = shape(&days, &cells, &pending, &a.products, &a.projects); |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 313 | let filtered = !a.products.is_empty() || !a.projects.is_empty(); |
| 314 | // Credit is the workspace's, not a product's or a project's: only | |
| 315 | // without a filter is it taken off. | |
| 316 | totals.credits_micros = if filtered { 0 } else { credits_paid.max(0) }; | |
| 317 | totals.charged_micros = charged(&totals); | |
| 318 | let plan = self.plan_kind_for(&workspace, &account).await?; | |
| 319 | // Allowances, for the range that includes this month. | |
| 320 | let mut included = None; | |
| 321 | let mut trial = None; | |
| 322 | if this_month { | |
| 323 | let (used, stored, operations) = futures_util::future::try_join3( | |
| 324 | async { | |
| 325 | if plan == PlanKind::Paid { self.allowance_used("plan_credit", &workspace, &month).await } else { Ok(0) } | |
| 326 | }, | |
| 327 | self.private_storage(&workspace), | |
| 328 | self.git_operations_this_month(&workspace), | |
| 329 | ) | |
| 330 | .await?; | |
| 331 | if plan == PlanKind::Paid { | |
| 332 | included = Some(Allowance { used: used as f64, of: self.plans.plan_included_micros as f64, unit: "micros".to_owned() }); | |
| 333 | } | |
| 334 | for product in &mut products { | |
| 335 | for meter in &mut product.meters { | |
| 336 | match meter.key.as_str() { | |
| 337 | "storage" => { | |
| 338 | meter.quantity = stored as f64; | |
| 339 | meter.allowance = Some(Allowance { used: stored as f64, of: self.plans.free_storage_bytes as f64, unit: "bytes".to_owned() }); | |
| 340 | } | |
| 341 | "git" => { | |
| 342 | meter.quantity = operations as f64; | |
| 343 | meter.allowance = Some(Allowance { used: operations as f64, of: self.plans.git_included as f64, unit: "operations".to_owned() }); | |
| 344 | } | |
| 345 | _ => {} | |
| 346 | } | |
| 347 | } | |
| 348 | } | |
| 349 | if plan == PlanKind::Free | |
| 350 | && let Some(grant) = self.grant_of(&workspace).await? | |
| 351 | { | |
| 352 | trial = Some(crate::credits::left(grant.granted_micros, grant.used_micros)); | |
| 353 | } | |
| 354 | } | |
| 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. | 355 | // AI credit bought before g1t stopped selling it: still the |
| 356 | // workspace's, and kept apart from credit for everything. | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 357 | let ai: i64 = credits.grants.iter().filter(|g| g.scope == "models").map(|g| g.left_micros).sum(); |
| 358 | Ok(Outcome::Ok(UsageReport { | |
| 359 | from, | |
| 360 | until, | |
| 361 | totals, | |
| 362 | days: days_out, | |
| 363 | products, | |
| 364 | projects: all_projects, | |
| 365 | included, | |
| 366 | discount_percent: (percent > 0).then_some(percent), | |
| 367 | ai_credit_micros: ai, | |
| 368 | credit_micros: credits.left_micros - ai, | |
| 369 | trial_micros: trial, | |
| 370 | plan, | |
| 371 | free: self.free, | |
| 372 | })) | |
| Merge branch 'model-routing' | 373 | } |
| 374 | } | |
| 375 | ||
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 376 | #[cfg(test)] |
| 377 | mod tests { | |
| 378 | use super::*; | |
| 379 | ||
| 380 | fn cell(day: &str, meter: &str, project: &str, price: f64) -> Cell { | |
| 381 | Cell { day: day.into(), meter: meter.into(), project: project.into(), price: Some(price), entries: Some(1.0), ..Cell::default() } | |
| 382 | } | |
| 383 | ||
| 384 | #[test] | |
| 385 | fn a_range_is_every_day_in_it() { | |
| 386 | let days = days_between("2026-10-01", "2026-10-03").unwrap(); | |
| 387 | assert_eq!(days, ["2026-10-01", "2026-10-02", "2026-10-03"]); | |
| 388 | assert_eq!(days_between("2026-10-01", "2026-10-01").unwrap().len(), 1); | |
| 389 | assert!(days_between("2026-10-03", "2026-10-01").is_err()); | |
| 390 | assert!(days_between("2025-01-01", "2026-10-01").is_err()); | |
| 391 | assert!(days_between("Oct 1", "2026-10-01").is_err()); | |
| 392 | } | |
| 393 | ||
| 394 | #[test] | |
| 395 | fn usage_is_shaped_into_products_meters_days_and_projects() { | |
| 396 | let days = days_between("2026-10-01", "2026-10-03").unwrap(); | |
| 397 | let mut sandbox = cell("2026-10-02", "sandbox", "acme/web", 120_000.0); | |
| 398 | sandbox.quantity = Some(600.0); | |
| 399 | let cells = vec![ | |
| 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. | 400 | cell("2026-10-01", "requests", "acme/web", 2_000_000.0), |
| 401 | cell("2026-10-03", "requests", "acme/api", 1_000_000.0), | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 402 | sandbox, |
| 403 | Cell { covered: Some(1_000_000.0), discount: Some(0.0), ..cell("2026-10-03", "storage", "", 30_000.0) }, | |
| 404 | ]; | |
| Spend adds up on one ledger. Charged this month has one definition, price less discount, included usage and credit, shared by the plan card, the spend limit, Spend and the top bar; the limit had counted usage not yet closed at full price before the discount, so a comped workspace read as charged a cent. Every agent line on the ledger names the agent and who asked, repository runs by g1t included, so Spent is the sum of its products, Agents is the agent product, and by agent adds up to it; the billing API returns by_agent and by_person. The usage and billing guide says how spend is counted. | 405 | let pending = vec![Pending { meter: "git", micros: 40_000, ..Pending::default() }]; |
| 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. | 406 | let (products, by_day, totals, projects) = shape(&days, &cells, &pending, &[], &[]); |
| 407 | // Every product family with meters, in order; no Other without usage on it. | |
| 408 | assert_eq!(products.iter().map(|p| p.key.as_str()).collect::<Vec<_>>(), ["sandboxes", "deployments", "git_storage", "packages", "security", "search"]); | |
| 409 | let deployments = &products[1]; | |
| 410 | assert_eq!(deployments.micros, 3_000_000); | |
| 411 | let requests = deployments.meters.iter().find(|m| m.key == "requests").unwrap(); | |
| 412 | assert_eq!(requests.daily, [2_000_000, 0, 1_000_000]); | |
| 413 | assert_eq!(requests.by_project[0].project, "acme/web"); | |
| 414 | let sandbox = products[0].meters.iter().find(|m| m.key == "sandbox").unwrap(); | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 415 | assert_eq!((sandbox.micros, sandbox.quantity), (120_000, 600.0)); |
| 416 | // Pending usage counts in the totals and its meter, not on a day. | |
| 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. | 417 | let git = products[2].meters.iter().find(|m| m.key == "git").unwrap(); |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 418 | assert_eq!((git.micros, git.pending_micros), (40_000, 40_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. | 419 | assert_eq!(totals.price_micros, 3_000_000 + 120_000 + 30_000 + 40_000); |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 420 | assert_eq!(totals.pending_micros, 40_000); |
| 421 | assert_eq!(totals.included_micros, 1_000_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. | 422 | assert_eq!(by_day.iter().filter(|d| d.product == "deployments").map(|d| d.micros).sum::<i64>(), 3_000_000); |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 423 | assert_eq!(projects, ["acme/api", "acme/web"]); |
| 424 | } | |
| 425 | ||
| 426 | #[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. | 427 | fn usage_from_retired_or_unknown_meters_is_other_and_never_lost() { |
| 428 | let days = days_between("2026-10-01", "2026-10-02").unwrap(); | |
| 429 | // Lines from when g1t ran agents, as the ledger still has them, and | |
| 430 | // a meter nothing knows. | |
| 431 | let cells = vec![ | |
| 432 | cell("2026-10-01", "other", "acme/web", 2_000_000.0), | |
| 433 | cell("2026-10-01", "agent_rate", "acme/web", 500_000.0), | |
| 434 | cell("2026-10-02", "gateway", "", 70_000.0), | |
| 435 | cell("2026-10-02", "sandbox", "acme/web", 10_000.0), | |
| 436 | ]; | |
| 437 | let (products, by_day, totals, _) = shape(&days, &cells, &[], &[], &[]); | |
| 438 | let other = products.last().unwrap(); | |
| 439 | assert_eq!((other.key.as_str(), other.label.as_str(), other.micros), ("other", "Other", 2_570_000)); | |
| 440 | assert_eq!(other.meters.len(), 1); | |
| 441 | assert_eq!(other.meters[0].daily, [2_500_000, 70_000]); | |
| 442 | // Everything charged is in the totals and on its day. | |
| 443 | assert_eq!(totals.price_micros, 2_580_000); | |
| 444 | assert_eq!(products.iter().map(|p| p.micros).sum::<i64>(), totals.price_micros); | |
| 445 | assert_eq!(by_day.iter().map(|d| d.micros).sum::<i64>(), totals.price_micros); | |
| 446 | // Asked for by name, Other is shown alone. | |
| 447 | let (only, _, totals, _) = shape(&days, &cells, &[], &["other".into()], &[]); | |
| 448 | assert_eq!(only.len(), 1); | |
| 449 | assert_eq!(totals.price_micros, 2_570_000); | |
| 450 | } | |
| 451 | ||
| 452 | #[test] | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 453 | fn filters_keep_only_the_products_and_projects_asked_for() { |
| 454 | let days = days_between("2026-10-01", "2026-10-01").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. | 455 | let cells = vec![cell("2026-10-01", "sandbox", "acme/web", 100.0), cell("2026-10-01", "sandbox", "acme/api", 50.0), cell("2026-10-01", "requests", "acme/web", 10.0)]; |
| Spend adds up on one ledger. Charged this month has one definition, price less discount, included usage and credit, shared by the plan card, the spend limit, Spend and the top bar; the limit had counted usage not yet closed at full price before the discount, so a comped workspace read as charged a cent. Every agent line on the ledger names the agent and who asked, repository runs by g1t included, so Spent is the sum of its products, Agents is the agent product, and by agent adds up to it; the billing API returns by_agent and by_person. The usage and billing guide says how spend is counted. | 456 | let pending = vec![Pending { meter: "git", micros: 7, ..Pending::default() }]; |
| 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. | 457 | let (products, _, totals, projects) = shape(&days, &cells, &pending, &["sandboxes".into()], &["acme/web".into()]); |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 458 | assert_eq!(products.len(), 1); |
| 459 | assert_eq!(totals.price_micros, 100); | |
| 460 | // Every project stays listed, for the filter itself. | |
| 461 | assert_eq!(projects, ["acme/api", "acme/web"]); | |
| 462 | } | |
| 463 | ||
| 464 | #[test] | |
| Spend adds up on one ledger. Charged this month has one definition, price less discount, included usage and credit, shared by the plan card, the spend limit, Spend and the top bar; the limit had counted usage not yet closed at full price before the discount, so a comped workspace read as charged a cent. Every agent line on the ledger names the agent and who asked, repository runs by g1t included, so Spent is the sum of its products, Agents is the agent product, and by agent adds up to it; the billing API returns by_agent and by_person. The usage and billing guide says how spend is counted. | 465 | fn pending_usage_counts_at_price_with_what_is_covered_and_discounted() { |
| 466 | let days = days_between("2026-10-01", "2026-10-02").unwrap(); | |
| 467 | // Storage on a 100% discount, and scans g1t covers on a free workspace. | |
| 468 | let pending = [ | |
| 469 | Pending { meter: "storage", micros: 9_000, covered: 0, discount: 9_000 }, | |
| 470 | Pending { meter: "security", micros: 2_400, covered: 2_400, discount: 0 }, | |
| 471 | ]; | |
| 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. | 472 | let (_, _, totals, _) = shape(&days, &[], &pending, &[], &[]); |
| Spend adds up on one ledger. Charged this month has one definition, price less discount, included usage and credit, shared by the plan card, the spend limit, Spend and the top bar; the limit had counted usage not yet closed at full price before the discount, so a comped workspace read as charged a cent. Every agent line on the ledger names the agent and who asked, repository runs by g1t included, so Spent is the sum of its products, Agents is the agent product, and by agent adds up to it; the billing API returns by_agent and by_person. The usage and billing guide says how spend is counted. | 473 | assert_eq!(totals.price_micros, 11_400); |
| 474 | assert_eq!(totals.pending_micros, 11_400); | |
| Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means. | 475 | assert_eq!(totals.pending_charged_micros, 11_400 - totals.included_micros - totals.discount_micros); |
| Spend adds up on one ledger. Charged this month has one definition, price less discount, included usage and credit, shared by the plan card, the spend limit, Spend and the top bar; the limit had counted usage not yet closed at full price before the discount, so a comped workspace read as charged a cent. Every agent line on the ledger names the agent and who asked, repository runs by g1t included, so Spent is the sum of its products, Agents is the agent product, and by agent adds up to it; the billing API returns by_agent and by_person. The usage and billing guide says how spend is counted. | 476 | assert_eq!(totals.included_micros, 2_400); |
| 477 | assert_eq!(totals.discount_micros, 9_000); | |
| 478 | assert_eq!(charged(&totals), 0); | |
| 479 | // With a project chosen, pending usage (no project's) is left out. | |
| 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. | 480 | let (_, _, filtered, _) = shape(&days, &[], &pending, &[], &["acme/api".to_owned()]); |
| Spend adds up on one ledger. Charged this month has one definition, price less discount, included usage and credit, shared by the plan card, the spend limit, Spend and the top bar; the limit had counted usage not yet closed at full price before the discount, so a comped workspace read as charged a cent. Every agent line on the ledger names the agent and who asked, repository runs by g1t included, so Spent is the sum of its products, Agents is the agent product, and by agent adds up to it; the billing API returns by_agent and by_person. The usage and billing guide says how spend is counted. | 481 | assert_eq!(filtered.pending_micros, 0); |
| 482 | } | |
| 483 | ||
| 484 | #[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. | 485 | fn pending_usage_is_on_its_meter() { |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 486 | assert_eq!(meter_of_pending("package_storage"), "packages"); |
| 487 | assert_eq!(meter_of_pending("nonsense"), "other"); | |
| 488 | } | |
| Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means. | 489 | |
| 490 | #[test] | |
| 491 | fn pending_usage_counts_in_any_range_that_reaches_into_this_month() { | |
| 492 | // The first of the month to today, as every page asks: counted. | |
| 493 | assert!(reaches_this_month("2026-10-01", "2026-10-10", "2026-10-10")); | |
| 494 | // To month end, or only the last week: counted too. | |
| 495 | assert!(reaches_this_month("2026-10-01", "2026-10-31", "2026-10-10")); | |
| 496 | assert!(reaches_this_month("2026-10-04", "2026-10-10", "2026-10-10")); | |
| 497 | // Last month, or a month that has not started: not this month's. | |
| 498 | assert!(!reaches_this_month("2026-09-01", "2026-09-30", "2026-10-10")); | |
| 499 | assert!(!reaches_this_month("2026-11-01", "2026-11-30", "2026-10-10")); | |
| 500 | // The month has turned on billing's clock: September's range no longer has it. | |
| 501 | assert!(!reaches_this_month("2026-09-01", "2026-09-30", "2026-10-01")); | |
| 502 | } | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 503 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.