@thehammer/danx-session-telemetry 0.1.8 → 0.1.9
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/dist/pricing.d.ts +29 -0
- package/dist/pricing.js +54 -4
- package/dist/usage-ledger.d.ts +12 -0
- package/dist/usage-ledger.js +48 -6
- package/package.json +1 -1
package/dist/pricing.d.ts
CHANGED
|
@@ -12,6 +12,25 @@
|
|
|
12
12
|
* Sonnet 5 at $3/$15 and Haiku 4.5 at $0.80/$4 where the page says $2/$10 and
|
|
13
13
|
* $1/$5, and Opus 5 / Fable 5.1 / Opus 5.5 were missing).
|
|
14
14
|
*
|
|
15
|
+
* DX-3261 — re-checked the WHOLE table row-by-row against the same page
|
|
16
|
+
* again, 2026-09-24, https://platform.claude.com/docs/en/about-claude/pricing
|
|
17
|
+
* (the "Model pricing" table). Found: `claude-opus-4-7` and `claude-opus-4-5`
|
|
18
|
+
* were both missing (page: $5 in / $25 out / $0.50 cache hit, same row shape
|
|
19
|
+
* as Opus 4.8 / Opus 4.6 — added below). Every other standard-speed row
|
|
20
|
+
* matched exactly: Fable 5.1 ($10/$50, 0.025x hit), Fable 5 ($10/$50, hit
|
|
21
|
+
* $1), Opus 5.5 ($4/$20, 0.05x hit), Opus 5 / Opus 4.8 / Opus 4.6 ($5/$25,
|
|
22
|
+
* $0.50 hit), Opus 4 retired ($15/$75, $1.50 hit), Sonnet 5 ($2/$10, $0.20
|
|
23
|
+
* hit), Sonnet 4.6 / Sonnet 4.5 / Sonnet 4 ($3/$15, $0.30 hit), Haiku 4.5
|
|
24
|
+
* ($1/$5, $0.10 hit), and the derived 1.25x/2h cache-write multipliers on
|
|
25
|
+
* every row. Fast-mode rows unchanged: Opus 5.5 ($8/$40), Opus 5 / Opus 4.8
|
|
26
|
+
* ($10/$50) — Opus 4.7 fast mode returns an error per the page (never
|
|
27
|
+
* priced), and Opus 4.6 fast mode is handled separately below (the page
|
|
28
|
+
* states it runs at standard speed, billed at standard rates — not an
|
|
29
|
+
* unpriced fast row). Claude Mythos 5.1/5 (limited-availability, same rates
|
|
30
|
+
* as their Fable counterparts) and the two retired-elsewhere rows (Opus 4.1,
|
|
31
|
+
* Haiku 3.5) are not in Claude Code's own model-tag surface and are left out
|
|
32
|
+
* on the same basis the table already excludes them.
|
|
33
|
+
*
|
|
15
34
|
* DX-2963 — cache writes are TWO prices, never one: a 5-minute write is
|
|
16
35
|
* 1.25x base input and a 1-hour write is 2x base input. Claude Code reports
|
|
17
36
|
* each lifetime separately (`usage.cache_creation.ephemeral_5m_input_tokens` /
|
|
@@ -58,6 +77,16 @@ export type PriceResult = {
|
|
|
58
77
|
* and records a visible `unpriced_model` event, instead of one new model id
|
|
59
78
|
* failing every write (the live tick used to fail every 30 seconds on it).
|
|
60
79
|
* `<synthetic>` is never passed here: callers exclude it before pricing.
|
|
80
|
+
*
|
|
81
|
+
* DX-3261 — both tables are looked up with `Object.hasOwn`, never bracket
|
|
82
|
+
* access + truthiness (`table[model]`): a model id that collides with an
|
|
83
|
+
* inherited `Object.prototype` key (`"toString"`, `"constructor"`, …) would
|
|
84
|
+
* otherwise resolve to that inherited FUNCTION — truthy, so the `!p` guard
|
|
85
|
+
* never fires — and every subsequent `p.input` / `p.output` / … reads
|
|
86
|
+
* `undefined` off it, so the whole cost total becomes `NaN`. `Object.hasOwn`
|
|
87
|
+
* answers "does the row itself have this key" and can never be fooled by
|
|
88
|
+
* the prototype chain, so an unpriced model always returns `unpriced`
|
|
89
|
+
* cleanly, whatever its id looks like.
|
|
61
90
|
*/
|
|
62
91
|
export declare function priceUsage(model: string, speed: string, tokens: PricedTokens): PriceResult;
|
|
63
92
|
/** True when `model` has a standard-speed price (used by tests + guards). */
|
package/dist/pricing.js
CHANGED
|
@@ -12,6 +12,25 @@
|
|
|
12
12
|
* Sonnet 5 at $3/$15 and Haiku 4.5 at $0.80/$4 where the page says $2/$10 and
|
|
13
13
|
* $1/$5, and Opus 5 / Fable 5.1 / Opus 5.5 were missing).
|
|
14
14
|
*
|
|
15
|
+
* DX-3261 — re-checked the WHOLE table row-by-row against the same page
|
|
16
|
+
* again, 2026-09-24, https://platform.claude.com/docs/en/about-claude/pricing
|
|
17
|
+
* (the "Model pricing" table). Found: `claude-opus-4-7` and `claude-opus-4-5`
|
|
18
|
+
* were both missing (page: $5 in / $25 out / $0.50 cache hit, same row shape
|
|
19
|
+
* as Opus 4.8 / Opus 4.6 — added below). Every other standard-speed row
|
|
20
|
+
* matched exactly: Fable 5.1 ($10/$50, 0.025x hit), Fable 5 ($10/$50, hit
|
|
21
|
+
* $1), Opus 5.5 ($4/$20, 0.05x hit), Opus 5 / Opus 4.8 / Opus 4.6 ($5/$25,
|
|
22
|
+
* $0.50 hit), Opus 4 retired ($15/$75, $1.50 hit), Sonnet 5 ($2/$10, $0.20
|
|
23
|
+
* hit), Sonnet 4.6 / Sonnet 4.5 / Sonnet 4 ($3/$15, $0.30 hit), Haiku 4.5
|
|
24
|
+
* ($1/$5, $0.10 hit), and the derived 1.25x/2h cache-write multipliers on
|
|
25
|
+
* every row. Fast-mode rows unchanged: Opus 5.5 ($8/$40), Opus 5 / Opus 4.8
|
|
26
|
+
* ($10/$50) — Opus 4.7 fast mode returns an error per the page (never
|
|
27
|
+
* priced), and Opus 4.6 fast mode is handled separately below (the page
|
|
28
|
+
* states it runs at standard speed, billed at standard rates — not an
|
|
29
|
+
* unpriced fast row). Claude Mythos 5.1/5 (limited-availability, same rates
|
|
30
|
+
* as their Fable counterparts) and the two retired-elsewhere rows (Opus 4.1,
|
|
31
|
+
* Haiku 3.5) are not in Claude Code's own model-tag surface and are left out
|
|
32
|
+
* on the same basis the table already excludes them.
|
|
33
|
+
*
|
|
15
34
|
* DX-2963 — cache writes are TWO prices, never one: a 5-minute write is
|
|
16
35
|
* 1.25x base input and a 1-hour write is 2x base input. Claude Code reports
|
|
17
36
|
* each lifetime separately (`usage.cache_creation.ephemeral_5m_input_tokens` /
|
|
@@ -51,8 +70,10 @@ const PRICING = {
|
|
|
51
70
|
"claude-opus-5-5": price(4, 20, 0.2),
|
|
52
71
|
"claude-opus-5": price(5, 25, 0.5),
|
|
53
72
|
"claude-opus-4-8": price(5, 25, 0.5),
|
|
73
|
+
"claude-opus-4-7": price(5, 25, 0.5),
|
|
54
74
|
"claude-opus-4-6": price(5, 25, 0.5),
|
|
55
75
|
"claude-opus-4-6-20250916": price(5, 25, 0.5),
|
|
76
|
+
"claude-opus-4-5": price(5, 25, 0.5),
|
|
56
77
|
"claude-opus-4-20250514": price(15, 75, 1.5),
|
|
57
78
|
"claude-sonnet-5": price(2, 10, 0.2),
|
|
58
79
|
"claude-sonnet-4-6": price(3, 15, 0.3),
|
|
@@ -78,6 +99,22 @@ const FAST_PRICING = {
|
|
|
78
99
|
"claude-opus-5": price(10, 50, 1),
|
|
79
100
|
"claude-opus-4-8": price(10, 50, 1),
|
|
80
101
|
};
|
|
102
|
+
/**
|
|
103
|
+
* DX-3261 — models whose `speed:"fast"` requests are billed at STANDARD
|
|
104
|
+
* rates, per the page's "Fast mode pricing" section: *"Fast mode is not
|
|
105
|
+
* available on Claude Opus 4.7 (requests with `speed: "fast"` return an
|
|
106
|
+
* error) or Claude Opus 4.6 (requests run at standard speed and are billed
|
|
107
|
+
* at standard rates)."* Opus 4.7 correctly falls through to `unpriced` below
|
|
108
|
+
* (it has no row in either table — matches the page's "returns an error").
|
|
109
|
+
* Opus 4.6 does NOT: DX-2963's own wording sent it down the same unpriced
|
|
110
|
+
* path as Opus 4.7, contradicting the page it pins — a fast-mode Opus 4.6
|
|
111
|
+
* reply silently lost its cost. The page wins: look it up in the STANDARD
|
|
112
|
+
* table instead of `unpriced`, never invent a separate fast row for it.
|
|
113
|
+
*/
|
|
114
|
+
const FAST_BILLS_AT_STANDARD_RATE = new Set([
|
|
115
|
+
"claude-opus-4-6",
|
|
116
|
+
"claude-opus-4-6-20250916",
|
|
117
|
+
]);
|
|
81
118
|
/**
|
|
82
119
|
* Price one unit of usage. NEVER throws (DX-2963 replaces DX-1720's throw):
|
|
83
120
|
* an unknown model — or a fast reply on a model with no fast price — returns
|
|
@@ -85,12 +122,23 @@ const FAST_PRICING = {
|
|
|
85
122
|
* and records a visible `unpriced_model` event, instead of one new model id
|
|
86
123
|
* failing every write (the live tick used to fail every 30 seconds on it).
|
|
87
124
|
* `<synthetic>` is never passed here: callers exclude it before pricing.
|
|
125
|
+
*
|
|
126
|
+
* DX-3261 — both tables are looked up with `Object.hasOwn`, never bracket
|
|
127
|
+
* access + truthiness (`table[model]`): a model id that collides with an
|
|
128
|
+
* inherited `Object.prototype` key (`"toString"`, `"constructor"`, …) would
|
|
129
|
+
* otherwise resolve to that inherited FUNCTION — truthy, so the `!p` guard
|
|
130
|
+
* never fires — and every subsequent `p.input` / `p.output` / … reads
|
|
131
|
+
* `undefined` off it, so the whole cost total becomes `NaN`. `Object.hasOwn`
|
|
132
|
+
* answers "does the row itself have this key" and can never be fooled by
|
|
133
|
+
* the prototype chain, so an unpriced model always returns `unpriced`
|
|
134
|
+
* cleanly, whatever its id looks like.
|
|
88
135
|
*/
|
|
89
136
|
export function priceUsage(model, speed, tokens) {
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
if (!
|
|
137
|
+
const useFastTable = speed === FAST_SPEED && !FAST_BILLS_AT_STANDARD_RATE.has(model);
|
|
138
|
+
const table = useFastTable ? FAST_PRICING : PRICING;
|
|
139
|
+
if (!Object.hasOwn(table, model))
|
|
93
140
|
return { kind: "unpriced", model, speed };
|
|
141
|
+
const p = table[model];
|
|
94
142
|
return {
|
|
95
143
|
kind: "priced",
|
|
96
144
|
costUsd: (tokens.input * p.input +
|
|
@@ -103,5 +151,7 @@ export function priceUsage(model, speed, tokens) {
|
|
|
103
151
|
}
|
|
104
152
|
/** True when `model` has a standard-speed price (used by tests + guards). */
|
|
105
153
|
export function isPricedModel(model) {
|
|
106
|
-
|
|
154
|
+
// DX-3261 — `in` walks the prototype chain (`"toString" in {}` is `true`);
|
|
155
|
+
// `Object.hasOwn` can't be fooled by it. See `priceUsage`'s own comment.
|
|
156
|
+
return Object.hasOwn(PRICING, model);
|
|
107
157
|
}
|
package/dist/usage-ledger.d.ts
CHANGED
|
@@ -55,6 +55,18 @@ export interface LedgerLine {
|
|
|
55
55
|
toolUseIds: readonly string[];
|
|
56
56
|
}
|
|
57
57
|
export declare const MAIN_SOURCE = "main";
|
|
58
|
+
/**
|
|
59
|
+
* DX-3261 — a producer handed this module a line it cannot safely key or
|
|
60
|
+
* date: no valid `byte_offset` on a no-`message.id` line (would collapse
|
|
61
|
+
* every such line in the source onto the same synthetic key, crediting only
|
|
62
|
+
* the largest — see {@link syntheticMessageKey}), or a `timestamp` that
|
|
63
|
+
* doesn't parse (would read as `repliedAt: 0`, invisible to every
|
|
64
|
+
* account-window lookback). Both were silent `0` defaults before this card;
|
|
65
|
+
* thrown here instead, so a broken producer fails immediately and visibly
|
|
66
|
+
* rather than quietly losing or mis-crediting usage.
|
|
67
|
+
*/
|
|
68
|
+
export declare class LedgerLineDataError extends Error {
|
|
69
|
+
}
|
|
58
70
|
/**
|
|
59
71
|
* DX-2963 — a usage line with no `message.id` (never observed in the 22,087
|
|
60
72
|
* usage lines of the 20 newest local transcripts, 2026-09-18) is keyed by its
|
package/dist/usage-ledger.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
export const MAIN_SOURCE = "main";
|
|
2
|
+
/**
|
|
3
|
+
* DX-3261 — a producer handed this module a line it cannot safely key or
|
|
4
|
+
* date: no valid `byte_offset` on a no-`message.id` line (would collapse
|
|
5
|
+
* every such line in the source onto the same synthetic key, crediting only
|
|
6
|
+
* the largest — see {@link syntheticMessageKey}), or a `timestamp` that
|
|
7
|
+
* doesn't parse (would read as `repliedAt: 0`, invisible to every
|
|
8
|
+
* account-window lookback). Both were silent `0` defaults before this card;
|
|
9
|
+
* thrown here instead, so a broken producer fails immediately and visibly
|
|
10
|
+
* rather than quietly losing or mis-crediting usage.
|
|
11
|
+
*/
|
|
12
|
+
export class LedgerLineDataError extends Error {
|
|
13
|
+
}
|
|
2
14
|
/**
|
|
3
15
|
* DX-2963 — a usage line with no `message.id` (never observed in the 22,087
|
|
4
16
|
* usage lines of the 20 newest local transcripts, 2026-09-18) is keyed by its
|
|
@@ -12,13 +24,25 @@ export function isSyntheticMessageKey(key) {
|
|
|
12
24
|
return key.startsWith("nomsg:");
|
|
13
25
|
}
|
|
14
26
|
/**
|
|
15
|
-
* A usage field as a non-negative count. A numeric STRING is read as
|
|
16
|
-
* number (drift-safe: a transcript that ever quotes a count is still
|
|
17
|
-
* never silently zeroed); anything else absent or non-numeric is 0.
|
|
27
|
+
* A usage field as a non-negative INTEGER count. A numeric STRING is read as
|
|
28
|
+
* its number (drift-safe: a transcript that ever quotes a count is still
|
|
29
|
+
* counted, never silently zeroed); anything else absent or non-numeric is 0.
|
|
30
|
+
*
|
|
31
|
+
* DX-3261 — rounded to the nearest integer, never passed through fractional.
|
|
32
|
+
* A token count is an integer by definition, so a fractional value here is
|
|
33
|
+
* itself the anomaly (a malformed/synthetic producer), not a legitimate
|
|
34
|
+
* reading to preserve verbatim — and the wire contract this feeds
|
|
35
|
+
* (`parseStatsPayload`'s `isNonNegInt`, `usage-ledger-wire.ts`) REQUIRES an
|
|
36
|
+
* integer and fails loud on anything else. Before this fix a fractional
|
|
37
|
+
* value here would sail through the counter, get queued as a changed entry,
|
|
38
|
+
* then 400 on every single stats-write attempt for that dispatch FOREVER —
|
|
39
|
+
* the worker's `flush()` re-queues a failed batch unconditionally, so a
|
|
40
|
+
* value that never changes never stops failing. Rounding at the source
|
|
41
|
+
* closes the gap without loosening the wire's own integer contract.
|
|
18
42
|
*/
|
|
19
43
|
function num(v) {
|
|
20
44
|
const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
21
|
-
return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : 0;
|
|
45
|
+
return typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.round(n) : 0;
|
|
22
46
|
}
|
|
23
47
|
/**
|
|
24
48
|
* Read a raw `message.usage` block into {@link LedgerUsage}. The two
|
|
@@ -202,6 +226,13 @@ export function ledgerLineFromRaw(raw, source, byteOffset) {
|
|
|
202
226
|
? message.content
|
|
203
227
|
: [];
|
|
204
228
|
const ts = typeof raw.timestamp === "string" ? Date.parse(raw.timestamp) : NaN;
|
|
229
|
+
// DX-3261 — an unparseable/missing timestamp used to silently become
|
|
230
|
+
// `repliedAt: 0` (epoch 0), which falls outside every account-window
|
|
231
|
+
// lookback and made that reply's tokens permanently invisible to the
|
|
232
|
+
// gate — with no event, no log, nothing. Fail loud instead.
|
|
233
|
+
if (!Number.isFinite(ts)) {
|
|
234
|
+
throw new LedgerLineDataError(`ledgerLineFromRaw: unparseable/missing timestamp (raw.timestamp=${JSON.stringify(raw.timestamp)})`);
|
|
235
|
+
}
|
|
205
236
|
return {
|
|
206
237
|
messageId: typeof message.id === "string" && message.id.length > 0 ? message.id : undefined,
|
|
207
238
|
source,
|
|
@@ -212,7 +243,7 @@ export function ledgerLineFromRaw(raw, source, byteOffset) {
|
|
|
212
243
|
usage: message.usage && typeof message.usage === "object"
|
|
213
244
|
? message.usage
|
|
214
245
|
: undefined,
|
|
215
|
-
timestamp:
|
|
246
|
+
timestamp: ts,
|
|
216
247
|
toolUseIds: content
|
|
217
248
|
.filter((b) => b.type === "tool_use" && typeof b.id === "string")
|
|
218
249
|
.map((b) => b.id),
|
|
@@ -260,8 +291,19 @@ export function ledgerLineFromLogEntry(entry) {
|
|
|
260
291
|
const content = Array.isArray(d.content)
|
|
261
292
|
? d.content
|
|
262
293
|
: [];
|
|
294
|
+
const hasMessageId = typeof d.messageId === "string" && d.messageId.length > 0;
|
|
295
|
+
// DX-3261 — `byteOffset` only matters as the synthetic key for a line with
|
|
296
|
+
// no `message.id` (`syntheticMessageKey`); a missing/invalid one used to
|
|
297
|
+
// silently default to `0`, collapsing EVERY no-id reply from this source
|
|
298
|
+
// onto one key so only the largest single reply was ever credited. Fail
|
|
299
|
+
// loud rather than repeat that silent collapse. Harmless when a real
|
|
300
|
+
// `messageId` is present (byteOffset is never used to key that line), so
|
|
301
|
+
// only fail when the line actually needs it.
|
|
302
|
+
if (!hasMessageId && typeof d.byte_offset !== "number") {
|
|
303
|
+
throw new LedgerLineDataError(`ledgerLineFromLogEntry: missing/invalid byte_offset on a no-message-id line (source=${typeof d.subagent_id === "string" ? d.subagent_id : MAIN_SOURCE})`);
|
|
304
|
+
}
|
|
263
305
|
return {
|
|
264
|
-
messageId:
|
|
306
|
+
messageId: hasMessageId ? d.messageId : undefined,
|
|
265
307
|
source: typeof d.subagent_id === "string" ? d.subagent_id : MAIN_SOURCE,
|
|
266
308
|
byteOffset: typeof d.byte_offset === "number" ? d.byte_offset : 0,
|
|
267
309
|
model: typeof d.model === "string" && d.model.length > 0 ? d.model : undefined,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thehammer/danx-session-telemetry",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Shared Claude Code transcript collector (DX-2962) — incremental JSONL tailing for the danxbot worker and the danx-dashboard-mcp desktop bridge, so both run identical collector code.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|