@gamegeek-saikel/dsh-cost-meter 0.1.0

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 (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +116 -0
  3. package/README.zh-CN.md +116 -0
  4. package/cordis.patch.yml +12 -0
  5. package/lib/client.js +1481 -0
  6. package/lib/client.js.map +1 -0
  7. package/lib/index.js +2403 -0
  8. package/lib/invariant.js +38 -0
  9. package/lib/types/client/AssistantCostChip.d.ts +29 -0
  10. package/lib/types/client/AssistantCostChip.d.ts.map +1 -0
  11. package/lib/types/client/AssistantCostChip.js +75 -0
  12. package/lib/types/client/CostPluginCard.d.ts +10 -0
  13. package/lib/types/client/CostPluginCard.d.ts.map +1 -0
  14. package/lib/types/client/CostPluginCard.js +124 -0
  15. package/lib/types/client/CostView.d.ts +10 -0
  16. package/lib/types/client/CostView.d.ts.map +1 -0
  17. package/lib/types/client/CostView.js +66 -0
  18. package/lib/types/client/SessionCostLine.d.ts +21 -0
  19. package/lib/types/client/SessionCostLine.d.ts.map +1 -0
  20. package/lib/types/client/SessionCostLine.js +173 -0
  21. package/lib/types/client/SessionCostPill.d.ts +13 -0
  22. package/lib/types/client/SessionCostPill.d.ts.map +1 -0
  23. package/lib/types/client/SessionCostPill.js +123 -0
  24. package/lib/types/client/cost-math.d.ts +88 -0
  25. package/lib/types/client/cost-math.d.ts.map +1 -0
  26. package/lib/types/client/cost-math.js +199 -0
  27. package/lib/types/client/format.d.ts +35 -0
  28. package/lib/types/client/format.d.ts.map +1 -0
  29. package/lib/types/client/format.js +61 -0
  30. package/lib/types/client/index.d.ts +28 -0
  31. package/lib/types/client/index.d.ts.map +1 -0
  32. package/lib/types/client/index.js +59 -0
  33. package/lib/types/client/locales.d.ts +78 -0
  34. package/lib/types/client/locales.d.ts.map +1 -0
  35. package/lib/types/client/locales.js +149 -0
  36. package/lib/types/index.d.ts +136 -0
  37. package/lib/types/index.d.ts.map +1 -0
  38. package/lib/types/index.js +401 -0
  39. package/lib/types/invariant.d.ts +16 -0
  40. package/lib/types/invariant.d.ts.map +1 -0
  41. package/lib/types/invariant.js +38 -0
  42. package/lib/types/pricebook.d.ts +467 -0
  43. package/lib/types/pricebook.d.ts.map +1 -0
  44. package/lib/types/pricebook.js +696 -0
  45. package/lib/types/pricing.d.ts +93 -0
  46. package/lib/types/pricing.d.ts.map +1 -0
  47. package/lib/types/pricing.js +346 -0
  48. package/lib/types/session-cost-projection.d.ts +70 -0
  49. package/lib/types/session-cost-projection.d.ts.map +1 -0
  50. package/lib/types/session-cost-projection.js +198 -0
  51. package/lib/types/subagent-cost.d.ts +51 -0
  52. package/lib/types/subagent-cost.d.ts.map +1 -0
  53. package/lib/types/subagent-cost.js +79 -0
  54. package/lib/types/types.d.ts +334 -0
  55. package/lib/types/types.d.ts.map +1 -0
  56. package/lib/types/types.js +6 -0
  57. package/package.json +134 -0
  58. package/src/client/AssistantCostChip.module.css +81 -0
  59. package/src/client/AssistantCostChip.tsx +109 -0
  60. package/src/client/CostPluginCard.module.css +186 -0
  61. package/src/client/CostPluginCard.tsx +264 -0
  62. package/src/client/CostView.module.css +168 -0
  63. package/src/client/CostView.tsx +145 -0
  64. package/src/client/SessionCostLine.module.css +19 -0
  65. package/src/client/SessionCostLine.tsx +186 -0
  66. package/src/client/SessionCostPill.module.css +118 -0
  67. package/src/client/SessionCostPill.tsx +192 -0
  68. package/src/client/cost-math.ts +230 -0
  69. package/src/client/format.ts +63 -0
  70. package/src/client/index.ts +108 -0
  71. package/src/client/locales.ts +165 -0
  72. package/src/css-modules.d.ts +4 -0
  73. package/src/index.ts +489 -0
  74. package/src/invariant.ts +47 -0
  75. package/src/pricebook.ts +825 -0
  76. package/src/pricing.ts +378 -0
  77. package/src/session-cost-projection.ts +238 -0
  78. package/src/subagent-cost.ts +105 -0
  79. package/src/types.ts +364 -0
package/lib/index.js ADDED
@@ -0,0 +1,2403 @@
1
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
2
+ import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
3
+ import { defineDomain } from "@deepseek-ai/dsh-storage-domain";
4
+ import { z } from "zod";
5
+ import { canonicalHeader } from "@deepseek-ai/dsh-session";
6
+ //#region node_modules/.pnpm/@deepseek-ai+cosmokit@1.8.2/node_modules/@deepseek-ai/cosmokit/lib/index.js
7
+ /** Return true when a value is `null` or `undefined`. */
8
+ function isNullable(value) {
9
+ return value === null || value === void 0;
10
+ }
11
+ /** Return true for non-array object values. */
12
+ function isPlainObject(data) {
13
+ return data && typeof data === "object" && !Array.isArray(data);
14
+ }
15
+ /** Filter object entries and return a new object. */
16
+ function filterKeys(object, filter) {
17
+ return Object.fromEntries(Object.entries(object).filter(([key, value]) => filter(key, value)));
18
+ }
19
+ /** Map object values while preserving the original key set. */
20
+ function mapValues(object, transform) {
21
+ return Object.fromEntries(Object.entries(object).map(([key, value]) => [key, transform(value, key)]));
22
+ }
23
+ /** Pick selected keys from an object, optionally including `undefined` values. */
24
+ function pick(source, keys, forced) {
25
+ if (!keys) return { ...source };
26
+ const result = {};
27
+ for (const key of keys) if (forced || source[key] !== void 0) result[key] = source[key];
28
+ return result;
29
+ }
30
+ /** Test values using `instanceof` with a `toStringTag` fallback. */
31
+ function is(type, value) {
32
+ if (arguments.length === 1) return (value) => is(type, value);
33
+ return type in globalThis && value instanceof globalThis[type] || Object.prototype.toString.call(value).slice(8, -1) === type;
34
+ }
35
+ function isArrayBufferLike(value) {
36
+ return is("ArrayBuffer", value) || is("SharedArrayBuffer", value);
37
+ }
38
+ function isArrayBufferSource(value) {
39
+ return isArrayBufferLike(value) || ArrayBuffer.isView(value);
40
+ }
41
+ /** Binary source detection and base64/hex conversion helpers. */
42
+ var Binary;
43
+ (function(Binary) {
44
+ Binary.is = isArrayBufferLike;
45
+ Binary.isSource = isArrayBufferSource;
46
+ function fromSource(source) {
47
+ if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
48
+ else return source;
49
+ }
50
+ Binary.fromSource = fromSource;
51
+ function toBase64(source) {
52
+ source = fromSource(source);
53
+ if (typeof Buffer !== "undefined") return Buffer.from(source).toString("base64");
54
+ let binary = "";
55
+ const bytes = new Uint8Array(source);
56
+ for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
57
+ return btoa(binary);
58
+ }
59
+ Binary.toBase64 = toBase64;
60
+ function fromBase64(source) {
61
+ if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "base64"));
62
+ return Uint8Array.from(atob(source), (c) => c.charCodeAt(0));
63
+ }
64
+ Binary.fromBase64 = fromBase64;
65
+ function toHex(source) {
66
+ source = fromSource(source);
67
+ if (typeof Buffer !== "undefined") return Buffer.from(source).toString("hex");
68
+ return Array.from(new Uint8Array(source), (byte) => byte.toString(16).padStart(2, "0")).join("");
69
+ }
70
+ Binary.toHex = toHex;
71
+ function fromHex(source) {
72
+ if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "hex"));
73
+ const hex = source.length % 2 === 0 ? source : source.slice(0, source.length - 1);
74
+ const buffer = [];
75
+ for (let i = 0; i < hex.length; i += 2) buffer.push(parseInt(`${hex[i]}${hex[i + 1]}`, 16));
76
+ return Uint8Array.from(buffer).buffer;
77
+ }
78
+ Binary.fromHex = fromHex;
79
+ })(Binary || (Binary = {}));
80
+ Binary.fromBase64;
81
+ Binary.toBase64;
82
+ Binary.fromHex;
83
+ Binary.toHex;
84
+ /** Deep-clone common JavaScript values while preserving prototypes and cycles. */
85
+ function clone(source, refs = /* @__PURE__ */ new Map()) {
86
+ if (!source || typeof source !== "object") return source;
87
+ if (is("Date", source)) return new Date(source.valueOf());
88
+ if (is("RegExp", source)) return new RegExp(source.source, source.flags);
89
+ if (isArrayBufferLike(source)) return source.slice(0);
90
+ if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
91
+ const cached = refs.get(source);
92
+ if (cached) return cached;
93
+ if (Array.isArray(source)) {
94
+ const result = [];
95
+ refs.set(source, result);
96
+ source.forEach((value, index) => {
97
+ result[index] = Reflect.apply(clone, null, [value, refs]);
98
+ });
99
+ return result;
100
+ }
101
+ const result = Object.create(Object.getPrototypeOf(source));
102
+ refs.set(source, result);
103
+ for (const key of Reflect.ownKeys(source)) {
104
+ const descriptor = { ...Reflect.getOwnPropertyDescriptor(source, key) };
105
+ if ("value" in descriptor) descriptor.value = Reflect.apply(clone, null, [descriptor.value, refs]);
106
+ Reflect.defineProperty(result, key, descriptor);
107
+ }
108
+ return result;
109
+ }
110
+ /** Deeply compare arrays, dates, regexps, buffers, and plain object fields. */
111
+ function deepEqual(a, b, strict) {
112
+ if (a === b) return true;
113
+ if (!strict && isNullable(a) && isNullable(b)) return true;
114
+ if (typeof a !== typeof b) return false;
115
+ if (typeof a !== "object") return false;
116
+ if (!a || !b) return false;
117
+ function check(test, then) {
118
+ return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : void 0;
119
+ }
120
+ return check(Array.isArray, (a, b) => a.length === b.length && a.every((item, index) => deepEqual(item, b[index]))) ?? check(is("Date"), (a, b) => a.valueOf() === b.valueOf()) ?? check(is("RegExp"), (a, b) => a.source === b.source && a.flags === b.flags) ?? check(isArrayBufferLike, (a, b) => {
121
+ if (a.byteLength !== b.byteLength) return false;
122
+ const viewA = new Uint8Array(a);
123
+ const viewB = new Uint8Array(b);
124
+ for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
125
+ return true;
126
+ }) ?? Object.keys({
127
+ ...a,
128
+ ...b
129
+ }).every((key) => deepEqual(a[key], b[key], strict));
130
+ }
131
+ /** Time constants plus parsing and formatting helpers. */
132
+ var Time;
133
+ (function(Time) {
134
+ Time.millisecond = 1;
135
+ Time.second = 1e3;
136
+ Time.minute = Time.second * 60;
137
+ Time.hour = Time.minute * 60;
138
+ Time.day = Time.hour * 24;
139
+ Time.week = Time.day * 7;
140
+ let timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
141
+ function setTimezoneOffset(offset) {
142
+ timezoneOffset = offset;
143
+ }
144
+ Time.setTimezoneOffset = setTimezoneOffset;
145
+ function getTimezoneOffset() {
146
+ return timezoneOffset;
147
+ }
148
+ Time.getTimezoneOffset = getTimezoneOffset;
149
+ function getDateNumber(date = /* @__PURE__ */ new Date(), offset) {
150
+ if (typeof date === "number") date = new Date(date);
151
+ if (offset === void 0) offset = timezoneOffset;
152
+ return Math.floor((date.valueOf() / Time.minute - offset) / 1440);
153
+ }
154
+ Time.getDateNumber = getDateNumber;
155
+ function fromDateNumber(value, offset) {
156
+ const date = new Date(value * Time.day);
157
+ if (offset === void 0) offset = timezoneOffset;
158
+ return new Date(+date + offset * Time.minute);
159
+ }
160
+ Time.fromDateNumber = fromDateNumber;
161
+ const numeric = /\d+(?:\.\d+)?/.source;
162
+ const timeRegExp = new RegExp(`^${[
163
+ "w(?:eek(?:s)?)?",
164
+ "d(?:ay(?:s)?)?",
165
+ "h(?:our(?:s)?)?",
166
+ "m(?:in(?:ute)?(?:s)?)?",
167
+ "s(?:ec(?:ond)?(?:s)?)?"
168
+ ].map((unit) => `(${numeric}${unit})?`).join("")}$`);
169
+ function parseTime(source) {
170
+ const capture = timeRegExp.exec(source);
171
+ if (!capture) return 0;
172
+ return (parseFloat(capture[1]) * Time.week || 0) + (parseFloat(capture[2]) * Time.day || 0) + (parseFloat(capture[3]) * Time.hour || 0) + (parseFloat(capture[4]) * Time.minute || 0) + (parseFloat(capture[5]) * Time.second || 0);
173
+ }
174
+ Time.parseTime = parseTime;
175
+ function parseDate(date) {
176
+ const parsed = parseTime(date);
177
+ if (parsed) date = Date.now() + parsed;
178
+ else if (/^\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).toLocaleDateString()}-${date}`;
179
+ else if (/^\d{1,2}-\d{1,2}-\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).getFullYear()}-${date}`;
180
+ return date ? new Date(date) : /* @__PURE__ */ new Date();
181
+ }
182
+ Time.parseDate = parseDate;
183
+ function format(ms) {
184
+ const abs = Math.abs(ms);
185
+ if (abs >= Time.day - Time.hour / 2) return Math.round(ms / Time.day) + "d";
186
+ else if (abs >= Time.hour - Time.minute / 2) return Math.round(ms / Time.hour) + "h";
187
+ else if (abs >= Time.minute - Time.second / 2) return Math.round(ms / Time.minute) + "m";
188
+ else if (abs >= Time.second) return Math.round(ms / Time.second) + "s";
189
+ return ms + "ms";
190
+ }
191
+ Time.format = format;
192
+ function toDigits(source, length = 2) {
193
+ return source.toString().padStart(length, "0");
194
+ }
195
+ Time.toDigits = toDigits;
196
+ function template(template, time = /* @__PURE__ */ new Date()) {
197
+ return template.replace("yyyy", time.getFullYear().toString()).replace("yy", time.getFullYear().toString().slice(2)).replace("MM", toDigits(time.getMonth() + 1)).replace("dd", toDigits(time.getDate())).replace("hh", toDigits(time.getHours())).replace("mm", toDigits(time.getMinutes())).replace("ss", toDigits(time.getSeconds())).replace("SSS", toDigits(time.getMilliseconds(), 3));
198
+ }
199
+ Time.template = template;
200
+ })(Time || (Time = {}));
201
+ //#endregion
202
+ //#region node_modules/.pnpm/@deepseek-ai+schemastery@3.18.1/node_modules/@deepseek-ai/schemastery/lib/index.mjs
203
+ const kSchema = Symbol.for("schemastery");
204
+ const kValidationError = Symbol.for("ValidationError");
205
+ globalThis.__schemastery_index__ ??= 0;
206
+ globalThis.__schemastery_refs__ = void 0;
207
+ var ValidationError = class extends TypeError {
208
+ options;
209
+ name = "ValidationError";
210
+ constructor(message, options) {
211
+ let prefix = "$";
212
+ for (const segment of options.path || []) if (typeof segment === "string") prefix += "." + segment;
213
+ else if (typeof segment === "number") prefix += "[" + segment + "]";
214
+ else if (typeof segment === "symbol") prefix += `[Symbol(${segment.toString()})]`;
215
+ if (prefix.startsWith(".")) prefix = prefix.slice(1);
216
+ super((prefix === "$" ? "" : `${prefix} `) + message);
217
+ this.options = options;
218
+ }
219
+ static is(error) {
220
+ return !!error?.[kValidationError];
221
+ }
222
+ };
223
+ Object.defineProperty(ValidationError.prototype, kValidationError, { value: true });
224
+ const Schema = function(options) {
225
+ const schema = function(data, options = {}) {
226
+ return Schema.resolve(data, schema, options)[0];
227
+ };
228
+ if (options.refs) {
229
+ const refs = mapValues(options.refs, (options) => new Schema(options));
230
+ const getRef = (uid) => refs[uid];
231
+ for (const key in refs) {
232
+ const options = refs[key];
233
+ options.sKey = getRef(options.sKey);
234
+ options.inner = getRef(options.inner);
235
+ options.list = options.list && options.list.map(getRef);
236
+ options.dict = options.dict && mapValues(options.dict, getRef);
237
+ }
238
+ return refs[options.uid];
239
+ }
240
+ Object.assign(schema, options);
241
+ if (typeof schema.callback === "string") try {
242
+ schema.callback = new Function("return " + schema.callback)();
243
+ } catch {}
244
+ Object.defineProperty(schema, "uid", { value: globalThis.__schemastery_index__++ });
245
+ Object.setPrototypeOf(schema, Schema.prototype);
246
+ schema.meta ||= {};
247
+ schema.toString = schema.toString.bind(schema);
248
+ return schema;
249
+ };
250
+ Schema.prototype = Object.create(Function.prototype);
251
+ Schema.prototype[kSchema] = true;
252
+ Object.defineProperty(Schema.prototype, "~standard", { get() {
253
+ return {
254
+ version: 1,
255
+ vendor: "schemastery",
256
+ validate: (value) => {
257
+ try {
258
+ return { value: Schema.resolve(value, this, {})[0] };
259
+ } catch (error) {
260
+ if (ValidationError.is(error)) return { issues: [{
261
+ message: error.message,
262
+ path: error.options.path
263
+ }] };
264
+ throw error;
265
+ }
266
+ }
267
+ };
268
+ } });
269
+ Schema.ValidationError = ValidationError;
270
+ Schema.prototype.toJSON = function toJSON() {
271
+ if (globalThis.__schemastery_refs__) {
272
+ globalThis.__schemastery_refs__[this.uid] ??= JSON.parse(JSON.stringify({ ...this }));
273
+ return this.uid;
274
+ }
275
+ globalThis.__schemastery_refs__ = { [this.uid]: { ...this } };
276
+ globalThis.__schemastery_refs__[this.uid] = JSON.parse(JSON.stringify({ ...this }));
277
+ const result = {
278
+ uid: this.uid,
279
+ refs: globalThis.__schemastery_refs__
280
+ };
281
+ globalThis.__schemastery_refs__ = void 0;
282
+ return result;
283
+ };
284
+ Schema.prototype.set = function set(key, value) {
285
+ this.dict[key] = value;
286
+ return this;
287
+ };
288
+ Schema.prototype.push = function push(value) {
289
+ this.list.push(value);
290
+ return this;
291
+ };
292
+ function mergeDesc(original, messages) {
293
+ const result = typeof original === "string" ? { "": original } : { ...original };
294
+ for (const locale in messages) {
295
+ const value = messages[locale];
296
+ if (value?.$description || value?.$desc) result[locale] = value.$description || value.$desc;
297
+ else if (typeof value === "string") result[locale] = value;
298
+ }
299
+ return result;
300
+ }
301
+ function getInner(value) {
302
+ return value?.$value ?? value?.$inner;
303
+ }
304
+ function extractKeys(data) {
305
+ return filterKeys(data ?? {}, (key) => !key.startsWith("$"));
306
+ }
307
+ Schema.prototype.i18n = function i18n(messages) {
308
+ const schema = Schema(this);
309
+ const desc = mergeDesc(schema.meta.description, messages);
310
+ if (Object.keys(desc).length) schema.meta.description = desc;
311
+ if (schema.dict) schema.dict = mapValues(schema.dict, (inner, key) => {
312
+ return inner.i18n(mapValues(messages, (data) => getInner(data)?.[key] ?? data?.[key]));
313
+ });
314
+ if (schema.list) schema.list = schema.list.map((inner, index) => {
315
+ return inner.i18n(mapValues(messages, (data = {}) => {
316
+ if (Array.isArray(getInner(data))) return getInner(data)[index];
317
+ if (Array.isArray(data)) return data[index];
318
+ return extractKeys(data);
319
+ }));
320
+ });
321
+ if (schema.inner) schema.inner = schema.inner.i18n(mapValues(messages, (data) => {
322
+ if (getInner(data)) return getInner(data);
323
+ return extractKeys(data);
324
+ }));
325
+ if (schema.sKey) schema.sKey = schema.sKey.i18n(mapValues(messages, (data) => data?.$key));
326
+ return schema;
327
+ };
328
+ Schema.prototype.extra = function extra(key, value) {
329
+ const schema = Schema(this);
330
+ schema.meta = {
331
+ ...schema.meta,
332
+ [key]: value
333
+ };
334
+ return schema;
335
+ };
336
+ for (const key of [
337
+ "required",
338
+ "disabled",
339
+ "collapse",
340
+ "hidden",
341
+ "loose"
342
+ ]) Object.assign(Schema.prototype, { [key](value = true) {
343
+ const schema = Schema(this);
344
+ schema.meta = {
345
+ ...schema.meta,
346
+ [key]: value
347
+ };
348
+ return schema;
349
+ } });
350
+ Schema.prototype.deprecated = function deprecated() {
351
+ const schema = Schema(this);
352
+ schema.meta.badges ||= [];
353
+ schema.meta.badges.push({
354
+ text: "deprecated",
355
+ type: "danger"
356
+ });
357
+ return schema;
358
+ };
359
+ Schema.prototype.experimental = function experimental() {
360
+ const schema = Schema(this);
361
+ schema.meta.badges ||= [];
362
+ schema.meta.badges.push({
363
+ text: "experimental",
364
+ type: "warning"
365
+ });
366
+ return schema;
367
+ };
368
+ Schema.prototype.pattern = function pattern(regexp) {
369
+ const schema = Schema(this);
370
+ const pattern = pick(regexp, ["source", "flags"]);
371
+ schema.meta = {
372
+ ...schema.meta,
373
+ pattern
374
+ };
375
+ return schema;
376
+ };
377
+ Schema.prototype.simplify = function simplify(value) {
378
+ if (deepEqual(value, this.meta.default, this.type === "dict")) return null;
379
+ if (isNullable(value)) return value;
380
+ if (this.type === "object" || this.type === "dict") {
381
+ const result = {};
382
+ for (const key in value) {
383
+ const item = (this.type === "object" ? this.dict[key] : this.inner)?.simplify(value[key]);
384
+ if (this.type === "dict" || !isNullable(item)) result[key] = item;
385
+ }
386
+ if (deepEqual(result, this.meta.default, this.type === "dict")) return null;
387
+ return result;
388
+ } else if (this.type === "array" || this.type === "tuple") {
389
+ const result = [];
390
+ value.forEach((value, index) => {
391
+ const schema = this.type === "array" ? this.inner : this.list[index];
392
+ const item = schema ? schema.simplify(value) : value;
393
+ result.push(item);
394
+ });
395
+ return result;
396
+ } else if (this.type === "intersect") {
397
+ const result = {};
398
+ for (const item of this.list) Object.assign(result, item.simplify(value));
399
+ return result;
400
+ } else if (this.type === "union") for (const schema of this.list) try {
401
+ Schema.resolve(value, schema, {});
402
+ return schema.simplify(value);
403
+ } catch {}
404
+ return value;
405
+ };
406
+ Schema.prototype.toString = function toString(inline) {
407
+ return formatters[this.type]?.(this, inline) ?? `Schema<${this.type}>`;
408
+ };
409
+ Schema.prototype.role = function role(role, extra) {
410
+ const schema = Schema(this);
411
+ schema.meta = {
412
+ ...schema.meta,
413
+ role,
414
+ extra
415
+ };
416
+ return schema;
417
+ };
418
+ for (const key of [
419
+ "default",
420
+ "link",
421
+ "comment",
422
+ "description",
423
+ "max",
424
+ "min",
425
+ "step"
426
+ ]) Object.assign(Schema.prototype, { [key](value) {
427
+ const schema = Schema(this);
428
+ schema.meta = {
429
+ ...schema.meta,
430
+ [key]: value
431
+ };
432
+ return schema;
433
+ } });
434
+ const resolvers = {};
435
+ Schema.extend = function extend(type, resolve) {
436
+ resolvers[type] = resolve;
437
+ };
438
+ Schema.resolve = function resolve(data, schema, options = {}, strict = false) {
439
+ if (!schema) return [data];
440
+ if (options.ignore?.(data, schema)) return [data];
441
+ if (isNullable(data) && schema.type !== "lazy") {
442
+ if (schema.meta.required) throw new ValidationError(`missing required value`, options);
443
+ let current = schema;
444
+ let fallback = schema.meta.default;
445
+ while (current?.type === "intersect" && isNullable(fallback)) {
446
+ current = current.list[0];
447
+ fallback = current?.meta.default;
448
+ }
449
+ if (isNullable(fallback)) return [data];
450
+ data = clone(fallback);
451
+ }
452
+ const callback = resolvers[schema.type];
453
+ if (!callback) throw new ValidationError(`unsupported type "${schema.type}"`, options);
454
+ try {
455
+ return callback(data, schema, options, strict);
456
+ } catch (error) {
457
+ if (!schema.meta.loose) throw error;
458
+ return [schema.meta.default];
459
+ }
460
+ };
461
+ Schema.from = function from(source) {
462
+ if (isNullable(source)) return Schema.any();
463
+ else if ([
464
+ "string",
465
+ "number",
466
+ "boolean"
467
+ ].includes(typeof source)) return Schema.const(source).required();
468
+ else if (source[kSchema]) return source;
469
+ else if (typeof source === "function") switch (source) {
470
+ case String: return Schema.string().required();
471
+ case Number: return Schema.number().required();
472
+ case Boolean: return Schema.boolean().required();
473
+ case Function: return Schema.function().required();
474
+ default: return Schema.is(source).required();
475
+ }
476
+ else throw new TypeError(`cannot infer schema from ${source}`);
477
+ };
478
+ Schema.lazy = function lazy(builder) {
479
+ const toJSON = () => {
480
+ if (!schema.inner[kSchema]) {
481
+ schema.inner = schema.builder();
482
+ schema.inner.meta = {
483
+ ...schema.meta,
484
+ ...schema.inner.meta
485
+ };
486
+ }
487
+ return schema.inner.toJSON();
488
+ };
489
+ const schema = new Schema({
490
+ type: "lazy",
491
+ builder,
492
+ inner: { toJSON }
493
+ });
494
+ return schema;
495
+ };
496
+ Schema.natural = function natural() {
497
+ return Schema.number().step(1).min(0);
498
+ };
499
+ Schema.percent = function percent() {
500
+ return Schema.number().step(.01).min(0).max(1).role("slider");
501
+ };
502
+ Schema.date = function date() {
503
+ return Schema.union([Schema.is(Date), Schema.transform(Schema.string().role("datetime"), (value, options) => {
504
+ const date = new Date(value);
505
+ if (isNaN(+date)) throw new ValidationError(`invalid date "${value}"`, options);
506
+ return date;
507
+ }, true)]);
508
+ };
509
+ Schema.regExp = function regExp(flag = "") {
510
+ return Schema.union([Schema.is(RegExp), Schema.transform(Schema.string().role("regexp", { flag }), (value, options) => {
511
+ try {
512
+ return new RegExp(value, flag);
513
+ } catch (e) {
514
+ throw new ValidationError(e.message, options);
515
+ }
516
+ }, true)]);
517
+ };
518
+ Schema.arrayBuffer = function arrayBuffer(encoding) {
519
+ return Schema.union([
520
+ Schema.is(ArrayBuffer),
521
+ Schema.is(SharedArrayBuffer),
522
+ Schema.transform(Schema.any(), (value, options) => {
523
+ if (Binary.isSource(value)) return Binary.fromSource(value);
524
+ throw new ValidationError(`expected ArrayBufferSource but got ${value}`, options);
525
+ }, true),
526
+ ...encoding ? [Schema.transform(Schema.string(), (value, options) => {
527
+ try {
528
+ return encoding === "base64" ? Binary.fromBase64(value) : Binary.fromHex(value);
529
+ } catch (e) {
530
+ throw new ValidationError(e.message, options);
531
+ }
532
+ }, true)] : []
533
+ ]);
534
+ };
535
+ Schema.extend("lazy", (data, schema, options, strict) => {
536
+ if (!schema.inner[kSchema]) {
537
+ schema.inner = schema.builder();
538
+ schema.inner.meta = {
539
+ ...schema.meta,
540
+ ...schema.inner.meta
541
+ };
542
+ }
543
+ return Schema.resolve(data, schema.inner, options, strict);
544
+ });
545
+ Schema.extend("any", (data) => {
546
+ return [data];
547
+ });
548
+ Schema.extend("never", (data, _, options) => {
549
+ throw new ValidationError(`expected nullable but got ${data}`, options);
550
+ });
551
+ Schema.extend("const", (data, { value }, options) => {
552
+ if (deepEqual(data, value)) return [value];
553
+ throw new ValidationError(`expected ${value} but got ${data}`, options);
554
+ });
555
+ function checkWithinRange(data, meta, description, options, skipMin = false) {
556
+ const { max = Infinity, min = -Infinity } = meta;
557
+ if (data > max) throw new ValidationError(`expected ${description} <= ${max} but got ${data}`, options);
558
+ if (data < min && !skipMin) throw new ValidationError(`expected ${description} >= ${min} but got ${data}`, options);
559
+ }
560
+ Schema.extend("string", (data, { meta }, options) => {
561
+ if (typeof data !== "string") throw new ValidationError(`expected string but got ${data}`, options);
562
+ if (meta.pattern) {
563
+ const regexp = new RegExp(meta.pattern.source, meta.pattern.flags);
564
+ if (!regexp.test(data)) throw new ValidationError(`expect string to match regexp ${regexp}`, options);
565
+ }
566
+ checkWithinRange(data.length, meta, "string length", options);
567
+ return [data];
568
+ });
569
+ function decimalShift(data, digits) {
570
+ const str = data.toString();
571
+ if (str.includes("e")) return data * Math.pow(10, digits);
572
+ const index = str.indexOf(".");
573
+ if (index === -1) return data * Math.pow(10, digits);
574
+ const frac = str.slice(index + 1);
575
+ const integer = str.slice(0, index);
576
+ if (frac.length <= digits) return +(integer + frac.padEnd(digits, "0"));
577
+ return +(integer + frac.slice(0, digits) + "." + frac.slice(digits));
578
+ }
579
+ function isMultipleOf(data, min, step) {
580
+ step = Math.abs(step);
581
+ if (!/^\d+\.\d+$/.test(step.toString())) return (data - min) % step === 0;
582
+ const index = step.toString().indexOf(".");
583
+ const digits = step.toString().slice(index + 1).length;
584
+ return Math.abs(decimalShift(data, digits) - decimalShift(min, digits)) % decimalShift(step, digits) === 0;
585
+ }
586
+ Schema.extend("number", (data, { meta }, options) => {
587
+ if (typeof data !== "number") throw new ValidationError(`expected number but got ${data}`, options);
588
+ checkWithinRange(data, meta, "number", options);
589
+ const { step } = meta;
590
+ if (step && !isMultipleOf(data, meta.min ?? 0, step)) throw new ValidationError(`expected number multiple of ${step} but got ${data}`, options);
591
+ return [data];
592
+ });
593
+ Schema.extend("boolean", (data, _, options) => {
594
+ if (typeof data === "boolean") return [data];
595
+ throw new ValidationError(`expected boolean but got ${data}`, options);
596
+ });
597
+ Schema.extend("bitset", (data, { bits, meta }, options) => {
598
+ let value = 0, keys = [];
599
+ if (typeof data === "number") {
600
+ value = data;
601
+ for (const key in bits) if (data & bits[key]) keys.push(key);
602
+ } else if (Array.isArray(data)) {
603
+ keys = data;
604
+ for (const key of keys) {
605
+ if (typeof key !== "string") throw new ValidationError(`expected string but got ${key}`, options);
606
+ if (key in bits) value |= bits[key];
607
+ }
608
+ } else throw new ValidationError(`expected number or array but got ${data}`, options);
609
+ if (value === meta.default) return [value];
610
+ return [value, keys];
611
+ });
612
+ Schema.extend("function", (data, _, options) => {
613
+ if (typeof data === "function") return [data];
614
+ throw new ValidationError(`expected function but got ${data}`, options);
615
+ });
616
+ Schema.extend("is", (data, { constructor }, options) => {
617
+ if (typeof constructor === "function") {
618
+ if (data instanceof constructor) return [data];
619
+ throw new ValidationError(`expected ${constructor.name} but got ${data}`, options);
620
+ } else {
621
+ if (isNullable(data)) throw new ValidationError(`expected ${constructor} but got ${data}`, options);
622
+ let prototype = Object.getPrototypeOf(data);
623
+ while (prototype) {
624
+ if (prototype.constructor?.name === constructor) return [data];
625
+ prototype = Object.getPrototypeOf(prototype);
626
+ }
627
+ throw new ValidationError(`expected ${constructor} but got ${data}`, options);
628
+ }
629
+ });
630
+ function property(data, key, schema, options) {
631
+ try {
632
+ const [value, adapted] = Schema.resolve(data[key], schema, {
633
+ ...options,
634
+ path: [...options.path || [], key]
635
+ });
636
+ if (adapted !== void 0) data[key] = adapted;
637
+ return value;
638
+ } catch (e) {
639
+ if (!options?.autofix) throw e;
640
+ delete data[key];
641
+ return schema.meta.default;
642
+ }
643
+ }
644
+ Schema.extend("array", (data, { inner, meta }, options) => {
645
+ if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
646
+ checkWithinRange(data.length, meta, "array length", options, !isNullable(inner.meta.default));
647
+ return [data.map((_, index) => property(data, index, inner, options))];
648
+ });
649
+ Schema.extend("dict", (data, { inner, sKey }, options, strict) => {
650
+ if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options);
651
+ const result = {};
652
+ for (const key in data) {
653
+ let rKey;
654
+ try {
655
+ rKey = Schema.resolve(key, sKey, options)[0];
656
+ } catch (error) {
657
+ if (strict) continue;
658
+ throw error;
659
+ }
660
+ result[rKey] = property(data, key, inner, options);
661
+ data[rKey] = data[key];
662
+ if (key !== rKey) delete data[key];
663
+ }
664
+ return [result];
665
+ });
666
+ Schema.extend("tuple", (data, { list }, options, strict) => {
667
+ if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
668
+ const result = list.map((inner, index) => property(data, index, inner, options));
669
+ if (strict) return [result];
670
+ result.push(...data.slice(list.length));
671
+ return [result];
672
+ });
673
+ function merge(result, data) {
674
+ for (const key in data) {
675
+ if (key in result) continue;
676
+ result[key] = data[key];
677
+ }
678
+ }
679
+ Schema.extend("object", (data, { dict }, options, strict) => {
680
+ if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options);
681
+ const result = {};
682
+ for (const key in dict) {
683
+ const value = property(data, key, dict[key], options);
684
+ if (!isNullable(value) || key in data) result[key] = value;
685
+ }
686
+ if (!strict) merge(result, data);
687
+ return [result];
688
+ });
689
+ Schema.extend("union", (data, { list, toString }, options, strict) => {
690
+ const messages = [];
691
+ for (const inner of list) try {
692
+ return Schema.resolve(data, inner, options, strict);
693
+ } catch (error) {
694
+ messages.push(error);
695
+ }
696
+ throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
697
+ });
698
+ Schema.extend("intersect", (data, { list, toString }, options, strict) => {
699
+ if (!list.length) return [data];
700
+ let result;
701
+ for (const inner of list) {
702
+ const value = Schema.resolve(data, inner, options, true)[0];
703
+ if (isNullable(value)) continue;
704
+ if (isNullable(result)) result = value;
705
+ else if (typeof result !== typeof value) throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
706
+ else if (typeof value === "object") merge(result ??= {}, value);
707
+ else if (result !== value) throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
708
+ }
709
+ if (!strict && isPlainObject(data)) merge(result, data);
710
+ return [result];
711
+ });
712
+ Schema.extend("transform", (data, { inner, callback, preserve }, options) => {
713
+ const [result, adapted = data] = Schema.resolve(data, inner, options, true);
714
+ if (preserve) return [callback(result)];
715
+ else return [callback(result), callback(adapted)];
716
+ });
717
+ const formatters = {};
718
+ function defineMethod(name, keys, format) {
719
+ formatters[name] = format;
720
+ Object.assign(Schema, { [name](...args) {
721
+ const schema = new Schema({ type: name });
722
+ keys.forEach((key, index) => {
723
+ switch (key) {
724
+ case "sKey":
725
+ schema.sKey = args[index] ?? Schema.string();
726
+ break;
727
+ case "inner":
728
+ schema.inner = Schema.from(args[index]);
729
+ break;
730
+ case "list":
731
+ schema.list = args[index].map(Schema.from);
732
+ break;
733
+ case "dict":
734
+ schema.dict = mapValues(args[index], Schema.from);
735
+ break;
736
+ case "bits":
737
+ schema.bits = {};
738
+ for (const key in args[index]) {
739
+ if (typeof args[index][key] !== "number") continue;
740
+ schema.bits[key] = args[index][key];
741
+ }
742
+ break;
743
+ case "callback": {
744
+ const callback = schema.callback = args[index];
745
+ callback["toJSON"] ||= () => callback.toString();
746
+ break;
747
+ }
748
+ case "constructor": {
749
+ const constructor = schema.constructor = args[index];
750
+ if (typeof constructor === "function") constructor["toJSON"] ||= () => constructor["name"];
751
+ break;
752
+ }
753
+ default: schema[key] = args[index];
754
+ }
755
+ });
756
+ if (name === "object" || name === "dict") schema.meta.default = {};
757
+ else if (name === "array" || name === "tuple") schema.meta.default = [];
758
+ else if (name === "bitset") schema.meta.default = 0;
759
+ return schema;
760
+ } });
761
+ }
762
+ defineMethod("is", ["constructor"], ({ constructor }) => {
763
+ if (typeof constructor === "function") return constructor.name;
764
+ else return constructor;
765
+ });
766
+ defineMethod("any", [], () => "any");
767
+ defineMethod("never", [], () => "never");
768
+ defineMethod("const", ["value"], ({ value }) => typeof value === "string" ? JSON.stringify(value) : value);
769
+ defineMethod("string", [], () => "string");
770
+ defineMethod("number", [], () => "number");
771
+ defineMethod("boolean", [], () => "boolean");
772
+ defineMethod("bitset", ["bits"], () => "bitset");
773
+ defineMethod("function", [], () => "function");
774
+ defineMethod("array", ["inner"], ({ inner }) => `${inner.toString(true)}[]`);
775
+ defineMethod("dict", ["inner", "sKey"], ({ inner, sKey }) => `{ [key: ${sKey.toString()}]: ${inner.toString()} }`);
776
+ defineMethod("tuple", ["list"], ({ list }) => `[${list.map((inner) => inner.toString()).join(", ")}]`);
777
+ defineMethod("object", ["dict"], ({ dict }) => {
778
+ if (Object.keys(dict).length === 0) return "{}";
779
+ return `{ ${Object.entries(dict).map(([key, inner]) => {
780
+ return `${key}${inner.meta.required ? "" : "?"}: ${inner.toString()}`;
781
+ }).join(", ")} }`;
782
+ });
783
+ defineMethod("union", ["list"], ({ list }, inline) => {
784
+ const result = list.map(({ toString: format }) => format()).join(" | ");
785
+ return inline ? `(${result})` : result;
786
+ });
787
+ defineMethod("intersect", ["list"], ({ list }) => {
788
+ return `${list.map((inner) => inner.toString(true)).join(" & ")}`;
789
+ });
790
+ defineMethod("transform", [
791
+ "inner",
792
+ "callback",
793
+ "preserve"
794
+ ], ({ inner }, isInner) => inner.toString(isInner));
795
+ //#endregion
796
+ //#region src/pricing.ts
797
+ /** Official pricing page URL (zh-cn). */
798
+ const PRICING_URL = "https://api-docs.deepseek.com/zh-cn/quick_start/pricing/";
799
+ /** Official pricing page URL (en). */
800
+ const PRICING_URL_EN = "https://api-docs.deepseek.com/quick_start/pricing/";
801
+ /** Official peak-pricing rollout: 2026-08-17 00:00 Beijing time (UTC+8). */
802
+ const PEAK_PRICING_START_MS = Date.UTC(2026, 7, 16, 16, 0, 0);
803
+ /**
804
+ * Built-in fallback prices: the official list for deepseek-v4-flash and
805
+ * deepseek-v4-pro (CNY per 1M tokens). DeepSeek bills cache writes at the
806
+ * uncached input price (its wire usage reports only cache-hit vs cache-miss
807
+ * buckets), so no separate cache-write rate exists.
808
+ */
809
+ const FALLBACK_CURRENT = {
810
+ flash: {
811
+ cacheReadPerMillion: .02,
812
+ inputPerMillion: 1,
813
+ outputPerMillion: 2
814
+ },
815
+ pro: {
816
+ cacheReadPerMillion: .025,
817
+ inputPerMillion: 3,
818
+ outputPerMillion: 6
819
+ }
820
+ };
821
+ /**
822
+ * Built-in fallback for the upcoming peak table: the 2026-08-17 schedule
823
+ * (peak hours double the off-peak half).
824
+ */
825
+ const FALLBACK_PEAK = {
826
+ flash: {
827
+ offPeak: {
828
+ cacheReadPerMillion: .05,
829
+ inputPerMillion: 1.5,
830
+ outputPerMillion: 4.5
831
+ },
832
+ peak: {
833
+ cacheReadPerMillion: .1,
834
+ inputPerMillion: 3,
835
+ outputPerMillion: 9
836
+ }
837
+ },
838
+ pro: {
839
+ offPeak: {
840
+ cacheReadPerMillion: .15,
841
+ inputPerMillion: 4.5,
842
+ outputPerMillion: 13.5
843
+ },
844
+ peak: {
845
+ cacheReadPerMillion: .3,
846
+ inputPerMillion: 9,
847
+ outputPerMillion: 27
848
+ }
849
+ }
850
+ };
851
+ /** Built-in fallback USD prices from the English pricing page. */
852
+ const FALLBACK_CURRENT_USD = {
853
+ flash: {
854
+ cacheReadPerMillion: .007,
855
+ inputPerMillion: .22,
856
+ outputPerMillion: .66
857
+ },
858
+ pro: {
859
+ cacheReadPerMillion: .022,
860
+ inputPerMillion: .66,
861
+ outputPerMillion: 1.98
862
+ }
863
+ };
864
+ /** Built-in fallback USD peak table from the English pricing page. */
865
+ const FALLBACK_PEAK_USD = {
866
+ flash: {
867
+ offPeak: { ...FALLBACK_CURRENT_USD.flash },
868
+ peak: {
869
+ cacheReadPerMillion: .014,
870
+ inputPerMillion: .44,
871
+ outputPerMillion: 1.32
872
+ }
873
+ },
874
+ pro: {
875
+ offPeak: { ...FALLBACK_CURRENT_USD.pro },
876
+ peak: {
877
+ cacheReadPerMillion: .044,
878
+ inputPerMillion: 1.32,
879
+ outputPerMillion: 3.96
880
+ }
881
+ }
882
+ };
883
+ /** Number regex: `0.02`, `1`, `2`, `3.0` etc. Deliberately non-global: a
884
+ * shared global regex leaks `lastIndex` across `exec` calls and would skip
885
+ * the second price cell (the pro price). */
886
+ const PRICE_RE = /(\d+(?:\.\d+)?)\s*元/;
887
+ /** Strip HTML tags to plain text (keeps cell order). */
888
+ function stripHtml(html) {
889
+ return html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ").replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/\s+/g, " ").trim();
890
+ }
891
+ /** Parse one price cell text like `0.02元` into a number; undefined when absent. */
892
+ function parsePriceCell(text) {
893
+ const match = PRICE_RE.exec(text);
894
+ if (match === null) return void 0;
895
+ const value = Number(match[1]);
896
+ return Number.isFinite(value) ? value : void 0;
897
+ }
898
+ function secondPriceCell(text) {
899
+ return text.replace(/^\s*(\d+(?:\.\d+)?元)/, "");
900
+ }
901
+ /**
902
+ * Parse the current single-price table: three rows labeled with the bucket
903
+ * names, each carrying the flash and pro price cells.
904
+ * @param html - the raw pricing page.
905
+ * @returns the parsed table, or undefined when the labels are absent.
906
+ */
907
+ function parseCurrentTable(html) {
908
+ const hit = /百万tokens输入(缓存命中)([\s\S]{0,400}?)百万tokens输入(缓存未命中)([\s\S]{0,400}?)百万tokens输出([\s\S]{0,400}?)(?:并发限制|<\/table)/i.exec(stripHtml(html));
909
+ if (hit === null) return void 0;
910
+ const cacheReadCell = hit[1] ?? "";
911
+ const inputCell = hit[2] ?? "";
912
+ const outputCell = hit[3] ?? "";
913
+ const cacheReadFlash = parsePriceCell(cacheReadCell);
914
+ const cacheReadPro = parsePriceCell(secondPriceCell(cacheReadCell));
915
+ const inputFlash = parsePriceCell(inputCell);
916
+ const inputPro = parsePriceCell(secondPriceCell(inputCell));
917
+ const outputFlash = parsePriceCell(outputCell);
918
+ const outputPro = parsePriceCell(secondPriceCell(outputCell));
919
+ if (cacheReadFlash === void 0 || inputFlash === void 0 || outputFlash === void 0) return;
920
+ return {
921
+ flash: {
922
+ cacheReadPerMillion: cacheReadFlash,
923
+ inputPerMillion: inputFlash,
924
+ outputPerMillion: outputFlash
925
+ },
926
+ pro: {
927
+ cacheReadPerMillion: cacheReadPro ?? cacheReadFlash,
928
+ inputPerMillion: inputPro ?? inputFlash,
929
+ outputPerMillion: outputPro ?? outputFlash
930
+ }
931
+ };
932
+ }
933
+ /**
934
+ * Parse the upcoming peak-pricing table: model rows with off-peak and peak
935
+ * cells, e.g. `deepseek-v4-flash 空闲时段 0.05 1.5 4.5 高峰时段 0.10 3.0 9.0`.
936
+ * @param html - the raw pricing page.
937
+ * @returns the parsed table, or undefined when either model row is absent.
938
+ */
939
+ function parsePeakTable(html) {
940
+ const text = stripHtml(html);
941
+ const flash = /deepseek-v4-flash\s+空闲时段\s+(\d+(?:\.\d+)?)元\s+(\d+(?:\.\d+)?)元\s+(\d+(?:\.\d+)?)元\s+高峰时段\s+(\d+(?:\.\d+)?)元\s+(\d+(?:\.\d+)?)元\s+(\d+(?:\.\d+)?)元/i.exec(text);
942
+ const pro = /deepseek-v4-pro\s+空闲时段\s+(\d+(?:\.\d+)?)元\s+(\d+(?:\.\d+)?)元\s+(\d+(?:\.\d+)?)元\s+高峰时段\s+(\d+(?:\.\d+)?)元\s+(\d+(?:\.\d+)?)元\s+(\d+(?:\.\d+)?)元/i.exec(text);
943
+ if (flash === null || pro === null) return void 0;
944
+ return {
945
+ flash: {
946
+ offPeak: {
947
+ cacheReadPerMillion: Number(flash[1]),
948
+ inputPerMillion: Number(flash[2]),
949
+ outputPerMillion: Number(flash[3])
950
+ },
951
+ peak: {
952
+ cacheReadPerMillion: Number(flash[4]),
953
+ inputPerMillion: Number(flash[5]),
954
+ outputPerMillion: Number(flash[6])
955
+ }
956
+ },
957
+ pro: {
958
+ offPeak: {
959
+ cacheReadPerMillion: Number(pro[1]),
960
+ inputPerMillion: Number(pro[2]),
961
+ outputPerMillion: Number(pro[3])
962
+ },
963
+ peak: {
964
+ cacheReadPerMillion: Number(pro[4]),
965
+ inputPerMillion: Number(pro[5]),
966
+ outputPerMillion: Number(pro[6])
967
+ }
968
+ }
969
+ };
970
+ }
971
+ /**
972
+ * Parse the English current single-price table.
973
+ * @param html - the raw English pricing page.
974
+ * @returns the parsed table, or undefined when the labels are absent.
975
+ */
976
+ /**
977
+ * Parse the English combined pricing table. The English page lists all three
978
+ * buckets as rows with OFF-PEAK and PEAK cells, so we extract both the current
979
+ * off-peak list and the peak table from the same markup.
980
+ */
981
+ function parseEnglishPricing(html) {
982
+ const text = stripHtml(html);
983
+ const hit = /1M INPUT TOKENS \(CACHE HIT\)\s+OFF-PEAK\s+\$?\s*(\d+(?:\.\d+)?)\s+\$?\s*(\d+(?:\.\d+)?)\s+PEAK\s+\$?\s*(\d+(?:\.\d+)?)\s+\$?\s*(\d+(?:\.\d+)?)\s+1M INPUT TOKENS \(CACHE MISS\)\s+OFF-PEAK\s+\$?\s*(\d+(?:\.\d+)?)\s+\$?\s*(\d+(?:\.\d+)?)\s+PEAK\s+\$?\s*(\d+(?:\.\d+)?)\s+\$?\s*(\d+(?:\.\d+)?)\s+1M OUTPUT TOKENS\s+OFF-PEAK\s+\$?\s*(\d+(?:\.\d+)?)\s+\$?\s*(\d+(?:\.\d+)?)\s+PEAK\s+\$?\s*(\d+(?:\.\d+)?)\s+\$?\s*(\d+(?:\.\d+)?)/i.exec(text);
984
+ if (hit === null) return void 0;
985
+ const n = (index) => Number(hit[index]);
986
+ return {
987
+ current: {
988
+ flash: {
989
+ cacheReadPerMillion: n(1),
990
+ inputPerMillion: n(5),
991
+ outputPerMillion: n(9)
992
+ },
993
+ pro: {
994
+ cacheReadPerMillion: n(2),
995
+ inputPerMillion: n(6),
996
+ outputPerMillion: n(10)
997
+ }
998
+ },
999
+ peak: {
1000
+ flash: {
1001
+ offPeak: {
1002
+ cacheReadPerMillion: n(1),
1003
+ inputPerMillion: n(5),
1004
+ outputPerMillion: n(9)
1005
+ },
1006
+ peak: {
1007
+ cacheReadPerMillion: n(3),
1008
+ inputPerMillion: n(7),
1009
+ outputPerMillion: n(11)
1010
+ }
1011
+ },
1012
+ pro: {
1013
+ offPeak: {
1014
+ cacheReadPerMillion: n(2),
1015
+ inputPerMillion: n(6),
1016
+ outputPerMillion: n(10)
1017
+ },
1018
+ peak: {
1019
+ cacheReadPerMillion: n(4),
1020
+ inputPerMillion: n(8),
1021
+ outputPerMillion: n(12)
1022
+ }
1023
+ }
1024
+ }
1025
+ };
1026
+ }
1027
+ /**
1028
+ * Parse the English current single-price table.
1029
+ * @param html - the raw English pricing page.
1030
+ * @returns the parsed table, or undefined when the labels are absent.
1031
+ */
1032
+ function parseCurrentTableEn(html) {
1033
+ return parseEnglishPricing(html)?.current;
1034
+ }
1035
+ /**
1036
+ * Parse the English upcoming peak-pricing table.
1037
+ * @param html - the raw English pricing page.
1038
+ * @returns the parsed table, or undefined when either model row is absent.
1039
+ */
1040
+ function parsePeakTableEn(html) {
1041
+ return parseEnglishPricing(html)?.peak;
1042
+ }
1043
+ /**
1044
+ * Fetch and parse the official pricing page.
1045
+ * @param fetchImpl - fetch-compatible function (injected for testability).
1046
+ * @param timeoutMs - abort timeout.
1047
+ * @returns the parsed snapshot; `error` is set when fetch/parse failed.
1048
+ */
1049
+ async function fetchPricing(fetchImpl = globalThis.fetch, timeoutMs = 15e3, locale = "zh") {
1050
+ const fetchedAt = Date.now();
1051
+ const currency = locale === "en" ? "USD" : "CNY";
1052
+ const url = locale === "en" ? PRICING_URL_EN : PRICING_URL;
1053
+ const parseCurrent = locale === "en" ? parseCurrentTableEn : parseCurrentTable;
1054
+ const parsePeak = locale === "en" ? parsePeakTableEn : parsePeakTable;
1055
+ const fallbackCurrent = locale === "en" ? FALLBACK_CURRENT_USD : FALLBACK_CURRENT;
1056
+ try {
1057
+ const controller = new AbortController();
1058
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1059
+ let response;
1060
+ try {
1061
+ response = await fetchImpl(url, { signal: controller.signal });
1062
+ } finally {
1063
+ clearTimeout(timer);
1064
+ }
1065
+ if (!response.ok) return {
1066
+ fetchedAt,
1067
+ currency,
1068
+ current: fallbackCurrent,
1069
+ peakActive: false,
1070
+ effective: fallbackCurrent,
1071
+ error: `pricing page HTTP ${response.status}`
1072
+ };
1073
+ const html = await response.text();
1074
+ const current = parseCurrent(html);
1075
+ if (current === void 0) return {
1076
+ fetchedAt,
1077
+ currency,
1078
+ current: fallbackCurrent,
1079
+ peakActive: false,
1080
+ effective: fallbackCurrent,
1081
+ error: "pricing table not found"
1082
+ };
1083
+ const peak = parsePeak(html);
1084
+ const now = Date.now();
1085
+ const peakActive = now >= PEAK_PRICING_START_MS;
1086
+ return {
1087
+ fetchedAt,
1088
+ currency,
1089
+ current,
1090
+ ...peak === void 0 ? {} : { peak },
1091
+ peakActive,
1092
+ effective: effectivePricing(current, peak, peakActive, new Date(now)),
1093
+ ...peakActive ? { band: isPeakHour(new Date(now)) ? "peak" : "offPeak" } : {}
1094
+ };
1095
+ } catch (error) {
1096
+ return {
1097
+ fetchedAt,
1098
+ currency,
1099
+ current: fallbackCurrent,
1100
+ peakActive: false,
1101
+ effective: fallbackCurrent,
1102
+ error: error instanceof Error ? error.message : String(error)
1103
+ };
1104
+ }
1105
+ }
1106
+ /**
1107
+ * Whether the current moment is a peak-pricing hour in Beijing time:
1108
+ * 09:00-12:00 and 14:00-18:00 (peak); everything else is off-peak.
1109
+ * @param now - the instant to classify.
1110
+ * @returns true during peak hours.
1111
+ */
1112
+ function isPeakHour(now = /* @__PURE__ */ new Date()) {
1113
+ const parts = new Intl.DateTimeFormat("en-US", {
1114
+ timeZone: "Asia/Shanghai",
1115
+ hour: "numeric",
1116
+ hour12: false
1117
+ }).formatToParts(now);
1118
+ const hour = Number(parts.find((part) => part.type === "hour")?.value);
1119
+ if (Number.isNaN(hour)) return false;
1120
+ return hour >= 9 && hour < 12 || hour >= 14 && hour < 18;
1121
+ }
1122
+ /**
1123
+ * The price bucket in effect for one model at the given moment: the
1124
+ * peak/off-peak band once the rollout is live (with the parsed table, or the
1125
+ * built-in fallback), otherwise the current official list.
1126
+ * @param current - the current list prices.
1127
+ * @param peak - the upcoming peak table, when parsed.
1128
+ * @param peakActive - whether the rollout has started.
1129
+ * @param now - the instant to price for.
1130
+ * @param model - `flash` or `pro`.
1131
+ * @returns the bucket to apply.
1132
+ */
1133
+ function effectiveBucket(current, peak, peakActive, now, model) {
1134
+ if (peakActive) {
1135
+ const table = peak?.[model] ?? FALLBACK_PEAK[model];
1136
+ return isPeakHour(now) ? table.peak : table.offPeak;
1137
+ }
1138
+ return current[model];
1139
+ }
1140
+ /** Per-model effective prices at the given moment. */
1141
+ function effectivePricing(current, peak, peakActive, now) {
1142
+ return {
1143
+ flash: effectiveBucket(current, peak, peakActive, now, "flash"),
1144
+ pro: effectiveBucket(current, peak, peakActive, now, "pro")
1145
+ };
1146
+ }
1147
+ //#endregion
1148
+ //#region src/pricebook.ts
1149
+ /** Default manual rate used when auto-fetch fails and nothing was ever fetched. */
1150
+ const DEFAULT_FX_RATE = 7.2;
1151
+ /** Default OpenRouter cache-read fraction of the uncached input price. */
1152
+ const DEFAULT_CACHE_READ_DISCOUNT = .25;
1153
+ /** Default OpenRouter model-id → canonical key aliases (editable). */
1154
+ const DEFAULT_ALIASES = {
1155
+ "deepseek/deepseek-chat": "deepseek-v4-flash",
1156
+ "deepseek/deepseek-reasoner": "deepseek-v4-pro"
1157
+ };
1158
+ /** Free exchange-rate endpoint (no key); returns `rates.CNY` for USD base. */
1159
+ const FX_API_URL = "https://open.er-api.com/v6/latest/USD";
1160
+ /** OpenRouter models directory (prices in USD per token). */
1161
+ const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
1162
+ /** Default cap on the persisted snapshot history. */
1163
+ const DEFAULT_SNAPSHOT_HISTORY_LIMIT = 50;
1164
+ const bucketSchema = z.object({
1165
+ cacheReadPerMillion: z.number().nonnegative(),
1166
+ inputPerMillion: z.number().nonnegative(),
1167
+ outputPerMillion: z.number().nonnegative()
1168
+ }).strict();
1169
+ const modelPriceSchema$1 = z.object({
1170
+ source: z.enum([
1171
+ "manual",
1172
+ "official",
1173
+ "fallback",
1174
+ "openrouter",
1175
+ "none"
1176
+ ]),
1177
+ single: bucketSchema.optional(),
1178
+ offPeak: bucketSchema.optional(),
1179
+ peak: bucketSchema.optional()
1180
+ }).strict();
1181
+ const snapshotSchema = z.object({
1182
+ version: z.number().int().positive(),
1183
+ currency: z.enum(["CNY", "USD"]).default("CNY"),
1184
+ effectiveAt: z.number().int().nonnegative(),
1185
+ source: z.enum([
1186
+ "manual",
1187
+ "official",
1188
+ "fallback",
1189
+ "openrouter",
1190
+ "none"
1191
+ ]),
1192
+ usdCnyRate: z.number().nullable(),
1193
+ prices: z.record(z.string(), modelPriceSchema$1)
1194
+ }).strict();
1195
+ const fxSchema = z.object({
1196
+ mode: z.enum(["auto", "manual"]),
1197
+ manualRate: z.number().positive(),
1198
+ lastAutoRate: z.number().positive().optional(),
1199
+ rateFetchedAt: z.number().int().nonnegative().optional()
1200
+ }).strict();
1201
+ const pricebookStateSchema = z.object({
1202
+ snapshots: z.array(snapshotSchema),
1203
+ overrides: z.record(z.string(), modelPriceSchema$1),
1204
+ aliases: z.record(z.string(), z.string()),
1205
+ cacheReadDiscount: z.number().min(0).max(1),
1206
+ fx: fxSchema,
1207
+ balanceEnabled: z.boolean(),
1208
+ openRouterEnabled: z.boolean(),
1209
+ updatedAt: z.number().int().nonnegative()
1210
+ }).strict();
1211
+ /** Fresh pricebook state before any user configuration exists. */
1212
+ function initialPricebookState() {
1213
+ return {
1214
+ snapshots: [],
1215
+ overrides: {},
1216
+ aliases: { ...DEFAULT_ALIASES },
1217
+ cacheReadDiscount: DEFAULT_CACHE_READ_DISCOUNT,
1218
+ fx: {
1219
+ mode: "auto",
1220
+ manualRate: DEFAULT_FX_RATE
1221
+ },
1222
+ balanceEnabled: true,
1223
+ openRouterEnabled: true,
1224
+ updatedAt: 0
1225
+ };
1226
+ }
1227
+ /**
1228
+ * The pricebook storage domain. One global slot holds the whole state; the
1229
+ * domain name (`pricebook`) is unused by any shipped row, so the open never
1230
+ * collides with `already-open`.
1231
+ */
1232
+ const PRICEBOOK_DOMAIN = defineDomain({
1233
+ name: "pricebook",
1234
+ version: 1,
1235
+ global: {
1236
+ schema: pricebookStateSchema,
1237
+ initial: initialPricebookState()
1238
+ },
1239
+ tables: {}
1240
+ });
1241
+ /** USD pricebook storage domain, kept separate from the CNY domain. */
1242
+ const PRICEBOOK_DOMAIN_USD = defineDomain({
1243
+ name: "pricebook_usd",
1244
+ version: 1,
1245
+ global: {
1246
+ schema: pricebookStateSchema,
1247
+ initial: initialPricebookState()
1248
+ },
1249
+ tables: {}
1250
+ });
1251
+ /**
1252
+ * Map a model id to its official pricing key: anything containing `pro`
1253
+ * prices as pro, `flash` as flash. Mirror of the client-side helper.
1254
+ * @param model - the model id.
1255
+ * @returns the pricing key.
1256
+ */
1257
+ function pricingKeyOfModel(model) {
1258
+ return model.toLowerCase().includes("pro") ? "pro" : "flash";
1259
+ }
1260
+ /**
1261
+ * Candidate canonical price keys for one model, most specific first. The
1262
+ * pricing-key fallbacks (`pro`/`flash`) only apply to DeepSeek-family models
1263
+ * (id or provider names deepseek): an unrelated model must NOT silently pick
1264
+ * up the flash list price — it either resolves through an override, an
1265
+ * OpenRouter alias, or stays unpriced.
1266
+ * @param provider - the provider route, when known.
1267
+ * @param model - the model id.
1268
+ * @returns candidate keys in lookup order.
1269
+ */
1270
+ function modelKeys(provider, model) {
1271
+ const keys = [];
1272
+ if (provider !== void 0 && provider.length > 0) keys.push(`${provider}/${model}`);
1273
+ keys.push(model);
1274
+ if (model.toLowerCase().includes("deepseek") || provider !== void 0 && provider.toLowerCase().includes("deepseek")) keys.push(pricingKeyOfModel(model));
1275
+ return keys;
1276
+ }
1277
+ /**
1278
+ * The price band in effect at one instant: pre-rollout single price, or the
1279
+ * peak/off-peak band once the rollout is live.
1280
+ * @param time - epoch millis.
1281
+ * @returns the band.
1282
+ */
1283
+ function bandForTime(time) {
1284
+ if (time < PEAK_PRICING_START_MS) return "single";
1285
+ return isPeakHour(new Date(time)) ? "peak" : "offPeak";
1286
+ }
1287
+ /**
1288
+ * The bucket of one model's price entry for one band, with graceful
1289
+ * degradation to whatever tier exists.
1290
+ * @param modelPrice - the model's price entry.
1291
+ * @param band - the band to price for.
1292
+ * @returns the bucket, or undefined when the entry carries no tier at all.
1293
+ */
1294
+ function bucketOf(modelPrice, band) {
1295
+ if (band === "single") return modelPrice.single ?? modelPrice.offPeak ?? modelPrice.peak;
1296
+ if (band === "peak") return modelPrice.peak ?? modelPrice.offPeak ?? modelPrice.single;
1297
+ return modelPrice.offPeak ?? modelPrice.peak ?? modelPrice.single;
1298
+ }
1299
+ /**
1300
+ * Resolve one model's bucket at one instant from its price entry.
1301
+ * @param modelPrice - the model's price entry.
1302
+ * @param time - the instant to price for.
1303
+ * @returns the bucket and band, or null when the entry has no tier.
1304
+ */
1305
+ function priceAt(modelPrice, time) {
1306
+ const band = bandForTime(time);
1307
+ const bucket = bucketOf(modelPrice, band);
1308
+ return bucket === void 0 ? null : {
1309
+ bucket,
1310
+ band
1311
+ };
1312
+ }
1313
+ /**
1314
+ * Pick the snapshot effective at `time`: the newest with `effectiveAt <=
1315
+ * time`, or the earliest available when no snapshot predates it (pre-install
1316
+ * sessions anchor to the first snapshot once, as required).
1317
+ * @param snapshots - the snapshot history, any order.
1318
+ * @param time - the instant to anchor.
1319
+ * @returns the snapshot, or undefined when the history is empty.
1320
+ */
1321
+ function snapshotForTime(snapshots, time) {
1322
+ let best;
1323
+ for (const snapshot of snapshots) if (snapshot.effectiveAt <= time && (best === void 0 || snapshot.effectiveAt >= best.effectiveAt)) best = snapshot;
1324
+ return best ?? (snapshots.length > 0 ? snapshots[0] : void 0);
1325
+ }
1326
+ /**
1327
+ * The official cost formula, per million tokens: uncached input + cache
1328
+ * reads at the hit rate + cache writes at the uncached input rate (DeepSeek
1329
+ * reports only hit/miss buckets and bills writes as misses) + output.
1330
+ * @param usage - the step's token buckets.
1331
+ * @param bucket - the effective price bucket.
1332
+ * @returns the cost in the bucket's currency (CNY).
1333
+ */
1334
+ function stepCost(usage, bucket) {
1335
+ return (usage.uncached * bucket.inputPerMillion + usage.cacheRead * bucket.cacheReadPerMillion + usage.cacheWrite * bucket.inputPerMillion + usage.output * bucket.outputPerMillion) / 1e6;
1336
+ }
1337
+ function bucketEqual(left, right) {
1338
+ if (left === void 0 || right === void 0) return left === right;
1339
+ return Math.abs(left.cacheReadPerMillion - right.cacheReadPerMillion) < 1e-9 && Math.abs(left.inputPerMillion - right.inputPerMillion) < 1e-9 && Math.abs(left.outputPerMillion - right.outputPerMillion) < 1e-9;
1340
+ }
1341
+ function modelPriceEqual(left, right) {
1342
+ if (left.source !== right.source) return false;
1343
+ return bucketEqual(left.single, right.single) && bucketEqual(left.offPeak, right.offPeak) && bucketEqual(left.peak, right.peak);
1344
+ }
1345
+ /**
1346
+ * Whether two per-model price tables are equivalent (epsilon-compared).
1347
+ * Used to skip a new snapshot when a refresh changed nothing user-visible.
1348
+ */
1349
+ function pricesEqual(left, right) {
1350
+ const keys = /* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)]);
1351
+ for (const key of keys) {
1352
+ const a = left[key];
1353
+ const b = right[key];
1354
+ if (a === void 0 || b === void 0) return false;
1355
+ if (!modelPriceEqual(a, b)) return false;
1356
+ }
1357
+ return true;
1358
+ }
1359
+ /** Effective USD→CNY rate from the configured mode plus fallbacks. */
1360
+ function effectiveRate(fx, defaultRate) {
1361
+ if (fx.mode === "manual") return fx.manualRate;
1362
+ return fx.lastAutoRate ?? fx.manualRate ?? defaultRate;
1363
+ }
1364
+ /** Whether one canonical key already resolves to a non-OpenRouter (domestic) price. */
1365
+ function hasDomesticPrice(prices, key) {
1366
+ return (key.includes("/") ? [key] : [key, pricingKeyOfModel(key)]).some((candidate) => {
1367
+ const entry = prices[candidate];
1368
+ return entry !== void 0 && entry.source !== "openrouter";
1369
+ });
1370
+ }
1371
+ /**
1372
+ * Merge every source into one per-model price table through the priority
1373
+ * chain: manual overrides win; official (or the built-in fallback when the
1374
+ * page failed) covers the flash/pro pricing keys; OpenRouter covers only
1375
+ * models with no domestic price (fallback-only, USD→CNY, cache reads at the
1376
+ * configured discount). OpenRouter prices land under the aliased canonical
1377
+ * key so unaliased model ids still resolve through the key chain.
1378
+ * @param state - the pricebook state (overrides, aliases, discount, fx).
1379
+ * @param official - the official-page input (fallback table on failure).
1380
+ * @param openrouter - the OpenRouter input, or null when disabled/failed.
1381
+ * @param usdCnyRate - the effective exchange rate.
1382
+ * @param defaultRate - the configuration default rate (fallback chain end).
1383
+ * @returns the merged table and the highest-priority contributing source.
1384
+ */
1385
+ function computePricebook(state, official, openrouter, usdCnyRate, defaultRate, fallbackPeak = FALLBACK_PEAK, currency = "CNY") {
1386
+ const prices = {};
1387
+ for (const [key, override] of Object.entries(state.overrides)) prices[key] = {
1388
+ ...override,
1389
+ source: "manual"
1390
+ };
1391
+ const officialSource = official.error === void 0 ? "official" : "fallback";
1392
+ for (const key of ["flash", "pro"]) {
1393
+ if (prices[key] !== void 0) continue;
1394
+ const peakTable = official.peak ?? fallbackPeak;
1395
+ prices[key] = {
1396
+ source: officialSource,
1397
+ single: official.current[key],
1398
+ offPeak: peakTable[key].offPeak,
1399
+ peak: peakTable[key].peak
1400
+ };
1401
+ }
1402
+ const openRouterFx = currency === "USD" ? 1 : usdCnyRate;
1403
+ if (openrouter !== null && openrouter.error === void 0 && (currency === "USD" || usdCnyRate > 0)) {
1404
+ const discount = state.cacheReadDiscount;
1405
+ for (const [orId, usd] of Object.entries(openrouter.usdPrices)) {
1406
+ const alias = state.aliases[orId] ?? orId;
1407
+ if (hasDomesticPrice(prices, alias)) continue;
1408
+ if (hasDomesticPrice(prices, orId)) continue;
1409
+ const entry = {
1410
+ source: "openrouter",
1411
+ single: {
1412
+ cacheReadPerMillion: usd.inputPerMillionUsd * discount * openRouterFx,
1413
+ inputPerMillion: usd.inputPerMillionUsd * openRouterFx,
1414
+ outputPerMillion: usd.outputPerMillionUsd * openRouterFx
1415
+ }
1416
+ };
1417
+ prices[alias] = entry;
1418
+ if (alias !== orId) prices[orId] = entry;
1419
+ }
1420
+ }
1421
+ let primary = "none";
1422
+ for (const candidate of [
1423
+ "manual",
1424
+ "official",
1425
+ "fallback",
1426
+ "openrouter"
1427
+ ]) if (Object.values(prices).some((entry) => entry.source === candidate)) {
1428
+ primary = candidate;
1429
+ break;
1430
+ }
1431
+ return {
1432
+ prices,
1433
+ primarySource: primary
1434
+ };
1435
+ }
1436
+ /**
1437
+ * Fetch the USD→CNY rate from the free endpoint. Never throws; the caller
1438
+ * applies the fallback chain (last auto rate → manual/default rate).
1439
+ * @param fetchImpl - fetch-compatible function (injected for testability).
1440
+ * @param url - the endpoint.
1441
+ * @param timeoutMs - abort timeout.
1442
+ * @returns the fresh rate, or an error result.
1443
+ */
1444
+ async function fetchFxRate(fetchImpl = globalThis.fetch, url = FX_API_URL, timeoutMs = 1e4) {
1445
+ try {
1446
+ const controller = new AbortController();
1447
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1448
+ let response;
1449
+ try {
1450
+ response = await fetchImpl(url, { signal: controller.signal });
1451
+ } finally {
1452
+ clearTimeout(timer);
1453
+ }
1454
+ if (!response.ok) return {
1455
+ rate: null,
1456
+ error: `fx endpoint HTTP ${response.status}`
1457
+ };
1458
+ const wire = await response.json();
1459
+ const cny = Number(wire.rates?.CNY);
1460
+ if (!Number.isFinite(cny) || cny <= 0) return {
1461
+ rate: null,
1462
+ error: "fx response carries no rates.CNY"
1463
+ };
1464
+ return { rate: cny };
1465
+ } catch (error) {
1466
+ return {
1467
+ rate: null,
1468
+ error: error instanceof Error ? error.message : String(error)
1469
+ };
1470
+ }
1471
+ }
1472
+ /**
1473
+ * Fetch the OpenRouter models directory and fold it into USD-per-1M-token
1474
+ * buckets. Every entry with a valid prompt/completion price is kept: the
1475
+ * directory is small (a few hundred rows) and the alias map is editable, so
1476
+ * a newly aliased model must resolve without waiting for the next refresh.
1477
+ * Never throws.
1478
+ * @param fetchImpl - fetch-compatible function (injected for testability).
1479
+ * @param url - the directory endpoint.
1480
+ * @param timeoutMs - abort timeout.
1481
+ * @param aliases - the current alias map (kept for parity, not used as a filter).
1482
+ * @returns the model price table, or an error result.
1483
+ */
1484
+ async function fetchOpenRouter(fetchImpl = globalThis.fetch, url = OPENROUTER_MODELS_URL, timeoutMs = 15e3, aliases = DEFAULT_ALIASES) {
1485
+ try {
1486
+ const controller = new AbortController();
1487
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1488
+ let response;
1489
+ try {
1490
+ response = await fetchImpl(url, { signal: controller.signal });
1491
+ } finally {
1492
+ clearTimeout(timer);
1493
+ }
1494
+ if (!response.ok) return {
1495
+ usdPrices: {},
1496
+ error: `openrouter models HTTP ${response.status}`
1497
+ };
1498
+ const wire = await response.json();
1499
+ const usdPrices = {};
1500
+ for (const entry of wire.data ?? []) {
1501
+ if (typeof entry.id !== "string" || entry.pricing === void 0) continue;
1502
+ const prompt = Number(entry.pricing.prompt);
1503
+ const completion = Number(entry.pricing.completion);
1504
+ if (!Number.isFinite(prompt) || !Number.isFinite(completion) || prompt <= 0 || completion <= 0) continue;
1505
+ usdPrices[entry.id] = {
1506
+ inputPerMillionUsd: prompt * 1e6,
1507
+ outputPerMillionUsd: completion * 1e6
1508
+ };
1509
+ }
1510
+ return { usdPrices };
1511
+ } catch (error) {
1512
+ return {
1513
+ usdPrices: {},
1514
+ error: error instanceof Error ? error.message : String(error)
1515
+ };
1516
+ }
1517
+ }
1518
+ /** Fold a raw {@link PricingSnapshot} into the pricebook's official input. */
1519
+ function officialInputOf(snapshot) {
1520
+ return {
1521
+ current: snapshot.current,
1522
+ ...snapshot.peak === void 0 ? {} : { peak: snapshot.peak },
1523
+ ...snapshot.error === void 0 ? {} : { error: snapshot.error }
1524
+ };
1525
+ }
1526
+ /**
1527
+ * The synchronous read face the sessionCost projection folds against, plus
1528
+ * the mutation/refresh/persist surface the plugin's apply and route use.
1529
+ * Reads never touch the domain (mirror only); writes update the mirror first,
1530
+ * then queue a fail-soft durable write.
1531
+ */
1532
+ var PricebookHandle = class {
1533
+ ctx;
1534
+ config;
1535
+ currency;
1536
+ /** The authoritative in-memory state (mirror of the domain global). */
1537
+ state;
1538
+ domain;
1539
+ persistTail = Promise.resolve();
1540
+ lastOfficial;
1541
+ lastOpenRouter = null;
1542
+ lastFxError;
1543
+ fetchedAt = 0;
1544
+ constructor(ctx, config, persisted, currency = "CNY") {
1545
+ this.ctx = ctx;
1546
+ this.config = config;
1547
+ this.currency = currency;
1548
+ this.state = persisted ?? {
1549
+ ...initialPricebookState(),
1550
+ openRouterEnabled: config.openRouterEnabled
1551
+ };
1552
+ this.lastOfficial = { current: currency === "USD" ? FALLBACK_CURRENT_USD : FALLBACK_CURRENT };
1553
+ }
1554
+ /**
1555
+ * Open the storage domain and adopt the persisted state. Never throws:
1556
+ * a missing storageDomain or a failed open degrades to in-memory only.
1557
+ */
1558
+ async init() {
1559
+ const storageDomain = this.ctx.get("storageDomain");
1560
+ if (storageDomain === void 0) return;
1561
+ try {
1562
+ const domain = this.currency === "USD" ? PRICEBOOK_DOMAIN_USD : PRICEBOOK_DOMAIN;
1563
+ this.domain = await storageDomain.open(domain);
1564
+ this.state = this.domain.global.get();
1565
+ } catch (error) {
1566
+ console.warn(`cost-meter: pricebook domain unavailable, keeping in-memory state: ${error instanceof Error ? error.message : String(error)}`);
1567
+ this.domain = void 0;
1568
+ }
1569
+ }
1570
+ /** Queue one durable write of the whole state (fail-soft, serialized). */
1571
+ persist() {
1572
+ if (this.domain === void 0) return;
1573
+ const state = this.state;
1574
+ this.persistTail = this.persistTail.then(() => this.domain.global.set(state)).catch((error) => {
1575
+ console.warn(`cost-meter: pricebook persist failed: ${error instanceof Error ? error.message : String(error)}`);
1576
+ });
1577
+ }
1578
+ /** Resolve once every queued durable write has settled. */
1579
+ async whenIdle() {
1580
+ await this.persistTail;
1581
+ }
1582
+ /** Release the domain (called from the plugin's disposer). */
1583
+ async close() {
1584
+ await this.whenIdle();
1585
+ if (this.domain !== void 0) {
1586
+ const domain = this.domain;
1587
+ this.domain = void 0;
1588
+ await domain.close();
1589
+ }
1590
+ }
1591
+ /** The newest snapshot, or undefined before the first refresh. */
1592
+ currentSnapshot() {
1593
+ const snapshots = this.state.snapshots;
1594
+ return snapshots.length > 0 ? snapshots[snapshots.length - 1] : void 0;
1595
+ }
1596
+ /** Pick the snapshot effective at `time`. */
1597
+ snapshotFor(time) {
1598
+ return snapshotForTime(this.state.snapshots, time);
1599
+ }
1600
+ /**
1601
+ * Resolve the price of one model at one instant through the key chain and
1602
+ * the anchored snapshot. Synchronous: the fold calls this per usage event.
1603
+ * @param provider - the provider route, when known.
1604
+ * @param model - the model id.
1605
+ * @param time - the instant to price for.
1606
+ * @returns the hit, or null when no source carries a price.
1607
+ */
1608
+ priceFor(provider, model, time) {
1609
+ const snapshot = this.snapshotFor(time);
1610
+ if (snapshot === void 0) return null;
1611
+ for (const key of modelKeys(provider, model)) {
1612
+ const entry = snapshot.prices[key];
1613
+ if (entry === void 0) continue;
1614
+ const hit = priceAt(entry, time);
1615
+ if (hit === null) continue;
1616
+ return {
1617
+ bucket: hit.bucket,
1618
+ band: hit.band,
1619
+ source: entry.source,
1620
+ snapshotVersion: snapshot.version,
1621
+ snapshotEffectiveAt: snapshot.effectiveAt
1622
+ };
1623
+ }
1624
+ return null;
1625
+ }
1626
+ /** The effective USD→CNY rate under the current configuration. */
1627
+ rate() {
1628
+ return effectiveRate(this.state.fx, this.config.defaultFxRate);
1629
+ }
1630
+ /** Cap the snapshot history to the configured limit (newest kept). */
1631
+ capSnapshots() {
1632
+ const limit = this.config.snapshotHistoryLimit;
1633
+ if (this.state.snapshots.length > limit) this.state = {
1634
+ ...this.state,
1635
+ snapshots: this.state.snapshots.slice(-limit)
1636
+ };
1637
+ }
1638
+ /**
1639
+ * Recompute the resolved table from the cached inputs and start a new
1640
+ * snapshot when anything user-visible changed. Shared by the startup
1641
+ * refresh, the periodic refresh, the manual refresh, and every settings
1642
+ * mutation.
1643
+ */
1644
+ recompute(now) {
1645
+ const openrouter = this.config.openRouterEnabled ? this.lastOpenRouter : null;
1646
+ const { prices, primarySource } = computePricebook(this.state, this.lastOfficial, openrouter, this.rate(), this.config.defaultFxRate, this.currency === "USD" ? FALLBACK_PEAK_USD : FALLBACK_PEAK, this.currency);
1647
+ const last = this.currentSnapshot();
1648
+ if (last !== void 0 && pricesEqual(last.prices, prices)) return;
1649
+ const snapshot = {
1650
+ version: (last?.version ?? 0) + 1,
1651
+ currency: this.currency,
1652
+ effectiveAt: now,
1653
+ source: primarySource,
1654
+ usdCnyRate: this.rate(),
1655
+ prices
1656
+ };
1657
+ this.state = {
1658
+ ...this.state,
1659
+ snapshots: [...this.state.snapshots, snapshot],
1660
+ updatedAt: now
1661
+ };
1662
+ this.capSnapshots();
1663
+ this.persist();
1664
+ }
1665
+ /**
1666
+ * Refresh every remote source (official page, exchange rate, OpenRouter
1667
+ * when enabled) and re-anchor a snapshot on change. Never throws.
1668
+ * @param options - injected results (testability); omitted values are fetched.
1669
+ */
1670
+ async refresh(options) {
1671
+ const official = options?.official ?? await fetchPricing(globalThis.fetch, 15e3, this.currency === "USD" ? "en" : "zh");
1672
+ this.lastOfficial = officialInputOf(official);
1673
+ this.lastFxError = void 0;
1674
+ if (options?.fx !== void 0) this.applyFxResult(options.fx.rate, options.fx.error);
1675
+ else if (this.state.fx.mode === "auto") {
1676
+ const fx = await fetchFxRate(globalThis.fetch, this.config.fxApiUrl);
1677
+ this.applyFxResult(fx.rate, fx.error);
1678
+ }
1679
+ if (this.state.openRouterEnabled) this.lastOpenRouter = options?.openrouter ?? await fetchOpenRouter(globalThis.fetch, this.config.openRouterModelsUrl, 15e3, this.state.aliases);
1680
+ else this.lastOpenRouter = null;
1681
+ this.fetchedAt = Date.now();
1682
+ this.recompute(this.fetchedAt);
1683
+ }
1684
+ applyFxResult(rate, error) {
1685
+ this.lastFxError = error;
1686
+ if (rate !== null) this.state = {
1687
+ ...this.state,
1688
+ fx: {
1689
+ ...this.state.fx,
1690
+ lastAutoRate: rate,
1691
+ rateFetchedAt: Date.now()
1692
+ }
1693
+ };
1694
+ }
1695
+ /** Apply a settings mutation and re-anchor (snapshot on change). */
1696
+ mutate(mutator) {
1697
+ this.state = mutator(this.state);
1698
+ this.recompute(Date.now());
1699
+ this.persist();
1700
+ }
1701
+ async setOverrides(overrides) {
1702
+ this.mutate((state) => ({
1703
+ ...state,
1704
+ overrides
1705
+ }));
1706
+ await this.whenIdle();
1707
+ }
1708
+ async setAliases(aliases) {
1709
+ this.state = {
1710
+ ...this.state,
1711
+ aliases
1712
+ };
1713
+ if (this.state.openRouterEnabled) this.lastOpenRouter = await fetchOpenRouter(globalThis.fetch, this.config.openRouterModelsUrl, 15e3, aliases);
1714
+ this.recompute(Date.now());
1715
+ await this.whenIdle();
1716
+ }
1717
+ async setCacheReadDiscount(discount) {
1718
+ this.mutate((state) => ({
1719
+ ...state,
1720
+ cacheReadDiscount: discount
1721
+ }));
1722
+ await this.whenIdle();
1723
+ }
1724
+ async setFx(mode, manualRate) {
1725
+ this.mutate((state) => ({
1726
+ ...state,
1727
+ fx: {
1728
+ ...state.fx,
1729
+ mode,
1730
+ manualRate
1731
+ }
1732
+ }));
1733
+ await this.whenIdle();
1734
+ }
1735
+ async setBalanceEnabled(enabled) {
1736
+ this.mutate((state) => ({
1737
+ ...state,
1738
+ balanceEnabled: enabled
1739
+ }));
1740
+ await this.whenIdle();
1741
+ }
1742
+ async setOpenRouterEnabled(enabled) {
1743
+ this.mutate((state) => ({
1744
+ ...state,
1745
+ openRouterEnabled: enabled
1746
+ }));
1747
+ await this.whenIdle();
1748
+ }
1749
+ /**
1750
+ * Apply a resolved plugin-settings snapshot (the `cost-meter`
1751
+ * settings namespace) to the pricebook: the editable configuration fields
1752
+ * overwrite the persisted state and the resolved table re-anchors (a new
1753
+ * snapshot starts when anything user-visible changed). Never throws.
1754
+ * @param settings - the settings-section value (may carry only a subset).
1755
+ */
1756
+ applySettings(settings) {
1757
+ try {
1758
+ let next = this.state;
1759
+ if (settings.overrides !== void 0) next = {
1760
+ ...next,
1761
+ overrides: settings.overrides
1762
+ };
1763
+ if (settings.aliases !== void 0) next = {
1764
+ ...next,
1765
+ aliases: settings.aliases
1766
+ };
1767
+ if (settings.cacheReadDiscount !== void 0) next = {
1768
+ ...next,
1769
+ cacheReadDiscount: settings.cacheReadDiscount
1770
+ };
1771
+ if (settings.fxMode !== void 0 || settings.manualRate !== void 0) next = {
1772
+ ...next,
1773
+ fx: {
1774
+ ...next.fx,
1775
+ ...settings.fxMode === void 0 ? {} : { mode: settings.fxMode },
1776
+ ...settings.manualRate === void 0 ? {} : { manualRate: settings.manualRate }
1777
+ }
1778
+ };
1779
+ if (settings.balanceEnabled !== void 0) next = {
1780
+ ...next,
1781
+ balanceEnabled: settings.balanceEnabled
1782
+ };
1783
+ if (settings.openRouterEnabled !== void 0) next = {
1784
+ ...next,
1785
+ openRouterEnabled: settings.openRouterEnabled
1786
+ };
1787
+ if (next === this.state) return;
1788
+ this.state = next;
1789
+ if (settings.aliases !== void 0 && next.openRouterEnabled) fetchOpenRouter(globalThis.fetch, this.config.openRouterModelsUrl, 15e3, next.aliases).then((result) => {
1790
+ if (result.error === void 0) this.lastOpenRouter = result;
1791
+ }).catch(() => {});
1792
+ this.recompute(Date.now());
1793
+ this.persist();
1794
+ } catch (error) {
1795
+ console.warn(`cost-meter: settings apply failed: ${error instanceof Error ? error.message : String(error)}`);
1796
+ }
1797
+ }
1798
+ /** The read-side payload for the route. */
1799
+ view() {
1800
+ const errors = {};
1801
+ if (this.lastOfficial.error !== void 0) errors.official = this.lastOfficial.error;
1802
+ if (this.lastFxError !== void 0) errors.fx = this.lastFxError;
1803
+ if (this.lastOpenRouter?.error !== void 0) errors.openRouter = this.lastOpenRouter.error;
1804
+ return {
1805
+ currency: this.currency,
1806
+ current: this.currentSnapshot() ?? null,
1807
+ snapshots: this.state.snapshots,
1808
+ overrides: this.state.overrides,
1809
+ aliases: this.state.aliases,
1810
+ cacheReadDiscount: this.state.cacheReadDiscount,
1811
+ fx: this.state.fx,
1812
+ balanceEnabled: this.state.balanceEnabled,
1813
+ openRouterEnabled: this.state.openRouterEnabled,
1814
+ fetchedAt: this.fetchedAt,
1815
+ ...Object.keys(errors).length > 0 ? { errors } : {}
1816
+ };
1817
+ }
1818
+ };
1819
+ //#endregion
1820
+ //#region src/session-cost-projection.ts
1821
+ const zeroTotals = () => ({
1822
+ uncachedCost: 0,
1823
+ cacheReadCost: 0,
1824
+ outputCost: 0,
1825
+ cost: 0,
1826
+ pricedSteps: 0,
1827
+ unpricedSteps: 0,
1828
+ steps: 0
1829
+ });
1830
+ /** Wire schema of the projection value (validated before it leaves the host). */
1831
+ const projectionSchema = z.object({
1832
+ model: z.object({
1833
+ provider: z.string(),
1834
+ model: z.string()
1835
+ }).nullable(),
1836
+ steps: z.array(z.object({
1837
+ turn: z.number().int().nonnegative(),
1838
+ step: z.number().int().nonnegative(),
1839
+ time: z.number().int().nonnegative(),
1840
+ model: z.string(),
1841
+ provider: z.string().optional(),
1842
+ uncachedInputTokens: z.number().int().nonnegative(),
1843
+ cacheReadTokens: z.number().int().nonnegative(),
1844
+ cacheWriteTokens: z.number().int().nonnegative(),
1845
+ outputTokens: z.number().int().nonnegative(),
1846
+ uncachedCost: z.number().nonnegative(),
1847
+ cacheReadCost: z.number().nonnegative(),
1848
+ outputCost: z.number().nonnegative(),
1849
+ cost: z.number().nullable(),
1850
+ priced: z.boolean(),
1851
+ unpricedReason: z.enum(["NO_MODEL", "NO_PRICE"]).optional(),
1852
+ snapshotVersion: z.number().int().nonnegative().nullable(),
1853
+ snapshotEffectiveAt: z.number().int().nonnegative().nullable(),
1854
+ band: z.enum([
1855
+ "peak",
1856
+ "offPeak",
1857
+ "single"
1858
+ ]).nullable()
1859
+ }).strict()),
1860
+ totals: z.object({
1861
+ uncachedCost: z.number().nonnegative(),
1862
+ cacheReadCost: z.number().nonnegative(),
1863
+ outputCost: z.number().nonnegative(),
1864
+ cost: z.number().nonnegative(),
1865
+ pricedSteps: z.number().int().nonnegative(),
1866
+ unpricedSteps: z.number().int().nonnegative(),
1867
+ steps: z.number().int().nonnegative()
1868
+ }).strict()
1869
+ }).strict();
1870
+ /** Extract the disjoint token buckets from a wire usage object. */
1871
+ function usageBuckets(usage) {
1872
+ return {
1873
+ uncached: usage.inputTokens,
1874
+ cacheRead: usage.cacheReadTokens ?? 0,
1875
+ cacheWrite: usage.cacheWriteTokens ?? 0,
1876
+ output: usage.outputTokens
1877
+ };
1878
+ }
1879
+ function bucketsEmpty(usage) {
1880
+ return usage.uncached === 0 && usage.cacheRead === 0 && usage.cacheWrite === 0 && usage.output === 0;
1881
+ }
1882
+ /** Replace totals with one step, subtracting its predecessor when present. */
1883
+ function replaceTotals(totals, step, previous) {
1884
+ const subtract = (value) => previous === void 0 ? 0 : value;
1885
+ return {
1886
+ uncachedCost: totals.uncachedCost - subtract(previous?.uncachedCost ?? 0) + step.uncachedCost,
1887
+ cacheReadCost: totals.cacheReadCost - subtract(previous?.cacheReadCost ?? 0) + step.cacheReadCost,
1888
+ outputCost: totals.outputCost - subtract(previous?.outputCost ?? 0) + step.outputCost,
1889
+ cost: totals.cost - subtract(previous?.cost ?? 0) + (step.cost ?? 0),
1890
+ pricedSteps: totals.pricedSteps - (previous?.priced === true ? 1 : 0) + (step.priced ? 1 : 0),
1891
+ unpricedSteps: totals.unpricedSteps - (previous?.priced === false ? 1 : 0) + (step.priced ? 0 : 1),
1892
+ steps: totals.steps - (previous === void 0 ? 0 : 1) + 1
1893
+ };
1894
+ }
1895
+ function sameStep(left, right) {
1896
+ return left.uncachedInputTokens === right.uncachedInputTokens && left.cacheReadTokens === right.cacheReadTokens && left.cacheWriteTokens === right.cacheWriteTokens && left.outputTokens === right.outputTokens && left.cost === right.cost && left.snapshotVersion === right.snapshotVersion && left.band === right.band;
1897
+ }
1898
+ /**
1899
+ * Fold one committed event onto the ledger. Pure except for synchronous
1900
+ * pricebook mirror reads; returns the same state reference for events the
1901
+ * unit does not own (the framework's zero-work contract).
1902
+ * @param state - the state covering all prior events.
1903
+ * @param event - the next committed session event.
1904
+ * @param pricebook - the pricebook read face.
1905
+ * @returns the next state.
1906
+ */
1907
+ function foldSessionCost(state, event, pricebook) {
1908
+ if (event.type === "request/header") {
1909
+ const header = canonicalHeader(event.data.header);
1910
+ const model = {
1911
+ provider: header.config.provider,
1912
+ model: header.config.model
1913
+ };
1914
+ if (state.model !== null && state.model.provider === model.provider && state.model.model === model.model) return state;
1915
+ return {
1916
+ ...state,
1917
+ model
1918
+ };
1919
+ }
1920
+ let turn;
1921
+ let step;
1922
+ let usage;
1923
+ if (event.type === "assistant/chunk" && event.data.chunk.type === "usage") {
1924
+ turn = event.data.turn;
1925
+ step = event.data.step;
1926
+ usage = event.data.chunk.usage;
1927
+ } else if (event.type === "assistant/message" && event.data.usage !== void 0) {
1928
+ turn = event.data.turn;
1929
+ step = event.data.step;
1930
+ usage = event.data.usage;
1931
+ } else return state;
1932
+ const buckets = usageBuckets(usage);
1933
+ if (bucketsEmpty(buckets)) return state;
1934
+ const key = `${turn}:${step}`;
1935
+ const previous = state.steps[key];
1936
+ const model = state.model;
1937
+ const hit = model === null ? null : pricebook.priceFor(model.provider, model.model, event.time);
1938
+ const unpricedReason = hit === null ? model === null ? "NO_MODEL" : "NO_PRICE" : void 0;
1939
+ const uncachedCost = hit === null ? 0 : (buckets.uncached + buckets.cacheWrite) * hit.bucket.inputPerMillion / 1e6;
1940
+ const cacheReadCost = hit === null ? 0 : buckets.cacheRead * hit.bucket.cacheReadPerMillion / 1e6;
1941
+ const outputCost = hit === null ? 0 : buckets.output * hit.bucket.outputPerMillion / 1e6;
1942
+ const record = {
1943
+ turn,
1944
+ step,
1945
+ time: event.time,
1946
+ model: model?.model ?? "unknown",
1947
+ ...model?.provider === void 0 ? {} : { provider: model.provider },
1948
+ uncachedInputTokens: buckets.uncached,
1949
+ cacheReadTokens: buckets.cacheRead,
1950
+ cacheWriteTokens: buckets.cacheWrite,
1951
+ outputTokens: buckets.output,
1952
+ uncachedCost,
1953
+ cacheReadCost,
1954
+ outputCost,
1955
+ cost: hit === null ? null : uncachedCost + cacheReadCost + outputCost,
1956
+ priced: hit !== null,
1957
+ ...unpricedReason === void 0 ? {} : { unpricedReason },
1958
+ snapshotVersion: hit?.snapshotVersion ?? null,
1959
+ snapshotEffectiveAt: hit?.snapshotEffectiveAt ?? null,
1960
+ band: hit?.band ?? null
1961
+ };
1962
+ if (previous !== void 0 && sameStep(previous, record)) return state;
1963
+ return {
1964
+ ...state,
1965
+ steps: {
1966
+ ...state.steps,
1967
+ [key]: record
1968
+ },
1969
+ totals: replaceTotals(state.totals, record, previous)
1970
+ };
1971
+ }
1972
+ /** State → wire payload: steps ordered by turn then step. */
1973
+ function viewSessionCost(state) {
1974
+ const steps = Object.values(state.steps).sort((left, right) => left.turn - right.turn || left.step - right.step);
1975
+ return {
1976
+ model: state.model,
1977
+ steps,
1978
+ totals: state.totals
1979
+ };
1980
+ }
1981
+ /**
1982
+ * The projection unit. The fold reads the pricebook mirror synchronously;
1983
+ * the unit never touches the domain itself.
1984
+ * @param pricebook - the pricebook read face (in-memory mirror).
1985
+ * @returns the definition to register with `ctx.sessionProjections`.
1986
+ */
1987
+ function sessionCostProjection(pricebook, key) {
1988
+ return {
1989
+ key,
1990
+ schema: projectionSchema,
1991
+ init: () => ({
1992
+ model: null,
1993
+ steps: {},
1994
+ totals: zeroTotals()
1995
+ }),
1996
+ apply: (state, event) => foldSessionCost(state, event, pricebook),
1997
+ view: viewSessionCost,
1998
+ stateVersion: 1
1999
+ };
2000
+ }
2001
+ //#endregion
2002
+ //#region src/subagent-cost.ts
2003
+ const ZERO_TOTALS = {
2004
+ uncachedCost: 0,
2005
+ cacheReadCost: 0,
2006
+ outputCost: 0,
2007
+ cost: 0,
2008
+ pricedSteps: 0,
2009
+ unpricedSteps: 0,
2010
+ steps: 0
2011
+ };
2012
+ /**
2013
+ * Collect every live descendant session of `rootSessionId` (BFS over the
2014
+ * runtime ownership relation) with its anchored cost totals.
2015
+ * @param rootSessionId - the root conversation's session id.
2016
+ * @param agents - the agents service (live registry).
2017
+ * @param sessions - the sessions service (session store).
2018
+ * @param projections - the sessionProjections service.
2019
+ * @returns one entry per descendant with a priced or unpriced ledger;
2020
+ * empty when the root has no live agent or no descendants.
2021
+ */
2022
+ function collectSubagentCosts(rootSessionId, agents, sessions, projections, projectionKey = "sessionCost") {
2023
+ const rootAgent = agents.get(rootSessionId);
2024
+ if (rootAgent === void 0) return [];
2025
+ const result = [];
2026
+ const seen = /* @__PURE__ */ new Set([rootSessionId]);
2027
+ const queue = [rootAgent];
2028
+ while (queue.length > 0) {
2029
+ const parent = queue.shift();
2030
+ for (const candidate of agents.list()) {
2031
+ const candidateId = String(candidate.session.id);
2032
+ if (seen.has(candidateId)) continue;
2033
+ if (!agents.isOwnedBy(candidateId, parent)) continue;
2034
+ seen.add(candidateId);
2035
+ queue.push(candidate);
2036
+ const session = sessions.get(candidateId);
2037
+ if (session === void 0) continue;
2038
+ const cost = projections.snapshot(session).values[projectionKey];
2039
+ if (cost === void 0) continue;
2040
+ result.push({
2041
+ sessionId: candidateId,
2042
+ totals: cost.totals ?? { ...ZERO_TOTALS }
2043
+ });
2044
+ }
2045
+ }
2046
+ return result;
2047
+ }
2048
+ /** Sum several totals into one (empty list → the zero totals). */
2049
+ function sumTotals(totals) {
2050
+ const sum = { ...ZERO_TOTALS };
2051
+ for (const totalsEntry of totals) {
2052
+ sum.uncachedCost += totalsEntry.uncachedCost;
2053
+ sum.cacheReadCost += totalsEntry.cacheReadCost;
2054
+ sum.outputCost += totalsEntry.outputCost;
2055
+ sum.cost += totalsEntry.cost;
2056
+ sum.pricedSteps += totalsEntry.pricedSteps;
2057
+ sum.unpricedSteps += totalsEntry.unpricedSteps;
2058
+ sum.steps += totalsEntry.steps;
2059
+ }
2060
+ return sum;
2061
+ }
2062
+ //#endregion
2063
+ //#region src/index.ts
2064
+ /** Stable Cordis plugin name. */
2065
+ const name = "cost-meter";
2066
+ /** Services required before the route and the projection can register. */
2067
+ const inject = ["webServer", "sessionProjections"];
2068
+ /** Public DeepSeek API base; the internal endpoint comes from config `baseURL`. */
2069
+ const PUBLIC_BASE_URL = "https://api.deepseek.com";
2070
+ /** Default credential reference, matching the llm-deepseek adapter. */
2071
+ const DEFAULT_API_KEY_ENV = "DEEPSEEK_API_KEY";
2072
+ /** How long a successful balance snapshot stays fresh (milliseconds). */
2073
+ const DEFAULT_REFRESH_MS = 6e4;
2074
+ /** How long a failed refresh stays cached before retrying (milliseconds). */
2075
+ const FAILED_REFRESH_MS = 1e4;
2076
+ /** Per-request timeout for the provider balance call (milliseconds). */
2077
+ const BALANCE_TIMEOUT_MS = 1e4;
2078
+ /** Default cadence for re-fetching the official pricing page (hours). */
2079
+ const DEFAULT_PRICING_REFRESH_HOURS = 6;
2080
+ /** Max POST body accepted by the settings route (bytes). */
2081
+ const MAX_BODY_BYTES = 262144;
2082
+ const modelPriceSchema = Schema.object({
2083
+ single: Schema.object({
2084
+ cacheReadPerMillion: Schema.number().min(0),
2085
+ inputPerMillion: Schema.number().min(0),
2086
+ outputPerMillion: Schema.number().min(0)
2087
+ }),
2088
+ offPeak: Schema.object({
2089
+ cacheReadPerMillion: Schema.number().min(0),
2090
+ inputPerMillion: Schema.number().min(0),
2091
+ outputPerMillion: Schema.number().min(0)
2092
+ }),
2093
+ peak: Schema.object({
2094
+ cacheReadPerMillion: Schema.number().min(0),
2095
+ inputPerMillion: Schema.number().min(0),
2096
+ outputPerMillion: Schema.number().min(0)
2097
+ })
2098
+ });
2099
+ const Config = Schema.object({
2100
+ apiKeyEnv: Schema.string().role("credential-ref").default(DEFAULT_API_KEY_ENV),
2101
+ baseURL: Schema.string().default(PUBLIC_BASE_URL),
2102
+ refreshMs: Schema.natural().min(1e3).default(DEFAULT_REFRESH_MS),
2103
+ pricingRefreshHours: Schema.number().min(1).default(6),
2104
+ trustedHosts: Schema.array(Schema.string()).default([]),
2105
+ fxApiUrl: Schema.string().default(FX_API_URL),
2106
+ defaultFxRate: Schema.number().min(.001).default(DEFAULT_FX_RATE),
2107
+ openRouterModelsUrl: Schema.string().default(OPENROUTER_MODELS_URL),
2108
+ openRouterEnabled: Schema.boolean().default(true),
2109
+ snapshotHistoryLimit: Schema.natural().min(1).default(50),
2110
+ overrides: Schema.dict(modelPriceSchema, Schema.string()).default({}),
2111
+ aliases: Schema.dict(Schema.string(), Schema.string()).default({ ...DEFAULT_ALIASES }),
2112
+ cacheReadDiscount: Schema.number().min(0).max(1).default(DEFAULT_CACHE_READ_DISCOUNT),
2113
+ fxMode: Schema.union(["auto", "manual"]).default("auto"),
2114
+ manualRate: Schema.number().min(.001).default(DEFAULT_FX_RATE),
2115
+ balanceEnabled: Schema.boolean().default(true)
2116
+ });
2117
+ /** Normalized authority of a Host-header value, or undefined when unparsable. */
2118
+ function parseAuthority(authority) {
2119
+ try {
2120
+ return new URL(`http://${authority}`);
2121
+ } catch {
2122
+ return;
2123
+ }
2124
+ }
2125
+ /**
2126
+ * Whether a normalized URL hostname names the local loopback authority.
2127
+ * @param hostname - WHATWG URL hostname (IPv6 literals retain brackets).
2128
+ * @returns true for localhost, IPv6 loopback, or any IPv4 address in 127/8.
2129
+ */
2130
+ function isLoopbackHostname(hostname) {
2131
+ if (hostname === "localhost" || hostname === "[::1]") return true;
2132
+ const parts = hostname.split(".");
2133
+ return parts.length === 4 && parts[0] === "127" && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
2134
+ }
2135
+ /**
2136
+ * Browser-trust fence for the balance route, mirroring the /api fence: the
2137
+ * Host header is the one header DNS rebinding cannot forge, and loopback or
2138
+ * a declared trusted authority passes.
2139
+ * @param headers - request headers.
2140
+ * @param trustedHosts - non-loopback authorities this deployment serves.
2141
+ * @returns true when the request Host is ours.
2142
+ */
2143
+ function isTrustedRequest(headers, trustedHosts) {
2144
+ const host = headers.host;
2145
+ if (typeof host !== "string" || host.length === 0) return false;
2146
+ const hostUrl = parseAuthority(host);
2147
+ if (hostUrl === void 0) return false;
2148
+ if (isLoopbackHostname(hostUrl.hostname)) return true;
2149
+ return trustedHosts.some((entry) => {
2150
+ const entryUrl = parseAuthority(entry);
2151
+ return entryUrl !== void 0 && entryUrl.host === hostUrl.host;
2152
+ });
2153
+ }
2154
+ /**
2155
+ * Resolve the API key for one refresh: the credential seam first, the
2156
+ * process environment as fallback when no seam is composed.
2157
+ * @param ctx - host context.
2158
+ * @param env - the credential reference name.
2159
+ * @returns the resolved key, or undefined when absent everywhere.
2160
+ */
2161
+ async function resolveApiKey(ctx, env) {
2162
+ const credentials = ctx.get("credentials");
2163
+ if (credentials !== void 0) {
2164
+ const hit = await credentials.resolve(credentialRef(env));
2165
+ if (hit !== void 0 && hit.value.length > 0) return hit.value;
2166
+ }
2167
+ const ambient = process.env[env];
2168
+ return ambient !== void 0 && ambient.length > 0 ? ambient : void 0;
2169
+ }
2170
+ /**
2171
+ * Parse the first balance row, converting the provider's decimal strings to
2172
+ * numbers.
2173
+ * @param wire - the parsed provider response.
2174
+ * @returns the first row, or null when the response carried no rows.
2175
+ */
2176
+ function pickBalanceInfo(wire) {
2177
+ const info = wire.balance_infos?.[0];
2178
+ if (info === void 0) return null;
2179
+ return {
2180
+ currency: info.currency,
2181
+ total: Number(info.total_balance),
2182
+ granted: Number(info.granted_balance ?? 0),
2183
+ toppedUp: Number(info.topped_up_balance ?? 0)
2184
+ };
2185
+ }
2186
+ /**
2187
+ * One provider balance call: resolve the key, GET /user/balance, and fold
2188
+ * the result into a {@link BalanceSnapshot}. Never throws — every failure
2189
+ * becomes an ok:false snapshot.
2190
+ * @param ctx - host context.
2191
+ * @param config - resolved plugin facts.
2192
+ * @returns the folded snapshot.
2193
+ */
2194
+ async function fetchBalance(ctx, config) {
2195
+ const key = await resolveApiKey(ctx, config.apiKeyEnv);
2196
+ if (key === void 0) return {
2197
+ ok: false,
2198
+ code: "MISSING_CREDENTIAL",
2199
+ message: `cost-meter: no API key for "${config.apiKeyEnv}"; store it through the credentials service or export it in the launching environment`
2200
+ };
2201
+ let response;
2202
+ try {
2203
+ response = await fetch(`${config.baseURL}/user/balance`, {
2204
+ headers: { authorization: `Bearer ${key}` },
2205
+ signal: AbortSignal.timeout(BALANCE_TIMEOUT_MS)
2206
+ });
2207
+ } catch (error) {
2208
+ return {
2209
+ ok: false,
2210
+ code: "NETWORK",
2211
+ message: `cost-meter: balance request failed: ${error instanceof Error ? error.message : String(error)}`
2212
+ };
2213
+ }
2214
+ if (!response.ok) return {
2215
+ ok: false,
2216
+ code: `HTTP_${response.status}`,
2217
+ message: `cost-meter: balance endpoint answered ${response.status} ${response.statusText}`
2218
+ };
2219
+ let wire;
2220
+ try {
2221
+ wire = await response.json();
2222
+ } catch (error) {
2223
+ return {
2224
+ ok: false,
2225
+ code: "INVALID_RESPONSE",
2226
+ message: `cost-meter: balance body unparsable: ${error instanceof Error ? error.message : String(error)}`
2227
+ };
2228
+ }
2229
+ return {
2230
+ ok: true,
2231
+ isAvailable: wire.is_available === true,
2232
+ balance: pickBalanceInfo(wire),
2233
+ fetchedAt: Date.now()
2234
+ };
2235
+ }
2236
+ /** Read a bounded JSON request body. */
2237
+ async function readBody(req) {
2238
+ const chunks = [];
2239
+ let total = 0;
2240
+ for await (const chunk of req) {
2241
+ const part = chunk;
2242
+ total += part.length;
2243
+ if (total > MAX_BODY_BYTES) throw new Error("body too large");
2244
+ chunks.push(part);
2245
+ }
2246
+ const text = Buffer.concat(chunks).toString("utf8");
2247
+ if (text.length === 0) return {};
2248
+ return JSON.parse(text);
2249
+ }
2250
+ /** The `cost-meter` settings namespace (plugin configuration page). */
2251
+ const SETTINGS_NAMESPACE = settingsNamespace("cost-meter");
2252
+ /**
2253
+ * Mount the plugin: open the pricebook domain, refresh every remote source,
2254
+ * register the `sessionCost` projection, register the /cost-meter
2255
+ * route (GET view + POST settings/refresh), and keep the balance and the
2256
+ * official pricing fresh.
2257
+ * @param ctx - host context.
2258
+ * @param config - plugin config (schema defaults applied by the Loader).
2259
+ */
2260
+ async function apply(ctx, config) {
2261
+ const resolved = {
2262
+ apiKeyEnv: config?.apiKeyEnv ?? "DEEPSEEK_API_KEY",
2263
+ baseURL: config?.baseURL ?? "https://api.deepseek.com",
2264
+ refreshMs: config?.refreshMs ?? 6e4,
2265
+ pricingRefreshHours: config?.pricingRefreshHours ?? 6,
2266
+ trustedHosts: config?.trustedHosts ?? [],
2267
+ fxApiUrl: config?.fxApiUrl ?? "https://open.er-api.com/v6/latest/USD",
2268
+ defaultFxRate: config?.defaultFxRate ?? 7.2,
2269
+ openRouterModelsUrl: config?.openRouterModelsUrl ?? "https://openrouter.ai/api/v1/models",
2270
+ openRouterEnabled: config?.openRouterEnabled ?? true,
2271
+ snapshotHistoryLimit: config?.snapshotHistoryLimit ?? 50
2272
+ };
2273
+ const pricebookConfig = {
2274
+ fxApiUrl: resolved.fxApiUrl,
2275
+ defaultFxRate: resolved.defaultFxRate,
2276
+ openRouterModelsUrl: resolved.openRouterModelsUrl,
2277
+ openRouterEnabled: resolved.openRouterEnabled,
2278
+ snapshotHistoryLimit: resolved.snapshotHistoryLimit
2279
+ };
2280
+ const pricebookCny = new PricebookHandle(ctx, pricebookConfig, void 0, "CNY");
2281
+ const pricebookUsd = new PricebookHandle(ctx, pricebookConfig, void 0, "USD");
2282
+ await Promise.all([pricebookCny.init(), pricebookUsd.init()]);
2283
+ await Promise.all([pricebookCny.refresh(), pricebookUsd.refresh()]);
2284
+ ctx.effect(() => () => {
2285
+ pricebookCny.close();
2286
+ pricebookUsd.close();
2287
+ }, "cost-meter: pricebook domains");
2288
+ let currentSettings = () => config ?? {};
2289
+ installSettingsSection(ctx, SETTINGS_NAMESPACE, Config, config ?? {}, {
2290
+ setSource: (source) => {
2291
+ currentSettings = source;
2292
+ },
2293
+ onChange: () => {
2294
+ const settings = currentSettings();
2295
+ pricebookCny.applySettings(settings);
2296
+ pricebookUsd.applySettings(settings);
2297
+ }
2298
+ });
2299
+ ctx.effect(() => ctx.sessionProjections.register(sessionCostProjection(pricebookCny, "sessionCost")), "cost-meter: sessionCost projection");
2300
+ ctx.effect(() => ctx.sessionProjections.register(sessionCostProjection(pricebookUsd, "sessionCostUsd")), "cost-meter: sessionCostUsd projection");
2301
+ let lastBalance;
2302
+ let inflight;
2303
+ const refresh = () => {
2304
+ const now = Date.now();
2305
+ const ttl = lastBalance === void 0 ? 0 : lastBalance.snapshot.ok ? resolved.refreshMs : FAILED_REFRESH_MS;
2306
+ if (lastBalance !== void 0 && now - lastBalance.at < ttl) return Promise.resolve(lastBalance.snapshot);
2307
+ if (inflight === void 0) inflight = fetchBalance(ctx, resolved).then((snapshot) => {
2308
+ lastBalance = {
2309
+ at: Date.now(),
2310
+ snapshot
2311
+ };
2312
+ inflight = void 0;
2313
+ return snapshot;
2314
+ }, (error) => {
2315
+ inflight = void 0;
2316
+ return {
2317
+ ok: false,
2318
+ code: "INTERNAL",
2319
+ message: `cost-meter: ${error instanceof Error ? error.message : String(error)}`
2320
+ };
2321
+ });
2322
+ return inflight;
2323
+ };
2324
+ const pricingTimer = setInterval(() => {
2325
+ pricebookCny.refresh();
2326
+ pricebookUsd.refresh();
2327
+ }, resolved.pricingRefreshHours * 36e5);
2328
+ pricingTimer.unref?.();
2329
+ ctx.effect(() => () => clearInterval(pricingTimer), "cost-meter: pricing refresh timer");
2330
+ const respond = (res, status, body) => {
2331
+ res.writeHead(status, {
2332
+ "content-type": "application/json; charset=utf-8",
2333
+ "cache-control": "no-store"
2334
+ });
2335
+ res.end(JSON.stringify(body));
2336
+ };
2337
+ /** Parse the `session` query parameter (the requesting session id). */
2338
+ const sessionOf = (req) => {
2339
+ try {
2340
+ const value = new URL(req.url ?? "/", "http://localhost").searchParams.get("session");
2341
+ return value === null || value.length === 0 ? void 0 : value;
2342
+ } catch {
2343
+ return;
2344
+ }
2345
+ };
2346
+ /** Parse the `currency` query parameter; defaults to CNY. */
2347
+ const currencyOf = (req) => {
2348
+ try {
2349
+ return new URL(req.url ?? "/", "http://localhost").searchParams.get("currency") === "USD" ? "USD" : "CNY";
2350
+ } catch {
2351
+ return "CNY";
2352
+ }
2353
+ };
2354
+ const buildResponse = async (rootSessionId, currency) => {
2355
+ const agents = ctx.get("agents");
2356
+ const sessionsStore = ctx.get("sessions");
2357
+ const pricebook = currency === "USD" ? pricebookUsd : pricebookCny;
2358
+ const projectionKey = currency === "USD" ? "sessionCostUsd" : "sessionCost";
2359
+ const subagents = rootSessionId === void 0 || agents === void 0 || sessionsStore === void 0 ? [] : collectSubagentCosts(rootSessionId, agents, sessionsStore, ctx.sessionProjections, projectionKey);
2360
+ return {
2361
+ balance: await refresh(),
2362
+ pricebook: pricebook.view(),
2363
+ subagents
2364
+ };
2365
+ };
2366
+ const route = {
2367
+ kind: "exact",
2368
+ path: "/cost-meter",
2369
+ handler: async (req, res) => {
2370
+ if (req.method !== "GET" && req.method !== "POST") {
2371
+ res.writeHead(405, { allow: "GET, POST" });
2372
+ res.end("method not allowed");
2373
+ return;
2374
+ }
2375
+ if (!isTrustedRequest(req.headers, resolved.trustedHosts)) {
2376
+ res.writeHead(403);
2377
+ res.end("forbidden");
2378
+ return;
2379
+ }
2380
+ if (req.method === "POST") {
2381
+ try {
2382
+ const body = await readBody(req);
2383
+ if (body.action !== "refresh") throw new Error(`unknown action "${String(body.action)}"`);
2384
+ } catch (error) {
2385
+ respond(res, 400, { error: error instanceof Error ? error.message : String(error) });
2386
+ return;
2387
+ }
2388
+ try {
2389
+ await Promise.all([pricebookCny.refresh(), pricebookUsd.refresh()]);
2390
+ } catch (error) {
2391
+ respond(res, 500, { error: error instanceof Error ? error.message : String(error) });
2392
+ return;
2393
+ }
2394
+ respond(res, 200, await buildResponse(sessionOf(req), currencyOf(req)));
2395
+ return;
2396
+ }
2397
+ respond(res, 200, await buildResponse(sessionOf(req), currencyOf(req)));
2398
+ }
2399
+ };
2400
+ ctx.effect(() => ctx.webServer.register(route), "cost-meter: /cost-meter route");
2401
+ }
2402
+ //#endregion
2403
+ export { Config, DEFAULT_ALIASES, DEFAULT_API_KEY_ENV, DEFAULT_CACHE_READ_DISCOUNT, DEFAULT_FX_RATE, DEFAULT_PRICING_REFRESH_HOURS, DEFAULT_REFRESH_MS, DEFAULT_SNAPSHOT_HISTORY_LIMIT, FALLBACK_CURRENT, FALLBACK_CURRENT_USD, FALLBACK_PEAK, FALLBACK_PEAK_USD, FX_API_URL, OPENROUTER_MODELS_URL, PEAK_PRICING_START_MS, PRICEBOOK_DOMAIN, PRICEBOOK_DOMAIN_USD, PRICING_URL, PRICING_URL_EN, PUBLIC_BASE_URL, PricebookHandle, SETTINGS_NAMESPACE, apply, bandForTime, bucketOf, collectSubagentCosts, computePricebook, effectiveBucket, effectivePricing, effectiveRate, fetchBalance, fetchFxRate, fetchOpenRouter, fetchPricing, foldSessionCost, initialPricebookState, inject, isLoopbackHostname, isPeakHour, isTrustedRequest, modelKeys, name, officialInputOf, parseCurrentTable, parseCurrentTableEn, parsePeakTable, parsePeakTableEn, pickBalanceInfo, priceAt, pricesEqual, pricingKeyOfModel, resolveApiKey, sessionCostProjection, snapshotForTime, stepCost, stripHtml, sumTotals, usageBuckets, viewSessionCost };