copilotz 1.0.0-rc.3 → 1.0.0-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +13 -6
  3. package/package.json +1 -1
  4. package/plugins/billing/usage/actions/close.js +20 -1
  5. package/plugins/billing/usage/actions/org.js +39 -26
  6. package/plugins/billing/usage/actions/rerate.js +22 -2
  7. package/plugins/billing/usage/collections/rollup.d.ts +79 -0
  8. package/plugins/billing/usage/collections/rollup.js +8 -0
  9. package/plugins/billing/usage/collections/usage.d.ts +71 -2
  10. package/plugins/billing/usage/collections/usage.js +22 -6
  11. package/plugins/billing/usage/mod.d.ts +10 -5
  12. package/plugins/billing/usage/mod.js +10 -5
  13. package/plugins/billing/usage/resources/commands.js +1 -1
  14. package/plugins/billing/usage/shared/rate.d.ts +30 -9
  15. package/plugins/billing/usage/shared/rate.js +73 -30
  16. package/plugins/billing/usage/shared/totals.d.ts +19 -3
  17. package/plugins/billing/usage/shared/totals.js +43 -13
  18. package/plugins/billing/usage/shared/use.d.ts +12 -0
  19. package/plugins/billing/usage/shared/use.js +9 -0
  20. package/plugins/billing/usage/views/terminal/totals.d.ts +6 -2
  21. package/plugins/billing/usage/views/terminal/totals.js +38 -6
  22. package/plugins/billing/usage/views/web/chart.d.ts +7 -5
  23. package/plugins/billing/usage/views/web/chart.js +43 -21
  24. package/plugins/billing/usage/views/web/numbers.d.ts +110 -37
  25. package/plugins/billing/usage/views/web/numbers.js +119 -31
  26. package/plugins/billing/usage/views/web/org.d.ts +12 -5
  27. package/plugins/billing/usage/views/web/org.js +159 -53
  28. package/plugins/billing/usage/views/web/style.d.ts +8 -5
  29. package/plugins/billing/usage/views/web/style.js +87 -10
  30. package/plugins/billing/usage/web.d.ts +4 -3
  31. package/plugins/billing/usage/web.js +4 -3
  32. package/plugins/compute/computers/program/version.d.ts +1 -1
  33. package/plugins/compute/computers/program/version.js +1 -1
  34. package/plugins/compute/computers/resources/downloads.js +1 -1
  35. package/plugins/models/adapters/llm/openai-compatible.js +10 -4
