@itookit/dsht 0.2.4 → 0.3.2
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/README.i18n.yaml +2 -2
- package/README.md +54 -25
- package/README.zh.md +54 -25
- package/dist/catalog/controller.d.ts +32 -0
- package/dist/catalog/controller.js +88 -0
- package/dist/catalog/index.d.ts +2 -0
- package/dist/catalog/index.js +2 -0
- package/dist/{cli.js → cli/index.js} +40 -24
- package/dist/controller/connection.d.ts +80 -0
- package/dist/controller/connection.js +190 -0
- package/dist/controller/controller.d.ts +260 -0
- package/dist/controller/controller.js +362 -0
- package/dist/controller/index.d.ts +5 -0
- package/dist/controller/index.js +3 -0
- package/dist/controller/memory-log.d.ts +35 -0
- package/dist/controller/memory-log.js +95 -0
- package/dist/cost/controller.d.ts +34 -0
- package/dist/cost/controller.js +107 -0
- package/dist/cost/index.d.ts +9 -0
- package/dist/cost/index.js +7 -0
- package/dist/cost/ledger-files.d.ts +20 -0
- package/dist/cost/ledger-files.js +128 -0
- package/dist/cost/ledger.d.ts +65 -0
- package/dist/cost/ledger.js +136 -0
- package/dist/cost/pricing.d.ts +48 -0
- package/dist/cost/pricing.js +141 -0
- package/dist/cost/records.d.ts +17 -0
- package/dist/cost/records.js +66 -0
- package/dist/cost/scanner.d.ts +22 -0
- package/dist/cost/scanner.js +95 -0
- package/dist/cost/types.d.ts +69 -0
- package/dist/cost/types.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/session/connection-view.d.ts +18 -0
- package/dist/session/connection-view.js +1 -0
- package/dist/{controller.d.ts → session/controller.d.ts} +119 -137
- package/dist/session/controller.js +616 -0
- package/dist/session/export-html.d.ts +9 -0
- package/dist/session/export-html.js +39 -0
- package/dist/session/export.d.ts +9 -0
- package/dist/session/export.js +38 -0
- package/dist/{history.d.ts → session/history.d.ts} +18 -1
- package/dist/session/history.js +338 -0
- package/dist/session/index.d.ts +17 -0
- package/dist/session/index.js +10 -0
- package/dist/session/markdown.d.ts +39 -0
- package/dist/session/markdown.js +255 -0
- package/dist/session/math.d.ts +11 -0
- package/dist/session/math.js +82 -0
- package/dist/{navigation.d.ts → session/navigation.d.ts} +1 -1
- package/dist/{navigation.js → session/navigation.js} +1 -1
- package/dist/{references.js → session/references.js} +1 -1
- package/dist/{telemetry.d.ts → session/telemetry.d.ts} +12 -1
- package/dist/{telemetry.js → session/telemetry.js} +18 -4
- package/dist/{transcript.d.ts → session/transcript.d.ts} +23 -1
- package/dist/{transcript.js → session/transcript.js} +36 -18
- package/dist/session/types.d.ts +18 -0
- package/dist/session/types.js +2 -0
- package/dist/state.d.ts +41 -0
- package/dist/state.js +9 -0
- package/dist/storage/directories.d.ts +14 -0
- package/dist/storage/directories.js +24 -0
- package/dist/storage/files.d.ts +43 -0
- package/dist/storage/files.js +135 -0
- package/dist/storage/index.d.ts +3 -0
- package/dist/storage/index.js +3 -0
- package/dist/transport/auth.js +65 -0
- package/dist/{client.d.ts → transport/client.d.ts} +8 -1
- package/dist/{client.js → transport/client.js} +21 -4
- package/dist/transport/host.d.ts +13 -0
- package/dist/transport/host.js +1 -0
- package/dist/ui/app.d.ts +11 -0
- package/dist/ui/app.js +726 -0
- package/dist/ui/chat/header.d.ts +11 -0
- package/dist/ui/chat/header.js +14 -0
- package/dist/{history-view.d.ts → ui/chat/history-view.d.ts} +1 -1
- package/dist/{history-view.js → ui/chat/history-view.js} +3 -3
- package/dist/ui/chat/status.d.ts +79 -0
- package/dist/ui/chat/status.js +311 -0
- package/dist/ui/chat/viewport.d.ts +17 -0
- package/dist/ui/chat/viewport.js +14 -0
- package/dist/ui/commands/parse.d.ts +96 -0
- package/dist/ui/commands/parse.js +121 -0
- package/dist/ui/commands/registry.d.ts +33 -0
- package/dist/ui/commands/registry.js +72 -0
- package/dist/ui/copy-mode.d.ts +4 -0
- package/dist/ui/copy-mode.js +6 -0
- package/dist/{cost-view.d.ts → ui/dialogs/cost.d.ts} +1 -1
- package/dist/{cost-view.js → ui/dialogs/cost.js} +3 -3
- package/dist/ui/dialogs/index.d.ts +120 -0
- package/dist/ui/dialogs/index.js +113 -0
- package/dist/ui/dialogs/picker.d.ts +18 -0
- package/dist/ui/dialogs/picker.js +38 -0
- package/dist/ui/frozen.d.ts +8 -0
- package/dist/ui/frozen.js +7 -0
- package/dist/ui/input/references.d.ts +12 -0
- package/dist/ui/input/references.js +15 -0
- package/dist/ui/mount.d.ts +6 -0
- package/dist/ui/mount.js +11 -0
- package/dist/{theme.d.ts → ui/theme/index.d.ts} +1 -1
- package/dsht-m.png +0 -0
- package/package.json +20 -14
- package/dist/app.d.ts +0 -21
- package/dist/app.js +0 -746
- package/dist/auth.js +0 -108
- package/dist/controller.js +0 -923
- package/dist/cost.d.ts +0 -119
- package/dist/cost.js +0 -313
- package/dist/history.js +0 -177
- package/dist/status.d.ts +0 -28
- package/dist/status.js +0 -157
- package/dsht.png +0 -0
- /package/dist/{cli.d.ts → cli/index.d.ts} +0 -0
- /package/dist/{memory.d.ts → session/memory.d.ts} +0 -0
- /package/dist/{memory.js → session/memory.js} +0 -0
- /package/dist/{references.d.ts → session/references.d.ts} +0 -0
- /package/dist/{auth.d.ts → transport/auth.d.ts} +0 -0
- /package/dist/{endpoint.d.ts → transport/endpoint.d.ts} +0 -0
- /package/dist/{endpoint.js → transport/endpoint.js} +0 -0
- /package/dist/{wire.d.ts → transport/wire.d.ts} +0 -0
- /package/dist/{wire.js → transport/wire.js} +0 -0
- /package/dist/{input-history.d.ts → ui/input/history.d.ts} +0 -0
- /package/dist/{input-history.js → ui/input/history.js} +0 -0
- /package/dist/{input.d.ts → ui/input/input.d.ts} +0 -0
- /package/dist/{input.js → ui/input/input.js} +0 -0
- /package/dist/{mouse.d.ts → ui/input/mouse.d.ts} +0 -0
- /package/dist/{mouse.js → ui/input/mouse.js} +0 -0
- /package/dist/{theme.js → ui/theme/index.js} +0 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/** Versioned CNY price tables and the price decision taken for one request sample. */
|
|
2
|
+
import { object } from "../transport/wire.js";
|
|
3
|
+
import { MISSING_USAGE } from "./types.js";
|
|
4
|
+
const clocks = new Map();
|
|
5
|
+
/** Published rates verified on 2026-09-10; preceding dates require historical configuration.
|
|
6
|
+
* Flash and Pro are priced independently, and a separate cache write uses the cache-miss input rate.
|
|
7
|
+
*/
|
|
8
|
+
const OFFICIAL_PRICING = 'https://api-docs.deepseek.com/zh-cn/quick_start/pricing/';
|
|
9
|
+
const PEAK_SCHEDULE = { weekdays: [1, 2, 3, 4, 5], windows: [[540, 720], [840, 1080]] };
|
|
10
|
+
const FLASH_RATES = { peak: { input: 2, cacheRead: 0.04, cacheWrite: 2, output: 8 },
|
|
11
|
+
offPeak: { input: 1, cacheRead: 0.02, cacheWrite: 1, output: 4 } };
|
|
12
|
+
const PRO_RATES = { peak: { input: 9, cacheRead: 0.3, cacheWrite: 9, output: 27 },
|
|
13
|
+
offPeak: { input: 4.5, cacheRead: 0.15, cacheWrite: 4.5, output: 13.5 } };
|
|
14
|
+
export const DEFAULT_PRICES = [
|
|
15
|
+
{ id: 'deepseek-2026-09-10-flash', provider: 'deepseek-official', model: 'deepseek-flash',
|
|
16
|
+
from: '2026-09-10T00:00:00+08:00', currency: 'CNY', source: OFFICIAL_PRICING, timezone: 'Asia/Shanghai',
|
|
17
|
+
...PEAK_SCHEDULE, ...FLASH_RATES },
|
|
18
|
+
{ id: 'deepseek-2026-09-10-pro', provider: 'deepseek-official', model: 'deepseek-v4-pro',
|
|
19
|
+
from: '2026-09-10T00:00:00+08:00', until: '2026-09-14T12:00:00+08:00', currency: 'CNY',
|
|
20
|
+
source: OFFICIAL_PRICING, timezone: 'Asia/Shanghai', ...PEAK_SCHEDULE, ...PRO_RATES },
|
|
21
|
+
// The provider bills `deepseek-v4-pro` requests at Flash rates once V4 Pro is retired.
|
|
22
|
+
{ id: 'deepseek-2026-09-14-pro-served-by-flash', provider: 'deepseek-official', model: 'deepseek-v4-pro',
|
|
23
|
+
from: '2026-09-14T12:00:00+08:00', currency: 'CNY', source: OFFICIAL_PRICING, timezone: 'Asia/Shanghai',
|
|
24
|
+
...PEAK_SCHEDULE, ...FLASH_RATES },
|
|
25
|
+
];
|
|
26
|
+
/** Validate user-maintained price versions, rejecting ambiguous overlapping intervals.
|
|
27
|
+
* @param value - Parsed prices.json array.
|
|
28
|
+
* @returns Price versions with validated rates and schedules.
|
|
29
|
+
*/
|
|
30
|
+
export function pricesFrom(value) {
|
|
31
|
+
if (!Array.isArray(value))
|
|
32
|
+
throw new Error('prices.json must contain an array');
|
|
33
|
+
const ids = new Set();
|
|
34
|
+
for (const raw of value) {
|
|
35
|
+
const p = object(raw);
|
|
36
|
+
for (const key of ['id', 'provider', 'model', 'source', 'timezone', 'from'])
|
|
37
|
+
if (typeof p[key] !== 'string' || !p[key])
|
|
38
|
+
throw new Error(`Invalid price ${key}`);
|
|
39
|
+
if (ids.has(String(p.id)))
|
|
40
|
+
throw new Error('Duplicate price id');
|
|
41
|
+
ids.add(String(p.id));
|
|
42
|
+
const from = Date.parse(String(p.from));
|
|
43
|
+
const until = p.until === undefined ? Infinity : Date.parse(String(p.until));
|
|
44
|
+
if (!Number.isFinite(from) || !(until > from) || p.currency !== 'CNY')
|
|
45
|
+
throw new Error('Invalid price interval or currency');
|
|
46
|
+
new Intl.DateTimeFormat('en', { timeZone: String(p.timezone) }).format();
|
|
47
|
+
for (const key of ['peak', 'offPeak'])
|
|
48
|
+
for (const bucket of ['input', 'cacheRead', 'cacheWrite', 'output']) {
|
|
49
|
+
const rate = object(p[key])[bucket];
|
|
50
|
+
if (typeof rate !== 'number' || !Number.isFinite(rate) || rate < 0)
|
|
51
|
+
throw new Error('Invalid token rate');
|
|
52
|
+
}
|
|
53
|
+
if (!Array.isArray(p.weekdays) || p.weekdays.some(d => typeof d !== 'number' || !Number.isInteger(d) || d < 0 || d > 6)
|
|
54
|
+
|| !Array.isArray(p.windows) || p.windows.some(w => !Array.isArray(w) || w.length !== 2 || w.some(n => typeof n !== 'number' || !Number.isInteger(n)) || Number(w[0]) < 0 || Number(w[1]) > 1440 || Number(w[0]) >= Number(w[1])))
|
|
55
|
+
throw new Error('Invalid peak schedule');
|
|
56
|
+
}
|
|
57
|
+
const prices = value;
|
|
58
|
+
for (const [index, p] of prices.entries())
|
|
59
|
+
for (const q of prices.slice(index + 1)) {
|
|
60
|
+
if (p.provider === q.provider && p.model === q.model && Date.parse(p.from) < (q.until ? Date.parse(q.until) : Infinity)
|
|
61
|
+
&& Date.parse(q.from) < (p.until ? Date.parse(p.until) : Infinity))
|
|
62
|
+
throw new Error('Overlapping price intervals');
|
|
63
|
+
}
|
|
64
|
+
return prices;
|
|
65
|
+
}
|
|
66
|
+
/** Return the calendar date used by both daily and three-calendar-day summaries.
|
|
67
|
+
* @param time - Epoch milliseconds.
|
|
68
|
+
* @returns Beijing calendar date, YYYY-MM-DD.
|
|
69
|
+
*/
|
|
70
|
+
export function costDay(time) { return new Date(time + 8 * 3600_000).toISOString().slice(0, 10); }
|
|
71
|
+
/** Price family used when a recorded model name has no exact entry. */
|
|
72
|
+
function priceFamily(model) { return model.toLowerCase().includes('pro') ? 'deepseek-v4-pro' : 'deepseek-flash'; }
|
|
73
|
+
/** Candidate versions for one request: its exact model first, then the official model family. */
|
|
74
|
+
function candidates(prices, provider, model) {
|
|
75
|
+
const exact = prices.filter(p => p.provider === provider && p.model === model);
|
|
76
|
+
if (exact.length)
|
|
77
|
+
return exact;
|
|
78
|
+
return provider === 'deepseek-official' ? prices.filter(p => p.model === priceFamily(model)) : [];
|
|
79
|
+
}
|
|
80
|
+
/** Select a price by event time, applying half-open local peak windows.
|
|
81
|
+
* @param prices - Validated versions.
|
|
82
|
+
* @param provider - Provider identity from the recorded request.
|
|
83
|
+
* @param model - Recorded model name; official DeepSeek aliases fall back to Pro when containing pro, otherwise Flash.
|
|
84
|
+
* @param time - Recorded settlement timestamp used as a billing-time estimate.
|
|
85
|
+
* @returns Matching price version and per-million-token rates, if known.
|
|
86
|
+
*/
|
|
87
|
+
export function priceAt(prices, provider, model, time) {
|
|
88
|
+
const price = candidates(prices, provider, model).find(p => Date.parse(p.from) <= time && (p.until === undefined || time < Date.parse(p.until)));
|
|
89
|
+
if (!price)
|
|
90
|
+
return;
|
|
91
|
+
let clock = clocks.get(price.timezone);
|
|
92
|
+
if (!clock) {
|
|
93
|
+
clock = new Intl.DateTimeFormat('en-US', { timeZone: price.timezone, weekday: 'short', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' });
|
|
94
|
+
clocks.set(price.timezone, clock);
|
|
95
|
+
}
|
|
96
|
+
const parts = clock.formatToParts(time);
|
|
97
|
+
const part = (name) => parts.find(p => p.type === name).value;
|
|
98
|
+
const day = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].indexOf(part('weekday'));
|
|
99
|
+
const minute = Number(part('hour')) * 60 + Number(part('minute'));
|
|
100
|
+
return { price, rates: price.weekdays.includes(day) && price.windows.some(([a, b]) => minute >= a && minute < b) ? price.peak : price.offPeak };
|
|
101
|
+
}
|
|
102
|
+
/** Select a rate without a settlement time, so an unattributable request still enters the total.
|
|
103
|
+
* The cheapest candidate off-peak rate is a floor: it never overstates, and the charge stays
|
|
104
|
+
* marked as estimated.
|
|
105
|
+
* @param prices - Validated versions.
|
|
106
|
+
* @param provider - Provider identity from the recorded request.
|
|
107
|
+
* @param model - Recorded model name.
|
|
108
|
+
* @returns The candidate version with the lowest off-peak input rate and its rates, if any.
|
|
109
|
+
*/
|
|
110
|
+
export function lowestPrice(prices, provider, model) {
|
|
111
|
+
let best;
|
|
112
|
+
for (const price of candidates(prices, provider, model)) {
|
|
113
|
+
if (best === undefined || price.offPeak.input < best.rates.input)
|
|
114
|
+
best = { price, rates: price.offPeak };
|
|
115
|
+
}
|
|
116
|
+
return best;
|
|
117
|
+
}
|
|
118
|
+
/** Decide the amount for one request sample using the table loaded at decision time.
|
|
119
|
+
*
|
|
120
|
+
* The returned decision is recorded once and never revisited: a later `prices.json` change must
|
|
121
|
+
* not move a historical amount. Only a sample with no usable usage (`missing usage`) is left
|
|
122
|
+
* undecided, because its request has not finished reporting tokens yet.
|
|
123
|
+
* @param prices - Validated versions currently loaded.
|
|
124
|
+
* @param provider - Provider identity from the recorded request.
|
|
125
|
+
* @param model - Recorded model name.
|
|
126
|
+
* @param time - Recorded settlement timestamp, when the host logged one.
|
|
127
|
+
* @param usage - Disjoint token buckets, when the host reported valid counts.
|
|
128
|
+
* @returns The selected price identity, the amount, and the reason when no amount exists.
|
|
129
|
+
*/
|
|
130
|
+
export function chargeFor(prices, provider, model, time, usage) {
|
|
131
|
+
if (!usage)
|
|
132
|
+
return { reason: MISSING_USAGE };
|
|
133
|
+
const selected = time === undefined ? lowestPrice(prices, provider, model) : priceAt(prices, provider, model, time);
|
|
134
|
+
if (!selected)
|
|
135
|
+
return { reason: 'no price version' };
|
|
136
|
+
const amount = (usage.input * selected.rates.input + usage.output * selected.rates.output
|
|
137
|
+
+ usage.cacheRead * selected.rates.cacheRead + usage.cacheWrite * selected.rates.cacheWrite) / 1e6;
|
|
138
|
+
if (!Number.isFinite(amount))
|
|
139
|
+
return { reason: 'invalid estimate' };
|
|
140
|
+
return { priceId: selected.price.id, amount, ...(time === undefined ? { estimated: true } : {}) };
|
|
141
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Fold host history records into per-request samples; conversation text never enters the ledger. */
|
|
2
|
+
import { type ObjectValue } from '../transport/wire.ts';
|
|
3
|
+
import type { ChargeSample } from './types.ts';
|
|
4
|
+
/** Keep only billing-relevant fields; prompts, tool bodies, cookies and keys never enter the ledger.
|
|
5
|
+
* @param records - One HTTP history page's records.
|
|
6
|
+
* @returns Minimal durable events for a deterministic usage fold.
|
|
7
|
+
*/
|
|
8
|
+
export declare function costRecords(records: unknown): ObjectValue[];
|
|
9
|
+
/** Fold minimal billing events into one sample per model attempt.
|
|
10
|
+
*
|
|
11
|
+
* A replacement sample in the same turn and step updates its attempt's sample, a retry starts a
|
|
12
|
+
* new one, and fork-inherited records are excluded. The sample carries no amount: deciding one
|
|
13
|
+
* belongs to the ledger, which records the decision once.
|
|
14
|
+
* @param events - Minimal events returned by `costRecords`, across all history pages.
|
|
15
|
+
* @returns Ordered samples with the last valid usage observed for each attempt.
|
|
16
|
+
*/
|
|
17
|
+
export declare function foldSamples(events: readonly ObjectValue[]): ChargeSample[];
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Fold host history records into per-request samples; conversation text never enters the ledger. */
|
|
2
|
+
import { array, object } from "../transport/wire.js";
|
|
3
|
+
/** Host event types that carry billing-relevant usage or route context. */
|
|
4
|
+
const BILLING_EVENTS = new Set(['request/context', 'assistant/message', 'assistant/attempt', 'llm/retry-started', 'session/end-seed']);
|
|
5
|
+
/** Keep only billing-relevant fields; prompts, tool bodies, cookies and keys never enter the ledger.
|
|
6
|
+
* @param records - One HTTP history page's records.
|
|
7
|
+
* @returns Minimal durable events for a deterministic usage fold.
|
|
8
|
+
*/
|
|
9
|
+
export function costRecords(records) {
|
|
10
|
+
return array(records).map(raw => object(object(raw).event)).filter(e => BILLING_EVENTS.has(String(e.type))).map(e => {
|
|
11
|
+
const d = object(e.data);
|
|
12
|
+
const m = object(d.message ?? {});
|
|
13
|
+
const stream = array(d.stream ?? []).map(r => object(object(r).chunk ?? {})).filter(c => c.type === 'usage');
|
|
14
|
+
return { seq: e.seq ?? null, time: e.time ?? null, type: e.type, data: {
|
|
15
|
+
inherited: d.inherited ?? false, turn: d.turn ?? null, step: d.step ?? null, provider: d.provider ?? null, model: d.model ?? null,
|
|
16
|
+
source: m.source ?? null, usage: d.usage ?? stream.at(-1)?.usage ?? null,
|
|
17
|
+
} };
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/** Fold minimal billing events into one sample per model attempt.
|
|
21
|
+
*
|
|
22
|
+
* A replacement sample in the same turn and step updates its attempt's sample, a retry starts a
|
|
23
|
+
* new one, and fork-inherited records are excluded. The sample carries no amount: deciding one
|
|
24
|
+
* belongs to the ledger, which records the decision once.
|
|
25
|
+
* @param events - Minimal events returned by `costRecords`, across all history pages.
|
|
26
|
+
* @returns Ordered samples with the last valid usage observed for each attempt.
|
|
27
|
+
*/
|
|
28
|
+
export function foldSamples(events) {
|
|
29
|
+
const inheritedCut = Math.max(-1, ...events.filter(e => e.type === 'session/end-seed' && object(e.data).inherited === true).map(e => Number(e.seq)));
|
|
30
|
+
const samples = [];
|
|
31
|
+
let route = {};
|
|
32
|
+
let last;
|
|
33
|
+
for (const e of [...new Map(events.map(e => [Number(e.seq), e])).values()].sort((a, b) => Number(a.seq) - Number(b.seq))) {
|
|
34
|
+
const d = object(e.data);
|
|
35
|
+
if (e.type === 'request/context') {
|
|
36
|
+
route = d;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (Number(e.seq) <= inheritedCut || e.type === 'session/end-seed')
|
|
40
|
+
continue;
|
|
41
|
+
if (e.type === 'llm/retry-started') {
|
|
42
|
+
if (last?.turn === d.turn && last?.step === d.step)
|
|
43
|
+
last = undefined;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const source = object(d.source ?? {});
|
|
47
|
+
const provider = String(source.provider ?? route.provider ?? '');
|
|
48
|
+
const model = String(source.model ?? route.model ?? '');
|
|
49
|
+
const time = typeof e.time === 'number' && Number.isFinite(e.time) && e.time >= 0 && e.time <= 8.64e15 ? e.time : undefined;
|
|
50
|
+
const usage = validUsage(d.usage);
|
|
51
|
+
const index = last && d.turn !== null && d.step !== null && last.turn === d.turn && last.step === d.step ? last.index : samples.length;
|
|
52
|
+
if (!usage && samples[index]?.usage)
|
|
53
|
+
continue;
|
|
54
|
+
samples[index] = { key: samples[index]?.key ?? String(e.seq), ...(time === undefined ? {} : { time }), provider, model, ...(usage ? { usage } : {}) };
|
|
55
|
+
last = { turn: d.turn, step: d.step, index };
|
|
56
|
+
}
|
|
57
|
+
return samples;
|
|
58
|
+
}
|
|
59
|
+
/** Accept a token report only when every bucket is a non-negative integer and totals agree. */
|
|
60
|
+
function validUsage(value) {
|
|
61
|
+
const u = object(value ?? {});
|
|
62
|
+
const buckets = [u.inputTokens, u.outputTokens, u.cacheReadTokens ?? 0, u.cacheWriteTokens ?? 0];
|
|
63
|
+
const valid = buckets.every(n => typeof n === 'number' && Number.isSafeInteger(n) && n >= 0)
|
|
64
|
+
&& (u.totalTokens === undefined || typeof u.totalTokens === 'number' && Number.isSafeInteger(u.totalTokens) && u.totalTokens === buckets.reduce((sum, n) => sum + Number(n), 0));
|
|
65
|
+
return valid ? { input: Number(buckets[0]), output: Number(buckets[1]), cacheRead: Number(buckets[2]), cacheWrite: Number(buckets[3]) } : undefined;
|
|
66
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Address and page one session's complete billing history over the host connection. */
|
|
2
|
+
import { type Client } from '../transport/client.ts';
|
|
3
|
+
import { type ObjectValue } from '../transport/wire.ts';
|
|
4
|
+
/** Wire addresses for one `session/list` row, in the order the cost scan should try them.
|
|
5
|
+
*
|
|
6
|
+
* A subagent child is reachable only under its durable parent, and the list row omits the delivery
|
|
7
|
+
* mode, so both modes are offered with the continuable form first.
|
|
8
|
+
* @param session - One row from the host session list.
|
|
9
|
+
* @returns One plain-session address, or both subagent forms when the row is a child.
|
|
10
|
+
*/
|
|
11
|
+
export declare function costAddresses(session: ObjectValue): ObjectValue[];
|
|
12
|
+
/** Read one session's complete cost history, retrying a subagent child with its other delivery mode.
|
|
13
|
+
* @param client - Authenticated host transport.
|
|
14
|
+
* @param session - One row from the host session list.
|
|
15
|
+
* @param signal - Cancels paging without cancelling any agent work.
|
|
16
|
+
* @param onPage - Counts each history request, so a scan can report how much it re-read.
|
|
17
|
+
* @returns Opening cursor and the minimal billing events behind it.
|
|
18
|
+
*/
|
|
19
|
+
export declare function sessionCostHistory(client: Client, session: ObjectValue, signal: AbortSignal, onPage?: () => void): Promise<{
|
|
20
|
+
cursor: number;
|
|
21
|
+
events: ObjectValue[];
|
|
22
|
+
}>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/** Address and page one session's complete billing history over the host connection. */
|
|
2
|
+
import { RemoteError } from "../transport/client.js";
|
|
3
|
+
import { array, errorText, object, string } from "../transport/wire.js";
|
|
4
|
+
import { costRecords } from "./records.js";
|
|
5
|
+
/** Wire addresses for one `session/list` row, in the order the cost scan should try them.
|
|
6
|
+
*
|
|
7
|
+
* A subagent child is reachable only under its durable parent, and the list row omits the delivery
|
|
8
|
+
* mode, so both modes are offered with the continuable form first.
|
|
9
|
+
* @param session - One row from the host session list.
|
|
10
|
+
* @returns One plain-session address, or both subagent forms when the row is a child.
|
|
11
|
+
*/
|
|
12
|
+
export function costAddresses(session) {
|
|
13
|
+
const sessionId = string(session.sessionId);
|
|
14
|
+
const parentSessionId = typeof session.parentSessionId === 'string' ? session.parentSessionId : '';
|
|
15
|
+
if (session.origin !== 'subagent' || parentSessionId === '')
|
|
16
|
+
return [{ kind: 'session', sessionId }];
|
|
17
|
+
return [
|
|
18
|
+
{ kind: 'subagent', parentSessionId, childSessionId: sessionId, mode: 'continuable' },
|
|
19
|
+
{ kind: 'subagent', parentSessionId, childSessionId: sessionId, mode: 'one-shot' },
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
/** Read one session's complete cost history, retrying a subagent child with its other delivery mode.
|
|
23
|
+
* @param client - Authenticated host transport.
|
|
24
|
+
* @param session - One row from the host session list.
|
|
25
|
+
* @param signal - Cancels paging without cancelling any agent work.
|
|
26
|
+
* @param onPage - Counts each history request, so a scan can report how much it re-read.
|
|
27
|
+
* @returns Opening cursor and the minimal billing events behind it.
|
|
28
|
+
*/
|
|
29
|
+
export async function sessionCostHistory(client, session, signal, onPage) {
|
|
30
|
+
let lastError;
|
|
31
|
+
for (const address of costAddresses(session)) {
|
|
32
|
+
try {
|
|
33
|
+
return await readCostHistory(client, address, signal, onPage);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
lastError = error;
|
|
37
|
+
// Only a delivery-mode mismatch justifies the other form; every other failure is final here.
|
|
38
|
+
if (!(error instanceof RemoteError && error.code === 'subagent/unauthorized'))
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
throw lastError;
|
|
43
|
+
}
|
|
44
|
+
/** Page one addressed session's history into the billing events the ledger folds. */
|
|
45
|
+
async function readCostHistory(client, address, signal, onPage) {
|
|
46
|
+
onPage?.();
|
|
47
|
+
const snapshot = await new Promise((resolve, reject) => {
|
|
48
|
+
let sub;
|
|
49
|
+
const timeout = setTimeout(() => finish(new Error('Cost history snapshot timed out')), client.timeoutMs);
|
|
50
|
+
const onAbort = () => finish(new Error('Cost refresh cancelled'));
|
|
51
|
+
const finish = (error, frame) => {
|
|
52
|
+
clearTimeout(timeout);
|
|
53
|
+
signal.removeEventListener('abort', onAbort);
|
|
54
|
+
sub?.cancel();
|
|
55
|
+
if (error)
|
|
56
|
+
reject(error);
|
|
57
|
+
else
|
|
58
|
+
resolve(frame);
|
|
59
|
+
};
|
|
60
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
61
|
+
try {
|
|
62
|
+
sub = client.subscribe('session/follow', { request: { address, maxMessages: 80, assistantStream: true } }, {
|
|
63
|
+
item: value => { const frame = object(value); if (frame.type === 'snapshot')
|
|
64
|
+
finish(undefined, frame); },
|
|
65
|
+
end: error => finish(error ?? new Error('Cost history stream ended')),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
finish(error instanceof Error ? error : new Error(errorText(error)));
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
const cursor = snapshot.cursor;
|
|
73
|
+
if (typeof cursor !== 'number' || !Number.isSafeInteger(cursor))
|
|
74
|
+
throw new Error('Invalid cost history cursor');
|
|
75
|
+
let page = snapshot;
|
|
76
|
+
const events = [];
|
|
77
|
+
while (true) {
|
|
78
|
+
signal.throwIfAborted();
|
|
79
|
+
const records = array(page.records);
|
|
80
|
+
events.push(...costRecords(records));
|
|
81
|
+
if (!page.hasMore)
|
|
82
|
+
break;
|
|
83
|
+
const seqs = records.map(r => object(object(r).event).seq);
|
|
84
|
+
if (!seqs.length || seqs.some(n => typeof n !== 'number' || !Number.isSafeInteger(n)))
|
|
85
|
+
throw new Error('Invalid cost history page');
|
|
86
|
+
const beforeSeq = Math.min(...seqs);
|
|
87
|
+
onPage?.();
|
|
88
|
+
page = object(await client.call('session/page', { request: { address, throughSeq: cursor, beforeSeq, maxMessages: 80 } }, signal));
|
|
89
|
+
if (page.hasMore && array(page.records).every(r => Number(object(object(r).event).seq) >= beforeSeq))
|
|
90
|
+
throw new Error('Cost history page did not advance');
|
|
91
|
+
}
|
|
92
|
+
if (object(snapshot.header).isSeeded === true && !events.some(e => e.type === 'session/end-seed' && object(e.data).inherited === true))
|
|
93
|
+
throw new Error('Cannot attribute inherited session usage');
|
|
94
|
+
return { cursor, events };
|
|
95
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Cost-domain types shared by pricing, record folding, storage and the in-memory ledger. */
|
|
2
|
+
/** Per-million-token rates for one peak or off-peak bucket. */
|
|
3
|
+
export interface Rates {
|
|
4
|
+
input: number;
|
|
5
|
+
cacheRead: number;
|
|
6
|
+
cacheWrite: number;
|
|
7
|
+
output: number;
|
|
8
|
+
}
|
|
9
|
+
/** An explicit validity interval and weekday peak windows in the named time zone. */
|
|
10
|
+
export interface PriceVersion {
|
|
11
|
+
id: string;
|
|
12
|
+
provider: string;
|
|
13
|
+
model: string;
|
|
14
|
+
from: string;
|
|
15
|
+
until?: string;
|
|
16
|
+
currency: 'CNY';
|
|
17
|
+
source: string;
|
|
18
|
+
timezone: string;
|
|
19
|
+
peak: Rates;
|
|
20
|
+
offPeak: Rates;
|
|
21
|
+
weekdays: number[];
|
|
22
|
+
windows: [number, number][];
|
|
23
|
+
}
|
|
24
|
+
/** Disjoint token buckets reported for one model request. */
|
|
25
|
+
export interface Usage {
|
|
26
|
+
input: number;
|
|
27
|
+
output: number;
|
|
28
|
+
cacheRead: number;
|
|
29
|
+
cacheWrite: number;
|
|
30
|
+
}
|
|
31
|
+
/** One folded request sample before a price decision is attached. */
|
|
32
|
+
export interface ChargeSample {
|
|
33
|
+
key: string;
|
|
34
|
+
time?: number;
|
|
35
|
+
provider: string;
|
|
36
|
+
model: string;
|
|
37
|
+
usage?: Usage;
|
|
38
|
+
}
|
|
39
|
+
/** The price decision recorded the first time a request sample was evaluated. */
|
|
40
|
+
export interface PriceDecision {
|
|
41
|
+
priceId?: string;
|
|
42
|
+
amount?: number;
|
|
43
|
+
estimated?: true;
|
|
44
|
+
reason?: string;
|
|
45
|
+
}
|
|
46
|
+
/** One ledger entry: a request sample plus the price decision that seals it. */
|
|
47
|
+
export interface Charge extends ChargeSample, PriceDecision {
|
|
48
|
+
}
|
|
49
|
+
/** One session's persisted ledger slice. */
|
|
50
|
+
export interface SavedCost {
|
|
51
|
+
version: 2;
|
|
52
|
+
sessionId: string;
|
|
53
|
+
cut: number;
|
|
54
|
+
charges: Charge[];
|
|
55
|
+
}
|
|
56
|
+
/** Summary retains the known subtotal, the records it could not price, and the coarse estimates.
|
|
57
|
+
* `unknown` counts records with no amount at all; `estimated` counts records that only have a
|
|
58
|
+
* floor amount, including dated requests whose timestamp cannot place them inside the range.
|
|
59
|
+
*/
|
|
60
|
+
export interface CostTotal {
|
|
61
|
+
amount: number;
|
|
62
|
+
unknown: number;
|
|
63
|
+
estimated: number;
|
|
64
|
+
records: number;
|
|
65
|
+
}
|
|
66
|
+
/** How much of the visible history the cached ledger currently covers. */
|
|
67
|
+
export type Coverage = 'complete' | 'scanning' | 'partial';
|
|
68
|
+
/** Reason recorded when a sample carries no usable token counts. */
|
|
69
|
+
export declare const MISSING_USAGE = "missing usage";
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** What the session domain reads from the connection the controller owns. */
|
|
2
|
+
import type { Telemetry } from './telemetry.ts';
|
|
3
|
+
import type { ObjectValue } from '../transport/wire.ts';
|
|
4
|
+
/** Read-only connection facts and actions the session controller needs. */
|
|
5
|
+
export interface ConnectionView {
|
|
6
|
+
/** Projection store of the current generation. */
|
|
7
|
+
telemetryView(): Telemetry;
|
|
8
|
+
/** Cached host running flag for one session, or undefined when never reported. */
|
|
9
|
+
runningFor(sessionId: string): boolean | undefined;
|
|
10
|
+
/** When this client first observed the session, for the elapsed-time fallback. */
|
|
11
|
+
observedAt(sessionId: string): number | undefined;
|
|
12
|
+
/** Record an observation start for a session this client just opened. */
|
|
13
|
+
observe(sessionId: string): void;
|
|
14
|
+
/** Fail the current generation, so the controller reopens a snapshot. */
|
|
15
|
+
fail(error: Error): void;
|
|
16
|
+
/** Answer one retained host waterfall through the event-result endpoint. */
|
|
17
|
+
reply(frame: ObjectValue, outcome: ObjectValue): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|