@frockbot/plugin-billing 0.3.38 → 0.3.39
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/package.json +6 -6
- package/src/bot.test.ts +75 -0
- package/src/client/format.ts +24 -6
- package/src/client/index.test.ts +11 -0
- package/src/pricing.ts +19 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-billing",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.39",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
"typecheck": "vue-tsc --noEmit -p tsconfig.json"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@frockbot/client-core": "0.3.
|
|
27
|
-
"@frockbot/client-ui": "0.3.
|
|
28
|
-
"@frockbot/kernel-contracts": "0.3.
|
|
29
|
-
"@frockbot/plugin-flock": "0.3.
|
|
30
|
-
"@frockbot/plugin-shell": "0.3.
|
|
26
|
+
"@frockbot/client-core": "0.3.39",
|
|
27
|
+
"@frockbot/client-ui": "0.3.39",
|
|
28
|
+
"@frockbot/kernel-contracts": "0.3.39",
|
|
29
|
+
"@frockbot/plugin-flock": "0.3.39",
|
|
30
|
+
"@frockbot/plugin-shell": "0.3.39",
|
|
31
31
|
"cordis": "4.0.0-rc.8",
|
|
32
32
|
"vue": "3.5.41"
|
|
33
33
|
},
|
package/src/bot.test.ts
CHANGED
|
@@ -52,6 +52,81 @@ describe("usage projection", () => {
|
|
|
52
52
|
]);
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
+
test("prices two short platform-model turns in cents, not dollars", () => {
|
|
56
|
+
/*
|
|
57
|
+
* The defect this pins. A Bot on the platform default runs `@frock/auto`,
|
|
58
|
+
* which was absent from the price table, so every Turn a User has ever had
|
|
59
|
+
* was priced at the unknown-model fallback of $10 per million input
|
|
60
|
+
* tokens. The token counts here are the real shape of a two-sentence Turn:
|
|
61
|
+
* the transport reports nothing, so the loop estimates from the normalized
|
|
62
|
+
* request, and that request carries the system prompt and every tool
|
|
63
|
+
* schema whatever the person typed. Two of them read as "$0.50".
|
|
64
|
+
*/
|
|
65
|
+
const turnEvents = (turn: number, requestId: string): SessionEvent[] =>
|
|
66
|
+
[
|
|
67
|
+
{
|
|
68
|
+
type: "model/usage",
|
|
69
|
+
seq: turn * 10,
|
|
70
|
+
timestamp: `2026-09-05T0${turn}:00:00.000Z`,
|
|
71
|
+
turn,
|
|
72
|
+
step: 1,
|
|
73
|
+
requestId,
|
|
74
|
+
provider: "flock-ai",
|
|
75
|
+
model: "@frock/auto",
|
|
76
|
+
modelBinding: { connectionId: "flock-ai-ambient" },
|
|
77
|
+
inputTokens: 24_000,
|
|
78
|
+
outputTokens: 40,
|
|
79
|
+
cachedInputTokens: 0,
|
|
80
|
+
reasoningTokens: 0,
|
|
81
|
+
latencyMs: 1_200,
|
|
82
|
+
estimated: true,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
type: "turn/end",
|
|
86
|
+
seq: turn * 10 + 1,
|
|
87
|
+
timestamp: `2026-09-05T0${turn}:00:01.000Z`,
|
|
88
|
+
turn,
|
|
89
|
+
outcome: "completed",
|
|
90
|
+
},
|
|
91
|
+
] as SessionEvent[];
|
|
92
|
+
|
|
93
|
+
const entries = [
|
|
94
|
+
...usageEntriesFromTurnV1({
|
|
95
|
+
botId: "bot-a",
|
|
96
|
+
runId: "run-a",
|
|
97
|
+
turn: 1,
|
|
98
|
+
events: turnEvents(1, "request-1"),
|
|
99
|
+
}),
|
|
100
|
+
...usageEntriesFromTurnV1({
|
|
101
|
+
botId: "bot-a",
|
|
102
|
+
runId: "run-a",
|
|
103
|
+
turn: 2,
|
|
104
|
+
events: turnEvents(2, "request-2"),
|
|
105
|
+
}),
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
// 24,000 input at $0.44/M plus 40 output at $1.32/M, per Turn.
|
|
109
|
+
expect(entries.map((entry) => entry.costMicros)).toEqual([10_613, 10_613]);
|
|
110
|
+
// Priced, not guessed: the total is two cents rather than fifty.
|
|
111
|
+
expect(entries.every((entry) => entry.unknownPrice)).toBe(false);
|
|
112
|
+
expect(entries.reduce((total, entry) => total + entry.costMicros, 0)).toBe(
|
|
113
|
+
21_226,
|
|
114
|
+
);
|
|
115
|
+
// The legacy spelling of the same model is the same price, not a fallback.
|
|
116
|
+
expect(
|
|
117
|
+
usageEntriesFromTurnV1({
|
|
118
|
+
botId: "bot-a",
|
|
119
|
+
runId: "run-b",
|
|
120
|
+
turn: 1,
|
|
121
|
+
events: turnEvents(1, "request-3").map((event) =>
|
|
122
|
+
event.type === "model/usage"
|
|
123
|
+
? { ...event, model: "@flock/auto" }
|
|
124
|
+
: event,
|
|
125
|
+
) as SessionEvent[],
|
|
126
|
+
})[0],
|
|
127
|
+
).toMatchObject({ costMicros: 10_613, unknownPrice: false });
|
|
128
|
+
});
|
|
129
|
+
|
|
55
130
|
test("keeps entries until an idempotent sink accepts them", async () => {
|
|
56
131
|
const values = new Map<string, unknown>();
|
|
57
132
|
const outbox = new UsageOutboxV1({
|
package/src/client/format.ts
CHANGED
|
@@ -1,10 +1,28 @@
|
|
|
1
|
+
/** One cent, in the micro-dollars the ledger records. */
|
|
2
|
+
const CENT_IN_MICROS_V1 = 10_000;
|
|
3
|
+
|
|
4
|
+
const USD_V1 = new Intl.NumberFormat("en", {
|
|
5
|
+
style: "currency",
|
|
6
|
+
currency: "USD",
|
|
7
|
+
minimumFractionDigits: 2,
|
|
8
|
+
maximumFractionDigits: 2,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Money, to the cent — except when rounding to the cent would say nothing
|
|
13
|
+
* happened.
|
|
14
|
+
*
|
|
15
|
+
* The ledger counts micro-dollars and a short Turn costs a few hundred of
|
|
16
|
+
* them, so two cents' worth of conversation used to render as "$0.00" beside
|
|
17
|
+
* a list of Bots that had plainly run. "<$0.01" is the honest reading of a
|
|
18
|
+
* real, sub-cent amount; exactly zero still reads as zero, because that is a
|
|
19
|
+
* different fact. Totals are never floored to build a bigger number: every
|
|
20
|
+
* figure here is the sum of what was recorded.
|
|
21
|
+
*/
|
|
1
22
|
export function formatCostV1(costMicros: number): string {
|
|
2
|
-
return
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
minimumFractionDigits: 2,
|
|
6
|
-
maximumFractionDigits: 2,
|
|
7
|
-
}).format(costMicros / 1_000_000);
|
|
23
|
+
if (costMicros > 0 && costMicros < CENT_IN_MICROS_V1) return "<$0.01";
|
|
24
|
+
if (costMicros < 0 && costMicros > -CENT_IN_MICROS_V1) return ">-$0.01";
|
|
25
|
+
return USD_V1.format(costMicros / 1_000_000);
|
|
8
26
|
}
|
|
9
27
|
|
|
10
28
|
export function shortModelNameV1(value: string): string {
|
package/src/client/index.test.ts
CHANGED
|
@@ -83,6 +83,17 @@ describe("billing client contribution", () => {
|
|
|
83
83
|
expect(mounted.state.value.report?.bots[0]?.id).toBe("bot-a");
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
+
test("says a real sub-cent amount rather than rounding it to nothing", () => {
|
|
87
|
+
// Two short platform-model Turns: real spend, well under a cent.
|
|
88
|
+
expect(formatCostV1(21_226)).toBe("$0.02");
|
|
89
|
+
expect(formatCostV1(10_613)).toBe("$0.01");
|
|
90
|
+
expect(formatCostV1(300)).toBe("<$0.01");
|
|
91
|
+
expect(formatCostV1(9_999)).toBe("<$0.01");
|
|
92
|
+
// Nothing spent is a different fact from a little spent.
|
|
93
|
+
expect(formatCostV1(0)).toBe("$0.00");
|
|
94
|
+
expect(formatCostV1(10_000)).toBe("$0.01");
|
|
95
|
+
});
|
|
96
|
+
|
|
86
97
|
test("formats dollars and compact model names for the rendered view", () => {
|
|
87
98
|
expect(formatCostV1(1_250_000)).toBe("$1.25");
|
|
88
99
|
expect(shortModelNameV1("ollama-cloud/glm-5.3-flash:cloud")).toBe(
|
package/src/pricing.ts
CHANGED
|
@@ -50,6 +50,25 @@ export const MODEL_PRICE_TABLE_V1: readonly ModelPriceV1[] = [
|
|
|
50
50
|
outputUsdPerMillion: 1.32,
|
|
51
51
|
sourceUrl: CLOUDFLARE_SOURCE,
|
|
52
52
|
},
|
|
53
|
+
/*
|
|
54
|
+
* Auto is what almost every Bot runs on, and it was absent from this table:
|
|
55
|
+
* every platform-model Turn was priced at the unknown-model fallback, which
|
|
56
|
+
* is $10/$50 per million — twenty-three times what the model it routes to
|
|
57
|
+
* actually costs. Two short Turns read as fifty cents.
|
|
58
|
+
*
|
|
59
|
+
* Auto is a Cloudflare AI Gateway dynamic route over Frock AI's text
|
|
60
|
+
* models, and Frock AI publishes exactly one, so the route's price is that
|
|
61
|
+
* model's price. A second text model would make this an approximation and
|
|
62
|
+
* the honest fix then is to record the model the route chose.
|
|
63
|
+
*/
|
|
64
|
+
{
|
|
65
|
+
provider: "flock-ai",
|
|
66
|
+
model: "@frock/auto",
|
|
67
|
+
inputUsdPerMillion: 0.44,
|
|
68
|
+
cachedInputUsdPerMillion: 0.014,
|
|
69
|
+
outputUsdPerMillion: 1.32,
|
|
70
|
+
sourceUrl: CLOUDFLARE_SOURCE,
|
|
71
|
+
},
|
|
53
72
|
...[
|
|
54
73
|
["gpt-6-astra", 10, 1, 50],
|
|
55
74
|
["gpt-5.6", 4, 0.4, 20],
|