pr_01m47d24b0e6n91zwymwxg0vpx/crates/contracts/src/billing.rs

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