pr_01m47d24b0e6n91zwymwxg0vpx/crates/contracts/src/billing.rs

242 lines8,060 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
37/// A workspace's standing.
38#[derive(Clone, Debug, Serialize, Deserialize)]
39#[serde(rename_all = "camelCase")]
40pub struct Account {
41 pub workspace: String,
42 /// Credit left, in millionths of a dollar. Can dip below zero by the
43 /// cost of the runs that were under way when it ran out.
44 pub balance_micros: i64,
45 pub status: Status,
46 /// What is added to a run's cost, in percent.
47 pub margin_percent: u32,
Integrations: your own model provider, alerts that open issues, tickets agents read48 /// What a run on the workspace's own model provider is charged: g1t's
49 /// sandbox and orchestration, with the model paid for elsewhere.
50 pub orchestration_fee_micros: i64,
Agents as a team: lifecycle, merge queue, billing and a new shell51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(rename_all = "snake_case")]
55pub enum EntryKind {
56 /// Credit bought with a card.
57 TopUp,
58 /// An agent's run.
59 Usage,
60}
61
62/// One line of a workspace's statement.
63#[derive(Clone, Debug, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub struct LedgerEntry {
66 pub id: String,
67 pub kind: EntryKind,
68 /// Positive for credit added, negative for usage.
69 pub amount_micros: i64,
70 pub description: String,
71 /// For usage: the repository and pull request the agent worked on.
72 pub repo: Option<String>,
73 pub number: Option<u32>,
74 /// For usage: `implement`, `review` or `update`.
75 pub task: Option<String>,
76 /// For usage: the model, by its public name.
77 pub model: Option<String>,
Integrations: your own model provider, alerts that open issues, tickets agents read78 /// For usage: `g1t` when g1t paid the model provider, `workspace` when
79 /// the workspace's own account did and only orchestration is charged.
80 #[serde(default = "g1t")]
81 pub billed_to: String,
Agents as a team: lifecycle, merge queue, billing and a new shell82 /// For a top-up: the username of whoever paid.
83 pub created_by: Option<String>,
84 /// RFC 3339.
85 pub created_at: String,
86}
87
Integrations: your own model provider, alerts that open issues, tickets agents read88fn g1t() -> String {
89 "g1t".to_owned()
90}
91
Agents as a team: lifecycle, merge queue, billing and a new shell92/// `account` (`Outcome<Account>`) and `ledger` (`Outcome<Vec<LedgerEntry>>`,
93/// newest first). Members of the workspace only.
94#[derive(Debug, Serialize, Deserialize)]
95pub struct AccountArgs {
96 pub workspace: String,
97 pub viewer: Viewer,
98}
99
100/// `checkout`: starts a card payment for credit. Owners of the workspace
101/// only. Returns `Outcome<Checkout>`.
102#[derive(Debug, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct CheckoutArgs {
105 pub actor: User,
106 pub workspace: String,
107 /// How much credit to buy, in cents.
108 pub amount_cents: u32,
109 /// Where the payment page sends the person afterwards. The payment's
110 /// id is appended as `session`.
111 pub return_url: String,
112}
113
114#[derive(Debug, Serialize, Deserialize)]
115pub struct Checkout {
116 /// The payment page to send the person to.
117 pub url: String,
118}
119
120/// `confirm`: credits a payment once the provider says it was made. Safe
121/// to call any number of times. Returns `Outcome<Account>`.
122#[derive(Debug, Serialize, Deserialize)]
123pub struct ConfirmArgs {
124 pub workspace: String,
125 pub viewer: Viewer,
126 /// The payment's id, as returned to `return_url`.
127 pub session: String,
128}
129
130/// `can_start`: whether a workspace may start an agent now, asked before
131/// anything is opened for it. Returns `Outcome<bool>`: a failure, with the
132/// reason to show, when it has no credit.
133#[derive(Debug, Serialize, Deserialize)]
134pub struct CanStartArgs {
135 pub workspace: String,
136}
137
138/// `start_run`: asks whether a workspace may start an agent, and opens the
139/// run it will be charged for. Called by the runner service. Returns
140/// `Outcome<Option<RunTicket>>`: no ticket when billing is off, a failure
141/// when the workspace has no credit.
142#[derive(Debug, Serialize, Deserialize)]
143pub struct StartRunArgs {
144 pub workspace: String,
145 pub repo: RepoPath,
146 pub number: u32,
147 /// `implement`, `review` or `update`.
148 pub task: String,
149 /// The model, by its public name.
150 pub model: String,
Integrations: your own model provider, alerts that open issues, tickets agents read151 /// `workspace` when the run uses the workspace's own model provider.
Models per workspace: several providers, routed by kind of work152 /// The runner, which is TypeScript, sends it as `billedTo`.
153 #[serde(default = "g1t", alias = "billedTo")]
Integrations: your own model provider, alerts that open issues, tickets agents read154 pub billed_to: String,
Agents as a team: lifecycle, merge queue, billing and a new shell155}
156
157#[derive(Clone, Debug, Serialize, Deserialize)]
158#[serde(rename_all = "camelCase")]
159pub struct RunTicket {
160 pub run_id: String,
161 /// Lets the sandbox, and nothing else, report what this run cost.
162 pub token: String,
163}
164
165/// `finish_run`: what a run cost, as its sandbox reports it. Charged once.
166/// Returns `Outcome<bool>`.
167#[derive(Debug, Serialize, Deserialize)]
168#[serde(rename_all = "camelCase")]
169pub struct FinishRunArgs {
170 pub run_id: String,
171 pub token: String,
172 /// What the model provider charged, in US dollars.
173 pub cost_usd: f64,
174 #[serde(default)]
175 pub turns: u32,
176}
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request177
178
179/// `usage`: what a workspace's agents cost over a period, broken down.
180/// Members only. Returns `Outcome<Usage>`.
181#[derive(Debug, Serialize, Deserialize)]
182pub struct UsageArgs {
183 pub workspace: String,
184 pub viewer: Viewer,
185 /// RFC 3339: the start of the period. The period runs to now.
186 pub since: String,
187}
188
189/// One slice of usage: what it was for, what it cost, how many runs.
190#[derive(Clone, Debug, Serialize, Deserialize)]
191#[serde(rename_all = "camelCase")]
192pub struct UsageSlice {
193 pub key: String,
194 pub micros: i64,
195 pub runs: u32,
196}
197
198/// What a workspace's agents cost over a period.
199#[derive(Clone, Debug, Serialize, Deserialize)]
200#[serde(rename_all = "camelCase")]
201pub struct Usage {
202 pub since: String,
203 /// Charged, including g1t's margin.
204 pub spent_micros: i64,
Integrations: your own model provider, alerts that open issues, tickets agents read205 /// What g1t's model provider charged, before the margin.
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request206 pub cost_micros: i64,
Integrations: your own model provider, alerts that open issues, tickets agents read207 /// What runs on the workspace's own provider cost there, as the harness
208 /// estimated it. Not charged by g1t.
209 pub provider_micros: i64,
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request210 pub runs: u32,
211 /// Spend per day (`YYYY-MM-DD`) and task, as `day/task` keys.
212 pub by_day: Vec<UsageSlice>,
213 /// Per task: implement, review, revise, update, plan.
214 pub by_task: Vec<UsageSlice>,
215 /// Per repository, `namespace/name`.
216 pub by_repo: Vec<UsageSlice>,
217 /// The pull requests that cost most, as `namespace/name#number`.
218 pub by_pull: Vec<UsageSlice>,
219 /// Per model, by its public name.
220 pub by_model: Vec<UsageSlice>,
221 /// Credit bought in the period.
222 pub added_micros: i64,
223}
Models per workspace: several providers, routed by kind of work224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn who_pays_is_read_as_the_runner_sends_it() {
231 let run: StartRunArgs = serde_json::from_value(serde_json::json!({
232 "workspace": "acme",
233 "repo": { "namespace": "acme", "name": "web" },
234 "number": 7,
235 "task": "implement",
236 "model": "Claude Sonnet 5.5",
237 "billedTo": "workspace",
238 }))
239 .unwrap();
240 assert_eq!(run.billed_to, "workspace");
241 }
242}