g1t/crates/contracts/src/billing.rs

988 lines33,595 bytesCodeBlame

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.

Agents as a team: lifecycle, merge queue, billing and a new shell1//! The billing service: what agents cost, charged to the workspace they
2//! worked for.
3//!
4//! A workspace buys credit and each agent run deducts what it cost, plus
5//! g1t's margin. With no credit, no agent starts. Money is held in
6//! millionths of a US dollar, so that a run costing a fraction of a cent is
7//! recorded exactly.
8//!
9//! Each `*Args` struct is the argument of the method of the same name,
10//! served at `POST /rpc/<method>`.
11
12use serde::{Deserialize, Serialize};
13
14use crate::repos::RepoPath;
15use crate::{User, Viewer};
16
17/// Millionths of a US dollar in one dollar.
18pub const MICROS_PER_DOLLAR: i64 = 1_000_000;
19
20/// Whether workspaces are charged for agents at all, and with real money.
21/// `status` takes nothing and returns this.
22#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
23pub struct Status {
24 /// False when no payment provider is configured: nothing is charged,
25 /// and who may run agents is decided some other way.
26 pub enabled: bool,
27 /// False while the payment provider is in its test mode, where cards
28 /// are not real.
29 pub live: bool,
Free while g1t is being built out; agents can check out their own forks30 /// True while g1t is being built out: runs are recorded, with what
31 /// they cost, but nothing is charged and no credit is needed. Not a
32 /// promise that it stays free.
33 #[serde(default)]
34 pub free: bool,
Agents as a team: lifecycle, merge queue, billing and a new shell35}
36
A free allowance on g1t's models, so anyone can try its agents37/// `trial`: the free allowance on g1t's hosted models for a workspace that
38/// is not otherwise open to them, so people can try g1t's agents without a
39/// key of their own. Each workspace gets a few dollars of model cost, out
40/// of one pool, until an end date. Returns `Trial`.
41#[derive(Debug, Serialize, Deserialize)]
42#[serde(rename_all = "camelCase")]
43pub struct TrialArgs {
44 pub workspace: String,
45 /// Workspaces open to hosted models anyway, whose use is not counted
46 /// against the pool.
47 #[serde(default)]
48 pub exempt: Vec<String>,
49}
50
51#[derive(Clone, Debug, Serialize, Deserialize)]
52#[serde(rename_all = "camelCase")]
53pub struct Trial {
54 /// Whether its agents may use g1t's hosted models on the allowance now.
55 pub open: bool,
56 /// What its runs on g1t's models have cost, in millionths of a dollar.
57 pub used_micros: i64,
58 pub limit_micros: i64,
59 /// RFC 3339; when the allowance ends for everyone.
60 pub ends_at: Option<String>,
61 /// Why it is closed: `off` (no allowance), `ended`, `used` (this
62 /// workspace's is spent) or `pool` (everyone's is).
63 pub reason: Option<String>,
64}
65
Agents as a team: lifecycle, merge queue, billing and a new shell66/// A workspace's standing.
67#[derive(Clone, Debug, Serialize, Deserialize)]
68#[serde(rename_all = "camelCase")]
69pub struct Account {
70 pub workspace: String,
71 /// Credit left, in millionths of a dollar. Can dip below zero by the
72 /// cost of the runs that were under way when it ran out.
73 pub balance_micros: i64,
74 pub status: Status,
75 /// What is added to a run's cost, in percent.
76 pub margin_percent: u32,
Integrations: your own model provider, alerts that open issues, tickets agents read77 /// What a run on the workspace's own model provider is charged: g1t's
78 /// sandbox and orchestration, with the model paid for elsewhere.
79 pub orchestration_fee_micros: i64,
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace80 /// The card g1t charges as the workspace nears its limit and when a
81 /// month closes, if one is on file.
82 #[serde(default)]
83 pub card: Option<Card>,
Agents as a team: lifecycle, merge queue, billing and a new shell84}
85
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace86/// A saved card, as far as it is safe to show.
87#[derive(Clone, Debug, Serialize, Deserialize)]
88#[serde(rename_all = "camelCase")]
89pub struct Card {
90 /// `visa`, `mastercard`, ...
91 pub brand: String,
92 pub last4: String,
93 pub exp_month: u32,
94 pub exp_year: u32,
95}
96
97/// `billing_portal`: Stripe's hosted billing page for the workspace, where
98/// an owner adds or replaces the card, sees invoices and receipts, and sets
99/// the billing email and address. g1t never handles card numbers. Owners
100/// only. Returns `Outcome<Checkout>` (its `url`); Stripe sends them back
101/// to `return_url`.
102#[derive(Debug, Serialize, Deserialize)]
103pub struct BillingPortalArgs {
104 pub actor: User,
105 pub workspace: String,
106 pub return_url: String,
107}
108
109/// `admin_billing_link`: for staff to send a customer: their Stripe billing
110/// page. Returns `Outcome<BillingLink>`.
111#[derive(Debug, Serialize, Deserialize)]
112pub struct AdminBillingLinkArgs {
113 pub workspace: String,
114 pub by: String,
115}
116
117#[derive(Clone, Debug, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub struct BillingLink {
120 /// A one-time session on Stripe's billing page, signed in already.
121 pub portal_url: String,
122 /// The billing page's sign-in page, which does not expire: the
123 /// customer signs in with the email Stripe has for them.
124 pub login_url: Option<String>,
125 pub customer_email: Option<String>,
126 pub expires_note: String,
127}
128
Agents as a team: lifecycle, merge queue, billing and a new shell129#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
130#[serde(rename_all = "snake_case")]
131pub enum EntryKind {
132 /// Credit bought with a card.
133 TopUp,
Paid features: a workspace turns on Deployments with a monthly plan134 /// An agent's run, or a paid feature's usage past its allowance.
Agents as a team: lifecycle, merge queue, billing and a new shell135 Usage,
136}
137
138/// One line of a workspace's statement.
139#[derive(Clone, Debug, Serialize, Deserialize)]
140#[serde(rename_all = "camelCase")]
141pub struct LedgerEntry {
142 pub id: String,
143 pub kind: EntryKind,
144 /// Positive for credit added, negative for usage.
145 pub amount_micros: i64,
146 pub description: String,
147 /// For usage: the repository and pull request the agent worked on.
148 pub repo: Option<String>,
149 pub number: Option<u32>,
150 /// For usage: `implement`, `review` or `update`.
151 pub task: Option<String>,
152 /// For usage: the model, by its public name.
153 pub model: Option<String>,
Integrations: your own model provider, alerts that open issues, tickets agents read154 /// For usage: `g1t` when g1t paid the model provider, `workspace` when
155 /// the workspace's own account did and only orchestration is charged.
156 #[serde(default = "g1t")]
157 pub billed_to: String,
Agents as a team: lifecycle, merge queue, billing and a new shell158 /// For a top-up: the username of whoever paid.
159 pub created_by: Option<String>,
160 /// RFC 3339.
161 pub created_at: String,
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace162 /// The workspace the line belongs to, which tells an enterprise's
163 /// lines apart.
164 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub workspace: Option<String>,
Agents as a team: lifecycle, merge queue, billing and a new shell166}
167
Integrations: your own model provider, alerts that open issues, tickets agents read168fn g1t() -> String {
169 "g1t".to_owned()
170}
171
Agents as a team: lifecycle, merge queue, billing and a new shell172/// `account` (`Outcome<Account>`) and `ledger` (`Outcome<Vec<LedgerEntry>>`,
173/// newest first). Members of the workspace only.
174#[derive(Debug, Serialize, Deserialize)]
175pub struct AccountArgs {
176 pub workspace: String,
177 pub viewer: Viewer,
178}
179
180/// `checkout`: starts a card payment for credit. Owners of the workspace
181/// only. Returns `Outcome<Checkout>`.
182#[derive(Debug, Serialize, Deserialize)]
183#[serde(rename_all = "camelCase")]
184pub struct CheckoutArgs {
185 pub actor: User,
186 pub workspace: String,
187 /// How much credit to buy, in cents.
188 pub amount_cents: u32,
189 /// Where the payment page sends the person afterwards. The payment's
190 /// id is appended as `session`.
191 pub return_url: String,
192}
193
194#[derive(Debug, Serialize, Deserialize)]
195pub struct Checkout {
196 /// The payment page to send the person to.
197 pub url: String,
198}
199
200/// `confirm`: credits a payment once the provider says it was made. Safe
201/// to call any number of times. Returns `Outcome<Account>`.
202#[derive(Debug, Serialize, Deserialize)]
203pub struct ConfirmArgs {
204 pub workspace: String,
205 pub viewer: Viewer,
206 /// The payment's id, as returned to `return_url`.
207 pub session: String,
208}
209
210/// `can_start`: whether a workspace may start an agent now, asked before
211/// anything is opened for it. Returns `Outcome<bool>`: a failure, with the
212/// reason to show, when it has no credit.
213#[derive(Debug, Serialize, Deserialize)]
214pub struct CanStartArgs {
215 pub workspace: String,
216}
217
218/// `start_run`: asks whether a workspace may start an agent, and opens the
219/// run it will be charged for. Called by the runner service. Returns
220/// `Outcome<Option<RunTicket>>`: no ticket when billing is off, a failure
221/// when the workspace has no credit.
222#[derive(Debug, Serialize, Deserialize)]
223pub struct StartRunArgs {
224 pub workspace: String,
225 pub repo: RepoPath,
226 pub number: u32,
227 /// `implement`, `review` or `update`.
228 pub task: String,
229 /// The model, by its public name.
230 pub model: String,
Integrations: your own model provider, alerts that open issues, tickets agents read231 /// `workspace` when the run uses the workspace's own model provider.
Models per workspace: several providers, routed by kind of work232 /// The runner, which is TypeScript, sends it as `billedTo`.
233 #[serde(default = "g1t", alias = "billedTo")]
Integrations: your own model provider, alerts that open issues, tickets agents read234 pub billed_to: String,
Prices keep themselves current with what g1t pays235 /// The model session's id, when its requests go through g1t's AI
236 /// Gateway: settling charges the run what the gateway priced them at.
237 #[serde(default)]
238 pub session: Option<String>,
Agents as a team: lifecycle, merge queue, billing and a new shell239}
240
241#[derive(Clone, Debug, Serialize, Deserialize)]
242#[serde(rename_all = "camelCase")]
243pub struct RunTicket {
244 pub run_id: String,
245 /// Lets the sandbox, and nothing else, report what this run cost.
246 pub token: String,
247}
248
249/// `finish_run`: what a run cost, as its sandbox reports it. Charged once.
250/// Returns `Outcome<bool>`.
251#[derive(Debug, Serialize, Deserialize)]
252#[serde(rename_all = "camelCase")]
253pub struct FinishRunArgs {
254 pub run_id: String,
255 pub token: String,
256 /// What the model provider charged, in US dollars.
257 pub cost_usd: f64,
258 #[serde(default)]
259 pub turns: u32,
260}
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request261
262
263/// `usage`: what a workspace's agents cost over a period, broken down.
264/// Members only. Returns `Outcome<Usage>`.
265#[derive(Debug, Serialize, Deserialize)]
266pub struct UsageArgs {
267 pub workspace: String,
268 pub viewer: Viewer,
269 /// RFC 3339: the start of the period. The period runs to now.
270 pub since: String,
271}
272
273/// One slice of usage: what it was for, what it cost, how many runs.
274#[derive(Clone, Debug, Serialize, Deserialize)]
275#[serde(rename_all = "camelCase")]
276pub struct UsageSlice {
277 pub key: String,
278 pub micros: i64,
279 pub runs: u32,
280}
281
282/// What a workspace's agents cost over a period.
283#[derive(Clone, Debug, Serialize, Deserialize)]
284#[serde(rename_all = "camelCase")]
285pub struct Usage {
286 pub since: String,
287 /// Charged, including g1t's margin.
288 pub spent_micros: i64,
Integrations: your own model provider, alerts that open issues, tickets agents read289 /// What g1t's model provider charged, before the margin.
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request290 pub cost_micros: i64,
Integrations: your own model provider, alerts that open issues, tickets agents read291 /// What runs on the workspace's own provider cost there, as the harness
292 /// estimated it. Not charged by g1t.
293 pub provider_micros: i64,
Usage while free is shown at cost; agents get rustfmt and clippy294 /// What the runs used, at cost: g1t's models and the workspace's own
295 /// provider together, whatever was charged for them.
296 pub used_micros: i64,
297 /// g1t charges nothing for now. The slices then measure usage at cost,
298 /// since every charge is zero.
299 pub free: bool,
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request300 pub runs: u32,
301 /// Spend per day (`YYYY-MM-DD`) and task, as `day/task` keys.
302 pub by_day: Vec<UsageSlice>,
303 /// Per task: implement, review, revise, update, plan.
304 pub by_task: Vec<UsageSlice>,
305 /// Per repository, `namespace/name`.
306 pub by_repo: Vec<UsageSlice>,
307 /// The pull requests that cost most, as `namespace/name#number`.
308 pub by_pull: Vec<UsageSlice>,
309 /// Per model, by its public name.
310 pub by_model: Vec<UsageSlice>,
311 /// Credit bought in the period.
312 pub added_micros: i64,
313}
Models per workspace: several providers, routed by kind of work314
Paid features: a workspace turns on Deployments with a monthly plan315/// A paid feature a workspace turns on with a monthly plan, the way
316/// Cloudflare's Workers for Platforms or Vercel's Pro are bought. Never
317/// free: `FREE_WHILE_BUILDING` and the free model allowance do not cover
318/// it.
319#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
320#[serde(rename_all = "snake_case")]
321pub enum Feature {
322 /// Previews per pull request and production on g1t.page.
323 Deployments,
324}
325
326impl Feature {
327 pub const ALL: [Feature; 1] = [Feature::Deployments];
328
329 pub fn as_str(self) -> &'static str {
330 match self {
331 Feature::Deployments => "deployments",
332 }
333 }
334
335 pub fn parse(name: &str) -> Option<Feature> {
336 Feature::ALL.into_iter().find(|feature| feature.as_str() == name)
337 }
338
339 pub fn title(self) -> &'static str {
340 match self {
341 Feature::Deployments => "Deployments",
342 }
343 }
344}
345
346/// What the Deployments plan includes each month; usage past it is charged
347/// at cost plus the margin. The billing service describes the plan with
348/// these and the deployments service meters against them.
349pub mod deployments_allowance {
350 /// Apps deployed at once: production and previews together.
351 pub const APPS: u32 = 10;
352 pub const REQUESTS: u64 = 1_000_000;
353 pub const CPU_MS: u64 = 3_000_000;
354 /// What Cloudflare charges g1t past that, in millionths of a dollar.
355 pub const MICROS_PER_APP_MONTH: i64 = 20_000;
356 pub const MICROS_PER_MILLION_REQUESTS: i64 = 300_000;
357 pub const MICROS_PER_MILLION_CPU_MS: i64 = 20_000;
Deployments: a preview for every pull request, production on g1t.page358 /// What one second of a build's sandbox costs g1t (Cloudflare
359 /// Containers, standard-1: half a vCPU, 4 GiB, 8 GB disk), rounded up.
360 /// Builds are not in the allowance: each is charged at this plus the
361 /// margin.
362 pub const MICROS_PER_BUILD_SECOND: i64 = 21;
Paid features: a workspace turns on Deployments with a monthly plan363}
364
Every sandbox is metered by the second365/// Sandbox time: every sandbox g1t starts for a workspace (agents,
366/// reviews, checks, the merge queue, workflow jobs) is metered by the
367/// second. Deploy builds are charged by the Deployments plan instead.
368pub mod sandbox_allowance {
369 /// Free each calendar month (UTC): 500 minutes.
370 pub const FREE_SECONDS: i64 = 30_000;
371 /// What one second costs g1t (Cloudflare Containers, standard-1),
372 /// rounded up. Recorded with every entry.
373 pub const COST_MICROS_PER_SECOND: i64 = super::deployments_allowance::MICROS_PER_BUILD_SECOND;
374 /// What one second past the free minutes is charged: $0.003 a minute.
375 pub const MICROS_PER_SECOND: i64 = 50;
376}
377
378/// `record_sandbox`: how long one sandbox ran for a workspace, reported by
379/// the runner when it stops. Recorded once per `reference`, with what it
380/// cost g1t; seconds past the month's free minutes are charged at
381/// `sandbox_allowance::MICROS_PER_SECOND`, unless `FREE_WHILE_BUILDING`.
382/// Returns `Outcome<bool>`: false if that reference was recorded before.
383#[derive(Debug, Serialize, Deserialize)]
384#[serde(rename_all = "camelCase")]
385pub struct RecordSandboxArgs {
386 pub workspace: String,
387 pub seconds: u32,
388 /// What ran, e.g. `Checks on acme/api#12`.
389 pub description: String,
390 /// `namespace/name`.
391 pub repo: Option<String>,
392 /// Unique to the run.
393 pub reference: String,
394}
395
Usage limits: unpaid usage can only go so far396/// How much a workspace has earned g1t's trust with money, which sets how
397/// far its unpaid usage can go before its work stops.
398#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
399#[serde(rename_all = "snake_case")]
400pub enum Trust {
401 /// No live payment yet: only a little past the free allowances.
402 New,
403 /// Has paid g1t real money: the ceiling grows with what it has paid.
404 Paid,
405 /// A ceiling g1t set by hand, after talking to the workspace.
406 Reviewed,
407 /// g1t's own workspaces: no ceiling.
408 Internal,
409}
410
411#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
412#[serde(rename_all = "snake_case")]
413pub enum LimitState {
414 Ok,
415 /// Past 80% of the ceiling.
416 Warning,
417 /// At or past it: no new sandboxes, builds or app requests.
418 Stopped,
419}
420
421/// How far a workspace's unpaid usage has gone this month, and where its
422/// work stops: like Fly's or Cloudflare's limits for new accounts, so no
423/// one runs up costs g1t cannot collect. Usage counts at what it cost g1t
424/// or what it is charged, whichever is more, so it counts while g1t is
425/// free too.
426#[derive(Clone, Debug, Serialize, Deserialize)]
427#[serde(rename_all = "camelCase")]
428pub struct Limit {
429 pub workspace: String,
Billing accounts, terms and enterprises; g1t is no longer free430 /// The account that pays, whose usage and payments the limit counts:
431 /// the workspace's own, or its enterprise's.
432 #[serde(default)]
433 pub account: String,
434 #[serde(default)]
435 pub account_name: String,
Usage limits: unpaid usage can only go so far436 pub trust: Trust,
437 /// Usage this month (UTC) less what was paid this month.
438 pub exposure_micros: i64,
439 /// Where work stops: the lower of g1t's ceiling and the owner's own
440 /// spend limit. None for g1t's own workspaces.
441 pub ceiling_micros: Option<i64>,
442 /// The ceiling g1t sets from `trust`.
443 pub trust_ceiling_micros: Option<i64>,
444 /// The owner's own monthly limit, if they set one.
445 pub spend_limit_micros: Option<i64>,
446 pub state: LimitState,
447 /// What to tell people when work is stopped or close to it.
448 pub message: Option<String>,
449}
450
451/// `limit`: a workspace's limit, for its members. Returns `Outcome<Limit>`.
452#[derive(Debug, Serialize, Deserialize)]
453pub struct LimitArgs {
454 pub workspace: String,
455 pub viewer: Viewer,
456}
457
458/// `check_limit`: the same, for the services that enforce it. Returns
459/// `Outcome<Limit>`.
460#[derive(Debug, Serialize, Deserialize)]
461pub struct CheckLimitArgs {
462 pub workspace: String,
463}
464
Prices keep themselves current with what g1t pays465/// `note_pending`: usage this month that will be charged later, such as
466/// app traffic past a plan, so the workspace's limit counts it now. Each
467/// report replaces the last for that workspace, source and month. Called
468/// by the service that meters it. Returns `bool`.
469#[derive(Debug, Serialize, Deserialize)]
470#[serde(rename_all = "camelCase")]
471pub struct NotePendingArgs {
472 pub workspace: String,
473 /// `deployments`.
474 pub source: String,
475 /// What it cost g1t so far this month, before the margin.
476 pub cost_micros: i64,
477}
478
Usage limits: unpaid usage can only go so far479/// `set_spend_limit`: the owner's own monthly ceiling, under g1t's; None
480/// removes it. Owners only. Returns `Outcome<Limit>`.
481#[derive(Debug, Serialize, Deserialize)]
482#[serde(rename_all = "camelCase")]
483pub struct SetSpendLimitArgs {
484 pub actor: User,
485 pub workspace: String,
486 pub spend_limit_micros: Option<i64>,
487}
488
Prices keep themselves current with what g1t pays489/// One metered unit: what it costs g1t, and what it is sold at. The price
490/// is always `cost × (100 + markup) / 100`, so it follows the cost.
491#[derive(Clone, Debug, Serialize, Deserialize)]
492#[serde(rename_all = "camelCase")]
493pub struct Price {
494 /// `sandbox_second`, `build_second`, `app_requests`, `app_cpu`, `app_month`.
495 pub meter: String,
496 pub title: String,
497 pub unit: String,
498 /// Millionths of a dollar per unit; may have a fraction.
499 pub cost_micros: f64,
500 pub markup_percent: u32,
501 pub price_micros: f64,
502 /// `list`: Cloudflare's published price. `cloudflare`: what Cloudflare
503 /// actually billed g1t, measured.
504 pub source: String,
505 /// When it was last checked against Cloudflare's bill.
506 pub checked_at: Option<String>,
507 pub updated_at: String,
508}
509
510impl Price {
511 pub fn price_for(cost_micros: f64, markup_percent: u32) -> f64 {
512 cost_micros * f64::from(100 + markup_percent) / 100.0
513 }
514}
515
516/// A cost that moved.
517#[derive(Clone, Debug, Serialize, Deserialize)]
518#[serde(rename_all = "camelCase")]
519pub struct PriceChange {
520 pub meter: String,
521 pub old_cost_micros: f64,
522 pub new_cost_micros: f64,
523 pub markup_percent: u32,
524 pub reason: String,
525 pub created_at: String,
526}
527
528/// `prices`: every metered price and the recent changes. Public. Returns
529/// `PriceBook`.
530#[derive(Clone, Debug, Serialize, Deserialize)]
531#[serde(rename_all = "camelCase")]
532pub struct PriceBook {
533 pub prices: Vec<Price>,
534 pub changes: Vec<PriceChange>,
535 /// The margin on model usage, which is charged at what AI Gateway
536 /// priced each request at.
537 pub model_margin_percent: u32,
538}
539
Billing accounts, terms and enterprises; g1t is no longer free540/// Who pays: a billing account. Every workspace has one; by default its
541/// own. An enterprise account pays for several workspaces at once, as
542/// GitHub Enterprise does: one bill, one limit, one set of terms.
543#[derive(Clone, Debug, Serialize, Deserialize)]
544#[serde(rename_all = "camelCase")]
545pub struct BillingAccount {
546 /// `ws_<slug>` for a workspace's own account; `ent_…` for an enterprise.
547 pub id: String,
548 pub kind: AccountKind,
549 pub name: String,
550 pub terms: Terms,
551 /// The workspaces it pays for.
552 pub workspaces: Vec<String>,
Stripe webhooks, enterprise invoices, and sudo for both553 /// Where an enterprise's invoices go.
554 #[serde(default)]
555 pub billing_email: Option<String>,
556 /// An enterprise's invoices, newest first. Empty for a workspace's own.
557 #[serde(default)]
558 pub invoices: Vec<EnterpriseInvoice>,
Billing accounts, terms and enterprises; g1t is no longer free559 pub created_at: String,
560}
561
562#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
563#[serde(rename_all = "snake_case")]
564pub enum AccountKind {
565 Workspace,
566 Enterprise,
567}
568
569/// How an account is charged. Standard unless g1t set otherwise in sudo.
570#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
571#[serde(rename_all = "camelCase")]
572pub struct Terms {
573 pub kind: TermsKind,
574 /// Off every usage charge, in percent. Custom terms only.
575 #[serde(default)]
576 pub discount_percent: u32,
577 /// A ceiling on unpaid usage that replaces the one trust would give.
578 #[serde(default)]
579 pub ceiling_micros: Option<i64>,
580 /// Why, for whoever looks next.
581 #[serde(default)]
582 pub note: String,
583 /// When the terms end and the account goes back to standard.
584 #[serde(default)]
585 pub until: Option<String>,
586 #[serde(default)]
587 pub set_by: Option<String>,
588 #[serde(default)]
589 pub set_at: Option<String>,
590}
591
592impl Terms {
593 pub fn standard() -> Self {
594 Terms {
595 kind: TermsKind::Standard,
596 discount_percent: 0,
597 ceiling_micros: None,
598 note: String::new(),
599 until: None,
600 set_by: None,
601 set_at: None,
602 }
603 }
604
605 /// What a charge becomes under these terms.
606 pub fn apply(&self, charge_micros: i64) -> i64 {
607 match self.kind {
608 TermsKind::Comped => 0,
609 TermsKind::Custom => charge_micros * i64::from(100 - self.discount_percent.min(100)) / 100,
610 TermsKind::Standard => charge_micros,
611 }
612 }
613}
614
615#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
616#[serde(rename_all = "snake_case")]
617pub enum TermsKind {
618 /// Prices as published, limits by trust.
619 Standard,
620 /// Nothing charged; usage still recorded with its cost. Paid features
621 /// are on without a plan. For g1t's own workspaces, partners, and the
622 /// like.
623 Comped,
624 /// A discount, a ceiling, or both.
625 Custom,
626}
627
Stripe webhooks, enterprise invoices, and sudo for both628/// `stripe_webhook`: an event from Stripe, as the API received it: the raw
629/// body and its `Stripe-Signature` header. Billing checks the signature
630/// against the secret of the endpoint it registered, and handles each
631/// event once. Returns `Outcome<bool>`: false for one already handled.
632#[derive(Debug, Serialize, Deserialize)]
633pub struct StripeWebhookArgs {
634 pub payload: String,
635 pub signature: String,
636}
637
638/// `admin_stripe`: where billing stands with Stripe. Staff only. Returns
639/// `StripeStatus`. With `setup: true`, registers (or replaces) the webhook
640/// endpoint for the current mode first.
641#[derive(Debug, Default, Serialize, Deserialize)]
642pub struct AdminStripeArgs {
643 #[serde(default)]
644 pub setup: bool,
645 #[serde(default)]
646 pub by: Option<String>,
647}
648
649#[derive(Clone, Debug, Serialize, Deserialize)]
650#[serde(rename_all = "camelCase")]
651pub struct StripeStatus {
652 /// `test` or `live`, from the key; `off` without one.
653 pub mode: String,
654 pub webhook: Option<StripeWebhook>,
655 /// The latest events handled, newest first.
656 pub recent_events: Vec<StripeEventSummary>,
657 /// What went wrong setting up, if it did.
658 pub error: Option<String>,
659}
660
661#[derive(Clone, Debug, Serialize, Deserialize)]
662#[serde(rename_all = "camelCase")]
663pub struct StripeWebhook {
664 pub url: String,
665 pub endpoint_id: String,
666 pub events: Vec<String>,
667 pub created_by: String,
668 pub created_at: String,
669}
670
671#[derive(Clone, Debug, Serialize, Deserialize)]
672#[serde(rename_all = "camelCase")]
673pub struct StripeEventSummary {
674 pub id: String,
675 pub kind: String,
676 pub outcome: String,
677 pub received_at: String,
678}
679
680/// `admin_enterprise_billing`: where an enterprise's invoices go. Creates
681/// or updates its Stripe customer. Returns `Outcome<BillingAccount>`.
682#[derive(Debug, Serialize, Deserialize)]
683pub struct AdminEnterpriseBillingArgs {
684 pub id: String,
685 pub email: String,
686 pub by: String,
687}
688
689/// `admin_invoice_enterprise`: sends an enterprise its invoice now, for
690/// what its workspaces owe, rather than waiting for the month to close.
691/// Returns `Outcome<EnterpriseInvoice>`.
692#[derive(Debug, Serialize, Deserialize)]
693pub struct AdminInvoiceEnterpriseArgs {
694 pub id: String,
695 pub by: String,
696}
697
698/// An enterprise's invoice: one line per workspace, paid on Stripe.
699#[derive(Clone, Debug, Serialize, Deserialize)]
700#[serde(rename_all = "camelCase")]
701pub struct EnterpriseInvoice {
702 pub invoice_id: String,
703 /// Stripe's page for it, where it is paid.
704 pub hosted_url: Option<String>,
705 pub amount_micros: i64,
706 /// `open`, `paid`, `overdue` or `void`.
707 pub status: String,
708 pub period: String,
709 pub lines: Vec<InvoiceLine>,
710 pub created_at: String,
711}
712
713#[derive(Clone, Debug, Serialize, Deserialize)]
714#[serde(rename_all = "camelCase")]
715pub struct InvoiceLine {
716 pub workspace: String,
717 pub amount_micros: i64,
718}
719
Billing accounts, terms and enterprises; g1t is no longer free720// --- Staff (sudo.g1t.sh) ------------------------------------------------------
721//
722// Called only by the sudo app, which only g1t staff can reach (behind
723// Cloudflare Access). Each change names who made it, and is kept in the
724// audit log.
725
726/// `admin_accounts`: every billing account, with where each stands this
727/// month. Returns `Vec<AccountSummary>`.
728#[derive(Debug, Default, Serialize, Deserialize)]
729pub struct AdminAccountsArgs {
730 #[serde(default)]
731 pub query: Option<String>,
Stripe webhooks, enterprise invoices, and sudo for both732 /// Exactly these workspaces' accounts, such as one page of sudo's
733 /// list; every account with activity when absent.
734 #[serde(default)]
735 pub workspaces: Option<Vec<String>>,
Billing accounts, terms and enterprises; g1t is no longer free736}
737
738#[derive(Clone, Debug, Serialize, Deserialize)]
739#[serde(rename_all = "camelCase")]
740pub struct AccountSummary {
741 pub account: BillingAccount,
742 pub limit: Limit,
743 /// Charged this month, after terms.
744 pub charged_micros: i64,
745 /// What this month's usage cost g1t.
746 pub cost_micros: i64,
747 /// Paid, ever.
748 pub paid_micros: i64,
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace749 /// The same figures for each of the account's workspaces that has
750 /// any, so staff can see what one member of an enterprise used.
751 #[serde(default)]
752 pub by_workspace: Vec<WorkspaceFigures>,
753}
754
755/// One workspace's share of an [`AccountSummary`].
756#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
757#[serde(rename_all = "camelCase")]
758pub struct WorkspaceFigures {
759 pub workspace: String,
760 pub charged_micros: i64,
761 pub cost_micros: i64,
762 pub paid_micros: i64,
Billing accounts, terms and enterprises; g1t is no longer free763}
764
765/// `admin_account`: one account in full. Returns `Outcome<AccountDetail>`.
766#[derive(Debug, Serialize, Deserialize)]
767pub struct AdminAccountArgs {
768 /// An account id, or a workspace slug.
769 pub id: String,
770}
771
772#[derive(Clone, Debug, Serialize, Deserialize)]
773#[serde(rename_all = "camelCase")]
774pub struct AccountDetail {
775 pub summary: AccountSummary,
776 /// Each workspace's limit, for an enterprise.
777 pub workspaces: Vec<Limit>,
778 pub ledger: Vec<LedgerEntry>,
779 pub audit: Vec<AdminAction>,
780}
781
782/// `admin_set_terms`. Returns `Outcome<BillingAccount>`.
783#[derive(Debug, Serialize, Deserialize)]
784pub struct AdminSetTermsArgs {
785 pub id: String,
786 pub terms: Terms,
787 pub by: String,
788}
789
790/// `admin_create_enterprise`. Returns `Outcome<BillingAccount>`.
791#[derive(Debug, Serialize, Deserialize)]
792pub struct AdminCreateEnterpriseArgs {
793 pub name: String,
794 pub workspaces: Vec<String>,
795 pub by: String,
796}
797
798/// `admin_attach`: moves a workspace onto an enterprise account, or back
799/// onto its own with `account: None`. Returns `Outcome<BillingAccount>`.
800#[derive(Debug, Serialize, Deserialize)]
801pub struct AdminAttachArgs {
802 pub workspace: String,
803 pub account: Option<String>,
804 pub by: String,
805}
806
807/// `admin_credit`: money g1t gives a workspace, such as a refund or a
808/// goodwill credit. Returns `Outcome<LedgerEntry>`.
809#[derive(Debug, Serialize, Deserialize)]
810pub struct AdminCreditArgs {
811 pub workspace: String,
812 pub amount_micros: i64,
813 pub note: String,
814 pub by: String,
815}
816
817/// One change made in sudo.
818#[derive(Clone, Debug, Serialize, Deserialize)]
819#[serde(rename_all = "camelCase")]
820pub struct AdminAction {
821 pub id: String,
822 pub account: String,
823 pub action: String,
824 pub detail: String,
825 pub by: String,
826 pub created_at: String,
827}
828
Paid features: a workspace turns on Deployments with a monthly plan829/// What a feature's plan costs and includes.
830#[derive(Clone, Debug, Serialize, Deserialize)]
831#[serde(rename_all = "camelCase")]
832pub struct Plan {
833 pub feature: Feature,
834 pub title: String,
835 /// Charged every month while the plan is on, in cents.
836 pub monthly_cents: u32,
837 /// What the monthly price includes, one line each, for people to read.
838 pub includes: Vec<String>,
839 /// How usage past the allowance is charged, for people to read.
840 pub overage: String,
841}
842
843#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
844#[serde(rename_all = "snake_case")]
845pub enum SubscriptionStatus {
846 /// Paid up; the feature works.
847 Active,
848 /// Paid up to the end of the period, and ends then.
849 Canceling,
850 /// The last payment failed; the feature is off until it is paid.
851 PastDue,
852 /// Ended.
853 Canceled,
854}
855
856impl SubscriptionStatus {
857 /// Whether the feature works in this state.
858 pub fn on(self) -> bool {
859 matches!(self, SubscriptionStatus::Active | SubscriptionStatus::Canceling)
860 }
861}
862
863/// A workspace's plan for one feature.
864#[derive(Clone, Debug, Serialize, Deserialize)]
865#[serde(rename_all = "camelCase")]
866pub struct Subscription {
867 pub feature: Feature,
868 pub status: SubscriptionStatus,
869 /// RFC 3339: when the period paid for ends, and the plan renews or
870 /// ends.
871 pub period_end: Option<String>,
872 /// Username of whoever turned it on.
873 pub started_by: String,
874 /// RFC 3339.
875 pub started_at: String,
876}
877
878/// A feature as a workspace sees it: what it costs, and its plan if it has
879/// one.
880#[derive(Clone, Debug, Serialize, Deserialize)]
881#[serde(rename_all = "camelCase")]
882pub struct FeatureState {
883 pub plan: Plan,
884 pub subscription: Option<Subscription>,
885 /// Whether the feature works for the workspace now.
886 pub on: bool,
887}
888
889/// `features`: every paid feature and the workspace's plan for each.
890/// Members only. Returns `Outcome<Vec<FeatureState>>`.
891#[derive(Debug, Serialize, Deserialize)]
892pub struct FeaturesArgs {
893 pub workspace: String,
894 pub viewer: Viewer,
895}
896
897/// `subscribe`: starts the card page for a feature's monthly plan. Owners
898/// only. Returns `Outcome<Checkout>`; the page's id comes back to
899/// `return_url` as `session`, for `confirm_subscription`.
900#[derive(Debug, Serialize, Deserialize)]
901#[serde(rename_all = "camelCase")]
902pub struct SubscribeArgs {
903 pub actor: User,
904 pub workspace: String,
905 pub feature: Feature,
906 pub return_url: String,
907}
908
909/// `confirm_subscription`: turns the feature on once the processor says
910/// the plan was paid for. Safe to call any number of times. Returns
911/// `Outcome<FeatureState>`.
912#[derive(Debug, Serialize, Deserialize)]
913pub struct ConfirmSubscriptionArgs {
914 pub workspace: String,
915 pub viewer: Viewer,
916 pub session: String,
917}
918
919/// `cancel_subscription` (`resume` false) ends a plan at the end of the
920/// period paid for; with `resume` true, takes that back. Owners only.
921/// Returns `Outcome<FeatureState>`.
922#[derive(Debug, Serialize, Deserialize)]
923pub struct CancelSubscriptionArgs {
924 pub actor: User,
925 pub workspace: String,
926 pub feature: Feature,
927 #[serde(default)]
928 pub resume: bool,
929}
930
931/// `has_feature`: whether a feature works for a workspace now, asked by the
932/// service that provides it before doing paid work. Returns
933/// `Outcome<bool>`: a failure, with the reason to show, when it does not.
934/// True everywhere when no card processor is configured.
935#[derive(Debug, Serialize, Deserialize)]
936pub struct HasFeatureArgs {
937 pub workspace: String,
938 pub feature: Feature,
939}
940
941/// `charge_feature`: usage of a feature past its plan's allowance, charged
942/// from the workspace's credit at cost plus the margin, whatever
943/// `FREE_WHILE_BUILDING` says. Called by the service that provides it.
944/// Charged once per `reference`. Returns `Outcome<bool>`: false if that
945/// reference was charged before.
946#[derive(Debug, Serialize, Deserialize)]
947#[serde(rename_all = "camelCase")]
948pub struct ChargeFeatureArgs {
949 pub workspace: String,
950 pub feature: Feature,
951 /// What it cost g1t, in millionths of a dollar, before the margin.
952 pub cost_micros: i64,
953 pub description: String,
954 /// `namespace/name`, when the usage was one repository's.
955 pub repo: Option<String>,
956 /// Unique to this charge, e.g. `deployments/acme/2026-10`.
957 pub reference: String,
958}
959
Models per workspace: several providers, routed by kind of work960#[cfg(test)]
961mod tests {
962 use super::*;
963
964 #[test]
Paid features: a workspace turns on Deployments with a monthly plan965 fn features_are_named_as_the_site_sends_them() {
966 assert_eq!(
967 serde_json::to_value(Feature::Deployments).unwrap(),
968 serde_json::json!("deployments")
969 );
970 assert_eq!(Feature::parse("deployments"), Some(Feature::Deployments));
971 assert!(SubscriptionStatus::Canceling.on());
972 assert!(!SubscriptionStatus::PastDue.on());
973 }
974
975 #[test]
Models per workspace: several providers, routed by kind of work976 fn who_pays_is_read_as_the_runner_sends_it() {
977 let run: StartRunArgs = serde_json::from_value(serde_json::json!({
978 "workspace": "acme",
979 "repo": { "namespace": "acme", "name": "web" },
980 "number": 7,
981 "task": "implement",
982 "model": "Claude Sonnet 5.5",
983 "billedTo": "workspace",
984 }))
985 .unwrap();
986 assert_eq!(run.billed_to, "workspace");
987 }
988}