@ychris12138/dsh-usage-stats 0.2.9 → 0.3.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.
package/lib/network.js ADDED
@@ -0,0 +1,65 @@
1
+ /** Shared pure network classification used by identity and SSRF policy. */
2
+
3
+ import { isIP } from "node:net";
4
+
5
+ function ipv4Private(octets) {
6
+ const [a, b, c] = octets;
7
+ return a === 0
8
+ || a === 10
9
+ || a === 127
10
+ || a === 169 && b === 254
11
+ || a === 172 && b >= 16 && b <= 31
12
+ || a === 192 && b === 168
13
+ || a === 192 && b === 0 && (c === 0 || c === 2)
14
+ || a === 192 && b === 88 && c === 99
15
+ || a === 100 && b >= 64 && b <= 127
16
+ || a === 198 && (b === 18 || b === 19)
17
+ || a === 198 && b === 51 && c === 100
18
+ || a === 203 && b === 0 && c === 113
19
+ || a >= 224;
20
+ }
21
+
22
+ function ipv6Bytes(address) {
23
+ let value = address.toLowerCase().split("%")[0];
24
+ const lastColon = value.lastIndexOf(":");
25
+ if (value.slice(lastColon + 1).includes(".")) {
26
+ const octets = value.slice(lastColon + 1).split(".").map(Number);
27
+ if (octets.length !== 4 || octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return null;
28
+ value = `${value.slice(0, lastColon)}:${((octets[0] << 8) | octets[1]).toString(16)}:${((octets[2] << 8) | octets[3]).toString(16)}`;
29
+ }
30
+ const halves = value.split("::");
31
+ if (halves.length > 2) return null;
32
+ const left = halves[0] === "" ? [] : halves[0].split(":");
33
+ const right = halves.length === 1 || halves[1] === "" ? [] : halves[1].split(":");
34
+ const missing = 8 - left.length - right.length;
35
+ if (missing < 0 || halves.length === 1 && missing !== 0) return null;
36
+ const words = [...left, ...Array(missing).fill("0"), ...right].map((part) => Number.parseInt(part || "0", 16));
37
+ if (words.length !== 8 || words.some((part) => !Number.isInteger(part) || part < 0 || part > 0xffff)) return null;
38
+ const bytes = [];
39
+ for (const word of words) bytes.push(word >> 8, word & 0xff);
40
+ return bytes;
41
+ }
42
+
43
+ /** True for loopback, private, link-local, documentation, multicast, and other non-public IP space. */
44
+ export function isPrivateAddress(address) {
45
+ const value = String(address ?? "").trim().replace(/^\[|\]$/g, "");
46
+ if (isIP(value) === 4) return ipv4Private(value.split(".").map(Number));
47
+ if (isIP(value) !== 6) return false;
48
+ const bytes = ipv6Bytes(value);
49
+ if (bytes === null) return true;
50
+ if (bytes.slice(0, 10).every((byte) => byte === 0) && bytes[10] === 0xff && bytes[11] === 0xff) return ipv4Private(bytes.slice(12));
51
+ const globalUnicast = (bytes[0] & 0xe0) === 0x20;
52
+ const word0 = (bytes[0] << 8) | bytes[1];
53
+ const word1 = (bytes[2] << 8) | bytes[3];
54
+ const ietfSpecial = word0 === 0x2001 && word1 <= 0x01ff;
55
+ const sixToFour = word0 === 0x2002;
56
+ const documentation = word0 === 0x2001 && word1 === 0x0db8
57
+ || word0 === 0x3fff && (word1 & 0xf000) === 0;
58
+ return !globalUnicast || ietfSpecial || sixToFour || documentation;
59
+ }
60
+
61
+ /** Whether a URL hostname is local/private without performing DNS resolution. */
62
+ export function isPrivateHostname(hostname) {
63
+ const host = String(hostname ?? "").toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
64
+ return host === "localhost" || host.endsWith(".localhost") || isPrivateAddress(host);
65
+ }
package/lib/pricing.js ADDED
@@ -0,0 +1,391 @@
1
+ /**
2
+ * Pure, provider-aware token pricing primitives.
3
+ *
4
+ * Pricing is derived from route identity, exact model id, event timestamp,
5
+ * token buckets, and an immutable historical rule catalog. This module owns no
6
+ * network access, timer, cache, UI, endpoint, or token aggregation state.
7
+ *
8
+ * @module dsh-usage-stats/pricing
9
+ */
10
+
11
+ const TOKENS_PER_MILLION = 1_000_000;
12
+ const DEEPSEEK_PRICE_SOURCE = Object.freeze({
13
+ kind: "official",
14
+ provider: "deepseek",
15
+ url: "https://api-docs.deepseek.com/quick_start/pricing/"
16
+ });
17
+ const CATALOG_UPDATED_AT = "2026-08-23T00:00:00.000Z";
18
+ const TIME_BAND_V1_FROM = "2026-08-16T16:00:00.000Z";
19
+ const WEEKDAY_SCHEDULE_FROM = "2026-08-22T16:00:00.000Z";
20
+ const SHANGHAI_TIME_BANDS = Object.freeze([
21
+ Object.freeze(["09:00", "12:00"]),
22
+ Object.freeze(["14:00", "18:00"])
23
+ ]);
24
+ const BUCKET_COMPONENTS = Object.freeze([
25
+ Object.freeze(["inputTokens", "input"]),
26
+ Object.freeze(["cacheReadTokens", "cacheRead"]),
27
+ Object.freeze(["cacheWriteTokens", "cacheWrite"]),
28
+ Object.freeze(["outputTokens", "output"])
29
+ ]);
30
+ const WEEKDAY_NUMBER = Object.freeze({ Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 });
31
+
32
+ function exactModel(model) {
33
+ return Object.freeze({ type: "exact", model });
34
+ }
35
+
36
+ function unitPrices({ input, cacheRead, cacheWrite = null, output }) {
37
+ return Object.freeze({ input, cacheRead, cacheWrite, output });
38
+ }
39
+
40
+ function flatRule({ id, model, effectiveFrom = null, effectiveTo = null, prices }) {
41
+ return Object.freeze({
42
+ id,
43
+ providerFamily: "deepseek",
44
+ pricingFamily: "deepseek",
45
+ modelMatcher: exactModel(model),
46
+ effectiveFrom,
47
+ effectiveTo,
48
+ pricing: Object.freeze({ flat: unitPrices(prices) }),
49
+ currency: "USD",
50
+ schedule: null,
51
+ source: DEEPSEEK_PRICE_SOURCE,
52
+ updatedAt: CATALOG_UPDATED_AT
53
+ });
54
+ }
55
+
56
+ function timeBandRule({ id, model, effectiveFrom, effectiveTo = null, peakDays, offPeak, peak }) {
57
+ return Object.freeze({
58
+ id,
59
+ providerFamily: "deepseek",
60
+ pricingFamily: "deepseek",
61
+ modelMatcher: exactModel(model),
62
+ effectiveFrom,
63
+ effectiveTo,
64
+ pricing: Object.freeze({
65
+ offPeak: unitPrices(offPeak),
66
+ peak: unitPrices(peak)
67
+ }),
68
+ currency: "USD",
69
+ schedule: Object.freeze({
70
+ timezone: "Asia/Shanghai",
71
+ peakDays: Object.freeze([...peakDays]),
72
+ peakWindows: SHANGHAI_TIME_BANDS,
73
+ otherwise: "offPeak"
74
+ }),
75
+ source: DEEPSEEK_PRICE_SOURCE,
76
+ updatedAt: CATALOG_UPDATED_AT
77
+ });
78
+ }
79
+
80
+ /** Immutable first-party DeepSeek USD rule catalog (prices per 1M tokens). */
81
+ export const DEEPSEEK_PRICING_RULES = Object.freeze([
82
+ flatRule({
83
+ id: "deepseek-v4-flash-usd-flat-before-2026-08-16",
84
+ model: "deepseek-v4-flash",
85
+ effectiveTo: TIME_BAND_V1_FROM,
86
+ prices: { cacheRead: 0.0028, input: 0.14, output: 0.28 }
87
+ }),
88
+ flatRule({
89
+ id: "deepseek-v4-pro-usd-flat-before-2026-08-16",
90
+ model: "deepseek-v4-pro",
91
+ effectiveTo: TIME_BAND_V1_FROM,
92
+ prices: { cacheRead: 0.003625, input: 0.435, output: 0.87 }
93
+ }),
94
+ timeBandRule({
95
+ id: "deepseek-v4-flash-usd-time-band-v1",
96
+ model: "deepseek-v4-flash",
97
+ effectiveFrom: TIME_BAND_V1_FROM,
98
+ effectiveTo: WEEKDAY_SCHEDULE_FROM,
99
+ peakDays: [0, 1, 2, 3, 4, 5, 6],
100
+ offPeak: { cacheRead: 0.007, input: 0.22, output: 0.66 },
101
+ peak: { cacheRead: 0.014, input: 0.44, output: 1.32 }
102
+ }),
103
+ timeBandRule({
104
+ id: "deepseek-v4-pro-usd-time-band-v1",
105
+ model: "deepseek-v4-pro",
106
+ effectiveFrom: TIME_BAND_V1_FROM,
107
+ effectiveTo: WEEKDAY_SCHEDULE_FROM,
108
+ peakDays: [0, 1, 2, 3, 4, 5, 6],
109
+ offPeak: { cacheRead: 0.022, input: 0.66, output: 1.98 },
110
+ peak: { cacheRead: 0.044, input: 1.32, output: 3.96 }
111
+ }),
112
+ timeBandRule({
113
+ id: "deepseek-v4-flash-usd-weekday-schedule",
114
+ model: "deepseek-v4-flash",
115
+ effectiveFrom: WEEKDAY_SCHEDULE_FROM,
116
+ peakDays: [1, 2, 3, 4, 5],
117
+ offPeak: { cacheRead: 0.007, input: 0.22, output: 0.66 },
118
+ peak: { cacheRead: 0.014, input: 0.44, output: 1.32 }
119
+ }),
120
+ timeBandRule({
121
+ id: "deepseek-v4-pro-usd-weekday-schedule",
122
+ model: "deepseek-v4-pro",
123
+ effectiveFrom: WEEKDAY_SCHEDULE_FROM,
124
+ peakDays: [1, 2, 3, 4, 5],
125
+ offPeak: { cacheRead: 0.022, input: 0.66, output: 1.98 },
126
+ peak: { cacheRead: 0.044, input: 1.32, output: 3.96 }
127
+ })
128
+ ]);
129
+
130
+ /** Alias reserved for future additive provider catalogs. */
131
+ export const PRICING_RULES = DEEPSEEK_PRICING_RULES;
132
+
133
+ function nonEmptyString(value) {
134
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
135
+ }
136
+
137
+ function timestampOf(value) {
138
+ if (typeof value === "number" && Number.isFinite(value)) return value;
139
+ if (value instanceof Date) {
140
+ const timestamp = value.getTime();
141
+ return Number.isFinite(timestamp) ? timestamp : null;
142
+ }
143
+ if (typeof value === "string" && value.trim() !== "") {
144
+ const normalized = value.trim();
145
+ // Date.parse() interprets offset-less date-times in the machine's local
146
+ // timezone. Pricing timestamps must name an instant so recomputation is
147
+ // deterministic across hosts.
148
+ if (!/(?:Z|[+-]\d{2}:\d{2})$/i.test(normalized)) return null;
149
+ const timestamp = Date.parse(normalized);
150
+ return Number.isFinite(timestamp) ? timestamp : null;
151
+ }
152
+ return null;
153
+ }
154
+
155
+ function boundaryOf(value, fallback) {
156
+ if (value === null || value === void 0) return fallback;
157
+ const timestamp = timestampOf(value);
158
+ if (timestamp === null) throw new Error(`invalid pricing effective boundary: ${String(value)}`);
159
+ return timestamp;
160
+ }
161
+
162
+ function minuteOf(clock) {
163
+ const match = /^(\d{2}):(\d{2})$/.exec(clock);
164
+ if (match === null) return null;
165
+ const hour = Number(match[1]);
166
+ const minute = Number(match[2]);
167
+ if (!Number.isInteger(hour) || hour < 0 || hour > 23 || !Number.isInteger(minute) || minute < 0 || minute > 59) return null;
168
+ return hour * 60 + minute;
169
+ }
170
+
171
+ function validateUnitPrices(prices, label) {
172
+ if (prices === null || typeof prices !== "object" || Array.isArray(prices)) throw new Error(`${label} must be an object`);
173
+ for (const component of ["input", "cacheRead", "cacheWrite", "output"]) {
174
+ if (!Object.hasOwn(prices, component)) throw new Error(`${label}.${component} is required`);
175
+ const value = prices[component];
176
+ if (value !== null && (typeof value !== "number" || !Number.isFinite(value) || value < 0)) {
177
+ throw new Error(`${label}.${component} must be a non-negative number or null`);
178
+ }
179
+ }
180
+ }
181
+
182
+ function validateSchedule(schedule, label) {
183
+ if (schedule === null || typeof schedule !== "object" || Array.isArray(schedule)) throw new Error(`${label} must be an object`);
184
+ if (nonEmptyString(schedule.timezone) === null) throw new Error(`${label}.timezone is required`);
185
+ try {
186
+ new Intl.DateTimeFormat("en-US", { timeZone: schedule.timezone }).format(0);
187
+ } catch {
188
+ throw new Error(`${label}.timezone is invalid`);
189
+ }
190
+ if (!Array.isArray(schedule.peakDays) || schedule.peakDays.length === 0) throw new Error(`${label}.peakDays must be non-empty`);
191
+ const days = new Set();
192
+ for (const day of schedule.peakDays) {
193
+ if (!Number.isInteger(day) || day < 0 || day > 6 || days.has(day)) throw new Error(`${label}.peakDays must contain unique integers from 0 to 6`);
194
+ days.add(day);
195
+ }
196
+ if (!Array.isArray(schedule.peakWindows) || schedule.peakWindows.length === 0) throw new Error(`${label}.peakWindows must be non-empty`);
197
+ for (const [index, window] of schedule.peakWindows.entries()) {
198
+ if (!Array.isArray(window) || window.length !== 2) throw new Error(`${label}.peakWindows[${index}] must be [start, end]`);
199
+ const start = minuteOf(window[0]);
200
+ const end = minuteOf(window[1]);
201
+ if (start === null || end === null || start >= end) throw new Error(`${label}.peakWindows[${index}] must be a non-empty same-day half-open interval`);
202
+ }
203
+ if (schedule.otherwise !== "offPeak") throw new Error(`${label}.otherwise must be offPeak`);
204
+ }
205
+
206
+ function pricingKey(rule) {
207
+ return JSON.stringify([rule.providerFamily, rule.pricingFamily, rule.modelMatcher.model, rule.currency]);
208
+ }
209
+
210
+ /**
211
+ * Validate rule shape and reject overlapping effective windows for one exact
212
+ * provider/pricing/model/currency key. Invalid catalogs are programmer errors.
213
+ */
214
+ export function validatePricingRules(rules) {
215
+ if (!Array.isArray(rules) || rules.length === 0) throw new Error("pricing rules must be a non-empty array");
216
+ const ids = new Set();
217
+ const intervals = new Map();
218
+ for (const [index, rule] of rules.entries()) {
219
+ const label = `pricingRules[${index}]`;
220
+ if (rule === null || typeof rule !== "object" || Array.isArray(rule)) throw new Error(`${label} must be an object`);
221
+ if (nonEmptyString(rule.id) === null || ids.has(rule.id)) throw new Error(`${label}.id must be unique and non-empty`);
222
+ ids.add(rule.id);
223
+ if (nonEmptyString(rule.providerFamily) === null || nonEmptyString(rule.pricingFamily) === null) throw new Error(`${label} provider/pricing family is required`);
224
+ if (rule.modelMatcher?.type !== "exact" || nonEmptyString(rule.modelMatcher?.model) === null) throw new Error(`${label}.modelMatcher must be an exact model identity`);
225
+ if (typeof rule.currency !== "string" || !/^[A-Z]{3}$/.test(rule.currency)) throw new Error(`${label}.currency must be an uppercase ISO-style code`);
226
+ const from = boundaryOf(rule.effectiveFrom, -Infinity);
227
+ const to = boundaryOf(rule.effectiveTo, Infinity);
228
+ if (from >= to) throw new Error(`${label} effectiveFrom must be earlier than effectiveTo`);
229
+ if (timestampOf(rule.updatedAt) === null) throw new Error(`${label}.updatedAt must be a timestamp`);
230
+ if (rule.source?.kind !== "official" || nonEmptyString(rule.source?.provider) === null) throw new Error(`${label}.source must identify an official provider`);
231
+ try {
232
+ const sourceURL = new URL(rule.source.url);
233
+ if (sourceURL.protocol !== "https:") throw new Error("not HTTPS");
234
+ } catch {
235
+ throw new Error(`${label}.source.url must be HTTPS`);
236
+ }
237
+ const pricingKeys = Object.keys(rule.pricing ?? {}).sort();
238
+ if (rule.schedule === null) {
239
+ if (pricingKeys.join(",") !== "flat") throw new Error(`${label}.pricing must contain only flat without a schedule`);
240
+ validateUnitPrices(rule.pricing.flat, `${label}.pricing.flat`);
241
+ } else {
242
+ validateSchedule(rule.schedule, `${label}.schedule`);
243
+ if (pricingKeys.join(",") !== "offPeak,peak") throw new Error(`${label}.pricing must contain peak and offPeak with a schedule`);
244
+ validateUnitPrices(rule.pricing.peak, `${label}.pricing.peak`);
245
+ validateUnitPrices(rule.pricing.offPeak, `${label}.pricing.offPeak`);
246
+ }
247
+ const key = pricingKey(rule);
248
+ const previous = intervals.get(key) ?? [];
249
+ for (const interval of previous) {
250
+ if (Math.max(from, interval.from) < Math.min(to, interval.to)) {
251
+ throw new Error(`overlapping pricing rules for ${key}: ${interval.id} and ${rule.id}`);
252
+ }
253
+ }
254
+ previous.push({ id: rule.id, from, to });
255
+ intervals.set(key, previous);
256
+ }
257
+ return rules;
258
+ }
259
+
260
+ function ensureValidatedRules(rules) {
261
+ // The immutable built-in catalog is validated once at module load. Custom
262
+ // catalogs stay fail-fast without imposing repeated schema work on PR5's
263
+ // future per-sample estimation path.
264
+ if (rules !== PRICING_RULES) validatePricingRules(rules);
265
+ }
266
+
267
+ function isEffective(rule, timestamp) {
268
+ return timestamp >= boundaryOf(rule.effectiveFrom, -Infinity)
269
+ && timestamp < boundaryOf(rule.effectiveTo, Infinity);
270
+ }
271
+
272
+ function hostnameOf(baseURL) {
273
+ if (nonEmptyString(baseURL) === null) return null;
274
+ try {
275
+ return new URL(baseURL).hostname.toLowerCase().replace(/\.$/, "");
276
+ } catch {
277
+ return null;
278
+ }
279
+ }
280
+
281
+ function isOfficialDeepSeekIdentity(identity) {
282
+ if (identity?.providerFamily !== "deepseek" || identity?.pricingFamily !== "deepseek") return false;
283
+ const configuredBaseURL = nonEmptyString(identity.baseURL);
284
+ // An exact official billing host remains authoritative even when an explicit
285
+ // account monitor caused the shared resolver's confidence to be `explicit`.
286
+ if (configuredBaseURL !== null) return hostnameOf(configuredBaseURL) === "api.deepseek.com";
287
+ // Without a configured URL, a canonical route id is the remaining safe
288
+ // signal. An explicit custom or malformed URL must never be overridden by it.
289
+ if (identity.confidence === "canonical-id" && (identity.routeId === "deepseek" || identity.routeId === "deepseek-official")) return true;
290
+ return false;
291
+ }
292
+
293
+ function isEligibleIdentity(identity, rule) {
294
+ if (identity?.providerFamily !== rule.providerFamily || identity?.pricingFamily !== rule.pricingFamily) return false;
295
+ if (rule.providerFamily === "deepseek" && rule.pricingFamily === "deepseek") return isOfficialDeepSeekIdentity(identity);
296
+ return false;
297
+ }
298
+
299
+ /** Match one exact historical rule, or null when the route/model/currency is unpriced. */
300
+ export function matchPricingRule({ identity, model, timestamp, currency }, rules = PRICING_RULES) {
301
+ ensureValidatedRules(rules);
302
+ const at = timestampOf(timestamp);
303
+ if (at === null || nonEmptyString(model) === null || typeof currency !== "string") return null;
304
+ const matches = rules.filter((rule) => (
305
+ isEligibleIdentity(identity, rule)
306
+ && rule.modelMatcher.model === model
307
+ && rule.currency === currency
308
+ && isEffective(rule, at)
309
+ ));
310
+ if (matches.length > 1) throw new Error(`ambiguous pricing rules at ${new Date(at).toISOString()}: ${matches.map((rule) => rule.id).join(", ")}`);
311
+ return matches[0] ?? null;
312
+ }
313
+
314
+ function zonedClock(timestamp, timezone) {
315
+ const parts = Object.fromEntries(new Intl.DateTimeFormat("en-US", {
316
+ timeZone: timezone,
317
+ weekday: "short",
318
+ hour: "2-digit",
319
+ minute: "2-digit",
320
+ hourCycle: "h23"
321
+ }).formatToParts(new Date(timestamp)).filter((part) => part.type !== "literal").map((part) => [part.type, part.value]));
322
+ return {
323
+ weekday: WEEKDAY_NUMBER[parts.weekday],
324
+ minute: Number(parts.hour) * 60 + Number(parts.minute)
325
+ };
326
+ }
327
+
328
+ function tariffForSchedule(schedule, timestamp) {
329
+ const clock = zonedClock(timestamp, schedule.timezone);
330
+ if (!schedule.peakDays.includes(clock.weekday)) return schedule.otherwise;
331
+ return schedule.peakWindows.some(([start, end]) => clock.minute >= minuteOf(start) && clock.minute < minuteOf(end))
332
+ ? "peak"
333
+ : schedule.otherwise;
334
+ }
335
+
336
+ function resolveUnitPricingUnchecked(rule, timestamp) {
337
+ if (!isEffective(rule, timestamp)) return null;
338
+ const tariff = rule.schedule === null ? "flat" : tariffForSchedule(rule.schedule, timestamp);
339
+ return { tariff, unitPricing: { ...rule.pricing[tariff] } };
340
+ }
341
+
342
+ /** Resolve the applicable tariff and per-million unit prices for one rule. */
343
+ export function resolveUnitPricing(rule, timestamp) {
344
+ validatePricingRules([rule]);
345
+ const at = timestampOf(timestamp);
346
+ return at === null ? null : resolveUnitPricingUnchecked(rule, at);
347
+ }
348
+
349
+ function normalizedBuckets(buckets) {
350
+ if (buckets === null || typeof buckets !== "object" || Array.isArray(buckets)) return null;
351
+ const normalized = {};
352
+ for (const [bucket] of BUCKET_COMPONENTS) {
353
+ const value = buckets[bucket];
354
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return null;
355
+ normalized[bucket] = value;
356
+ }
357
+ return normalized;
358
+ }
359
+
360
+ /**
361
+ * Estimate cost without rounding. Unknown route/model/currency or a positive
362
+ * bucket with no reliable unit price makes the whole estimate unknown (null).
363
+ */
364
+ export function estimateTokenCost(input, rules = PRICING_RULES) {
365
+ if (input === null || typeof input !== "object") return null;
366
+ const at = timestampOf(input.timestamp);
367
+ const buckets = normalizedBuckets(input.buckets);
368
+ if (at === null || buckets === null) return null;
369
+ const rule = matchPricingRule(input, rules);
370
+ if (rule === null) return null;
371
+ const resolved = resolveUnitPricingUnchecked(rule, at);
372
+ if (resolved === null) return null;
373
+ const components = {};
374
+ for (const [bucket, component] of BUCKET_COMPONENTS) {
375
+ const tokens = buckets[bucket];
376
+ const price = resolved.unitPricing[component];
377
+ if (tokens > 0 && price === null) return null;
378
+ components[component] = tokens === 0 ? 0 : tokens / TOKENS_PER_MILLION * price;
379
+ }
380
+ return {
381
+ amount: components.input + components.cacheRead + components.cacheWrite + components.output,
382
+ currency: rule.currency,
383
+ components,
384
+ tariff: resolved.tariff,
385
+ ruleId: rule.id,
386
+ source: { ...rule.source },
387
+ updatedAt: rule.updatedAt
388
+ };
389
+ }
390
+
391
+ validatePricingRules(PRICING_RULES);
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Provider identity policy shared by account monitoring and session context.
3
+ *
4
+ * Identity is route-aware: a configured route id remains the account boundary,
5
+ * even when two routes use the same upstream model. Classification follows one
6
+ * strict precedence order: explicit monitor adapter, canonical route id,
7
+ * canonical base-URL hostname, then unknown. Display labels are presentation
8
+ * only and never participate in inference.
9
+ *
10
+ * @module dsh-usage-stats/provider-identity
11
+ */
12
+
13
+ import { isPrivateHostname } from "./network.js";
14
+
15
+ /** Bump whenever route classification can change pricing eligibility. */
16
+ export const PROVIDER_IDENTITY_POLICY_VERSION = 1;
17
+
18
+ const ADAPTER_IDENTITIES = Object.freeze({
19
+ "deepseek-balance": { providerFamily: "deepseek", pricingFamily: "deepseek" },
20
+ "openrouter-balance": { providerFamily: "openrouter", pricingFamily: "openrouter" },
21
+ "moonshot-balance": { providerFamily: "moonshot", pricingFamily: "moonshot" },
22
+ "zai-balance": { providerFamily: "zai", pricingFamily: "zai" },
23
+ general: { providerFamily: "unknown", pricingFamily: "unknown" },
24
+ "new-api": { providerFamily: "new-api", pricingFamily: "unknown" },
25
+ sub2api: { providerFamily: "sub2api", pricingFamily: "unknown" },
26
+ "sub2api-auth": { providerFamily: "sub2api", pricingFamily: "unknown" },
27
+ "opencode-go": { providerFamily: "opencode-go", pricingFamily: "opencode-go" },
28
+ "zai-token-plan": { providerFamily: "zai", pricingFamily: "zai" },
29
+ "kimi-token-plan": { providerFamily: "kimi", pricingFamily: "kimi" },
30
+ "minimax-token-plan": { providerFamily: "minimax", pricingFamily: "minimax" },
31
+ ollama: { providerFamily: "ollama", pricingFamily: "ollama" },
32
+ declarative: { providerFamily: "unknown", pricingFamily: "unknown" }
33
+ });
34
+
35
+ const CANONICAL_ROUTES = Object.freeze({
36
+ "deepseek-official": { providerFamily: "deepseek", accountAdapter: "deepseek-balance", balanceScheme: "deepseek" },
37
+ deepseek: { providerFamily: "deepseek", accountAdapter: "deepseek-balance", balanceScheme: "deepseek" },
38
+ openrouter: { providerFamily: "openrouter", accountAdapter: "openrouter-balance", balanceScheme: "openrouter" },
39
+ moonshotai: { providerFamily: "moonshot", accountAdapter: "moonshot-balance", balanceScheme: "moonshot" },
40
+ "moonshotai-cn": { providerFamily: "moonshot", accountAdapter: "moonshot-balance", balanceScheme: "moonshot" },
41
+ kimi: { providerFamily: "moonshot", accountAdapter: "moonshot-balance", balanceScheme: "moonshot" },
42
+ "kimi-coding": { providerFamily: "kimi", accountAdapter: "kimi-token-plan", balanceScheme: "moonshot" },
43
+ "kimi-for-coding": { providerFamily: "kimi", accountAdapter: "kimi-token-plan", balanceScheme: null },
44
+ zai: { providerFamily: "zai", accountAdapter: "zai-token-plan", balanceScheme: "zai" },
45
+ "zai-coding-cn": { providerFamily: "zai", accountAdapter: "zai-token-plan", balanceScheme: "zai" },
46
+ "opencode-go": { providerFamily: "opencode-go", accountAdapter: "opencode-go", balanceScheme: null },
47
+ minimax: { providerFamily: "minimax", accountAdapter: "minimax-token-plan", balanceScheme: null },
48
+ minimaxi: { providerFamily: "minimax", accountAdapter: "minimax-token-plan", balanceScheme: null },
49
+ "minimax-cn": { providerFamily: "minimax", accountAdapter: "minimax-token-plan", balanceScheme: null },
50
+ "minimax-coding": { providerFamily: "minimax", accountAdapter: "minimax-token-plan", balanceScheme: null },
51
+ passion: { providerFamily: "sub2api", accountAdapter: "sub2api", pricingFamily: "unknown", balanceScheme: null }
52
+ });
53
+
54
+ function nonEmptyString(value) {
55
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
56
+ }
57
+
58
+ function hostnameOf(baseURL) {
59
+ if (nonEmptyString(baseURL) === null) return null;
60
+ try {
61
+ return new URL(baseURL).hostname.toLowerCase().replace(/\.$/, "");
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ function hostRule(hostname) {
68
+ if (hostname === "api.deepseek.com") return { providerFamily: "deepseek", accountAdapter: "deepseek-balance" };
69
+ if (hostname === "passionapi.com" || hostname.endsWith(".passionapi.com")) return { providerFamily: "sub2api", accountAdapter: "sub2api", pricingFamily: "unknown" };
70
+ if (hostname === "ollama.com" || hostname.endsWith(".ollama.com")) return { providerFamily: "ollama", accountAdapter: "ollama" };
71
+ return null;
72
+ }
73
+
74
+ function buildProviderIdentity(provider, rule, confidence) {
75
+ const routeId = nonEmptyString(provider?.id) ?? "unknown";
76
+ const displayName = nonEmptyString(provider?.displayName) ?? routeId;
77
+ const baseURL = nonEmptyString(provider?.baseURL);
78
+ const providerFamily = rule?.providerFamily ?? "unknown";
79
+ return {
80
+ routeId,
81
+ displayName,
82
+ providerFamily,
83
+ accountAdapter: rule?.accountAdapter ?? null,
84
+ pricingFamily: rule?.pricingFamily ?? providerFamily,
85
+ baseURL,
86
+ confidence
87
+ };
88
+ }
89
+
90
+ /** Return the legacy built-in balance scheme without duplicating route policy. */
91
+ export function balanceSchemeForProviderId(providerId) {
92
+ return CANONICAL_ROUTES[providerId]?.balanceScheme ?? null;
93
+ }
94
+
95
+ /**
96
+ * Resolve one configured provider route to stable semantic boundaries.
97
+ * Explicit monitor configuration always wins. Unknown or malformed inputs
98
+ * remain unknown instead of falling back to the human-readable display name.
99
+ */
100
+ export function resolveProviderIdentity(provider, config = { monitors: {} }) {
101
+ const routeId = nonEmptyString(provider?.id) ?? "unknown";
102
+ const monitor = config?.monitors?.[routeId];
103
+ const explicitAdapter = nonEmptyString(monitor?.adapter);
104
+ if (explicitAdapter !== null) {
105
+ const identity = ADAPTER_IDENTITIES[explicitAdapter] ?? { providerFamily: "unknown", pricingFamily: "unknown" };
106
+ return buildProviderIdentity(provider, { ...identity, accountAdapter: explicitAdapter }, "explicit");
107
+ }
108
+
109
+ const canonical = CANONICAL_ROUTES[routeId];
110
+ if (canonical !== void 0) return buildProviderIdentity(provider, canonical, "canonical-id");
111
+
112
+ const hostname = hostnameOf(provider?.baseURL);
113
+ if (hostname !== null) {
114
+ // The canonical Ollama id is meaningful only for a non-private cloud
115
+ // endpoint. This deliberate safety gate prevents local Ollama from being
116
+ // mistaken for a subscription account while retaining canonical-id
117
+ // precedence for actual cloud routes.
118
+ if (routeId === "ollama" && !isPrivateHostname(hostname)) {
119
+ return buildProviderIdentity(provider, { providerFamily: "ollama", accountAdapter: "ollama" }, "canonical-id");
120
+ }
121
+ const canonicalHost = hostRule(hostname);
122
+ if (canonicalHost !== null) return buildProviderIdentity(provider, canonicalHost, "canonical-host");
123
+ }
124
+
125
+ return buildProviderIdentity(provider, null, "unknown");
126
+ }
@@ -38,6 +38,7 @@ const MINIMAX_LEGACY_HOSTS = {
38
38
  };
39
39
  const MINIMAX_USAGE_PATH = "/v1/api/openplatform/coding_plan/remains";
40
40
  const MINIMAX_TOKEN_PLAN_PATH = "/v1/token_plan/remains";
41
+ const OLLAMA_USAGE_URL = "https://ollama.com/api/usage";
41
42
  const DEFAULT_TIMEOUT_MS = 15000;
42
43
 
43
44
  const REFS = {
@@ -48,7 +49,8 @@ const REFS = {
48
49
  zaiRegion: "ZAI_API_REGION",
49
50
  kimiApiKey: "KIMI_API_KEY",
50
51
  minimaxApiKey: "MINIMAX_API_KEY",
51
- minimaxRegion: "MINIMAX_API_REGION"
52
+ minimaxRegion: "MINIMAX_API_REGION",
53
+ ollamaApiKey: "OLLAMA_API_KEY"
52
54
  };
53
55
 
54
56
  function numberOrNull(value) {
@@ -576,6 +578,61 @@ async function collectMiniMax(credentials, deps) {
576
578
  }
577
579
  }
578
580
 
581
+ /**
582
+ * Ollama Cloud usage monitor.
583
+ *
584
+ * Ollama's cloud /api/usage endpoint reports consumed usage ratios for two
585
+ * limit windows (a 5-hour session window and a weekly window), plus an
586
+ * activity cost. There is no monetary balance, so this adapter presents the
587
+ * two windows as subscription-style progress bars, mirroring OpenCode Go.
588
+ */
589
+ function ollamaWindowFromObject(limit, kind) {
590
+ if (limit === null || typeof limit !== "object") return null;
591
+ // limits.session.usage / limits.weekly.usage are 0..1 consumed ratios
592
+ // (observed 0.0x..0.3x on live data). clampPercent maps any numeric ratio
593
+ // onto 0..100, so an over-quota window renders as a fully-used bar rather
594
+ // than silently disappearing, mirroring the OpenCode Go adapter.
595
+ const ratio = numberOrNull(limit.usage);
596
+ if (ratio === null) return null;
597
+ const usedPercent = round1(clampPercent(ratio * 100));
598
+ return {
599
+ kind,
600
+ usedPercent,
601
+ remainingPercent: round1(100 - usedPercent)
602
+ };
603
+ }
604
+
605
+ function parseOllama(body) {
606
+ const limits = body?.limits;
607
+ if (limits === null || typeof limits !== "object") return [];
608
+ return [
609
+ ollamaWindowFromObject(limits.session, "session"),
610
+ ollamaWindowFromObject(limits.weekly, "weekly")
611
+ ].filter(Boolean);
612
+ }
613
+
614
+ async function collectOllama(credentials, deps) {
615
+ const apiKeyRef = deps.apiKeyRef ?? REFS.ollamaApiKey;
616
+ const apiKey = await resolveCredential(credentials, apiKeyRef);
617
+ if (apiKey === "") return { id: "ollama", displayName: "Ollama", mode: "subscription", status: "not-configured", plan: "Ollama", missingCredentials: [apiKeyRef], windows: [] };
618
+ try {
619
+ const body = await request(nonEmptyUrl(deps.baseURL, "/api/usage") ?? OLLAMA_USAGE_URL, {
620
+ headers: { authorization: `Bearer ${apiKey}`, accept: "application/json" }
621
+ }, deps, "json");
622
+ const windows = parseOllama(body);
623
+ return {
624
+ id: "ollama",
625
+ displayName: "Ollama",
626
+ mode: "subscription",
627
+ status: windows.length > 0 ? "ok" : "invalid-response",
628
+ plan: "Ollama",
629
+ windows
630
+ };
631
+ } catch (error) {
632
+ return { id: "ollama", displayName: "Ollama", mode: "subscription", status: normalizedStatus(error), plan: "Ollama", windows: [] };
633
+ }
634
+ }
635
+
579
636
  /** Query one subscription/token-plan adapter. */
580
637
  export async function collectSubscription(providerId, credentials, options = {}, deps = {}) {
581
638
  const shared = {
@@ -596,6 +653,7 @@ export async function collectSubscription(providerId, credentials, options = {},
596
653
  });
597
654
  if (providerId === "kimi") return collectKimi(credentials, shared);
598
655
  if (providerId === "minimax") return collectMiniMax(credentials, shared);
656
+ if (providerId === "ollama") return collectOllama(credentials, shared);
599
657
  return { id: providerId, displayName: providerId, mode: "subscription", status: "unavailable", windows: [] };
600
658
  }
601
659