package/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ ## 1.0.0-rc.4 — 2026-10-30
2
+
3
+ - **Usage: every use has a price.** It's the cost (a margin of 0) unless the app's price book says
4
+ otherwise: rates of its own, or a gross `margin` on the cost (`{ margin: 0.9 }` is ten times the
5
+ cost), from its `since` date on. `markup` is gone: a margin says it. Uses recorded before count at
6
+ their cost, in totals and when re-rated.
7
+ - **Usage: by part.** Each record keeps its cost and price by part (input, cache reads, cache
8
+ writes, output, reasoning…) and what caching saved; totals and rollups sum them.
9
+ - **Usage: people see prices.** `usage.totals`, `usage.org` and `usage.calls` give people prices
10
+ only; costs (and so margins) only platform staff and the system. Each of `usage.org` and
11
+ `usage.calls` says whether it shows costs (`costs`).
12
+ - **Usage page and `/usage`:** spend (the price) everywhere, with caching visible: hit rate, what
13
+ reading and writing the cache was charged, and what it saved, overall and by model; cached tokens
14
+ in the chart and in each call. Platform staff also see cost and gross margin, marked staff-only.
15
+ - **Models:** OpenAI-compatible providers' cache reads and writes are read as each reports them
16
+ (DeepSeek's cache hits, Moonshot's cached tokens, OpenRouter's cache writes).
17
+
1
18
  ## 1.0.0-rc.3 — 2026-10-30
2
19
 
3
20
  - **Computers:** connecting one more app (`computer install <app> --code`) no longer stops the
package/README.md CHANGED
@@ -22,7 +22,7 @@ bun add copilotz # Bun
22
22
  deno add jsr:@copilotz/copilotz # Deno (or `deno add npm:copilotz`)
23
23
  ```
24
24
 
25
- On Deno, name it `copilotz` in `deno.json` (`"copilotz": "jsr:@copilotz/copilotz@^1.0.0-rc.3"`), and
25
+ On Deno, name it `copilotz` in `deno.json` (`"copilotz": "jsr:@copilotz/copilotz@^1.0.0-rc.4"`), and
26
26
  the imports below are the same everywhere. Some pieces need a package of their own, installed beside
27
27
  it: `esbuild` to bundle the web host on Node, `@electric-sql/pglite` for PostgreSQL in the process,
28
28
  and `@types/react` for the front ends' types.
@@ -204,7 +204,7 @@ and an app serves it from there by default:
204
204
  import { computerDownloads } from "copilotz/plugins/compute/downloads";
205
205
 
206
206
  plugins: [...plugins, await computerDownloads()], // this Copilotz's release
207
- plugins: [...plugins, await computerDownloads({ dir: "dist/computer/1.0.0-rc.3" })], // its own: /computer/download/
207
+ plugins: [...plugins, await computerDownloads({ dir: "dist/computer/1.0.0-rc.4" })], // its own: /computer/download/
208
208
  plugins: [...plugins, await computerDownloads({ url: "https://downloads.example.com/computer/" })], // or elsewhere
209
209
  ```
210
210
 
@@ -382,9 +382,15 @@ name, never by importing its code.
382
382
  billing; billing builds on models. An app records its own uses with `usage.record`.
383
383
  - **Rated as recorded,** so a record keeps what applied. Cost comes from `resources.cost` (the
384
384
  app's rates: negotiated, a subscription at 0, compute by the second), else from the catalog's
385
- prices; price comes from `resources.price` only (rates of its own, or a `markup` on cost). Rates
386
- are keyed by subject, then `provider/*`, then the catalog, then `*`, and dated lists apply each
387
- rate from its `since`. A use nothing prices is kept and flagged `unpriced`.
385
+ prices (each kind of token at its own: input, cache reads and writes, output, reasoning). The
386
+ price is the cost, a margin of 0, unless `resources.price` says otherwise: rates of its own, or
387
+ a gross margin on the cost (`{ "*": { margin: 0.2 }, "cloud/*": { margin: 0.9 } }`: 0.9 is ten
388
+ times the cost). Rates are keyed by subject, then `provider/*`, then the catalog, then `*`, and
389
+ dated lists apply each rate from its `since`, so a new margin applies from its date on. Each
390
+ record keeps its cost and price by part too, and what caching saved. A use nothing prices is
391
+ kept and flagged `unpriced`.
392
+ - **People see prices** (what they're charged); costs, and so margins, only platform staff and the
393
+ system.
388
394
  - **Totals** by any field or tag, summed by the store (`aggregate`) over indexed fields (time,
389
395
  meter, subject, thread; more from the app:
390
396
  `collections: { usage: { indexes: [["tags.tenant", "at"]] } }`):
@@ -399,7 +405,8 @@ name, never by importing its code.
399
405
  - **Re-rating** (`usage.rerate`) never changes a record: it appends adjustments, dated when made,
400
406
  and totals follow.
401
407
  - **On the web** (`usageWeb`): an org's Usage page, day by day and against the period before, by
402
- person, agent and model; owners and admins see everyone's, anyone else their own.
408
+ person, agent and model, with what the cache read, wrote and saved; owners and admins see
409
+ everyone's, anyone else their own.
403
410
 
404
411
  **`compute/`**: **`computers`**, machines people connect (the computer program in
405
412
  `computers/program/`, a worker of the plugin's `computer` kind) and their desks, one per space
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "copilotz",
3
- "version": "1.0.0-rc.3",
3
+ "version": "1.0.0-rc.4",
4
4
  "description": "Copilotz: one runtime for Node, Deno, Bun, Cloudflare Workers and the browser.",
5
5
  "keywords": [
6
6
  "agents",
@@ -68,7 +68,7 @@ async function close(ctx, rollups, input) {
68
68
  // Each day's rollups, as the records hold their fields.
69
69
  const byDay = new Map();
70
70
  for (const group of groups) {
71
- const { day, meter, subject, _kind, _unpriced, ...dims } = group._id;
71
+ const { day, meter, subject, _kind, _unpriced, _rated, ...dims } = group._id;
72
72
  const doc = { _id: "", day, meter, subject, quantities: {}, n: group.n };
73
73
  rollups.dimensions.forEach((d, i) => {
74
74
  if (dims[`d${i}`] !== null && dims[`d${i}`] !== undefined)
@@ -78,14 +78,33 @@ async function close(ctx, rollups, input) {
78
78
  doc.kind = _kind;
79
79
  if (_unpriced !== null)
80
80
  doc.unpriced = _unpriced;
81
+ const costs = {};
82
+ const prices = {};
81
83
  for (const unit of rollups.units) {
82
84
  if (group[unit])
83
85
  doc.quantities[unit] = group[unit];
86
+ if (group[`cost_${unit}`])
87
+ costs[unit] = group[`cost_${unit}`];
88
+ if (group[`price_${unit}`])
89
+ prices[unit] = group[`price_${unit}`];
84
90
  }
85
91
  if (group.cost)
86
92
  doc.cost = group.cost;
87
93
  if (group.price)
88
94
  doc.price = group.price;
95
+ if (Object.keys(costs).length)
96
+ doc.costs = costs;
97
+ if (Object.keys(prices).length)
98
+ doc.prices = prices;
99
+ const saved = {
100
+ ...(group.savedCost ? { cost: group.savedCost } : {}),
101
+ ...(group.savedPrice ? { price: group.savedPrice } : {}),
102
+ };
103
+ if (Object.keys(saved).length)
104
+ doc.saved = saved;
105
+ // Who rated the price, as the records say (none: priced at cost, as they were before).
106
+ if (_rated !== null && _rated !== undefined)
107
+ doc.rated = { price: _rated };
89
108
  const list = byDay.get(day) ?? [];
90
109
  doc._id = `${day}#${list.length}`;
91
110
  list.push(doc);
@@ -9,19 +9,25 @@
9
9
  * by day, person (`tags.person`), agent (`tags.agent`), model (`subject`).
10
10
  * - `usage.calls`: the latest uses, newest first (`{ filter, limit }`).
11
11
  *
12
- * Both only read, so neither is recorded.
12
+ * People see prices: what they're charged. What it cost (and so the margin)
13
+ * only platform staff see; each says whether it shows them (`costs`). Both
14
+ * only read, so neither is recorded.
13
15
  */
14
16
  import { defineAction } from "../../../../runtime/mod.js";
15
17
  import { rollupsOf } from "../shared/settings.js";
16
- import { totals, UNITS } from "../shared/totals.js";
17
- import { personOf } from "../shared/use.js";
18
+ import { priceOnly, totals, UNITS } from "../shared/totals.js";
19
+ import { personOf, priceOf } from "../shared/use.js";
18
20
  const forbidden = (message) => Object.assign(new Error(message), { code: "forbidden" });
19
- /** Whether the caller runs the org: an owner or an admin there, or platform staff. */
20
- async function runsOrg(ctx) {
21
+ /**
22
+ * What the caller sees: everyone's (`all`) when they run the org (an owner or
23
+ * an admin there, or platform staff), and costs only as staff.
24
+ */
25
+ async function sight(ctx) {
21
26
  const me = await ctx.call("org.me", {})
22
27
  // An app without org roles has nobody who runs one.
23
28
  .catch(() => undefined);
24
- return !!me && (me.staff === true || me.role === "owner" || me.role === "admin");
29
+ const staff = me?.staff === true;
30
+ return { all: staff || me?.role === "owner" || me?.role === "admin", costs: staff };
25
31
  }
26
32
  const filter = { type: "object" };
27
33
  export const orgUsageAction = defineAction({
@@ -38,21 +44,19 @@ export const orgUsageAction = defineAction({
38
44
  },
39
45
  allow: (actor) => actor.kind === "person",
40
46
  run: async (ctx, input) => {
41
- const all = await runsOrg(ctx);
47
+ const { all, costs } = await sight(ctx);
42
48
  const person = personOf(ctx.actor);
43
49
  if (!all && !person)
44
50
  throw forbidden("Only people see usage.");
45
51
  // The ledger is read as the system: everyone's once the org says they run it, else theirs.
46
52
  const system = ctx.as();
47
53
  const asked = (input.filter ?? {});
48
- return {
49
- totals: await totals(system.db.collection("usage"), {
50
- filter: all ? asked : { ...asked, "tags.person": person },
51
- by: input.by ?? [],
52
- units: input.units ?? UNITS,
53
- }, { db: system.db, rollups: rollupsOf(ctx) }),
54
- all,
55
- };
54
+ const found = await totals(system.db.collection("usage"), {
55
+ filter: all ? asked : { ...asked, "tags.person": person },
56
+ by: input.by ?? [],
57
+ units: input.units ?? UNITS,
58
+ }, { db: system.db, rollups: rollupsOf(ctx) });
59
+ return { totals: costs ? found : found.map(priceOnly), all, costs };
56
60
  },
57
61
  });
58
62
  export const usageCallsAction = defineAction({
@@ -69,7 +73,7 @@ export const usageCallsAction = defineAction({
69
73
  },
70
74
  allow: (actor) => actor.kind === "person",
71
75
  run: async (ctx, input) => {
72
- const all = await runsOrg(ctx);
76
+ const { all, costs } = await sight(ctx);
73
77
  const person = personOf(ctx.actor);
74
78
  if (!all && !person)
75
79
  throw forbidden("Only people see usage.");
@@ -78,17 +82,26 @@ export const usageCallsAction = defineAction({
78
82
  // Uses only (adjustments correct a rating; they aren't calls); yours unless you run it.
79
83
  { ...asked, kind: { $exists: false }, ...(all ? {} : { "tags.person": person }) }, { sort: { at: -1 }, limit: input.limit ?? 20 });
80
84
  return {
81
- calls: found.map((r) => ({
82
- _id: r._id,
83
- meter: r.meter,
84
- subject: r.subject,
85
- quantities: r.quantities,
86
- ...(r.tags ? { tags: r.tags } : {}),
87
- ...(r.cost !== undefined ? { cost: r.cost } : {}),
88
- ...(r.unpriced?.length ? { unpriced: r.unpriced } : {}),
89
- at: r.at,
90
- })),
85
+ calls: found.map((r) => {
86
+ const price = priceOf(r);
87
+ const saved = r.saved && (costs ? r.saved : { price: r.saved.price });
88
+ return {
89
+ _id: r._id,
90
+ meter: r.meter,
91
+ subject: r.subject,
92
+ quantities: r.quantities,
93
+ ...(r.tags ? { tags: r.tags } : {}),
94
+ ...(price !== undefined ? { price } : {}),
95
+ ...(r.prices ? { prices: r.prices } : {}),
96
+ ...(costs && r.cost !== undefined ? { cost: r.cost } : {}),
97
+ ...(costs && r.costs ? { costs: r.costs } : {}),
98
+ ...(saved && Object.values(saved).some((v) => v !== undefined) ? { saved } : {}),
99
+ ...(r.unpriced?.length ? { unpriced: r.unpriced } : {}),
100
+ at: r.at,
101
+ };
102
+ }),
91
103
  all,
104
+ costs,
92
105
  };
93
106
  },
94
107
  });
@@ -1,14 +1,24 @@
1
1
  /** `usage.rerate`: brings records' cost and price to the books in force now, with adjustments. */
2
2
  import { defineAction } from "../../../../runtime/mod.js";
3
3
  import { rate, round } from "../shared/rate.js";
4
+ import { priceOf } from "../shared/use.js";
4
5
  /** Appends adjustments that bring records' cost and price to what the books say now. */
5
6
  async function rerate(ctx, filter) {
6
7
  const records = await ctx.db.collection("usage").find(filter);
7
8
  const net = new Map();
9
+ const add = (into, parts, sign = 1) => {
10
+ for (const [unit, usd] of Object.entries(parts ?? {})) {
11
+ into[unit] = (into[unit] ?? 0) + sign * usd;
12
+ }
13
+ };
8
14
  for (const record of records) {
9
15
  const id = record.adjusts ?? record._id;
10
- const sum = net.get(id) ?? { cost: 0, price: 0 };
11
- net.set(id, { cost: sum.cost + (record.cost ?? 0), price: sum.price + (record.price ?? 0) });
16
+ const sum = net.get(id) ?? { cost: 0, price: 0, costs: {}, prices: {} };
17
+ sum.cost += record.cost ?? 0;
18
+ sum.price += priceOf(record) ?? 0;
19
+ add(sum.costs, record.costs);
20
+ add(sum.prices, record.prices);
21
+ net.set(id, sum);
12
22
  }
13
23
  let adjusted = 0;
14
24
  for (const record of records) {
@@ -26,6 +36,14 @@ async function rerate(ctx, filter) {
26
36
  const price = round((now.price ?? 0) - was.price);
27
37
  if (!cost && !price)
28
38
  continue;
39
+ const delta = (to, from) => {
40
+ const out = {};
41
+ add(out, to);
42
+ add(out, from, -1);
43
+ return Object.fromEntries(Object.entries(out).map(([u, usd]) => [u, round(usd)]).filter(([, usd]) => usd !== 0));
44
+ };
45
+ const costs = delta(now.costs, was.costs);
46
+ const prices = delta(now.prices, was.prices);
29
47
  ctx.record("usage", {
30
48
  meter: record.meter,
31
49
  subject: record.subject,
@@ -33,6 +51,8 @@ async function rerate(ctx, filter) {
33
51
  ...(record.tags ? { tags: record.tags } : {}),
34
52
  cost,
35
53
  price,
54
+ ...(Object.keys(costs).length ? { costs } : {}),
55
+ ...(Object.keys(prices).length ? { prices } : {}),
36
56
  rated: now.rated,
37
57
  adjusts: record._id,
38
58
  kind: "adjustment",
@@ -40,6 +40,38 @@ export declare const rollupSchema: {
40
40
  readonly price: {
41
41
  readonly type: "number";
42
42
  };
43
+ readonly costs: {
44
+ readonly type: "object";
45
+ readonly additionalProperties: {
46
+ readonly type: "number";
47
+ };
48
+ };
49
+ readonly prices: {
50
+ readonly type: "object";
51
+ readonly additionalProperties: {
52
+ readonly type: "number";
53
+ };
54
+ };
55
+ readonly saved: {
56
+ readonly type: "object";
57
+ readonly properties: {
58
+ readonly cost: {
59
+ readonly type: "number";
60
+ };
61
+ readonly price: {
62
+ readonly type: "number";
63
+ };
64
+ };
65
+ };
66
+ /** Who rated the price (`rated.price`), as the records say: none for those priced at cost before. */
67
+ readonly rated: {
68
+ readonly type: "object";
69
+ readonly properties: {
70
+ readonly price: {
71
+ readonly type: "string";
72
+ };
73
+ };
74
+ };
43
75
  /** Records rolled up. */
44
76
  readonly n: {
45
77
  readonly type: "integer";
@@ -95,6 +127,38 @@ export declare const usageRollup: {
95
127
  readonly price: {
96
128
  readonly type: "number";
97
129
  };
130
+ readonly costs: {
131
+ readonly type: "object";
132
+ readonly additionalProperties: {
133
+ readonly type: "number";
134
+ };
135
+ };
136
+ readonly prices: {
137
+ readonly type: "object";
138
+ readonly additionalProperties: {
139
+ readonly type: "number";
140
+ };
141
+ };
142
+ readonly saved: {
143
+ readonly type: "object";
144
+ readonly properties: {
145
+ readonly cost: {
146
+ readonly type: "number";
147
+ };
148
+ readonly price: {
149
+ readonly type: "number";
150
+ };
151
+ };
152
+ };
153
+ /** Who rated the price (`rated.price`), as the records say: none for those priced at cost before. */
154
+ readonly rated: {
155
+ readonly type: "object";
156
+ readonly properties: {
157
+ readonly price: {
158
+ readonly type: "string";
159
+ };
160
+ };
161
+ };
98
162
  /** Records rolled up. */
99
163
  readonly n: {
100
164
  readonly type: "integer";
@@ -115,7 +179,22 @@ export declare const usageRollup: {
115
179
  [x: string]: string;
116
180
  } | undefined;
117
181
  cost?: number | undefined;
182
+ saved?: {
183
+ [x: string]: unknown;
184
+ cost?: number | undefined;
185
+ price?: number | undefined;
186
+ } | undefined;
118
187
  price?: number | undefined;
188
+ costs?: {
189
+ [x: string]: number;
190
+ } | undefined;
191
+ prices?: {
192
+ [x: string]: number;
193
+ } | undefined;
194
+ rated?: {
195
+ [x: string]: unknown;
196
+ price?: string | undefined;
197
+ } | undefined;
119
198
  unpriced?: string[] | undefined;
120
199
  n: number;
121
200
  quantities: {
@@ -18,6 +18,14 @@ export const rollupSchema = {
18
18
  quantities: { type: "object", additionalProperties: { type: "number" } },
19
19
  cost: { type: "number" },
20
20
  price: { type: "number" },
21
+ costs: { type: "object", additionalProperties: { type: "number" } },
22
+ prices: { type: "object", additionalProperties: { type: "number" } },
23
+ saved: {
24
+ type: "object",
25
+ properties: { cost: { type: "number" }, price: { type: "number" } },
26
+ },
27
+ /** Who rated the price (`rated.price`), as the records say: none for those priced at cost before. */
28
+ rated: { type: "object", properties: { price: { type: "string" } } },
21
29
  /** Records rolled up. */
22
30
  n: { type: "integer" },
23
31
  },
@@ -32,7 +32,36 @@ export declare const usageSchema: {
32
32
  readonly price: {
33
33
  readonly type: "number";
34
34
  };
35
- /** Where each came from: `app`, or the catalog and its date. */
35
+ /** Each by part (USD): the units, `input` and `output` less the parts priced apart. */
36
+ readonly costs: {
37
+ readonly type: "object";
38
+ readonly additionalProperties: {
39
+ readonly type: "number";
40
+ };
41
+ };
42
+ readonly prices: {
43
+ readonly type: "object";
44
+ readonly additionalProperties: {
45
+ readonly type: "number";
46
+ };
47
+ };
48
+ /** What caching saved (USD), less what writing the cache cost more. */
49
+ readonly saved: {
50
+ readonly type: "object";
51
+ readonly properties: {
52
+ readonly cost: {
53
+ readonly type: "number";
54
+ };
55
+ readonly price: {
56
+ readonly type: "number";
57
+ };
58
+ };
59
+ };
60
+ /**
61
+ * Who rated each: `app`, or the catalog and its date; `cost` for a price at
62
+ * cost. A record without a price's (from before every use had one) is
63
+ * priced at its cost.
64
+ */
36
65
  readonly rated: {
37
66
  readonly type: "object";
38
67
  };
@@ -98,7 +127,36 @@ export declare const usage: {
98
127
  readonly price: {
99
128
  readonly type: "number";
100
129
  };
101
- /** Where each came from: `app`, or the catalog and its date. */
130
+ /** Each by part (USD): the units, `input` and `output` less the parts priced apart. */
131
+ readonly costs: {
132
+ readonly type: "object";
133
+ readonly additionalProperties: {
134
+ readonly type: "number";
135
+ };
136
+ };
137
+ readonly prices: {
138
+ readonly type: "object";
139
+ readonly additionalProperties: {
140
+ readonly type: "number";
141
+ };
142
+ };
143
+ /** What caching saved (USD), less what writing the cache cost more. */
144
+ readonly saved: {
145
+ readonly type: "object";
146
+ readonly properties: {
147
+ readonly cost: {
148
+ readonly type: "number";
149
+ };
150
+ readonly price: {
151
+ readonly type: "number";
152
+ };
153
+ };
154
+ };
155
+ /**
156
+ * Who rated each: `app`, or the catalog and its date; `cost` for a price at
157
+ * cost. A record without a price's (from before every use had one) is
158
+ * priced at its cost.
159
+ */
102
160
  readonly rated: {
103
161
  readonly type: "object";
104
162
  };
@@ -136,7 +194,18 @@ export declare const usage: {
136
194
  [x: string]: string;
137
195
  } | undefined;
138
196
  cost?: number | undefined;
197
+ saved?: {
198
+ [x: string]: unknown;
199
+ cost?: number | undefined;
200
+ price?: number | undefined;
201
+ } | undefined;
139
202
  price?: number | undefined;
203
+ costs?: {
204
+ [x: string]: number;
205
+ } | undefined;
206
+ prices?: {
207
+ [x: string]: number;
208
+ } | undefined;
140
209
  rated?: {
141
210
  [x: string]: unknown;
142
211
  } | undefined;
@@ -1,7 +1,7 @@
1
1
  /** The ledger: one record per use, rated as it was recorded. Appended only. */
2
2
  import { defineCollection, } from "../../../../runtime/mod.js";
3
3
  import { rollupsOf } from "../shared/settings.js";
4
- import { totals } from "../shared/totals.js";
4
+ import { priceOnly, totals } from "../shared/totals.js";
5
5
  export const usageSchema = {
6
6
  type: "object",
7
7
  properties: {
@@ -14,7 +14,19 @@ export const usageSchema = {
14
14
  /** USD. */
15
15
  cost: { type: "number" },
16
16
  price: { type: "number" },
17
- /** Where each came from: `app`, or the catalog and its date. */
17
+ /** Each by part (USD): the units, `input` and `output` less the parts priced apart. */
18
+ costs: { type: "object", additionalProperties: { type: "number" } },
19
+ prices: { type: "object", additionalProperties: { type: "number" } },
20
+ /** What caching saved (USD), less what writing the cache cost more. */
21
+ saved: {
22
+ type: "object",
23
+ properties: { cost: { type: "number" }, price: { type: "number" } },
24
+ },
25
+ /**
26
+ * Who rated each: `app`, or the catalog and its date; `cost` for a price at
27
+ * cost. A record without a price's (from before every use had one) is
28
+ * priced at its cost.
29
+ */
18
30
  rated: { type: "object" },
19
31
  /** What nothing priced: `cost`, `price`. */
20
32
  unpriced: { type: "array", items: { type: "string" } },
@@ -43,11 +55,15 @@ export const usage = defineCollection({
43
55
  * days come from their rollups when those hold what's asked.
44
56
  */
45
57
  totals: {
46
- // A person sees their own usage; the system, everyone's.
58
+ // A person sees their own usage, at its price; the system, everyone's, cost too.
47
59
  allow: (actor) => actor.kind === "person",
48
- run: (records, params = {}, ctx) => totals(records, ctx.actor?.kind === "person"
49
- ? { ...params, filter: { ...params.filter, "tags.person": ctx.actor.id } }
50
- : params, { db: ctx.db, rollups: rollupsOf(ctx) }),
60
+ run: async (records, params = {}, ctx) => {
61
+ const person = ctx.actor?.kind === "person";
62
+ const found = await totals(records, person
63
+ ? { ...params, filter: { ...params.filter, "tags.person": ctx.actor.id } }
64
+ : params, { db: ctx.db, rollups: rollupsOf(ctx) });
65
+ return person ? found.map(priceOnly) : found;
66
+ },
51
67
  },
52
68
  /** The records matching `filter`, newest first. */
53
69
  list: (records, params = {}) => records.find(params.filter ?? {}, { sort: { at: -1 }, limit: params.limit ?? 100 }),
@@ -11,9 +11,14 @@
11
11
  * - its **cost** comes from `resources.cost` (the app's rates: negotiated,
12
12
  * a subscription at 0, a terminal's seconds), else from the models.dev
13
13
  * catalog for models it knows;
14
- * - its **price** (what the app charges) comes from `resources.price` only:
15
- * rates of its own, or a `markup` on the cost. Nothing ships one.
16
- * - A use nothing prices is recorded all the same, and flagged `unpriced`.
14
+ * - its **price** (what the app charges) is its cost (a margin of 0) unless
15
+ * `resources.price` says: rates of its own, or a gross `margin` on the
16
+ * cost (`{ "*": { margin: 0.2 } }`; 0.9 is ten times the cost);
17
+ * - each is kept by part (input, cached, cacheWrite, output, reasoning,
18
+ * seconds…), with what caching saved;
19
+ * - a use nothing prices is recorded all the same, and flagged `unpriced`.
20
+ * - **Who sees what:** people see prices (what they're charged); costs, and
21
+ * so margins, only platform staff and the system.
17
22
  * - **Totals** by any field or tag: `runtime.query("usage.totals", { filter, by })`.
18
23
  * Whole closed days come from rollups (`usage.close`, scheduled daily),
19
24
  * the rest from the records, with the same result.
@@ -21,7 +26,7 @@
21
26
  * adjustments for what new rates would change, so totals follow.
22
27
  * - **Across an org** (where the app serves many): `usage.org` (totals) and
23
28
  * `usage.calls` (the latest uses) give its owners and admins everyone's,
24
- * and anyone else their own. Its web front end (web.tsx) is the org
29
+ * and anyone else their own, at their price (cost too for staff). Its web front end (web.tsx) is the org
25
30
  * page's Usage section.
26
31
  *
27
32
  * Rates are keyed by subject (`"openai/gpt-6.1-sol"`), then by provider
@@ -33,7 +38,7 @@
33
38
  import { type Plugin } from "../../../runtime/mod.js";
34
39
  export type { Use } from "./shared/use.js";
35
40
  export type { UsageRecord } from "./collections/usage.js";
36
- export { priced, type PriceRate, type Quantities, type Rate, rate } from "./shared/rate.js";
41
+ export { type Parts, partsOf, priced, type PriceRate, type Quantities, type Rate, rate, } from "./shared/rate.js";
37
42
  export { type Rollups, type Totals, totals, UNITS } from "./shared/totals.js";
38
43
  export type { UsageSettings } from "./shared/settings.js";
39
44
  /** The usage ledger: records, rating, totals, rollups and re-rating. */
@@ -11,9 +11,14 @@
11
11
  * - its **cost** comes from `resources.cost` (the app's rates: negotiated,
12
12
  * a subscription at 0, a terminal's seconds), else from the models.dev
13
13
  * catalog for models it knows;
14
- * - its **price** (what the app charges) comes from `resources.price` only:
15
- * rates of its own, or a `markup` on the cost. Nothing ships one.
16
- * - A use nothing prices is recorded all the same, and flagged `unpriced`.
14
+ * - its **price** (what the app charges) is its cost (a margin of 0) unless
15
+ * `resources.price` says: rates of its own, or a gross `margin` on the
16
+ * cost (`{ "*": { margin: 0.2 } }`; 0.9 is ten times the cost);
17
+ * - each is kept by part (input, cached, cacheWrite, output, reasoning,
18
+ * seconds…), with what caching saved;
19
+ * - a use nothing prices is recorded all the same, and flagged `unpriced`.
20
+ * - **Who sees what:** people see prices (what they're charged); costs, and
21
+ * so margins, only platform staff and the system.
17
22
  * - **Totals** by any field or tag: `runtime.query("usage.totals", { filter, by })`.
18
23
  * Whole closed days come from rollups (`usage.close`, scheduled daily),
19
24
  * the rest from the records, with the same result.
@@ -21,7 +26,7 @@
21
26
  * adjustments for what new rates would change, so totals follow.
22
27
  * - **Across an org** (where the app serves many): `usage.org` (totals) and
23
28
  * `usage.calls` (the latest uses) give its owners and admins everyone's,
24
- * and anyone else their own. Its web front end (web.tsx) is the org
29
+ * and anyone else their own, at their price (cost too for staff). Its web front end (web.tsx) is the org
25
30
  * page's Usage section.
26
31
  *
27
32
  * Rates are keyed by subject (`"openai/gpt-6.1-sol"`), then by provider
@@ -40,7 +45,7 @@ import { DEFAULTS, rollupsOf } from "./shared/settings.js";
40
45
  import { recordAction } from "./actions/record.js";
41
46
  import { orgUsageAction, usageCallsAction } from "./actions/org.js";
42
47
  import { ledger } from "./resources/ledger.js";
43
- export { priced, rate } from "./shared/rate.js";
48
+ export { partsOf, priced, rate, } from "./shared/rate.js";
44
49
  export { totals, UNITS } from "./shared/totals.js";
45
50
  /** The usage ledger: records, rating, totals, rollups and re-rating. */
46
51
  export const usagePlugin = definePlugin({
@@ -2,7 +2,7 @@
2
2
  import { command } from "../../../../hosts/terminal/mod.js";
3
3
  export const commands = {
4
4
  "/usage": command({
5
- description: "What was used and what it cost, by model",
5
+ description: "What was used and what it was charged, by model",
6
6
  usage: "[all]",
7
7
  async run(rest, term) {
8
8
  const { open } = await term.state();