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.
- package/CHANGELOG.md +17 -0
- package/README.md +13 -6
- package/package.json +1 -1
- package/plugins/billing/usage/actions/close.js +20 -1
- package/plugins/billing/usage/actions/org.js +39 -26
- package/plugins/billing/usage/actions/rerate.js +22 -2
- package/plugins/billing/usage/collections/rollup.d.ts +79 -0
- package/plugins/billing/usage/collections/rollup.js +8 -0
- package/plugins/billing/usage/collections/usage.d.ts +71 -2
- package/plugins/billing/usage/collections/usage.js +22 -6
- package/plugins/billing/usage/mod.d.ts +10 -5
- package/plugins/billing/usage/mod.js +10 -5
- package/plugins/billing/usage/resources/commands.js +1 -1
- package/plugins/billing/usage/shared/rate.d.ts +30 -9
- package/plugins/billing/usage/shared/rate.js +73 -30
- package/plugins/billing/usage/shared/totals.d.ts +19 -3
- package/plugins/billing/usage/shared/totals.js +43 -13
- package/plugins/billing/usage/shared/use.d.ts +12 -0
- package/plugins/billing/usage/shared/use.js +9 -0
- package/plugins/billing/usage/views/terminal/totals.d.ts +6 -2
- package/plugins/billing/usage/views/terminal/totals.js +38 -6
- package/plugins/billing/usage/views/web/chart.d.ts +7 -5
- package/plugins/billing/usage/views/web/chart.js +43 -21
- package/plugins/billing/usage/views/web/numbers.d.ts +110 -37
- package/plugins/billing/usage/views/web/numbers.js +119 -31
- package/plugins/billing/usage/views/web/org.d.ts +12 -5
- package/plugins/billing/usage/views/web/org.js +159 -53
- package/plugins/billing/usage/views/web/style.d.ts +8 -5
- package/plugins/billing/usage/views/web/style.js +87 -10
- package/plugins/billing/usage/web.d.ts +4 -3
- package/plugins/billing/usage/web.js +4 -3
- package/plugins/compute/computers/program/version.d.ts +1 -1
- package/plugins/compute/computers/program/version.js +1 -1
- package/plugins/compute/computers/resources/downloads.js +1 -1
- 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.
|
|
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.
|
|
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
|
|
386
|
-
|
|
387
|
-
|
|
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
|
|
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
|
@@ -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
|
-
*
|
|
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
|
-
/**
|
|
20
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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) =>
|
|
49
|
-
|
|
50
|
-
|
|
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)
|
|
15
|
-
* rates of its own, or a `
|
|
16
|
-
*
|
|
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)
|
|
15
|
-
* rates of its own, or a `
|
|
16
|
-
*
|
|
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
|
|
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();
|