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.
| Chat and workspace agents: channels, DMs and named agents you talk to | 1 | /** |
| 2 | * An agent's budget: what it has spent, whether it may reply, and what a | |
| 3 | * reply cost. Pure, so it is tested on its own. | |
| 4 | * | |
| The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were. | 5 | * Spend is limited at several levels (docs.g1t.sh/guides/agent-budgets/). |
| 6 | * The workspace's own limit and AI credit are billing's, checked by the | |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 7 | * compute gate. The agent's monthly and daily caps, and the budget of the |
| 8 | * person who asked, are checked here, before anything is reserved. | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 9 | * Spend counts what a reply costs at price: the model at the provider's |
| 10 | * price with billing's model margin, plus g1t's agent rate on every token. | |
| 11 | * The ledger (with comped terms and discounts) is billing's; the agent's | |
| 12 | * cap is about how much work it does, so it counts the list price. | |
| 13 | */ | |
| 14 | import type { AgentBudget, AgentStatus } from "@g1t/contracts"; | |
| 15 | ||
| 16 | import type { TokenPrice } from "../../runner/src/model-env.ts"; | |
| 17 | ||
| 18 | /** `2026-10`: the month spend is rolled up under, in UTC. */ | |
| 19 | export function monthKey(now: Date): string { | |
| 20 | return now.toISOString().slice(0, 7); | |
| 21 | } | |
| 22 | ||
| 23 | /** `2026-10-08`: the day spend is rolled up under, in UTC. */ | |
| 24 | export function dayKey(now: Date): string { | |
| 25 | return now.toISOString().slice(0, 10); | |
| 26 | } | |
| 27 | ||
| 28 | const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; | |
| 29 | ||
| 30 | export type Spent = { month: number; day: number }; | |
| 31 | ||
| 32 | export type BudgetBlock = { cap: "month" | "day"; message: string }; | |
| 33 | ||
| 34 | const capSet = (cap: number | null | undefined): cap is number => typeof cap === "number" && Number.isFinite(cap) && cap > 0; | |
| 35 | ||
| 36 | /** | |
| 37 | * Why the agent may not take another reply on its own budget, as it says | |
| 38 | * so in chat, or null when it may. A cap of zero or null is no cap. | |
| 39 | */ | |
| 40 | export function budgetBlock(budget: AgentBudget, spent: Spent, now: Date): BudgetBlock | null { | |
| 41 | if (capSet(budget.monthly_micros) && spent.month >= budget.monthly_micros) { | |
| 42 | return { | |
| 43 | cap: "month", | |
| 44 | message: `I'm out of budget for ${MONTHS[now.getUTCMonth()]}. An owner can raise my monthly limit on my profile.`, | |
| 45 | }; | |
| 46 | } | |
| 47 | if (capSet(budget.daily_micros) && spent.day >= budget.daily_micros) { | |
| 48 | return { | |
| 49 | cap: "day", | |
| 50 | message: "I've used today's budget. I'll be back tomorrow (UTC), or an owner can raise my daily limit on my profile.", | |
| 51 | }; | |
| 52 | } | |
| 53 | return null; | |
| 54 | } | |
| 55 | ||
| 56 | /** | |
| 57 | * The most one reply may spend on models, in millionths of a dollar: the | |
| 58 | * lowest of what is left of the agent's month and day, its per-task cap, | |
| 59 | * and the plan's per-run cap. Null when nothing caps it. Never below 1, so | |
| 60 | * a cap is never read as "none". | |
| 61 | */ | |
| 62 | export function replyCapMicros(budget: AgentBudget, spent: Spent, planRunCapMicros: number | null): number | null { | |
| 63 | const caps: number[] = []; | |
| 64 | if (capSet(budget.monthly_micros)) caps.push(budget.monthly_micros - spent.month); | |
| 65 | if (capSet(budget.daily_micros)) caps.push(budget.daily_micros - spent.day); | |
| 66 | if (capSet(budget.task_micros)) caps.push(budget.task_micros); | |
| 67 | if (capSet(planRunCapMicros)) caps.push(planRunCapMicros); | |
| 68 | return caps.length ? Math.max(1, Math.floor(Math.min(...caps))) : null; | |
| 69 | } | |
| 70 | ||
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 71 | export type Span = { span: "month" | "last_month" | "7d" | "30d"; from: string; until: string; period: string }; |
| 72 | ||
| 73 | /** | |
| 74 | * The days a spend breakdown covers, in UTC, both ends included: this | |
| 75 | * month to today (anything not asked for), last month whole, or the last | |
| 76 | * 7 or 30 days to today. `period` is the month it ends in. | |
| 77 | */ | |
| 78 | export function spendSpan(asked: unknown, now: Date): Span { | |
| 79 | const today = dayKey(now); | |
| 80 | const day = (offset: number) => dayKey(new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + offset))); | |
| 81 | switch (asked) { | |
| 82 | case "last_month": { | |
| 83 | const end = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 0)); | |
| 84 | const until = dayKey(end); | |
| 85 | return { span: "last_month", from: `${until.slice(0, 7)}-01`, until, period: until.slice(0, 7) }; | |
| 86 | } | |
| 87 | case "7d": | |
| 88 | return { span: "7d", from: day(-6), until: today, period: monthKey(now) }; | |
| 89 | case "30d": | |
| 90 | return { span: "30d", from: day(-29), until: today, period: monthKey(now) }; | |
| 91 | default: | |
| 92 | return { span: "month", from: `${monthKey(now)}-01`, until: today, period: monthKey(now) }; | |
| 93 | } | |
| 94 | } | |
| 95 | ||
| 96 | /** | |
| 97 | * The budget that applies to one person: their own when an owner set one | |
| 98 | * (0 there means none at all), else the workspace's per-person default. | |
| 99 | * Null: no budget. | |
| 100 | */ | |
| 101 | export function personLimit(defaultMicros: number | null | undefined, own: number | null | undefined): number | null { | |
| 102 | if (typeof own === "number" && Number.isFinite(own)) return own > 0 ? Math.floor(own) : null; | |
| 103 | return capSet(defaultMicros) ? Math.floor(defaultMicros) : null; | |
| 104 | } | |
| 105 | ||
| 106 | /** | |
| 107 | * Why work asked for by `username` may not start: what agents spent for | |
| 108 | * them this month has reached their budget. Null when it may. | |
| 109 | */ | |
| 110 | export function personBlock(username: string, limit: number | null, spent: number, now: Date): string | null { | |
| 111 | if (!capSet(limit) || spent < limit) return null; | |
| 112 | return `@${username} has used their agent budget for ${MONTHS[now.getUTCMonth()]}. An owner can raise it under Workspace → Spend.`; | |
| 113 | } | |
| 114 | ||
| Chat and workspace agents: channels, DMs and named agents you talk to | 115 | /** The tokens one answer used, by kind. */ |
| 116 | export type Tokens = { input: number; output: number; cacheRead: number; cacheWrite: number }; | |
| 117 | ||
| 118 | export function totalTokens(tokens: Tokens): number { | |
| 119 | return tokens.input + tokens.output + tokens.cacheRead + tokens.cacheWrite; | |
| 120 | } | |
| 121 | ||
| 122 | /** What `tokens` cost at `price` (dollars per million tokens), in millionths of a dollar. */ | |
| 123 | export function costMicros(tokens: Tokens, price: TokenPrice | null): number { | |
| 124 | if (!price) return 0; | |
| 125 | const micros = tokens.input * price.input + tokens.output * price.output + tokens.cacheRead * price.cacheRead + tokens.cacheWrite * price.cacheWrite; | |
| 126 | return Math.ceil(Math.max(0, micros)); | |
| 127 | } | |
| 128 | ||
| 129 | /** | |
| 130 | * What a reply counts against the agent's budget: on g1t's models, the | |
| 131 | * model's cost with the model margin; on the workspace's own provider, | |
| 132 | * nothing for the model (the provider bills the workspace). On both, the | |
| 133 | * agent rate (`agent_tokens` or `agent_tokens_own` in the price book, per | |
| 134 | * million tokens) on every token. | |
| 135 | */ | |
| 136 | export function chargedMicros(input: { | |
| 137 | costMicros: number; | |
| 138 | hosted: boolean; | |
| 139 | marginPercent: number; | |
| 140 | ratePerMillionMicros: number; | |
| 141 | tokens: number; | |
| 142 | }): number { | |
| 143 | const model = input.hosted ? Math.ceil((input.costMicros * (100 + Math.max(0, input.marginPercent))) / 100) : 0; | |
| 144 | const rate = Math.ceil((Math.max(0, input.tokens) * Math.max(0, input.ratePerMillionMicros)) / 1_000_000); | |
| 145 | return model + rate; | |
| 146 | } | |
| 147 | ||
| 148 | /** The agent's presence, as the Agents page shows it. */ | |
| 149 | export function agentStatus(input: { busyUntil: string | null; now: Date; blocked: boolean }): AgentStatus { | |
| 150 | if (input.busyUntil && Date.parse(input.busyUntil) > input.now.getTime()) return "working"; | |
| 151 | if (input.blocked) return "out_of_budget"; | |
| 152 | return "idle"; | |
| 153 | } |