Skip to content

Commit

Spend adds up on one ledger. Charged this month has one definition, price less discount, included usage and credit, shared by the plan card, the spend limit, Spend and the top bar; the limit had counted usage not yet closed at full price before the discount, so a comped workspace read as charged a cent. Every agent line on the ledger names the agent and who asked, repository runs by g1t included, so Spent is the sum of its products, Agents is the agent product, and by agent adds up to it; the billing API returns by_agent and by_person. The usage and billing guide says how spend is counted.

syntaqxcommitted Parenta3af5aaBrowse files
21 files+552−220/21 viewed
+1−1
17671767 "Choose what happens when a team is asked to review a pull request. Off, everyone in it is asked. On (`enabled`), g1t picks `count` people from it (1 to 10, never the pull request's author) and asks them, and the team stays shown as asked beside them: `round_robin` picks whoever this team asked least recently, `load_balance` whoever has the fewest pull requests waiting on their review. `skip_busy` leaves out anyone with `busy_at` or more waiting; `include_child_teams` also picks from its child teams' people; `excluded` lists usernames never picked; `notify_team` also tells the rest of the team. Fields left out keep their current value. Owners of the workspace and the team's maintainers. People only. Returns the team."
17681768 }
17691769 Op::GetUsage => {
1770− "A workspace's usage over a range of days, at price, and what paid for it. `from` and `until` are UTC days, `YYYY-MM-DD`, with `until` included and at most 400 days in all; left out, the current month so far. `products` narrows it to product families (agent, sandboxes, gateway, deployments, git_storage, packages, security, search) and `projects` to repositories (\"owner/name\"). Returns `totals`: `price_micros` less `discount_micros`, `included_micros` and `credits_micros` is `charged_micros`, what is left for the workspace to pay; `pending_micros` is metered this month and charged when it closes; `cost_micros` is what it cost g1t. Then `days` (each day and product with usage), `products` (every family, with its meters: quantity, unit, amount, a `daily` amount for each day of the range, any `allowance`, the split `by_project`, and a `note` where the quantity needs one: the agent rate's meters, `agent_rate` and `agent_rate_own` (on the workspace's own model key), count weighted tokens and name the weights), `projects` (every repository with usage in the range), `models` (the agent's input, output, cache-read and cache-write tokens by model, most first), and the AI credit and other credit left now. With `group_by` (`product`, `project` or `day`), `groups` adds up the range that way. Amounts are whole millionths of a dollar. Members of the workspace only."
1770+ "A workspace's usage over a range of days, at price, and what paid for it. `from` and `until` are UTC days, `YYYY-MM-DD`, with `until` included and at most 400 days in all; left out, the current month so far. `products` narrows it to product families (agent, sandboxes, gateway, deployments, git_storage, packages, security, search) and `projects` to repositories (\"owner/name\"). Returns `totals`: `price_micros` less `discount_micros`, `included_micros` and `credits_micros` is `charged_micros`, what is left for the workspace to pay; `pending_micros` is metered this month and charged when it closes; `cost_micros` is what it cost g1t. Then `days` (each day and product with usage), `products` (every family, with its meters: quantity, unit, amount, a `daily` amount for each day of the range, any `allowance`, the split `by_project`, and a `note` where the quantity needs one: the agent rate's meters, `agent_rate` and `agent_rate_own` (on the workspace's own model key), count weighted tokens and name the weights), `projects` (every repository with usage in the range), `models` (the agent's input, output, cache-read and cache-write tokens by model, most first), `by_agent` and `by_person` (the agent product at price by the agent that did the work, by handle, and by who asked, by username; `g1t` is g1t's own runs on repositories, an empty key is work attributed to no one; each sums to the agent product), and the AI credit and other credit left now. With `group_by` (`product`, `project` or `day`), `groups` adds up the range that way. Amounts are whole millionths of a dollar. Members of the workspace only."
17711771 }
17721772 Op::GetBudget => {
17731773 "A workspace's budget: its monthly spend limit (`amount_micros`; `automatic` is true while the owners have not set one, and it is then $200 or twice last month's spend), what was charged this month (`spent_micros`), the most the owners may set it to themselves (`max_amount_micros`), its `alerts` (percent of the limit, each emailed to the owners once a month), whether usage pauses at the limit (`pause_at_limit`), the `webhook` told of each alert, and `state`: `ok`, `warning` or `stopped`, with a `message` when work is stopped or close to it. Members of the workspace only."
+28−0
63656365 "cache_write": 590000
63666366 }
63676367 ],
6368+ "by_agent": [
6369+ {
6370+ "key": "g1t",
6371+ "label": "@g1t",
6372+ "micros": 21400000,
6373+ "count": 14
6374+ },
6375+ {
6376+ "key": "mike",
6377+ "label": "@mike",
6378+ "micros": 800000,
6379+ "count": 6
6380+ }
6381+ ],
6382+ "by_person": [
6383+ {
6384+ "key": "chase",
6385+ "label": "@chase",
6386+ "micros": 21900000,
6387+ "count": 19
6388+ },
6389+ {
6390+ "key": "",
6391+ "label": "No one asked",
6392+ "micros": 300000,
6393+ "count": 1
6394+ }
6395+ ],
63686396 "included": {
63696397 "used": 20,
63706398 "of": 20,
+8−0
2424
2525 Months and days are UTC. An idle agent costs nothing.
2626
27+Every budget below the workspace's counts an agent's chat replies and
28+sessions at list price, as the ledger enters them, before any discount or
29+included usage; the workspace's spend limit is on what is charged after
30+those. g1t's own runs on your repositories are on the same ledger as
31+**@g1t** and count toward the spend limit, not toward the agent budgets.
32+[How spend is counted](/guides/usage-and-billing/#how-spend-is-counted)
33+defines each figure.
34+
2735 An agent on several teams with budgets is held to each of them. Its work
2836 counts towards every team it is on, and towards its own budget, at once.
2937 A team's header shows what its agents spent this month against its
+4−4
3131
3232 | View | Figures |
3333 | --- | --- |
34−| **Workspace** | **Spent**: usage at price over the period, every product, the same figure as [Usage](/guides/usage-and-billing/#the-usage-page). **Agents**: every agent's replies and sessions. **Charged**: what the workspace was charged this month, against its spend limit when it has one. |
34+| **Workspace** | **Spent**: usage at price over the period, every product, the same figure as [Usage](/guides/usage-and-billing/#the-usage-page). **Agents**: the agent product within it, every agent's replies, sessions and runs on repositories at price, from the same ledger. **Charged**: what the workspace was charged this month after included usage, credit and any discount, the same figure as Billing, against its spend limit when it has one. [How spend is counted](/guides/usage-and-billing/#how-spend-is-counted) defines each. |
3535 | **You** | **Agents for you** over the period. **Your budget** this month, and what is spent against it. **Your chats with agents**: the replies agents wrote you. |
3636
3737 **By day** shows the period one day at a time. Hover or focus a day for
4343
4444 | Slice | Shows |
4545 | --- | --- |
46−| **Agents** | Each agent's share. A session's helpers and subagents count on the agent that started it. Open one for its own Spend tab. |
47−| **People** | Who asked. Routines and agent-to-agent work have no asker. Workspace view only. |
46+| **Agents** | Each agent's share of the agent product, from the ledger: its replies, sessions, runs on repositories and sandbox time at price. **@g1t** is g1t's own work on your repositories. A session's helpers and subagents count on the agent that started it. Open one for its own Spend tab. In the **You** view, the agents service's count of what agents did for you. |
47+| **People** | Who asked, from the ledger. Routines, agent-to-agent work and runs from before attribution have no asker. Workspace view only. |
4848 | **Channels** | Where it was asked: each channel, direct messages together, and channels you aren't in together, without their names. |
4949 | **Models** | Each model's share. |
5050 | **Kind of work** | Chat replies, sessions, routines, helping colleagues and subagents. |
185185
186186 - [Effort](/guides/agents/#effort): how hard each agent works, and what each level costs.
187187 - [Agent budgets and spend](/guides/agent-budgets/): who pays for a session tree.
188−- [Usage and billing](/guides/usage-and-billing/): the spend limit, caps, AI credit and rates.
188+- [Usage and billing](/guides/usage-and-billing/): the spend limit, caps, AI credit and rates, and [how spend is counted](/guides/usage-and-billing/#how-spend-is-counted).
189189 - [Sessions](/guides/agent-sessions/): caps and approving more spend.
+28−2
526526 workspace: its pushes to private repositories stop at 1 GB instead. On the
527527 plan they never stop.
528528
529+## How spend is counted
530+
531+Everything a workspace uses is entered on one ledger, as it happens, and
532+every figure on every page is read back from it. There is no second count
533+to disagree with it.
534+
535+| Figure | What it is | Where it shows |
536+| --- | --- | --- |
537+| **Spent**, or usage **at price** | Every entry at its price: what was charged, plus what the plan's included usage, the trial, a pool or g1t paid of it, plus what a discount took off. The one measure of usage. | Home's **Spent** card, Spend's **Spent**, the [Usage page](#the-usage-page), the [statement](#the-statement) |
538+| **Agents** | The agent product within Spent: model tokens, the [agent rate](#the-agent-rate) and agents' sandbox time, for every agent's chat replies, sessions and runs on repositories. Each entry names the agent that did the work (g1t's own runs on your repositories are **@g1t**) and the person who asked, so **by agent** and **by person** are the same money sliced two ways, and the agents add up to the product. | The top bar's spend pill, Spend's **Agents** tile and its **Agents** and **People** slices, Usage's **Agent** product |
539+| **Charged** | What is left for the workspace to pay this month: usage at price, less your discount, less what included usage, the trial, a pool or g1t paid, less what [credit](#credits-from-g1t) paid. Never below zero: a workspace with a 100% discount is charged exactly nothing. Usage metered through the month and charged when it closes (storage, git operations, scans, embeddings, domains, app traffic) counts now on the same terms. | Billing's plan card, Spend's **Charged**, the top bar's **Workspace** view, and the figure your [spend limit](#your-spend-limit) is measured against |
540+
541+The [agent budgets](/guides/agent-budgets/) count the same entries at
542+the same price, before any discount or included usage: an agent's chat
543+replies and sessions against its own budget, its team's, the person's and
544+the workspace's agent budget. Budgets at list price are what makes them
545+predictable; the spend limit is on what is charged.
546+
547+Through the API, `GET /workspaces/:workspace/usage` answers with the
548+products, `by_agent` and `by_person`, and `totals.charged_micros`;
549+`GET /workspaces/:workspace/budget` answers with the spend limit and
550+`spent_micros`, the same charged figure.
551+
529552 ## Limits
530553
531554 Every workspace has two limits: **g1t's ceiling** on usage not yet paid
567590
568591 ### Your spend limit
569592
570−What the workspace may be charged in a month (UTC). At it, new work stops
571−until the month turns or an owner raises it. Until owners set one, it is
593+What the workspace may be charged in a month (UTC): usage at price after
594+your discount, included usage, the trial, pools and credit, as
595+[How spend is counted](#how-spend-is-counted) defines it, the same figure
596+as the plan card's **charged**. At it, new work stops until the month
597+turns or an owner raises it. Until owners set one, it is
572598 **automatic**: $200, or twice last month's spend, whichever is more, up to
573599 what is available.
574600
+0−0

Binary or large file; its contents are not shown.

+9−5
99 */
1010 import type { AgentPolicy, AgentSpendBreakdown, Limit, PersonBudgets, SpendPeriod, UsageReport, User, WorkspaceAgent } from "@g1t/contracts";
1111
12−import { type Pricing, type SpendScope, pricingOf, spanFor } from "./spend";
12+import { type Pricing, type SpendScope, agentMicros, attributionSlices, pricingOf, spanFor } from "./spend";
1313 import { billing, workspaceAgents } from "./services.server";
1414
1515 const warn = (what: string) => (error: unknown) => {
9696
9797 export async function loadPill(viewer: User, slug: string, mayWorkspace: boolean): Promise<PillData> {
9898 const me = viewer.username.toLowerCase();
99− const [mine, people, everyone, limit, used] = await Promise.all([
99+ const [mine, people, everyone, limit, usage] = await Promise.all([
100100 loadBreakdown(viewer, slug, "me", "month"),
101101 workspaceAgents.personBudgets(slug, viewer).then(value).catch(warn("person budgets")),
102102 mayWorkspace ? loadBreakdown(viewer, slug, "workspace", "month") : Promise.resolve(null),
103103 mayWorkspace ? billing.limit(slug, viewer).then(value).catch(warn("limit")) : Promise.resolve(null),
104+ // The workspace's agents figure is the agent product on billing's
105+ // ledger, the same ledger "charged" is read from; the agents service's
106+ // own count stands in only while billing has not answered.
104107 mayWorkspace ? loadUsage(viewer, slug, "month", new Date()) : Promise.resolve(null),
105108 ]);
106109 const own = people?.people.find((p) => p.username === me);
107110 const budget = own ? own.monthly_micros : (people?.default_micros ?? null);
111+ const attributed = attributionSlices(usage);
108112 return {
109113 month: (mine ?? everyone)?.period ?? new Date().toISOString().slice(0, 7),
110114 me: mine ? { spentMicros: mine.total_micros, budgetMicros: budget, byKind: mine.by_kind, byAgent: mine.by_agent } : null,
111115 workspace: mayWorkspace
112116 ? {
113− spentMicros: used ? (used.free ? used.totals.costMicros : used.totals.priceMicros) : null,
117+ spentMicros: usage ? (usage.free ? usage.totals.costMicros : usage.totals.priceMicros) : null,
114118 chargedMicros: limit?.spentMicros ?? null,
115119 limitMicros: limit ? (limit.spendLimitMicros ?? limit.ceilingMicros) : null,
116− agentsMicros: everyone?.total_micros ?? null,
117− byAgent: everyone?.by_agent ?? [],
120+ agentsMicros: usage ? agentMicros(usage) : (everyone?.total_micros ?? null),
121+ byAgent: attributed?.agent ?? everyone?.by_agent ?? [],
118122 }
119123 : null,
120124 };
+61−2
11 import assert from "node:assert/strict";
22 import { test } from "node:test";
33
4−import type { AgentSession, PriceBook } from "@g1t/contracts";
4+import type { AgentSession, MeterLine, PriceBook, UsageReport } from "@g1t/contracts";
55
6−import { agentRateLabel, daySeries, markupLabel, percentLabel, pricingOf, readPeriod, readScope, receiptOf, spanFor, tokenCount } from "./spend.ts";
6+import { agentMicros, agentRateLabel, agentSpendHref, attributionSlices, daySeries, markupLabel, percentLabel, pricingOf, readPeriod, readScope, receiptOf, spanFor, tokenCount } from "./spend.ts";
77
88 const OCT_8 = new Date("2026-10-08T15:00:00Z");
99
9898 assert.equal(percentLabel(62, 100), "62%");
9999 assert.equal(percentLabel(62, null), "");
100100 });
101+
102+/** A usage report as billing answers it for a comped workspace in October: every line discounted in full, attributed to its agent. */
103+function octoberReport(): Pick<UsageReport, "totals" | "products" | "byAgent" | "byPerson"> {
104+ const meter = (key: string, product: string, micros: number): MeterLine => ({ key, label: key, product, unit: "entries", quantity: 1, micros, daily: [micros], byProject: [] });
105+ const products = [
106+ { key: "agent", label: "Agent", micros: 2_544_000, meters: [meter("agent_models", "agent", 1_900_000), meter("agent_rate", "agent", 400_000), meter("agent_sandbox", "agent", 244_000)] },
107+ { key: "sandboxes", label: "Sandboxes", micros: 3_360_000, meters: [meter("sandbox", "sandboxes", 3_360_000)] },
108+ { key: "deployments", label: "Deployments", micros: 4_480_000, meters: [meter("requests", "deployments", 4_480_000)] },
109+ ];
110+ return {
111+ totals: { priceMicros: 10_384_000, discountMicros: 10_384_000, includedMicros: 0, creditsMicros: 0, chargedMicros: 0, pendingMicros: 0, costMicros: 8_650_000 },
112+ products,
113+ byAgent: [
114+ { key: "g1t", label: "@g1t", micros: 2_520_000, count: 9 },
115+ { key: "mike", label: "@mike", micros: 20_000, count: 2 },
116+ { key: "margo", label: "@margo", micros: 4_000, count: 1 },
117+ ],
118+ byPerson: [
119+ { key: "chase", label: "@chase", micros: 2_540_000, count: 11 },
120+ { key: "", label: "No one asked", micros: 4_000, count: 1 },
121+ ],
122+ };
123+}
124+
125+test("Home's Spent, Spend's tiles and the top bar's Agents reconcile on the one ledger", () => {
126+ const report = octoberReport();
127+ // Spent: every product at price, which is the report's total.
128+ const spent = report.products.reduce((n, p) => n + p.micros, 0);
129+ assert.equal(spent, report.totals.priceMicros);
130+ // Agents: the agent product, part of Spent, and what the agents by handle add up to.
131+ assert.equal(agentMicros(report), 2_544_000);
132+ assert.ok(agentMicros(report) <= spent);
133+ const slices = attributionSlices(report);
134+ assert.ok(slices);
135+ assert.equal(
136+ slices.agent.reduce((n, s) => n + s.micros, 0),
137+ agentMicros(report),
138+ );
139+ assert.equal(
140+ slices.person.reduce((n, s) => n + s.micros, 0),
141+ agentMicros(report),
142+ );
143+ assert.ok(slices.agent.every((s) => s.micros <= agentMicros(report)));
144+ // Charged: nothing, on a 100% discount, however much was spent.
145+ assert.equal(report.totals.chargedMicros, 0);
146+ assert.equal(report.totals.priceMicros - report.totals.discountMicros - report.totals.includedMicros - report.totals.creditsMicros, 0);
147+});
148+
149+test("an agent's slice opens its own Spend tab; work attributed to no one opens nothing", () => {
150+ const slices = attributionSlices(octoberReport());
151+ assert.ok(slices);
152+ assert.equal(agentSpendHref("flagon-io", slices.agent[0]), "/flagon-io/-/agents/g1t/spend");
153+ assert.equal(agentSpendHref("flagon-io", slices.person[1]), null);
154+ assert.deepEqual(slices.agent[1], { key: "mike", label: "@mike", micros: 20_000, count: 2 });
155+ // A report from before attribution has no slices: the caller falls back.
156+ assert.equal(attributionSlices({ byAgent: undefined, byPerson: undefined }), null);
157+ assert.equal(attributionSlices(null), null);
158+ assert.equal(agentMicros({ products: [] }), 0);
159+});
+29−1
66 * imports, so it is tested on its own. Every number comes from a service;
77 * nothing here invents one.
88 */
9−import type { AgentSession, PriceBook, SpendPeriod, SpendSlice, UsageDay } from "@g1t/contracts";
9+import type { AgentSession, PriceBook, SpendPeriod, SpendSlice, UsageDay, UsageReport, UsageShare } from "@g1t/contracts";
1010
1111 /** Whose spend: your own (what agents did for you), or the whole workspace's. */
1212 export type SpendScope = "me" | "workspace";
1313
14+/**
15+ * What agents cost over the report's range, at price: the agent product
16+ * (model tokens, the agent rate and agents' sandbox time), every agent's
17+ * replies, sessions and runs on repositories together. The top bar's and
18+ * Spend's "Agents" figure, from the same ledger as "Spent", so it is part
19+ * of it: Spent = Agents + the other products.
20+ */
21+export function agentMicros(report: Pick<UsageReport, "products">): number {
22+ return report.products.find((p) => p.key === "agent")?.micros ?? 0;
23+}
24+
25+/**
26+ * The agent product by the agent that did the work and by who asked, as
27+ * Spend's slices, from the ledger's own attribution. Null when billing did
28+ * not attribute the range (a report from before attribution), so the
29+ * caller can fall back to the agents service's own count.
30+ */
31+export function attributionSlices(report: Pick<UsageReport, "byAgent" | "byPerson"> | null): { agent: SpendSlice[]; person: SpendSlice[] } | null {
32+ if (!report?.byAgent || !report.byPerson) return null;
33+ const slice = (s: UsageShare): SpendSlice => ({ key: s.key, label: s.label, micros: s.micros, count: s.count });
34+ return { agent: report.byAgent.map(slice), person: report.byPerson.map(slice) };
35+}
36+
37+/** Where an agent's slice opens: its own Spend tab, for a slice that names one. */
38+export function agentSpendHref(slug: string, slice: Pick<SpendSlice, "key">): string | null {
39+ return slice.key ? `/${slug}/-/agents/${slice.key}/spend` : null;
40+}
41+
1442 export const SPEND_PERIODS: { key: SpendPeriod; label: string; short: string }[] = [
1543 { key: "month", label: "This month", short: "This month" },
1644 { key: "last_month", label: "Last month", short: "Last month" },
+34−4
3232 import { page } from "../../lib/meta";
3333 import { workspaceAgents } from "../../lib/services.server";
3434 import { managesBilling, requireUser, roleIn } from "../../lib/session.server";
35−import { SPEND_PERIODS, type SpendScope, daySeries, periodLabel, readPeriod, readScope, spanFor, usageByDay, withoutSelf } from "../../lib/spend";
35+import { SPEND_PERIODS, type SpendScope, agentMicros, agentSpendHref, attributionSlices, daySeries, periodLabel, readPeriod, readScope, spanFor, usageByDay, withoutSelf } from "../../lib/spend";
3636 import { loadBreakdown, loadBudgets, loadPricing, loadUsage } from "../../lib/spend.server";
3737 import { money } from "../../lib/usage";
3838
196196 {scope === "workspace" ? (
197197 <>
198198 <Tile label={`Spent, ${label}`} value={used ? money(used.free ? used.totals.costMicros : used.totals.priceMicros) : "—"} sub={used ? (used.free ? "At cost: g1t charges nothing for now" : "Usage at price, every product") : "Usage couldn't be read"} />
199− <Tile label={`Agents, ${label}`} value={spent ? money(spent.total_micros) : "—"} sub={spent ? `${countOf(spent.by_kind)} replies and sessions` : "The agents service didn't answer"} />
200199 <Tile
200+ label={`Agents, ${label}`}
201+ value={used ? money(agentMicros(used)) : spent ? money(spent.total_micros) : "—"}
202+ sub={
203+ used
204+ ? `Every agent's replies, sessions and runs, at price${spent ? `; ${countOf(spent.by_kind)} replies and sessions` : ""}`
205+ : spent
206+ ? `${countOf(spent.by_kind)} replies and sessions, from the agents service`
207+ : "Usage couldn't be read"
208+ }
209+ />
210+ <Tile
201211 label="Charged, this month"
202212 value={levels.limit?.spentMicros != null ? money(levels.limit.spentMicros) : "—"}
203213 sub={levels.limit ? limitLine(levels.limit.spentMicros ?? 0, levels.limit.spendLimitMicros ?? levels.limit.ceilingMicros) : "Billing couldn't be read"}
296306 </Section>
297307
298308 <p className="text-xs text-faint">
299− Agent figures count every reply and session at list price, as budgets do. What the workspace is charged, after included usage, credit and any
300− discount, is on{" "}
309+ Every figure is the ledger&apos;s, at price: Spent is every product, Agents is the agent product within it, and the agent budgets count replies and
310+ sessions at the same price. What the workspace is charged, after included usage, credit and any discount, is on{" "}
301311 <Link to={`/${slug}/-/billing`} className="hover:text-fg hover:underline">
302312 Billing
303313 </Link>
352362 </SlicePanel>
353363 );
354364 }
365+ // The workspace's agents and people come from the ledger that is
366+ // charged: every agent's replies, sessions and runs on repositories, by
367+ // the agent that did them and by who asked. Your own view, and the
368+ // channels, models and kinds of work, are the agents service's.
369+ const attributed = scope === "workspace" ? attributionSlices(used) : null;
370+ if (attributed && (by === "agent" || by === "person")) {
371+ const chosen =
372+ by === "agent"
373+ ? {
374+ slices: attributed.agent,
375+ foot: "From the ledger, at price: each agent's replies, sessions, runs on repositories and sandbox time. @g1t is g1t's own work on your repositories.",
376+ link: (s: SpendSlice) => agentSpendHref(slug, s),
377+ }
378+ : { slices: attributed.person, foot: "Who asked, from the ledger. Routines, agent-to-agent work and runs from before attribution have no asker.", link: undefined };
379+ return (
380+ <SlicePanel foot={chosen.foot}>
381+ <SliceList slices={chosen.slices} unit="entries" empty="Nothing spent in this period." href={chosen.link} />
382+ </SlicePanel>
383+ );
384+ }
355385 if (!spent) return <Unavailable what="The agents service" />;
356386 const pick: Record<Exclude<SliceKey, "extension" | "product">, { slices: SpendSlice[]; unit: string; foot?: ReactNode; link?: (s: SpendSlice) => string | null }> = {
357387 agent: {
+50−1
284284 /// None when the workspace's own provider names its model.
285285 #[serde(default)]
286286 pub tier: Option<String>,
287+ /// The agent doing the work, by handle: a workspace agent's (the one
288+ /// whose budget pays), or `g1t` for g1t's own work on a repository.
289+ /// Every line the run puts on the ledger carries it, so Spend's "by
290+ /// agent" reads from the ledger that is charged.
291+ #[serde(default)]
292+ pub agent: Option<String>,
293+ /// Who asked for the work, by username; none for a routine's or
294+ /// another agent's. The runner, which is TypeScript, sends `askedBy`.
295+ #[serde(default, alias = "askedBy")]
296+ pub asked_by: Option<String>,
287297 }
288298
289299 #[derive(Clone, Debug, Serialize, Deserialize)]
11021112 /// one. A larger machine's memory and disk cost more each second.
11031113 #[serde(default)]
11041114 pub instance: Option<String>,
1115+ /// The agent whose work this was, by handle (`g1t` for g1t's own runs
1116+ /// on a repository), so an agent's sandbox time is attributed with
1117+ /// the rest of its work. Absent for checks, workflows and builds.
1118+ #[serde(default)]
1119+ pub agent: Option<String>,
1120+ /// Who asked for the work, by username.
1121+ #[serde(default, alias = "asked_by")]
1122+ pub asked_by: Option<String>,
11051123 }
11061124
11071125 /// How much a workspace has earned g1t's trust with money, which sets how
11601178 pub state: LimitState,
11611179 /// What to tell people when work is stopped or close to it.
11621180 pub message: Option<String>,
1163− /// Charged this month, which the spend limit is measured against.
1181+ /// Charged this month, which the spend limit is measured against: the
1182+ /// month's usage at price, less the account's discount, what the
1183+ /// plan's included usage, the trial or a pool paid, and what credit
1184+ /// paid; usage metered through the month and charged when it closes
1185+ /// counted on the same terms. The one figure the Billing page's plan
1186+ /// card, Spend's "Charged" and the top bar show (`UsageTotals::charged_micros`
1187+ /// over the month is the same number).
11641188 #[serde(default)]
11651189 pub spent_micros: i64,
11661190 /// True while the owners have not chosen a spend limit of their own, so
40304054 pub cost_micros: i64,
40314055 }
40324056
4057+/// One agent's or one person's share of the agent product over the range,
4058+/// at price, from the ledger lines attributed to them.
4059+#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
4060+#[serde(rename_all = "camelCase")]
4061+pub struct UsageShare {
4062+ /// The agent's handle or the person's username; empty for lines
4063+ /// attributed to no one (work from before attribution, or that no
4064+ /// person asked for).
4065+ pub key: String,
4066+ pub label: String,
4067+ pub micros: i64,
4068+ /// Ledger lines.
4069+ pub count: u32,
4070+}
4071+
40334072 /// One day's usage of one product, at price.
40344073 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
40354074 #[serde(rename_all = "camelCase")]
41394178 /// Agent tokens by model over the range, most first.
41404179 #[serde(default)]
41414180 pub models: Vec<ModelTokens>,
4181+ /// The agent product (model tokens, the agent rate and agents'
4182+ /// sandbox time) by the agent that did the work, most first. Their sum
4183+ /// is the agent product's total: what agents cost is what the ledger
4184+ /// charges for them, not a second count.
4185+ #[serde(default)]
4186+ pub by_agent: Vec<UsageShare>,
4187+ /// The same by who asked; work no person asked for (routines, agents
4188+ /// helping agents, lines from before attribution) under an empty key.
4189+ #[serde(default)]
4190+ pub by_person: Vec<UsageShare>,
41424191 /// The plan's included usage this month, when the workspace has it.
41434192 #[serde(default)]
41444193 pub included: Option<Allowance>,
+21−1
727727 spendLimitMicros: number | null;
728728 state: "ok" | "warning" | "stopped";
729729 message: string | null;
730− /** Charged this month: what the spend limit is measured against. */
730+ /**
731+ * Charged this month, which the spend limit is measured against: usage at
732+ * price less the discount, included usage, the trial, pools and credit,
733+ * with usage metered through the month counted on the same terms. The
734+ * same number as the month's `UsageTotals.chargedMicros`.
735+ */
731736 spentMicros?: number;
732737 /** True while the owners have not chosen a limit, so the automatic one applies: $200, or twice last month's spend. */
733738 defaultSpendLimit?: boolean;
11251130 selfHosted?: boolean;
11261131 /** The machine it ran on, by label (`g1t-4core`); absent, the standard one. */
11271132 instance?: string | null;
1133+ /** The agent whose work this was, by handle (`g1t` for g1t's own runs on a repository); absent for checks, workflows and builds. */
1134+ agent?: string | null;
1135+ /** Who asked for the work, by username. */
1136+ askedBy?: string | null;
11281137 }): Promise<Result<boolean>>;
11291138 startRun(run: {
11301139 workspace: string;
11421151 session?: string | null;
11431152 /** The tier g1t routed the run to: `small`, `large` or `frontier`. */
11441153 tier?: "small" | "large" | "frontier" | null;
1154+ /** The agent doing the work, by handle: a workspace agent's, or `g1t` for g1t's own work on a repository. Every line of the run carries it. */
1155+ agent?: string | null;
1156+ /** Who asked, by username; none for a routine's or another agent's work. */
1157+ askedBy?: string | null;
11451158 }): Promise<Result<RunTicket | null>>;
11461159 /** Usage over a range of days (`YYYY-MM-DD`, both included), at price, by product, meter, project and day. Members only. */
11471160 usageReport(
18871900
18881901 export type UsageDay = { day: string; product: string; micros: number };
18891902
1903+/** One agent's (by handle) or one person's (by username) share of the agent product, at price; an empty key for lines attributed to no one. */
1904+export type UsageShare = { key: string; label: string; micros: number; count: number };
1905+
18901906 export type UsageAllowance = { used: number; of: number; unit: string };
18911907
18921908 export type ProjectUsage = { project: string; micros: number; quantity: number };
19251941 projects: string[];
19261942 /** Agent tokens by model over the range, most first. */
19271943 models?: ModelTokens[];
1944+ /** The agent product by the agent that did the work, most first; their sum is the agent product's total. */
1945+ byAgent?: UsageShare[];
1946+ /** The agent product by who asked; work no person asked for under an empty key. */
1947+ byPerson?: UsageShare[];
19281948 /** The plan's included usage this month, in micros. */
19291949 included?: UsageAllowance | null;
19301950 discountPercent?: number | null;
+6−1
1515 * 4. The compute gate's reservation (`ComputeGate.admit`, kind `agent`):
1616 * the workspace's spend limit, AI credit, pauses and g1t's breaker.
1717 * 5. A billing run (`start_run`), so the work is charged as Agent tokens on
18− * the workspace's bill, under the paying agent.
18+ * the workspace's bill, under the paying agent, attributed to it and to
19+ * who asked: Spend's "by agent" and "by person" read the ledger.
1920 * 6. The work itself, through the model proxy with the session's token.
2021 * 7. `finish_run` with its cost and tokens, the reservation settled at
2122 * cost, and the charge added to the paying agent's and the workspace's
293294 billedTo: ownModel ? "workspace" : "g1t",
294295 session: session.id,
295296 tier: named ? null : model.tier,
297+ // Whose work it is, on every line the run puts on the ledger: the
298+ // agent that pays, and who asked. Spend reads both from the ledger.
299+ agent: payer.handle,
300+ askedBy: input.askerName,
296301 });
297302 if (!started.ok) return { ok: false, reason: "billing", message: started.error.message };
298303 const ticket: RunTicket | null = started.value;
+25−0
1+-- Who an agent's usage is attributed to: the agent that did the work, by
2+-- handle, and the person who asked, by username. Every reply and session
3+-- step the agents service bills (a run under `<workspace>/@<handle>`), and
4+-- every repository run by @g1t from the runner, carries both, so Spend's
5+-- "by agent" and "by person" come from the one ledger that is charged,
6+-- with no second ledger to disagree with it.
7+ALTER TABLE runs ADD COLUMN agent TEXT;
8+ALTER TABLE runs ADD COLUMN asked_by TEXT;
9+ALTER TABLE ledger ADD COLUMN agent TEXT;
10+ALTER TABLE ledger ADD COLUMN asked_by TEXT;
11+
12+-- What is already on the ledger, attributed once from what it says: a
13+-- line billed under `<workspace>/@<handle>` is that agent's (no repository
14+-- has an `@` in its name)…
15+UPDATE ledger SET agent = substr(repo, instr(repo, '/@') + 2)
16+ WHERE kind = 'usage' AND agent IS NULL AND repo LIKE '%/@%';
17+UPDATE runs SET agent = substr(repo, instr(repo, '/@') + 2)
18+ WHERE agent IS NULL AND repo LIKE '%/@%';
19+-- …and every other run, and every agent's sandbox, is g1t's own agent at
20+-- work on a repository: all g1t's work shows as @g1t. Who asked is not
21+-- known for these; lines from here on carry it.
22+UPDATE ledger SET agent = 'g1t'
23+ WHERE kind = 'usage' AND agent IS NULL
24+ AND (reference LIKE 'run%' OR (task = 'sandbox' AND compute = 'agent'));
25+UPDATE runs SET agent = 'g1t' WHERE agent IS NULL;
+42−0
1+//! Whose work a usage line is: the agent that did it, by handle, and the
2+//! person who asked, by username. Both are written on the run
3+//! (`start_run`) and copied onto every ledger line it makes, and on an
4+//! agent's sandbox time (`record_sandbox`), so Spend's "by agent" and "by
5+//! person" are read from the one ledger that is charged
6+//! (`report::attribution`), never from a second count.
7+
8+/// A handle or username as the ledger stores it: lowercase, without a
9+/// leading `@`; none when empty.
10+pub(crate) fn handle(given: Option<&str>) -> Option<String> {
11+ let name = given?.trim().trim_start_matches('@').to_lowercase();
12+ (!name.is_empty()).then_some(name)
13+}
14+
15+/// The agent a run is attributed to: the one the caller names; else the
16+/// one a run billed under `<workspace>/@<handle>` names (the agents
17+/// service's replies and sessions; no repository has an `@` in its name);
18+/// else g1t's own agent at work on a repository, which all shows as @g1t.
19+pub(crate) fn agent_of(given: Option<&str>, repo_name: &str) -> Option<String> {
20+ handle(given).or_else(|| repo_name.strip_prefix('@').and_then(|h| handle(Some(h)))).or_else(|| Some("g1t".to_owned()))
21+}
22+
23+#[cfg(test)]
24+mod tests {
25+ use super::*;
26+
27+ #[test]
28+ fn a_run_is_the_agent_named_or_the_one_it_is_billed_under_or_g1t() {
29+ assert_eq!(agent_of(Some("Mike"), "@mike").as_deref(), Some("mike"));
30+ assert_eq!(agent_of(None, "@margo").as_deref(), Some("margo"));
31+ assert_eq!(agent_of(Some(" "), "@margo").as_deref(), Some("margo"));
32+ assert_eq!(agent_of(None, "g1t").as_deref(), Some("g1t"));
33+ assert_eq!(agent_of(Some("@g1t"), "api").as_deref(), Some("g1t"));
34+ }
35+
36+ #[test]
37+ fn who_asked_is_a_username_or_no_one() {
38+ assert_eq!(handle(Some("@Chase")).as_deref(), Some("chase"));
39+ assert_eq!(handle(Some("")), None);
40+ assert_eq!(handle(None), None);
41+ }
42+}
+206−0
1+//! What a workspace is charged in a month: one definition, used by the
2+//! spend limit (`limits::limit_with`, the `spent_micros` every signed-in
3+//! page shows), the Usage page and the Billing page's plan card
4+//! (`report::usage_report`), and so by Spend's "Charged" tile and the top
5+//! bar, which read the limit.
6+//!
7+//! **Charged** is usage at price, less the account's discount, less what
8+//! the plan's included usage, the trial, the open-source pool or g1t paid,
9+//! less what credit paid; never below zero. Every ledger line carries each
10+//! part (`statement::PRICE_SQL` reads the price back from them), and usage
11+//! metered through the month and charged when it closes (`pending_usage`:
12+//! storage, git operations, scans, embeddings, domains, app traffic) counts
13+//! now on the same terms it will be entered on: at cost plus the margin,
14+//! with the discount off, and nothing for a source g1t covers on a free
15+//! workspace. So a workspace with a 100% discount is charged exactly
16+//! nothing, however much is pending.
17+
18+use g1t_contracts::billing::UsageTotals;
19+use serde::Deserialize;
20+use worker::wasm_bindgen::JsValue;
21+use worker::Result;
22+
23+use crate::Billing;
24+use crate::credits::with_margin;
25+
26+/// What is left to pay: usage at price less the discount, included usage
27+/// and credit. The one arithmetic for "charged".
28+pub(crate) fn charged(totals: &UsageTotals) -> i64 {
29+ (totals.price_micros - totals.discount_micros - totals.included_micros - totals.credits_micros).max(0)
30+}
31+
32+/// A month-end source's usage so far this month, metered and not charged
33+/// yet (`pending_usage`).
34+#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
35+pub(crate) struct PendingLine {
36+ pub source: String,
37+ /// What it cost g1t so far.
38+ #[serde(default)]
39+ pub cost_micros: Option<f64>,
40+ /// What it will be charged before the account's terms: at cost plus
41+ /// the margin, or nothing for a source g1t covers on a free workspace
42+ /// (`storage::set_pending`).
43+ #[serde(default)]
44+ pub charge_micros: Option<f64>,
45+}
46+
47+impl PendingLine {
48+ pub fn cost(&self) -> i64 {
49+ self.cost_micros.unwrap_or(0.0).round().max(0.0) as i64
50+ }
51+
52+ pub fn charge(&self) -> i64 {
53+ self.charge_micros.unwrap_or(0.0).round().max(0.0) as i64
54+ }
55+}
56+
57+/// A pending line's parts, as the month's close will enter them: its
58+/// price, what g1t covers of it, and what the discount takes off the rest.
59+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
60+pub(crate) struct PendingSplit {
61+ pub price: i64,
62+ pub covered: i64,
63+ pub discount: i64,
64+}
65+
66+impl PendingSplit {
67+ /// What the workspace will be charged for it.
68+ #[cfg(test)]
69+ pub fn charged(&self) -> i64 {
70+ self.price - self.covered - self.discount
71+ }
72+}
73+
74+/// Splits a pending line: `cost` at cost plus `margin_percent` is its
75+/// price; `charge` (what it will be charged before terms) below that is
76+/// covered by g1t; `percent_off` comes off what is charged.
77+pub(crate) fn pending_split(cost: i64, charge: i64, margin_percent: u32, percent_off: u32) -> PendingSplit {
78+ let price = with_margin(cost.max(0), margin_percent);
79+ let base = charge.clamp(0, price);
80+ PendingSplit { price, covered: price - base, discount: base * i64::from(percent_off.min(100)) / 100 }
81+}
82+
83+/// Counts a pending line into the totals: at price (and as pending), with
84+/// what g1t covers as included and the discount as discount.
85+pub(crate) fn add_pending(totals: &mut UsageTotals, split: &PendingSplit) {
86+ totals.price_micros += split.price;
87+ totals.pending_micros += split.price;
88+ totals.included_micros += split.covered;
89+ totals.discount_micros += split.discount;
90+}
91+
92+/// The month's totals from its ledger lines and pending usage, and what
93+/// credit paid, with `charged_micros` worked out.
94+pub(crate) fn month_totals(price: i64, discount: i64, covered: i64, credits: i64, pending: &[PendingSplit]) -> UsageTotals {
95+ let mut totals = UsageTotals {
96+ price_micros: price,
97+ discount_micros: discount,
98+ included_micros: covered,
99+ credits_micros: credits.max(0),
100+ ..UsageTotals::default()
101+ };
102+ for split in pending {
103+ add_pending(&mut totals, split);
104+ }
105+ totals.charged_micros = charged(&totals);
106+ totals
107+}
108+
109+impl Billing {
110+ /// What the workspaces have metered this month (`YYYY-MM`) that is
111+ /// charged when it closes, not charged yet.
112+ pub(crate) async fn pending_this_month(&self, members: &[JsValue], month: &str) -> Result<Vec<PendingLine>> {
113+ let marks = vec!["?"; members.len().max(1)].join(", ");
114+ let mut args = members.to_vec();
115+ args.push(month.into());
116+ self.db
117+ .prepare(format!(
118+ "SELECT source, cost_micros, charge_micros FROM pending_usage
119+ WHERE workspace IN ({marks}) AND month = ? AND charged_at IS NULL"
120+ ))
121+ .bind(&args)?
122+ .all()
123+ .await?
124+ .results::<PendingLine>()
125+ }
126+}
127+
128+#[cfg(test)]
129+mod tests {
130+ use super::*;
131+
132+ const MARGIN: u32 = 20;
133+
134+ #[test]
135+ fn a_full_discount_is_charged_exactly_nothing_pending_usage_included() {
136+ // flagon-io, October 2026: $10.29 of usage at price on the ledger,
137+ // every line discounted in full; and $0.0075 of storage metered so
138+ // far, which the limit used to count at its charge, $0.009.
139+ let pending = [pending_split(7_500, with_margin(7_500, MARGIN), MARGIN, 100)];
140+ assert_eq!(pending[0], PendingSplit { price: 9_000, covered: 0, discount: 9_000 });
141+ let totals = month_totals(10_290_000, 10_290_000, 0, 0, &pending);
142+ assert_eq!(totals.price_micros, 10_299_000);
143+ assert_eq!(totals.discount_micros, 10_299_000);
144+ assert_eq!(totals.charged_micros, 0);
145+ assert_eq!(charged(&totals), 0);
146+ }
147+
148+ #[test]
149+ fn included_usage_pays_first_and_only_the_rest_is_charged() {
150+ // The plan: $10 included. $12.50 of runs at price, $10 of it drawn
151+ // from the included usage on the lines themselves; $0.60 of storage
152+ // pending, which the close will charge in full (the included usage
153+ // is spent).
154+ let pending = [pending_split(500_000, with_margin(500_000, MARGIN), MARGIN, 0)];
155+ assert_eq!(pending[0], PendingSplit { price: 600_000, covered: 0, discount: 0 });
156+ let totals = month_totals(12_500_000, 0, 10_000_000, 0, &pending);
157+ assert_eq!(totals.included_micros, 10_000_000);
158+ assert_eq!(totals.charged_micros, 2_500_000 + 600_000);
159+ // Credit from g1t pays after that.
160+ let with_credit = month_totals(12_500_000, 0, 10_000_000, 1_000_000, &pending);
161+ assert_eq!(with_credit.charged_micros, 2_100_000);
162+ // Never below zero: more credit than usage charges nothing.
163+ assert_eq!(month_totals(1_000_000, 0, 0, 5_000_000, &[]).charged_micros, 0);
164+ }
165+
166+ #[test]
167+ fn pay_as_you_go_is_charged_at_price() {
168+ // No discount, nothing included, no credit: price is what is
169+ // charged, pending usage counted at its price too.
170+ let pending = [pending_split(1_000_000, with_margin(1_000_000, MARGIN), MARGIN, 0), pending_split(250, 300, MARGIN, 0)];
171+ let totals = month_totals(4_000_000, 0, 0, 0, &pending);
172+ assert_eq!(totals.pending_micros, 1_200_000 + 300);
173+ assert_eq!(totals.charged_micros, 4_000_000 + 1_200_000 + 300);
174+ }
175+
176+ #[test]
177+ fn a_partial_discount_comes_off_pending_usage_as_the_close_will_take_it() {
178+ // 30% off: $0.009 of pending storage is charged $0.0063.
179+ let split = pending_split(7_500, 9_000, MARGIN, 30);
180+ assert_eq!(split, PendingSplit { price: 9_000, covered: 0, discount: 2_700 });
181+ assert_eq!(split.charged(), 6_300);
182+ }
183+
184+ #[test]
185+ fn what_g1t_covers_on_a_free_workspace_is_included_not_charged() {
186+ // Security scans on a free workspace: metered at cost, charged
187+ // nothing (`set_pending` writes a charge of 0), shown at price.
188+ let split = pending_split(2_000, 0, MARGIN, 0);
189+ assert_eq!(split, PendingSplit { price: 2_400, covered: 2_400, discount: 0 });
190+ assert_eq!(split.charged(), 0);
191+ let totals = month_totals(0, 0, 0, 0, &[split]);
192+ assert_eq!(totals.price_micros, 2_400);
193+ assert_eq!(totals.included_micros, 2_400);
194+ assert_eq!(totals.charged_micros, 0);
195+ // A charge above the price (an older margin) is held to the price.
196+ assert_eq!(pending_split(1_000, 5_000, MARGIN, 0).covered, 0);
197+ }
198+
199+ #[test]
200+ fn a_pending_row_reads_whole_micros_and_never_less_than_nothing() {
201+ let line = PendingLine { source: "storage".into(), cost_micros: Some(7_499.6), charge_micros: Some(-1.0) };
202+ assert_eq!(line.cost(), 7_500);
203+ assert_eq!(line.charge(), 0);
204+ assert_eq!(PendingLine::default().cost(), 0);
205+ }
206+}
+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

This change is too large to show in full.