Skip to content

g1t/services/billing/src/ai.rs

1,000 lines45,981 bytesCodeBlame
1//! Prepaid AI credit: what Agent and AI Gateway usage draws on, bought in
2//! advance so g1t never fronts a model's cost.
3//!
4//! - **Buying.** An owner buys AI credit on Stripe's page: one payment by
5//! card, $10 to $1,000, with Stripe's card fee as its own line when the
6//! `card_fee` cost setting is on (`card_fee_cents`, a gross-up of the
7//! price book's `card_fee_percent` and `card_fee_fixed`). The card is
8//! kept for auto-reload. The credit is entered once, whichever comes
9//! first: the person coming back (`confirm_ai_credit`) or Stripe's
10//! `checkout.session.completed` (`webhooks.rs`). Both claim the same
11//! `checkouts` row, and the grant's id is the page's id, so a payment is
12//! credited exactly once.
13//! - **What it is.** A `credit_grants` row of kind `purchased`, scope
14//! `models`, source `purchase`, expiring a year after purchase, and its
15//! ledger line (a payment: its reference is Stripe's id, never `crd…`).
16//! Model usage draws on it before anything else (`grants::replay`), and
17//! it counts as money paid, never as given.
18//! - **Auto-reload.** Off by default. When AI credit falls below the
19//! threshold, the saved card is charged off-session to bring it back to
20//! the target, at most the monthly maximum. Each attempt has its own
21//! idempotency key (`ai_reloads.id`), so a retry is the same payment. A
22//! failed charge turns auto-reload off and tells the owners.
23//! - **At $0.** A workspace on the plan with no AI credit and none of its
24//! included usage left cannot start a run on g1t's models: `start_run`
25//! refuses with what to do. Auto-reload, when on, is tried first. A 100%
26//! discount (Flagon) pays for everything, so nothing is needed; an
27//! enterprise is invoiced for models after use.
28//! - **Once on upgrading.** A workspace that starts the paid plan is given
29//! $5 of AI credit once (promotional: given, not revenue), expiring in a
30//! year.
31//! - **The agent rate.** Every agent run's tokens (input, output and
32//! cached, as the model proxy counts them) are charged at the price
33//! book's `agent_tokens` price per million, on a line of their own
34//! (`<run>/agent`), on top of the model at the provider's price
35//! (`agent_models`, no markup).
36
37use g1t_contracts::billing::{
38 AccountArgs, AiCredit, AiReload, BuyAiCreditArgs, CardFee, Checkout, ConfirmAiCreditArgs, CreditKind, EntryKind, PlanKind,
39 SetAiReloadArgs, MICROS_PER_DOLLAR,
40};
41use g1t_contracts::time::rfc3339;
42use g1t_contracts::{FailureCode, Outcome, Role};
43use g1t_kit::now_ms;
44use serde::Deserialize;
45use worker::Result;
46
47use crate::features::cents;
48use crate::{Billing, RunRow, members_only, optional};
49
50/// What a checkout row for AI credit is marked with.
51pub(crate) const AI_CREDIT: &str = "ai_credit";
52/// The amounts offered, in cents, and the bounds of a custom one.
53pub(crate) const PRESETS_CENTS: [u32; 4] = [1_000, 2_500, 5_000, 10_000];
54pub(crate) const MIN_CENTS: u32 = 1_000;
55pub(crate) const MAX_CENTS: u32 = 100_000;
56/// Bought credit lasts a year.
57pub(crate) const EXPIRES_DAYS: u64 = 365;
58/// Given once, on starting the paid plan.
59pub(crate) const UPGRADE_CREDIT_MICROS: i64 = 5_000_000;
60/// Auto-reload's bounds: a reload of at least $10, a target of at most
61/// $1,000, and at most $10,000 a month.
62const MIN_RELOAD_MICROS: i64 = 10 * MICROS_PER_DOLLAR;
63const MAX_TARGET_MICROS: i64 = 1_000 * MICROS_PER_DOLLAR;
64const MAX_MONTHLY_MICROS: i64 = 10_000 * MICROS_PER_DOLLAR;
65const DAY_MS: u64 = 24 * 60 * 60 * 1000;
66
67// ---------------------------------------------------------------------
68// The arithmetic, apart from the database so it can be tested.
69// ---------------------------------------------------------------------
70
71/// Whether an amount of AI credit can be bought, in cents.
72pub(crate) fn amount_ok(cents: u32) -> std::result::Result<(), String> {
73 if (MIN_CENTS..=MAX_CENTS).contains(&cents) && cents.is_multiple_of(100) {
74 Ok(())
75 } else {
76 Err(format!("Buy between ${} and ${} of AI credit, in whole dollars.", MIN_CENTS / 100, crate::group(MAX_CENTS / 100)))
77 }
78}
79
80/// The card fee on `credit_cents`, so that what is left after Stripe's fee
81/// is the credit: the total is `(credit + fixed) / (1 − percent)`, rounded
82/// up to the cent. None when the fee is off.
83pub(crate) fn card_fee_cents(credit_cents: u32, fee: &CardFee) -> u32 {
84 if !fee.on || credit_cents == 0 {
85 return 0;
86 }
87 let rate = fee.percent_micros / MICROS_PER_DOLLAR as f64;
88 if !(0.0..0.5).contains(&rate) {
89 return 0;
90 }
91 let total = ((f64::from(credit_cents) + f64::from(fee.fixed_cents)) / (1.0 - rate)).ceil();
92 (total as u32).saturating_sub(credit_cents)
93}
94
95/// What auto-reload should buy now, if anything: enough to bring the
96/// credit from `balance` back to the target, in whole dollars, within
97/// what is left of the monthly maximum, and never less than $10.
98pub(crate) fn reload_amount(reload: &AiReload, balance: i64, reloaded_this_month: i64) -> Option<i64> {
99 if !reload.enabled || reload.failed_at.is_some() || balance >= reload.threshold_micros {
100 return None;
101 }
102 let wanted = (reload.target_micros - balance).max(MIN_RELOAD_MICROS);
103 let wanted = (wanted + MICROS_PER_DOLLAR - 1) / MICROS_PER_DOLLAR * MICROS_PER_DOLLAR;
104 let room = (reload.monthly_max_micros - reloaded_this_month).max(0) / MICROS_PER_DOLLAR * MICROS_PER_DOLLAR;
105 let amount = wanted.min(room);
106 (amount >= MIN_RELOAD_MICROS).then_some(amount)
107}
108
109/// What is wrong with auto-reload's settings, if anything.
110pub(crate) fn reload_invalid(threshold: i64, target: i64, monthly_max: i64) -> Option<&'static str> {
111 if threshold < 0 || target <= 0 || monthly_max <= 0 {
112 return Some("Amounts are in dollars, more than $0.");
113 }
114 if target < threshold + MIN_RELOAD_MICROS {
115 return Some("Reload to at least $10 more than the amount it reloads below.");
116 }
117 if target > MAX_TARGET_MICROS {
118 return Some("Reload to at most $1,000.");
119 }
120 if monthly_max < target - threshold {
121 return Some("The monthly maximum has to cover at least one reload.");
122 }
123 if monthly_max > MAX_MONTHLY_MICROS {
124 return Some("The monthly maximum is at most $10,000.");
125 }
126 if [threshold, target, monthly_max].iter().any(|m| m % MICROS_PER_DOLLAR != 0) {
127 return Some("Use whole dollars.");
128 }
129 None
130}
131
132/// Whether a purchase's page was paid for what was asked: Stripe says it
133/// is paid, and what was paid covers the credit (the fee is Stripe's).
134pub(crate) fn purchase_paid(payment_status: &str, amount_total: Option<u32>, credit_cents: u32) -> std::result::Result<(), String> {
135 if payment_status != "paid" {
136 return Err("The payment is not finished yet. It is credited as soon as Stripe says it was paid.".to_owned());
137 }
138 if amount_total.unwrap_or(0) < credit_cents {
139 return Err("Stripe says less was paid than the credit asked for; nothing was credited. Write to support@g1t.sh.".to_owned());
140 }
141 Ok(())
142}
143
144/// The id of the AI credit given for starting the plan: one per workspace,
145/// so however often the plan is recorded, it is given once.
146pub(crate) fn upgrade_reference(workspace: &str) -> String {
147 format!("crd_upgrade_{}", workspace.to_lowercase())
148}
149
150/// The agent rate on `tokens`, at `per_million` micros a million, rounded
151/// up to a whole millionth of a dollar.
152pub(crate) fn agent_rate_micros(tokens: u64, per_million: f64) -> i64 {
153 if tokens == 0 || !per_million.is_finite() || per_million <= 0.0 {
154 return 0;
155 }
156 (tokens as f64 * per_million / 1_000_000.0).ceil() as i64
157}
158
159/// Whether a workspace's runs on g1t's models need AI credit (or included
160/// usage) to start: on the plan, paying full or part price. A 100%
161/// discount pays for all of it; an enterprise is invoiced after use; a
162/// free workspace runs on its trial, which has its own limits.
163pub(crate) fn needs_credit(plan: PlanKind) -> bool {
164 plan == PlanKind::Paid
165}
166
167/// The refusal at $0.
168pub(crate) fn out_of_credit_message(workspace: &str, reload_failed: bool) -> String {
169 let reload = if reload_failed { " Auto-reload was turned off after its last charge failed." } else { "" };
170 format!(
171 "The {workspace} workspace is out of AI credit and has used this month's included usage, so g1t does not start new runs on its models.{reload} An owner can buy AI credit or turn on auto-reload at /{workspace}/-/billing#ai-credit."
172 )
173}
174
175#[derive(Deserialize)]
176struct ReloadRow {
177 enabled: i64,
178 threshold_micros: i64,
179 target_micros: i64,
180 monthly_max_micros: i64,
181 failed_at: Option<String>,
182 error: Option<String>,
183}
184
185#[derive(Deserialize)]
186struct Sum {
187 micros: Option<f64>,
188}
189
190#[derive(Deserialize)]
191struct Open {
192 workspace: String,
193 created_by: String,
194 amount_cents: u32,
195 fee_cents: Option<u32>,
196}
197
198impl Billing {
199 // --- The price book ----------------------------------------------------
200
201 /// The card fee, as the price book and the `card_fee` setting have it.
202 pub(crate) async fn card_fee(&self) -> Result<CardFee> {
203 #[derive(Deserialize)]
204 struct Row {
205 value: String,
206 }
207 let on = self
208 .db
209 .prepare("SELECT value FROM cost_settings WHERE key = 'card_fee'")
210 .first::<Row>(None)
211 .await?
212 .is_none_or(|row| row.value.trim() != "off");
213 let percent = self.price("card_fee_percent").await?.map_or(29_000.0, |(_, price)| price);
214 let fixed = self.price("card_fee_fixed").await?.map_or(300_000.0, |(_, price)| price);
215 Ok(CardFee { on, percent_micros: percent, fixed_cents: (fixed / 10_000.0).round().max(0.0) as u32 })
216 }
217
218 /// The agent rate per million tokens, at price.
219 pub(crate) async fn agent_rate(&self) -> Result<f64> {
220 Ok(self.price("agent_tokens").await?.map_or(0.0, |(_, price)| price))
221 }
222
223 /// The markup on a price-book meter, in percent.
224 async fn markup_of(&self, meter: &str) -> Result<Option<u32>> {
225 #[derive(Deserialize)]
226 struct Row {
227 markup_percent: u32,
228 }
229 Ok(self
230 .db
231 .prepare("SELECT markup_percent FROM prices WHERE meter = ?")
232 .bind(&[meter.into()])?
233 .first::<Row>(None)
234 .await?
235 .map(|row| row.markup_percent))
236 }
237
238 /// The markup on a model's provider price for agent runs: the price
239 /// book's `agent_models` (0 from 2026-10-08), or `MARGIN_PERCENT`
240 /// where the price book has no row.
241 pub(crate) async fn model_markup(&self) -> Result<u32> {
242 Ok(self.markup_of("agent_models").await?.unwrap_or(self.margin_percent))
243 }
244
245 /// The markup on AI Gateway's provider price: 0 while it is in beta.
246 pub(crate) async fn gateway_markup(&self) -> Result<u32> {
247 Ok(self.markup_of("gateway_models").await?.unwrap_or(0))
248 }
249
250 // --- Balances ------------------------------------------------------------
251
252 /// AI credit left: the open grants scoped to models, by kind.
253 pub(crate) async fn ai_balance(&self, workspace: &str) -> Result<(i64, i64, i64)> {
254 let credits = self.credits_of(workspace).await?;
255 let models: Vec<_> = credits.grants.iter().filter(|g| g.scope == "models").collect();
256 let purchased = models.iter().filter(|g| g.kind == CreditKind::Purchased).map(|g| g.left_micros).sum();
257 let given = models.iter().filter(|g| g.kind != CreditKind::Purchased).map(|g| g.left_micros).sum();
258 Ok((purchased + given, purchased, given))
259 }
260
261 /// What is owed now, with the balance `balance`: credit scoped to
262 /// models is not money for anything else, so what is left of it is
263 /// owed on top of a balance it props up.
264 pub(crate) async fn owed_with(&self, workspace: &str, balance: i64) -> Result<i64> {
265 let (left, _, _) = self.ai_balance(workspace).await?;
266 Ok((left - balance).max(0))
267 }
268
269 async fn reload_settings(&self, workspace: &str) -> Result<AiReload> {
270 let row = self
271 .db
272 .prepare("SELECT enabled, threshold_micros, target_micros, monthly_max_micros, failed_at, error FROM ai_reload WHERE workspace = ?")
273 .bind(&[workspace.into()])?
274 .first::<ReloadRow>(None)
275 .await?;
276 let reloaded = self.reloaded_this_month(workspace).await?;
277 Ok(match row {
278 Some(row) => AiReload {
279 enabled: row.enabled != 0,
280 threshold_micros: row.threshold_micros,
281 target_micros: row.target_micros,
282 monthly_max_micros: row.monthly_max_micros,
283 reloaded_micros: reloaded,
284 failed_at: row.failed_at,
285 error: row.error,
286 },
287 // The suggestion the form starts from: below $10, back to $25,
288 // at most $100 a month.
289 None => AiReload {
290 enabled: false,
291 threshold_micros: 10 * MICROS_PER_DOLLAR,
292 target_micros: 25 * MICROS_PER_DOLLAR,
293 monthly_max_micros: 100 * MICROS_PER_DOLLAR,
294 reloaded_micros: reloaded,
295 failed_at: None,
296 error: None,
297 },
298 })
299 }
300
301 async fn reloaded_this_month(&self, workspace: &str) -> Result<i64> {
302 let month = &rfc3339(now_ms())[..7];
303 Ok(self
304 .db
305 .prepare("SELECT SUM(amount_micros) AS micros FROM ai_reloads WHERE workspace = ? AND month = ? AND status IN ('paid', 'pending')")
306 .bind(&[workspace.into(), month.into()])?
307 .first::<Sum>(None)
308 .await?
309 .and_then(|s| s.micros)
310 .unwrap_or(0.0) as i64)
311 }
312
313 /// What the plan's included usage has left this month, on the plan.
314 async fn included_left(&self, workspace: &str) -> Result<i64> {
315 let month = crate::credits::month_of(&rfc3339(now_ms()));
316 let used = self.allowance_used("plan_credit", workspace, &month).await?;
317 Ok(crate::credits::left(self.plans.plan_included_micros, used))
318 }
319
320 // --- The page --------------------------------------------------------------
321
322 /// `ai_credit`: the workspace's AI credit, for its members.
323 pub(crate) async fn ai_credit(&self, a: AccountArgs) -> Result<Outcome<AiCredit>> {
324 let workspace = a.workspace.to_lowercase();
325 if !a.viewer.is_some_and(|viewer| viewer.is_member(&workspace)) {
326 return Ok(members_only());
327 }
328 Ok(Outcome::Ok(self.ai_credit_of(&workspace).await?))
329 }
330
331 pub(crate) async fn ai_credit_of(&self, workspace: &str) -> Result<AiCredit> {
332 let account = self.account_of(workspace).await?;
333 let plan = self.plan_kind_for(workspace, &account).await?;
334 let credits = self.credits_of(workspace).await?;
335 let grants: Vec<_> = credits.grants.into_iter().filter(|g| g.scope == "models").collect();
336 let purchased: i64 = grants.iter().filter(|g| g.kind == CreditKind::Purchased).map(|g| g.left_micros).sum();
337 let given: i64 = grants.iter().filter(|g| g.kind != CreditKind::Purchased).map(|g| g.left_micros).sum();
338 let balance = purchased + given;
339 let reload = self.reload_settings(workspace).await?;
340 let blocked = self.stripe.is_some()
341 && !self.free
342 && needs_credit(plan)
343 && balance <= 0
344 && self.included_left(workspace).await? <= 0;
345 Ok(AiCredit {
346 balance_micros: balance,
347 purchased_micros: purchased,
348 given_micros: given,
349 grants,
350 free_via_discount: account.terms.full_discount(),
351 postpaid: plan == PlanKind::Enterprise,
352 blocked,
353 can_buy: self.stripe.is_some() && plan == PlanKind::Paid,
354 presets_cents: PRESETS_CENTS.to_vec(),
355 min_cents: MIN_CENTS,
356 max_cents: MAX_CENTS,
357 card_fee: self.card_fee().await?,
358 reload,
359 agent_rate_micros: self.agent_rate().await?,
360 model_markup_percent: self.model_markup().await?,
361 gateway_markup_percent: self.gateway_markup().await?,
362 upgrade_credit_micros: UPGRADE_CREDIT_MICROS,
363 expires_days: EXPIRES_DAYS as u32,
364 })
365 }
366
367 // --- Buying --------------------------------------------------------------
368
369 /// `buy_ai_credit`: Stripe's page for a purchase.
370 pub(crate) async fn buy_ai_credit(&self, a: BuyAiCreditArgs) -> Result<Outcome<Checkout>> {
371 let workspace = a.workspace.to_lowercase();
372 if a.actor.role_in(&workspace) != Some(Role::Owner) {
373 return Ok(Outcome::fail(FailureCode::Forbidden, "Only an owner can buy AI credit for the workspace."));
374 }
375 let Some(stripe) = &self.stripe else {
376 return Ok(Outcome::fail(FailureCode::Conflict, "Payments are not set up on this g1t."));
377 };
378 if let Err(why) = amount_ok(a.amount_cents) {
379 return Ok(Outcome::fail(FailureCode::Invalid, why));
380 }
381 let account = self.account_of(&workspace).await?;
382 if account.terms.full_discount() {
383 return Ok(Outcome::fail(FailureCode::Conflict, format!("{workspace}'s AI usage is free under its discount: there is nothing to buy.")));
384 }
385 match self.plan_kind_for(&workspace, &account).await? {
386 PlanKind::Paid => {}
387 PlanKind::Enterprise => {
388 return Ok(Outcome::fail(FailureCode::Conflict, format!("{workspace} is invoiced for AI usage after use, through its enterprise.")));
389 }
390 _ => {
391 return Ok(Outcome::fail(FailureCode::PaymentRequired, format!("AI credit is for workspaces on the g1t plan. Start the plan for {workspace} first; it comes with $5 of AI credit.")));
392 }
393 }
394 let fee = card_fee_cents(a.amount_cents, &self.card_fee().await?);
395 let customer = self.row(&workspace).await?.and_then(|row| row.customer_id);
396 let purchase = |customer| crate::stripe::CreditPurchase {
397 workspace: &workspace,
398 credit_cents: a.amount_cents,
399 fee_cents: fee,
400 customer,
401 return_url: &a.return_url,
402 };
403 let started = match stripe.start_credit_checkout(&purchase(customer.as_deref())).await {
404 Err(error) if customer.is_some() && crate::stripe::is_missing(&error) => {
405 self.forget_customer(&workspace).await?;
406 stripe.start_credit_checkout(&purchase(None)).await
407 }
408 other => other,
409 };
410 self.page_opened(started, &workspace, a.amount_cents, fee, &a.actor.username, Some(AI_CREDIT)).await
411 }
412
413 /// `confirm_ai_credit`: back from Stripe's page.
414 pub(crate) async fn confirm_ai_credit(&self, a: ConfirmAiCreditArgs) -> Result<Outcome<AiCredit>> {
415 let workspace = a.workspace.to_lowercase();
416 if !a.viewer.is_some_and(|viewer| viewer.is_member(&workspace)) {
417 return Ok(members_only());
418 }
419 let mine = self
420 .db
421 .prepare("SELECT workspace FROM checkouts WHERE id = ? AND workspace = ? AND feature = ?")
422 .bind(&[a.session.as_str().into(), workspace.as_str().into(), AI_CREDIT.into()])?
423 .first::<serde_json::Value>(None)
424 .await?;
425 if mine.is_some() {
426 match self.settle_ai_credit(&a.session).await {
427 Ok(Ok(_)) => {}
428 Ok(Err(why)) => return Ok(Outcome::fail(FailureCode::Conflict, why)),
429 Err(error) => return Ok(Outcome::fail(FailureCode::Conflict, crate::stripe::friendly(&error))),
430 }
431 }
432 Ok(Outcome::Ok(self.ai_credit_of(&workspace).await?))
433 }
434
435 /// Credits a purchase whose page is paid, once. What happened, or why
436 /// it was not credited (yet).
437 pub(crate) async fn settle_ai_credit(&self, session_id: &str) -> Result<std::result::Result<String, String>> {
438 let Some(open) = self
439 .db
440 .prepare("SELECT workspace, created_by, amount_cents, fee_cents FROM checkouts WHERE id = ? AND feature = ? AND status = 'open'")
441 .bind(&[session_id.into(), AI_CREDIT.into()])?
442 .first::<Open>(None)
443 .await?
444 else {
445 return Ok(Ok("ignored: already credited or not AI credit".to_owned()));
446 };
447 let Some(stripe) = &self.stripe else { return Ok(Ok("ignored: payments off".to_owned())) };
448 let session = stripe.session(session_id).await?;
449 if let Err(why) = purchase_paid(&session.payment_status, session.amount_total, open.amount_cents) {
450 return Ok(Err(why));
451 }
452 let claimed = self
453 .db
454 .prepare("UPDATE checkouts SET status = 'paid' WHERE id = ? AND status = 'open' RETURNING id")
455 .bind(&[session_id.into()])?
456 .first::<serde_json::Value>(None)
457 .await?;
458 if claimed.is_none() {
459 return Ok(Ok("ignored: credited meanwhile".to_owned()));
460 }
461 let micros = i64::from(open.amount_cents) * 10_000;
462 let fee = i64::from(open.fee_cents.unwrap_or(0)) * 10_000;
463 self.grant_purchased(&open.workspace, session_id, micros, fee, &open.created_by, session.customer.as_deref())
464 .await?;
465 Ok(Ok(format!("{}: {} of AI credit bought", open.workspace, cents(micros))))
466 }
467
468 /// Enters bought AI credit: its grant and its ledger line, once for
469 /// `reference` (Stripe's id for the payment).
470 pub(crate) async fn grant_purchased(
471 &self,
472 workspace: &str,
473 reference: &str,
474 micros: i64,
475 fee_micros: i64,
476 by: &str,
477 customer: Option<&str>,
478 ) -> Result<bool> {
479 let now = now_ms();
480 let expires = rfc3339(now + EXPIRES_DAYS * DAY_MS);
481 let fee = if fee_micros > 0 { format!(" (card fee {} paid to Stripe)", cents(fee_micros)) } else { String::new() };
482 let note = format!("AI credit bought{fee}");
483 let inserted = self
484 .db
485 .prepare(
486 "INSERT OR IGNORE INTO credit_grants (id, workspace, kind, scope, source, amount_micros, note, expires_at, created_by, created_at)
487 VALUES (?, ?, 'purchased', 'models', 'purchase', ?, ?, ?, ?, ?) RETURNING id",
488 )
489 .bind(&[
490 reference.into(),
491 workspace.into(),
492 (micros as f64).into(),
493 note.as_str().into(),
494 expires.as_str().into(),
495 by.into(),
496 rfc3339(now).into(),
497 ])?
498 .first::<serde_json::Value>(None)
499 .await?;
500 if inserted.is_none() {
501 return Ok(false);
502 }
503 let description = format!("AI credit bought: {}, until {}", cents(micros), &expires[..10]);
504 self.enter(workspace, EntryKind::TopUp, micros, &description, reference, None, None, Some(by), customer).await?;
505 self.db
506 .prepare("UPDATE ledger SET credit_kind = 'purchased' WHERE reference = ?")
507 .bind(&[reference.into()])?
508 .run()
509 .await?;
510 let account = self.account_of(workspace).await?;
511 self.audit(&account.id, "ai_credit", &format!("{workspace}: {} of AI credit bought{fee}", cents(micros)), by).await?;
512 Ok(true)
513 }
514
515 /// The $5 of AI credit a workspace gets once, on starting the paid
516 /// plan. Promotional: given, not revenue. Never twice, and never for a
517 /// workspace whose discount pays for everything anyway.
518 pub(crate) async fn grant_upgrade_credit(&self, workspace: &str) -> Result<()> {
519 let workspace = workspace.to_lowercase();
520 if self.terms_of(&workspace).await?.full_discount() {
521 return Ok(());
522 }
523 let reference = upgrade_reference(&workspace);
524 let now = now_ms();
525 let expires = rfc3339(now + EXPIRES_DAYS * DAY_MS);
526 let inserted = self
527 .db
528 .prepare(
529 "INSERT OR IGNORE INTO credit_grants (id, workspace, kind, scope, source, amount_micros, note, expires_at, created_by, created_at)
530 VALUES (?, ?, 'promotional', 'models', 'upgrade', ?, 'AI credit for starting the g1t plan', ?, 'g1t', ?) RETURNING id",
531 )
532 .bind(&[
533 reference.as_str().into(),
534 workspace.as_str().into(),
535 (UPGRADE_CREDIT_MICROS as f64).into(),
536 expires.as_str().into(),
537 rfc3339(now).into(),
538 ])?
539 .first::<serde_json::Value>(None)
540 .await?;
541 if inserted.is_none() {
542 return Ok(());
543 }
544 let description = format!("Credit from g1t (promotional, until {}): AI credit for starting the g1t plan", &expires[..10]);
545 self.enter(&workspace, EntryKind::TopUp, UPGRADE_CREDIT_MICROS, &description, &reference, None, None, Some("g1t"), None).await?;
546 self.db
547 .prepare("UPDATE ledger SET credit_kind = 'promotional' WHERE reference = ?")
548 .bind(&[reference.as_str().into()])?
549 .run()
550 .await?;
551 let account = self.account_of(&workspace).await?;
552 self.audit(&account.id, "credit", &format!("{} promotional AI credit to {workspace} for starting the plan", cents(UPGRADE_CREDIT_MICROS)), "g1t")
553 .await?;
554 Ok(())
555 }
556
557 // --- At $0 -----------------------------------------------------------------
558
559 /// Why a run on g1t's models cannot start for want of AI credit, if it
560 /// cannot. Auto-reload, when on, is tried first.
561 pub(crate) async fn ai_refusal(&self, workspace: &str) -> Result<Option<String>> {
562 if self.stripe.is_none() || self.free {
563 return Ok(None);
564 }
565 let account = self.account_of(workspace).await?;
566 if !needs_credit(self.plan_kind_for(workspace, &account).await?) {
567 return Ok(None);
568 }
569 if self.included_left(workspace).await? > 0 {
570 return Ok(None);
571 }
572 let (balance, _, _) = self.ai_balance(workspace).await?;
573 if balance > 0 {
574 return Ok(None);
575 }
576 let reload = self.reload_settings(workspace).await?;
577 if reload.enabled && reload.failed_at.is_none() {
578 if let Err(error) = self.reload_now(workspace).await {
579 worker::console_error!("{workspace}: auto-reload at a run's start failed: {error}");
580 }
581 if self.ai_balance(workspace).await?.0 > 0 {
582 return Ok(None);
583 }
584 }
585 let failed = self.reload_settings(workspace).await?.failed_at.is_some();
586 Ok(Some(out_of_credit_message(workspace, failed)))
587 }
588
589 // --- Auto-reload ---------------------------------------------------------
590
591 /// `set_ai_reload`: owners only.
592 pub(crate) async fn set_ai_reload(&self, a: SetAiReloadArgs) -> Result<Outcome<AiCredit>> {
593 let workspace = a.workspace.to_lowercase();
594 if a.actor.role_in(&workspace) != Some(Role::Owner) {
595 return Ok(Outcome::fail(FailureCode::Forbidden, "Only an owner can change auto-reload."));
596 }
597 if let Some(why) = reload_invalid(a.threshold_micros, a.target_micros, a.monthly_max_micros) {
598 return Ok(Outcome::fail(FailureCode::Invalid, why));
599 }
600 if a.enabled {
601 let credit = self.ai_credit_of(&workspace).await?;
602 if !credit.can_buy {
603 return Ok(Outcome::fail(FailureCode::Conflict, "Auto-reload is for workspaces on the g1t plan that buy AI credit."));
604 }
605 let has_card = match (&self.stripe, self.row(&workspace).await?.and_then(|row| row.customer_id)) {
606 (Some(stripe), Some(customer)) => stripe.default_payment_method(&customer).await.ok().flatten().is_some(),
607 _ => false,
608 };
609 if !has_card {
610 return Ok(Outcome::fail(FailureCode::Conflict, "Auto-reload charges the workspace's saved card, and it has none. Buy AI credit once, or add a card on Stripe's billing page, first."));
611 }
612 }
613 let now = rfc3339(now_ms());
614 self.db
615 .prepare(
616 "INSERT INTO ai_reload (workspace, enabled, threshold_micros, target_micros, monthly_max_micros, updated_by, updated_at, failed_at, error)
617 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, NULL, NULL)
618 ON CONFLICT (workspace) DO UPDATE SET enabled = ?2, threshold_micros = ?3, target_micros = ?4, monthly_max_micros = ?5,
619 updated_by = ?6, updated_at = ?7, failed_at = NULL, error = NULL",
620 )
621 .bind(&[
622 workspace.as_str().into(),
623 i32::from(a.enabled).into(),
624 (a.threshold_micros as f64).into(),
625 (a.target_micros as f64).into(),
626 (a.monthly_max_micros as f64).into(),
627 a.actor.username.as_str().into(),
628 now.as_str().into(),
629 ])?
630 .run()
631 .await?;
632 let account = self.account_of(&workspace).await?;
633 self.audit(
634 &account.id,
635 "ai_reload",
636 &format!(
637 "{workspace}: auto-reload {}: below {}, back to {}, at most {} a month",
638 if a.enabled { "on" } else { "off" },
639 cents(a.threshold_micros),
640 cents(a.target_micros),
641 cents(a.monthly_max_micros)
642 ),
643 &a.actor.username,
644 )
645 .await?;
646 // Below the threshold already: reload now rather than at the next run.
647 if a.enabled
648 && let Err(error) = self.reload_now(&workspace).await
649 {
650 worker::console_error!("{workspace}: auto-reload right after turning it on failed: {error}");
651 }
652 Ok(Outcome::Ok(self.ai_credit_of(&workspace).await?))
653 }
654
655 /// Every workspace with auto-reload on that is below its threshold,
656 /// reloaded: each cron run.
657 pub(crate) async fn reload_ai_credit(&self) -> Result<u32> {
658 if self.stripe.is_none() {
659 return Ok(0);
660 }
661 #[derive(Deserialize)]
662 struct Row {
663 workspace: String,
664 }
665 let due = self
666 .db
667 .prepare("SELECT workspace FROM ai_reload WHERE enabled = 1 AND failed_at IS NULL LIMIT 100")
668 .all()
669 .await?
670 .results::<Row>()?;
671 let mut done = 0;
672 for Row { workspace } in due {
673 match self.reload_now(&workspace).await {
674 Ok(Some(_)) => done += 1,
675 Ok(None) => {}
676 Err(error) => worker::console_error!("{workspace}: auto-reload failed: {error}"),
677 }
678 }
679 Ok(done)
680 }
681
682 /// Reloads the workspace's AI credit if it is below its threshold.
683 /// What was reloaded, or None.
684 pub(crate) async fn reload_now(&self, workspace: &str) -> Result<Option<i64>> {
685 let Some(stripe) = &self.stripe else { return Ok(None) };
686 let reload = self.reload_settings(workspace).await?;
687 let (balance, _, _) = self.ai_balance(workspace).await?;
688 let Some(amount) = reload_amount(&reload, balance, reload.reloaded_micros) else {
689 return Ok(None);
690 };
691 let Some(customer) = self.row(workspace).await?.and_then(|row| row.customer_id) else {
692 self.reload_failed(workspace, None, amount, "the workspace has no saved card").await?;
693 return Ok(None);
694 };
695 let method = match stripe.default_payment_method(&customer).await {
696 Ok(Some(method)) => method,
697 Ok(None) => {
698 self.reload_failed(workspace, None, amount, "the workspace has no saved card").await?;
699 return Ok(None);
700 }
701 Err(error) => return Err(error),
702 };
703 let month = rfc3339(now_ms())[..7].to_owned();
704 // One key per attempt: the month and how many reloads came before.
705 // A retry after a crash is the same attempt, so the same payment.
706 #[derive(Deserialize)]
707 struct Count {
708 n: Option<f64>,
709 }
710 let before = self
711 .db
712 .prepare("SELECT COUNT(*) AS n FROM ai_reloads WHERE workspace = ? AND month = ? AND status = 'paid'")
713 .bind(&[workspace.into(), month.as_str().into()])?
714 .first::<Count>(None)
715 .await?
716 .and_then(|c| c.n)
717 .unwrap_or(0.0) as u32;
718 let key = format!("reload/{workspace}/{month}/{}", before + 1);
719 let credit_cents = (amount / 10_000) as u32;
720 let fee_cents = card_fee_cents(credit_cents, &self.card_fee().await?);
721 self.db
722 .prepare(
723 "INSERT OR IGNORE INTO ai_reloads (id, workspace, month, amount_micros, fee_micros, status, created_at)
724 VALUES (?, ?, ?, ?, ?, 'pending', ?)",
725 )
726 .bind(&[
727 key.as_str().into(),
728 workspace.into(),
729 month.as_str().into(),
730 (amount as f64).into(),
731 (f64::from(fee_cents) * 10_000.0).into(),
732 rfc3339(now_ms()).into(),
733 ])?
734 .run()
735 .await?;
736 let charge = crate::stripe::SavedCharge {
737 workspace,
738 customer: &customer,
739 payment_method: &method.id,
740 credit_cents,
741 fee_cents,
742 key: &key,
743 };
744 let paid = match stripe.charge_saved(&charge).await {
745 Ok(intent) if intent["status"].as_str() == Some("succeeded") => intent["id"].as_str().map(str::to_owned),
746 Ok(intent) => {
747 let status = intent["status"].as_str().unwrap_or("unknown").to_owned();
748 self.reload_failed(workspace, Some(&key), amount, &format!("the card needs the bank's approval ({status})")).await?;
749 return Ok(None);
750 }
751 Err(error) if crate::stripe::is_card_error(&error) => {
752 self.reload_failed(workspace, Some(&key), amount, &crate::stripe::friendly(&error)).await?;
753 return Ok(None);
754 }
755 Err(error) => return Err(error),
756 };
757 let Some(intent) = paid else { return Ok(None) };
758 self.db
759 .prepare("UPDATE ai_reloads SET status = 'paid', payment_intent = ? WHERE id = ?")
760 .bind(&[intent.as_str().into(), key.as_str().into()])?
761 .run()
762 .await?;
763 self.grant_purchased(workspace, &intent, amount, i64::from(fee_cents) * 10_000, "g1t", Some(&customer)).await?;
764 Ok(Some(amount))
765 }
766
767 /// A reload that could not be charged: auto-reload is turned off, and
768 /// the owners are told.
769 async fn reload_failed(&self, workspace: &str, key: Option<&str>, amount: i64, why: &str) -> Result<()> {
770 let now = rfc3339(now_ms());
771 if let Some(key) = key {
772 self.db
773 .prepare("UPDATE ai_reloads SET status = 'failed', error = ? WHERE id = ?")
774 .bind(&[why.into(), key.into()])?
775 .run()
776 .await?;
777 }
778 self.db
779 .prepare("UPDATE ai_reload SET enabled = 0, failed_at = ?, error = ? WHERE workspace = ?")
780 .bind(&[now.as_str().into(), why.into(), workspace.into()])?
781 .run()
782 .await?;
783 let account = self.account_of(workspace).await?;
784 self.audit(&account.id, "ai_reload_failed", &format!("{workspace}: auto-reload of {} failed ({why}); turned off", cents(amount)), "g1t")
785 .await?;
786 if let Some(identity) = &self.identity {
787 crate::limits::notify_with(
788 identity,
789 workspace,
790 &format!("g1t: auto-reload for {workspace} failed and is off"),
791 &format!(
792 "g1t tried to reload {} of AI credit for {workspace} and could not: {why}. Auto-reload is off until an owner turns it on again. Until then, runs on g1t's models stop once the AI credit is spent."
793 , cents(amount)),
794 "Open billing",
795 &format!("https://g1t.sh/{workspace}/-/billing#ai-credit"),
796 "You get this because you own this workspace on g1t. AI credit is explained at https://docs.g1t.sh/guides/usage-and-billing/#ai-credit",
797 )
798 .await;
799 }
800 Ok(())
801 }
802
803 // --- The agent rate --------------------------------------------------------
804
805 /// Charges a run's agent rate for the tokens counted since it was last
806 /// charged, once each: when the run reports and again when it is
807 /// settled, so tokens counted late are charged too.
808 pub(crate) async fn charge_agent_rate(&self, run_id: &str, run: &RunRow) -> Result<()> {
809 if self.stripe.is_none() {
810 return Ok(());
811 }
812 #[derive(Deserialize)]
813 struct Row {
814 session_id: Option<String>,
815 agent_tokens: Option<f64>,
816 created_at: String,
817 }
818 let Some(row) = self
819 .db
820 .prepare("SELECT session_id, agent_tokens, created_at FROM runs WHERE id = ?")
821 .bind(&[run_id.into()])?
822 .first::<Row>(None)
823 .await?
824 else {
825 return Ok(());
826 };
827 let Some(session) = row.session_id else { return Ok(()) };
828 let counted = self
829 .db
830 .prepare(
831 "SELECT SUM(input + output + cache_read + cache_write) AS micros FROM token_usage
832 WHERE workspace = ? AND session = ? AND day >= ?",
833 )
834 .bind(&[run.workspace.as_str().into(), session.as_str().into(), row.created_at[..10].into()])?
835 .first::<Sum>(None)
836 .await?
837 .and_then(|s| s.micros)
838 .unwrap_or(0.0) as u64;
839 let charged = row.agent_tokens.unwrap_or(0.0) as u64;
840 if counted <= charged {
841 return Ok(());
842 }
843 // Claimed first: two callers never charge the same tokens.
844 let claimed = self
845 .db
846 .prepare("UPDATE runs SET agent_tokens = ?1 WHERE id = ?2 AND agent_tokens = ?3 RETURNING id")
847 .bind(&[(counted as f64).into(), run_id.into(), (charged as f64).into()])?
848 .first::<serde_json::Value>(None)
849 .await?;
850 if claimed.is_none() {
851 return Ok(());
852 }
853 let tokens = counted - charged;
854 let base = agent_rate_micros(tokens, self.agent_rate().await?);
855 // Before the rate takes effect: counted, and nothing charged.
856 if base == 0 {
857 return Ok(());
858 }
859 let (charge, terms_note, discount) = self.charged(&run.workspace, base).await?;
860 let now = rfc3339(now_ms());
861 let eligible = crate::credits::eligible_for(Some(g1t_contracts::billing::ComputeKind::Agent), None);
862 let drawn = self.draw(&run.workspace, charge, &crate::credits::month_of(&now), &eligible).await?;
863 let reference = if charged == 0 { format!("{run_id}/agent") } else { format!("{run_id}/agent/{counted}") };
864 let what = match run.task.as_str() {
865 "plan" => format!("planning for {}", run.repo),
866 "review" => format!("the review of {}#{}", run.repo, run.number),
867 "update" => format!("catching up {}#{}", run.repo, run.number),
868 _ => format!("work on {}#{}", run.repo, run.number),
869 };
870 let description = format!(
871 "g1t agent rate: {} tokens for {what}{terms_note}{}",
872 crate::features::thousands(tokens),
873 drawn.note()
874 );
875 self.enter(&run.workspace, EntryKind::Usage, -(charge - drawn.total()), &description, &reference, Some(run), Some(0), None, None)
876 .await?;
877 self.db
878 .prepare("UPDATE ledger SET quantity = ?, price_version = ? WHERE reference = ?")
879 .bind(&[(tokens as f64).into(), optional(self.version_now("agent_tokens").await?.as_deref()), reference.as_str().into()])?
880 .run()
881 .await?;
882 self.record_drawn(&reference, &drawn).await?;
883 self.record_discount(&reference, discount).await?;
884 self.count_spend(&run.workspace, 0, charge - drawn.total(), &drawn).await;
885 Ok(())
886 }
887}
888
889#[cfg(test)]
890mod tests {
891 use super::*;
892
893 fn fee(on: bool) -> CardFee {
894 CardFee { on, percent_micros: 29_000.0, fixed_cents: 30 }
895 }
896
897 #[test]
898 fn the_card_fee_is_stripes_fee_grossed_up_and_off_when_switched_off() {
899 // $25 of credit: ($25 + $0.30) / 0.971 = $26.06, so a $1.06 fee.
900 assert_eq!(card_fee_cents(2_500, &fee(true)), 106);
901 // What is left after Stripe's 2.9% + 30¢ is at least the credit.
902 for credit in [1_000u32, 2_500, 5_000, 10_000, 100_000] {
903 let total = credit + card_fee_cents(credit, &fee(true));
904 let net = f64::from(total) - (f64::from(total) * 0.029).round() - 30.0;
905 assert!(net >= f64::from(credit) - 1.0, "{credit}: {total} leaves {net}");
906 }
907 assert_eq!(card_fee_cents(2_500, &fee(false)), 0);
908 assert_eq!(card_fee_cents(0, &fee(true)), 0);
909 }
910
911 #[test]
912 fn amounts_are_whole_dollars_from_ten_to_a_thousand() {
913 assert!(amount_ok(1_000).is_ok() && amount_ok(100_000).is_ok() && amount_ok(2_500).is_ok());
914 assert!(amount_ok(999).is_err() && amount_ok(100_100).is_err() && amount_ok(1_050).is_err());
915 }
916
917 fn reload(threshold: i64, target: i64, max: i64) -> AiReload {
918 AiReload { enabled: true, threshold_micros: threshold, target_micros: target, monthly_max_micros: max, ..AiReload::default() }
919 }
920
921 const D: i64 = MICROS_PER_DOLLAR;
922
923 #[test]
924 fn auto_reload_tops_up_to_the_target_below_the_threshold_within_the_monthly_maximum() {
925 let r = reload(10 * D, 25 * D, 100 * D);
926 // Above the threshold: nothing.
927 assert_eq!(reload_amount(&r, 10 * D, 0), None);
928 // Below it: back to the target, in whole dollars.
929 assert_eq!(reload_amount(&r, 9 * D, 0), Some(16 * D));
930 assert_eq!(reload_amount(&r, 9_500_000, 0), Some(16 * D));
931 // Owing more than the target is still a reload to the target.
932 assert_eq!(reload_amount(&r, -3 * D, 0), Some(28 * D));
933 // Never less than $10.
934 assert_eq!(reload_amount(&reload(10 * D, 12 * D, 100 * D), 9 * D, 0), Some(10 * D));
935 // The monthly maximum caps it, and below $10 of room nothing is done.
936 assert_eq!(reload_amount(&r, 0, 90 * D), Some(10 * D));
937 assert_eq!(reload_amount(&r, 0, 95 * D), None);
938 assert_eq!(reload_amount(&r, 0, 100 * D), None);
939 }
940
941 #[test]
942 fn a_failed_or_disabled_reload_does_nothing() {
943 let off = AiReload { enabled: false, ..reload(10 * D, 25 * D, 100 * D) };
944 assert_eq!(reload_amount(&off, 0, 0), None);
945 let failed = AiReload { failed_at: Some("2026-10-08T00:00:00Z".into()), ..reload(10 * D, 25 * D, 100 * D) };
946 assert_eq!(reload_amount(&failed, 0, 0), None);
947 }
948
949 #[test]
950 fn auto_reload_settings_are_checked() {
951 assert_eq!(reload_invalid(10 * D, 25 * D, 100 * D), None);
952 assert!(reload_invalid(10 * D, 15 * D, 100 * D).is_some());
953 assert!(reload_invalid(10 * D, 2_000 * D, 10_000 * D).is_some());
954 assert!(reload_invalid(10 * D, 25 * D, 10 * D).is_some());
955 assert!(reload_invalid(10 * D, 25 * D, 20_000 * D).is_some());
956 assert!(reload_invalid(10 * D, 25_500_000, 100 * D).is_some());
957 assert!(reload_invalid(-1, 25 * D, 100 * D).is_some());
958 }
959
960 #[test]
961 fn the_agent_rate_is_per_million_tokens_rounded_up() {
962 // $0.25 a million: 2 million tokens are 50 cents.
963 assert_eq!(agent_rate_micros(2_000_000, 250_000.0), 500_000);
964 assert_eq!(agent_rate_micros(1, 250_000.0), 1);
965 assert_eq!(agent_rate_micros(0, 250_000.0), 0);
966 // Before it takes effect the price book says 0.
967 assert_eq!(agent_rate_micros(5_000_000, 0.0), 0);
968 }
969
970 #[test]
971 fn only_workspaces_paying_on_the_plan_need_ai_credit() {
972 assert!(needs_credit(PlanKind::Paid));
973 assert!(!needs_credit(PlanKind::Internal));
974 assert!(!needs_credit(PlanKind::Enterprise));
975 assert!(!needs_credit(PlanKind::Free));
976 assert!(out_of_credit_message("acme", false).contains("/acme/-/billing#ai-credit"));
977 assert!(out_of_credit_message("acme", true).contains("Auto-reload was turned off"));
978 }
979
980 #[test]
981 fn a_purchase_is_credited_only_once_paid_and_only_for_what_was_paid() {
982 assert!(purchase_paid("paid", Some(2_606), 2_500).is_ok());
983 assert!(purchase_paid("unpaid", Some(2_606), 2_500).unwrap_err().contains("not finished"));
984 assert!(purchase_paid("paid", Some(2_000), 2_500).is_err());
985 assert!(purchase_paid("paid", None, 2_500).is_err());
986 // The grant's id is the page's id and the ledger's reference is
987 // unique, and the checkout row is claimed open → paid before either
988 // is written: the webhook and the person coming back credit once.
989 let source = include_str!("ai.rs");
990 assert!(source.contains("UPDATE checkouts SET status = 'paid' WHERE id = ? AND status = 'open' RETURNING id"));
991 assert!(source.contains("INSERT OR IGNORE INTO credit_grants"));
992 }
993
994 #[test]
995 fn the_upgrade_credit_is_one_grant_per_workspace() {
996 assert_eq!(upgrade_reference("Acme"), upgrade_reference("acme"));
997 assert!(upgrade_reference("acme").starts_with("crd"), "given, never a payment");
998 assert_eq!(UPGRADE_CREDIT_MICROS, 5_000_000);
999 }
1000}