dsh-all-usage 1.1.13 → 1.1.15
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/CHANGELOG.md +372 -347
- package/README.md +399 -386
- package/assets/screenshot-2.png +0 -0
- package/lib/client.js +1 -1
- package/lib/http.js +18 -0
- package/lib/plugin.js +1 -0
- package/lib/pricing-runtime.js +163 -18
- package/lib/pricing.js +181 -6
- package/package.json +82 -82
package/lib/http.js
CHANGED
|
@@ -138,6 +138,7 @@ export function registerRoutes(host) {
|
|
|
138
138
|
updatePricingState,
|
|
139
139
|
persistPricing,
|
|
140
140
|
syncPricing,
|
|
141
|
+
loadHolidayCalendar,
|
|
141
142
|
} = host.pricing
|
|
142
143
|
const { runBaseline } = host.sessionSync
|
|
143
144
|
const { fetchBalance } = host.balance
|
|
@@ -239,6 +240,23 @@ export function registerRoutes(host) {
|
|
|
239
240
|
sendJson(res, result.ok ? 200 : 502, result)
|
|
240
241
|
},
|
|
241
242
|
}))
|
|
243
|
+
ctx.effect(() => webServer.register({
|
|
244
|
+
kind: 'exact',
|
|
245
|
+
path: '/api/all-usage/pricing/holidays',
|
|
246
|
+
handler: async (req, res) => {
|
|
247
|
+
if (req.method !== 'POST') { res.statusCode = 405; res.end(); return }
|
|
248
|
+
if (!isTrustedLocalApiRequest(req, true) || !hasWriteToken(req, state.requestToken)) { rejectRequest(res); return }
|
|
249
|
+
const body = await readBody(req, 8 * 1024)
|
|
250
|
+
if (body.tooLarge) { sendJson(res, 413, { ok: false, message: 'request-too-large' }); return }
|
|
251
|
+
let args = null
|
|
252
|
+
try { args = JSON.parse(body.text) } catch (err) { /* invalid json */ }
|
|
253
|
+
const year = args !== null && typeof args === 'object' && !Array.isArray(args) ? Number(args.year) : NaN
|
|
254
|
+
if (!Number.isSafeInteger(year) || year < 2000 || year > 2100) { sendJson(res, 400, { ok: false, message: 'bad-holiday-request' }); return }
|
|
255
|
+
// Fetch only: the client freezes the returned dates into a pricing policy.
|
|
256
|
+
const result = await loadHolidayCalendar(year)
|
|
257
|
+
sendJson(res, result.ok ? 200 : 502, result)
|
|
258
|
+
},
|
|
259
|
+
}))
|
|
242
260
|
ctx.effect(() => webServer.register({
|
|
243
261
|
kind: 'exact',
|
|
244
262
|
path: '/api/all-usage/status',
|
package/lib/plugin.js
CHANGED
|
@@ -100,6 +100,7 @@ function apply(ctx) {
|
|
|
100
100
|
ledgerWriteFailedSessions: new Set(),
|
|
101
101
|
pricingState: createEmptyPricingState(),
|
|
102
102
|
pricingResolutionCache: new Map(),
|
|
103
|
+
holidayCache: new Map(),
|
|
103
104
|
pricingUnit: null,
|
|
104
105
|
pricingReady: Promise.resolve(),
|
|
105
106
|
pricingWriteChain: Promise.resolve(),
|
package/lib/pricing-runtime.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { COST_SCHEMA_VERSION, calculateCost, fetchModelsDevCatalog, normalizeCostSnapshot, normalizePricingState, officialProviderIds, serializeCostAggregate, serializePricingState, temporalPlanFor } from './pricing.js'
|
|
1
|
+
import { COST_SCHEMA_VERSION, builtinTemporalProfileFor, calculateCost, fetchHolidayCalendar, fetchModelsDevCatalog, normalizeCostSnapshot, normalizeModelId, normalizePricingState, officialProviderIds, serializeCostAggregate, serializePricingState, temporalPlanFor } from './pricing.js'
|
|
2
2
|
import { billingInstantOf } from './usage-core.js'
|
|
3
3
|
|
|
4
4
|
const LEDGER_VERSION = 3
|
|
5
|
+
// One price table row per usage identity; the cap keeps the panel payload bounded.
|
|
6
|
+
const MAX_PRICING_ROWS = 500
|
|
7
|
+
// The holiday arrangement changes at most once a year (and only for the next
|
|
8
|
+
// one); a day-long cache keeps the panel from re-fetching on every open.
|
|
9
|
+
const HOLIDAY_CALENDAR_TTL_MS = 24 * 60 * 60 * 1000
|
|
5
10
|
|
|
6
11
|
export function createPricingRuntime(host) {
|
|
7
12
|
const { state, markStatsChanged } = host
|
|
@@ -44,7 +49,7 @@ export function createPricingRuntime(host) {
|
|
|
44
49
|
}
|
|
45
50
|
for (const target of targets) addCostAggregateDirection(target, cost, direction)
|
|
46
51
|
}
|
|
47
|
-
function usedPricingModels(tierSchedules = null) {
|
|
52
|
+
function usedPricingModels(tierSchedules = null, temporalSchedules = null, detailed = false) {
|
|
48
53
|
const rows = []
|
|
49
54
|
const seen = new Set()
|
|
50
55
|
const scheduleIds = new Map()
|
|
@@ -58,19 +63,58 @@ export function createPricingRuntime(host) {
|
|
|
58
63
|
tierSchedules.push({ id, tiers: tiers.map((tier) => ({ ...tier })) })
|
|
59
64
|
return id
|
|
60
65
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
// Peak/off-peak plans are shared by every route priced from the same entry,
|
|
67
|
+
// so they travel once per distinct plan exactly like tier schedules do.
|
|
68
|
+
const temporalIds = new Map()
|
|
69
|
+
const temporalScheduleIdFor = (profile) => {
|
|
70
|
+
if (!Array.isArray(temporalSchedules) || profile === null || typeof profile !== 'object') return null
|
|
71
|
+
const policies = Array.isArray(profile.policies) ? profile.policies : []
|
|
72
|
+
if (policies.length === 0) return null
|
|
73
|
+
// Every attached policy carries the hash of its own content, so the plan
|
|
74
|
+
// identity is a cheap join instead of a full serialization per row
|
|
75
|
+
// (falling back to JSON when a hash is missing).
|
|
76
|
+
const key = policies.every((policy) => typeof policy.policyHash === 'string' && policy.policyHash !== '')
|
|
77
|
+
? policies.map((policy) => policy.policyId + ':' + policy.policyHash).join('|')
|
|
78
|
+
: JSON.stringify(policies)
|
|
79
|
+
const existing = temporalIds.get(key)
|
|
80
|
+
if (existing !== undefined) return existing
|
|
81
|
+
const id = 'temporal-' + temporalSchedules.length
|
|
82
|
+
temporalIds.set(key, id)
|
|
83
|
+
temporalSchedules.push({ id, policies: policies.map((policy) => ({ ...policy })) })
|
|
84
|
+
return id
|
|
85
|
+
}
|
|
86
|
+
const providerKey = (value) => (typeof value === 'string' ? value.trim().toLowerCase() : '')
|
|
87
|
+
// Route identity keys are JSON tuples; rebuilding the exact tuple keeps a
|
|
88
|
+
// synthetic row on the same resolution-cache entry a real usage row uses.
|
|
89
|
+
const parseIdentityKey = (identityKey) => {
|
|
90
|
+
if (typeof identityKey !== 'string' || identityKey === '') return null
|
|
91
|
+
let parsed = null
|
|
92
|
+
try { parsed = JSON.parse(identityKey) } catch (err) { return null }
|
|
93
|
+
if (!Array.isArray(parsed) || parsed.length !== 4) return null
|
|
94
|
+
const textOrNull = (value) => (typeof value === 'string' && value.trim() !== '' ? value.trim() : null)
|
|
95
|
+
return { provider: textOrNull(parsed[0]), requestedModel: textOrNull(parsed[1]), actualModel: textOrNull(parsed[2]), legacyLabel: textOrNull(parsed[3]) }
|
|
96
|
+
}
|
|
97
|
+
// The price edit target of a row: the entry that is actually in effect. A
|
|
98
|
+
// priced row edits its resolved entry, an unpriced row gets a new
|
|
99
|
+
// provider-agnostic manual entry keyed by its own model id.
|
|
100
|
+
const basisKeyFor = (identity, resolved) => {
|
|
101
|
+
const priced = resolved.status === 'priced' && typeof resolved.pricingModel === 'string' && resolved.pricingModel !== ''
|
|
102
|
+
const modelId = priced ? resolved.pricingModel : (identity.actualModel || identity.requestedModel || resolved.pricingModel || identity.label || '')
|
|
103
|
+
const provider = priced ? resolved.providerId || '' : ''
|
|
104
|
+
return providerKey(provider) + '\u0000' + normalizeModelId(modelId)
|
|
105
|
+
}
|
|
106
|
+
const coveredBases = new Set()
|
|
107
|
+
const rowOf = (identity, resolved, options = {}) => {
|
|
66
108
|
const tiers = Array.isArray(resolved.tiers) ? resolved.tiers : []
|
|
67
109
|
const tierScheduleId = resolved.tiered === true ? scheduleIdFor(tiers) : null
|
|
68
|
-
|
|
69
|
-
|
|
110
|
+
const temporalScheduleId = detailed ? temporalScheduleIdFor(resolved.temporalProfile) : null
|
|
111
|
+
coveredBases.add(basisKeyFor(identity, resolved))
|
|
112
|
+
return {
|
|
113
|
+
identityKey: typeof options.identityKey === 'string' ? options.identityKey : identity.identityKey,
|
|
70
114
|
provider: identity.provider,
|
|
71
115
|
requestedModel: identity.requestedModel,
|
|
72
116
|
actualModel: identity.actualModel,
|
|
73
|
-
model: identity.label,
|
|
117
|
+
model: typeof options.model === 'string' && options.model !== '' ? options.model : identity.label,
|
|
74
118
|
status: resolved.status,
|
|
75
119
|
reason: resolved.reason || '',
|
|
76
120
|
pricingModel: resolved.pricingModel || null,
|
|
@@ -84,12 +128,67 @@ export function createPricingRuntime(host) {
|
|
|
84
128
|
tieredInvalid: resolved.tieredInvalid === true,
|
|
85
129
|
inputTokenSemantics: resolved.inputTokenSemantics || 'fresh',
|
|
86
130
|
multiplier: resolved.multiplier || '1',
|
|
131
|
+
temporalExplicit: resolved.temporalExplicit === true,
|
|
132
|
+
temporalBuiltin: resolved.temporalBuiltin === true,
|
|
133
|
+
temporalConfigInvalid: resolved.temporalConfigInvalid === true,
|
|
134
|
+
...(detailed ? { usageBacked: options.usageBacked === true } : {}),
|
|
135
|
+
...(temporalScheduleId === null ? {} : { temporalScheduleId }),
|
|
87
136
|
...(resolved.temporalRoute !== undefined && resolved.temporalRoute !== null ? { temporalRoute: resolved.temporalRoute } : {}),
|
|
88
137
|
...(resolved.temporalProfile && Array.isArray(resolved.temporalProfile.policies) && resolved.temporalProfile.policies.length > 0 ? { temporalPolicyId: String(resolved.temporalProfile.policies[0].policyId || ''), temporalTimezone: resolved.temporalProfile.policies[0].timezone === 'UTC' ? 'UTC' : null } : {}),
|
|
89
|
-
}
|
|
90
|
-
if (rows.length >= 500) break
|
|
138
|
+
}
|
|
91
139
|
}
|
|
92
|
-
|
|
140
|
+
for (const item of state.usageByStep.values()) {
|
|
141
|
+
const identity = coerceIdentity(item.identity || item.modelId)
|
|
142
|
+
if (seen.has(identity.identityKey)) continue
|
|
143
|
+
seen.add(identity.identityKey)
|
|
144
|
+
rows.push(rowOf(identity, resolveCurrentPricing(identity), { usageBacked: true }))
|
|
145
|
+
if (rows.length >= MAX_PRICING_ROWS) break
|
|
146
|
+
}
|
|
147
|
+
if (detailed) {
|
|
148
|
+
// Configured rows the ledger never saw: an override or an
|
|
149
|
+
// identity-scoped mapping would otherwise become invisible and
|
|
150
|
+
// un-editable in the merged price table.
|
|
151
|
+
for (const mapping of state.pricingState.mappings) {
|
|
152
|
+
if (rows.length >= MAX_PRICING_ROWS) break
|
|
153
|
+
const identityKey = typeof mapping.identityKey === 'string' ? mapping.identityKey.trim() : ''
|
|
154
|
+
if (identityKey === '' || seen.has(identityKey)) continue
|
|
155
|
+
seen.add(identityKey)
|
|
156
|
+
const parsed = parseIdentityKey(identityKey)
|
|
157
|
+
const provider = parsed !== null ? parsed.provider : (mapping.provider === '' ? null : mapping.provider)
|
|
158
|
+
const model = parsed !== null ? (parsed.actualModel || parsed.requestedModel || parsed.legacyLabel) : (mapping.model !== '' ? mapping.model : (mapping.catalogModelId === '' ? null : mapping.catalogModelId))
|
|
159
|
+
const identity = {
|
|
160
|
+
identityKey,
|
|
161
|
+
provider,
|
|
162
|
+
requestedModel: parsed !== null ? parsed.requestedModel : model,
|
|
163
|
+
actualModel: parsed !== null ? parsed.actualModel : null,
|
|
164
|
+
label: (provider === null ? '' : provider + ' / ') + (model === null || model === '' ? identityKey : model),
|
|
165
|
+
legacy: parsed !== null && parsed.legacyLabel !== null && parsed.provider === null && parsed.requestedModel === null && parsed.actualModel === null,
|
|
166
|
+
}
|
|
167
|
+
rows.push(rowOf(identity, resolveCurrentPricing(identity), { identityKey, model: identity.label, usageBacked: false }))
|
|
168
|
+
}
|
|
169
|
+
for (const entry of state.pricingState.overrides) {
|
|
170
|
+
if (rows.length >= MAX_PRICING_ROWS) break
|
|
171
|
+
const key = providerKey(entry.providerId) + '\u0000' + normalizeModelId(entry.modelId)
|
|
172
|
+
if (coveredBases.has(key)) continue
|
|
173
|
+
coveredBases.add(key)
|
|
174
|
+
// A non-JSON key: it can never collide with a real route key (those are
|
|
175
|
+
// always JSON tuples), so a configured row cannot poison the shared
|
|
176
|
+
// resolution cache the way a constant key would.
|
|
177
|
+
const identity = {
|
|
178
|
+
identityKey: 'override:' + key,
|
|
179
|
+
provider: entry.providerId === '' ? null : entry.providerId,
|
|
180
|
+
requestedModel: entry.modelId,
|
|
181
|
+
actualModel: null,
|
|
182
|
+
label: entry.displayName || entry.modelId,
|
|
183
|
+
legacy: false,
|
|
184
|
+
}
|
|
185
|
+
rows.push(rowOf(identity, resolveCurrentPricing(identity), { identityKey: '', model: entry.displayName || entry.modelId, usageBacked: false }))
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
rows.sort((left, right) => {
|
|
189
|
+
if (left.usageBacked !== right.usageBacked) return left.usageBacked === true ? -1 : 1
|
|
190
|
+
return String(left.model).localeCompare(String(right.model))
|
|
191
|
+
})
|
|
93
192
|
return rows
|
|
94
193
|
}
|
|
95
194
|
function pricingModelSearch(query, limit = 20) {
|
|
@@ -105,7 +204,20 @@ export function createPricingRuntime(host) {
|
|
|
105
204
|
if (!modelId.includes(normalized) && !displayName.includes(normalized)) continue
|
|
106
205
|
const score = modelId === normalized ? 0 : modelId.startsWith(normalized) ? 1 : displayName.startsWith(normalized) ? 2 : 3
|
|
107
206
|
const previous = selected.get(entry.modelId)
|
|
108
|
-
|
|
207
|
+
// The mapping column previews the target price before saving, so the
|
|
208
|
+
// search result must carry the rates and the built-in peak/off-peak
|
|
209
|
+
// capability (never the whole catalog entry).
|
|
210
|
+
if (previous === undefined || score < previous.score) selected.set(entry.modelId, {
|
|
211
|
+
value: entry.modelId,
|
|
212
|
+
label: entry.displayName,
|
|
213
|
+
providerId: entry.providerId,
|
|
214
|
+
providerName: entry.providerName || '',
|
|
215
|
+
tiered: entry.tiered === true,
|
|
216
|
+
tierCount: Array.isArray(entry.tiers) ? entry.tiers.length : 0,
|
|
217
|
+
rates: { input: entry.input, output: entry.output, cacheRead: entry.cacheRead, cacheWrite: entry.cacheWrite },
|
|
218
|
+
builtinTemporal: builtinTemporalProfileFor(entry.modelId) !== null,
|
|
219
|
+
score,
|
|
220
|
+
})
|
|
109
221
|
}
|
|
110
222
|
return Array.from(selected.values()).sort((a, b) => a.score - b.score || a.value.localeCompare(b.value)).slice(0, Math.max(1, Math.min(50, Number.isInteger(limit) ? limit : 20))).map(({ score, ...entry }) => entry)
|
|
111
223
|
}
|
|
@@ -113,6 +225,7 @@ export function createPricingRuntime(host) {
|
|
|
113
225
|
const pricingStateSnapshot = state.pricingState
|
|
114
226
|
const detailed = options.detailed !== false
|
|
115
227
|
const tierSchedules = detailed ? [] : null
|
|
228
|
+
const temporalSchedules = detailed ? [] : null
|
|
116
229
|
const snapshot = {
|
|
117
230
|
schemaVersion: pricingStateSnapshot.schemaVersion,
|
|
118
231
|
source: { ...pricingStateSnapshot.source },
|
|
@@ -121,16 +234,25 @@ export function createPricingRuntime(host) {
|
|
|
121
234
|
overrideCount: pricingStateSnapshot.overrides.length,
|
|
122
235
|
mappingCount: pricingStateSnapshot.mappings.length,
|
|
123
236
|
configured: pricingStateSnapshot.catalogEntries.length > 0 || pricingStateSnapshot.overrides.length > 0 || pricingStateSnapshot.mappings.length > 0,
|
|
124
|
-
usedModels: usedPricingModels(tierSchedules),
|
|
237
|
+
usedModels: usedPricingModels(tierSchedules, temporalSchedules, detailed),
|
|
125
238
|
cost: serializeCostAggregate(state.totals.cost),
|
|
126
239
|
}
|
|
127
240
|
if (detailed) {
|
|
128
241
|
snapshot.config = {
|
|
129
242
|
sync: { ...pricingStateSnapshot.sync },
|
|
130
243
|
mappings: pricingStateSnapshot.mappings.map((mapping) => ({ ...mapping })),
|
|
131
|
-
|
|
244
|
+
// providerId and the peak/off-peak plan must round-trip: dropping
|
|
245
|
+
// providerId would turn a provider-scoped manual price into a
|
|
246
|
+
// provider-agnostic one on the next save (and would stop it from
|
|
247
|
+
// being found as a mapping target).
|
|
248
|
+
overrides: pricingStateSnapshot.overrides.map((entry) => ({
|
|
249
|
+
...entry,
|
|
250
|
+
...(Array.isArray(entry.tiers) ? { tiers: entry.tiers.map((tier) => ({ ...tier })) } : {}),
|
|
251
|
+
...(entry.temporalPricing === undefined ? {} : { temporalPricing: JSON.parse(JSON.stringify(entry.temporalPricing)) }),
|
|
252
|
+
})),
|
|
132
253
|
}
|
|
133
254
|
snapshot.tierSchedules = tierSchedules
|
|
255
|
+
snapshot.temporalSchedules = temporalSchedules
|
|
134
256
|
}
|
|
135
257
|
return snapshot
|
|
136
258
|
}
|
|
@@ -264,7 +386,7 @@ export function createPricingRuntime(host) {
|
|
|
264
386
|
}
|
|
265
387
|
function costSnapshotEquivalent(left, right) {
|
|
266
388
|
if (left === null || right === null || typeof left !== 'object' || typeof right !== 'object') return false
|
|
267
|
-
if (left.status !== right.status || left.currency !== right.currency || left.pricingModel !== right.pricingModel || left.providerId !== right.providerId || left.inputTokenSemantics !== right.inputTokenSemantics || left.multiplier !== right.multiplier || left.billableInputTokens !== right.billableInputTokens || left.billableOutputTokens !== right.billableOutputTokens || left.baseTotal !== right.baseTotal || left.total !== right.total || left.reason !== right.reason || left.tiered !== right.tiered || left.contextTokens !== right.contextTokens || left.pricingAt !== right.pricingAt || left.pricingTimeSource !== right.pricingTimeSource || left.pricingBand !== right.pricingBand || left.pricingPolicyId !== right.pricingPolicyId || left.pricingPolicyHash !== right.pricingPolicyHash || left.pricingTimezone !== right.pricingTimezone || left.temporalApplicable !== right.temporalApplicable || left.temporalExemptReason !== right.temporalExemptReason) return false
|
|
389
|
+
if (left.status !== right.status || left.currency !== right.currency || left.pricingModel !== right.pricingModel || left.providerId !== right.providerId || left.inputTokenSemantics !== right.inputTokenSemantics || left.multiplier !== right.multiplier || left.billableInputTokens !== right.billableInputTokens || left.billableOutputTokens !== right.billableOutputTokens || left.baseTotal !== right.baseTotal || left.total !== right.total || left.reason !== right.reason || left.tiered !== right.tiered || left.contextTokens !== right.contextTokens || left.pricingAt !== right.pricingAt || left.pricingTimeSource !== right.pricingTimeSource || left.pricingBand !== right.pricingBand || left.pricingHoliday !== right.pricingHoliday || left.pricingPolicyId !== right.pricingPolicyId || left.pricingPolicyHash !== right.pricingPolicyHash || left.pricingTimezone !== right.pricingTimezone || left.temporalApplicable !== right.temporalApplicable || left.temporalExemptReason !== right.temporalExemptReason) return false
|
|
268
390
|
for (const key of ['input', 'output', 'cacheRead', 'cacheWrite']) {
|
|
269
391
|
if (left.breakdown !== null && left.breakdown !== undefined && right.breakdown !== null && right.breakdown !== undefined) {
|
|
270
392
|
if (left.breakdown[key] !== right.breakdown[key]) return false
|
|
@@ -377,6 +499,28 @@ export function createPricingRuntime(host) {
|
|
|
377
499
|
schedulePricingSync()
|
|
378
500
|
return { ...result, temporalReconciled: temporal.reconciled }
|
|
379
501
|
}
|
|
502
|
+
/**
|
|
503
|
+
* Fetch one year of the Chinese holiday arrangement for the editor. Nothing is
|
|
504
|
+
* persisted here: the panel freezes the returned dates into a pricing policy,
|
|
505
|
+
* which is what makes later reconciliation reproducible.
|
|
506
|
+
*/
|
|
507
|
+
async function loadHolidayCalendar(year) {
|
|
508
|
+
const cache = state.holidayCache instanceof Map ? state.holidayCache : (state.holidayCache = new Map())
|
|
509
|
+
const key = String(Number(year))
|
|
510
|
+
const cached = cache.get(key)
|
|
511
|
+
if (cached !== undefined && Date.now() - cached.fetchedAt < HOLIDAY_CALENDAR_TTL_MS) return { ok: true, cached: true, ...cached }
|
|
512
|
+
const result = await fetchHolidayCalendar(year)
|
|
513
|
+
if (!result.ok) return result
|
|
514
|
+
const entry = {
|
|
515
|
+
year: result.calendar.year,
|
|
516
|
+
dates: result.calendar.dates,
|
|
517
|
+
paperUrl: result.calendar.paperUrl,
|
|
518
|
+
sourceUrl: result.calendar.sourceUrl,
|
|
519
|
+
fetchedAt: result.calendar.fetchedAt,
|
|
520
|
+
}
|
|
521
|
+
cache.set(key, entry)
|
|
522
|
+
return { ok: true, cached: false, ...entry }
|
|
523
|
+
}
|
|
380
524
|
function schedulePricingSync() {
|
|
381
525
|
if (state.pricingSyncTimer !== null) { clearTimeout(state.pricingSyncTimer); state.pricingSyncTimer = null }
|
|
382
526
|
if (state.disposed || state.pricingState.sync.autoEnabled !== true) return
|
|
@@ -401,6 +545,7 @@ export function createPricingRuntime(host) {
|
|
|
401
545
|
backfillUnpricedCosts,
|
|
402
546
|
reconcileTemporalPricing,
|
|
403
547
|
updatePricingState,
|
|
404
|
-
schedulePricingSync
|
|
548
|
+
schedulePricingSync,
|
|
549
|
+
loadHolidayCalendar
|
|
405
550
|
}
|
|
406
551
|
}
|
package/lib/pricing.js
CHANGED
|
@@ -17,7 +17,18 @@ const MAX_CONTEXT_TIERS = 32
|
|
|
17
17
|
const MAX_TEMPORAL_RULES = 16
|
|
18
18
|
const MAX_TEMPORAL_WINDOWS = 16
|
|
19
19
|
const MAX_TEMPORAL_POLICIES = 8
|
|
20
|
+
const MAX_TEMPORAL_HOLIDAYS = 400
|
|
20
21
|
const TEMPORAL_TIMEZONE = 'UTC'
|
|
22
|
+
// Statutory-holiday dates are China Standard Time calendar days; a UTC policy
|
|
23
|
+
// band therefore runs from 16:00Z the day before to 16:00Z on the day itself.
|
|
24
|
+
const TEMPORAL_HOLIDAY_OFFSET_MS = 8 * 60 * 60 * 1000
|
|
25
|
+
// Public, no-key datasets of the State Council holiday arrangement (the file per
|
|
26
|
+
// year carries `days[].isOffDay`, and the official gov.cn notice in `papers`).
|
|
27
|
+
const HOLIDAY_CALENDAR_BASES = [
|
|
28
|
+
'https://cdn.jsdelivr.net/gh/NateScarlet/holiday-cn@master/',
|
|
29
|
+
'https://raw.githubusercontent.com/NateScarlet/holiday-cn/master/',
|
|
30
|
+
]
|
|
31
|
+
const MAX_HOLIDAY_CALENDAR_BYTES = 512 * 1024
|
|
21
32
|
const TEMPORAL_TIME_SOURCES = ['request-context', 'usage-event', 'legacy-unknown']
|
|
22
33
|
const RATE_KEYS = ['input', 'output', 'cacheRead', 'cacheWrite']
|
|
23
34
|
const INPUT_SEMANTICS = ['legacy', 'total', 'fresh']
|
|
@@ -362,6 +373,60 @@ function normalizeTemporalWindows(raw) {
|
|
|
362
373
|
return windows
|
|
363
374
|
}
|
|
364
375
|
|
|
376
|
+
function normalizeHolidayDate(value) {
|
|
377
|
+
if (typeof value !== 'string') return null
|
|
378
|
+
const text = value.trim()
|
|
379
|
+
const match = text.match(/^([0-9]{4})-([0-9]{2})-([0-9]{2})$/)
|
|
380
|
+
if (match === null) return null
|
|
381
|
+
const year = Number(match[1])
|
|
382
|
+
const month = Number(match[2])
|
|
383
|
+
const day = Number(match[3])
|
|
384
|
+
if (year < 2000 || year > 2100) return null
|
|
385
|
+
const date = new Date(Date.UTC(year, month - 1, day))
|
|
386
|
+
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) return null
|
|
387
|
+
return text
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Explicit statutory-holiday dates: sorted, de-duplicated, strictly shaped. */
|
|
391
|
+
function normalizeTemporalHolidays(raw) {
|
|
392
|
+
if (raw === undefined || raw === null) return []
|
|
393
|
+
if (!Array.isArray(raw)) return null
|
|
394
|
+
const seen = new Set()
|
|
395
|
+
const days = []
|
|
396
|
+
for (const candidate of raw) {
|
|
397
|
+
const text = normalizeHolidayDate(candidate)
|
|
398
|
+
if (text === null) return null
|
|
399
|
+
if (seen.has(text)) continue
|
|
400
|
+
seen.add(text)
|
|
401
|
+
days.push(text)
|
|
402
|
+
if (days.length > MAX_TEMPORAL_HOLIDAYS) return null
|
|
403
|
+
}
|
|
404
|
+
days.sort()
|
|
405
|
+
return days
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const TEMPORAL_HOLIDAY_SETS = new WeakMap()
|
|
409
|
+
/** Cached lookup set for one policy's holiday list. */
|
|
410
|
+
function holidaySetFor(policy) {
|
|
411
|
+
if (!isRecord(policy)) return null
|
|
412
|
+
const holidays = Array.isArray(policy.holidays) ? policy.holidays : null
|
|
413
|
+
if (holidays === null || holidays.length === 0) return null
|
|
414
|
+
let set = TEMPORAL_HOLIDAY_SETS.get(policy)
|
|
415
|
+
if (set === undefined) {
|
|
416
|
+
set = new Set(holidays)
|
|
417
|
+
TEMPORAL_HOLIDAY_SETS.set(policy, set)
|
|
418
|
+
}
|
|
419
|
+
return set
|
|
420
|
+
}
|
|
421
|
+
/** True when the instant falls on one of the policy's statutory holidays (CST day). */
|
|
422
|
+
function holidayCoversInstant(policy, atMs) {
|
|
423
|
+
const set = holidaySetFor(policy)
|
|
424
|
+
if (set === null || !Number.isFinite(atMs)) return false
|
|
425
|
+
const local = new Date(Math.trunc(atMs) + TEMPORAL_HOLIDAY_OFFSET_MS)
|
|
426
|
+
const text = String(local.getUTCFullYear()) + '-' + String(local.getUTCMonth() + 1).padStart(2, '0') + '-' + String(local.getUTCDate()).padStart(2, '0')
|
|
427
|
+
return set.has(text)
|
|
428
|
+
}
|
|
429
|
+
|
|
365
430
|
/** One versioned temporal policy: effective-windowed, UTC-band rules. */
|
|
366
431
|
function normalizeTemporalPolicy(raw, entryDefaultRates = undefined) {
|
|
367
432
|
if (!isRecord(raw)) return null
|
|
@@ -417,7 +482,13 @@ function normalizeTemporalPolicy(raw, entryDefaultRates = undefined) {
|
|
|
417
482
|
if (list[index].startMinute < list[index - 1].endMinute) return null
|
|
418
483
|
}
|
|
419
484
|
}
|
|
420
|
-
|
|
485
|
+
const holidays = normalizeTemporalHolidays(raw.holidays)
|
|
486
|
+
if (holidays === null) return null
|
|
487
|
+
// Provenance of a fetched holiday list. Deliberately NOT part of the policy
|
|
488
|
+
// hash: where the dates came from does not change what they price, but it is
|
|
489
|
+
// kept next to them so a frozen list can be traced back.
|
|
490
|
+
const holidaysSource = typeof raw.holidaysSource === 'string' ? raw.holidaysSource.trim().slice(0, 512) : ''
|
|
491
|
+
return { policyId, sourceUrl, timezone, effectiveFrom, effectiveUntil, defaultPlan, holidays, holidaysSource, rules }
|
|
421
492
|
}
|
|
422
493
|
|
|
423
494
|
/** Temporal pricing plan: an ordered, non-overlapping archive of policies. */
|
|
@@ -452,13 +523,34 @@ const DEEPSEEK_VISION_EFFECTIVE_FROM = Date.UTC(2026, 7, 21, 0, 0, 0)
|
|
|
452
523
|
// Built-in first-party profiles: each model carries its own explicit off-peak
|
|
453
524
|
// (inherited from the live catalog entry) and peak rates; the peak rates are
|
|
454
525
|
// stored as data, never derived as an automatic 'half price' rule.
|
|
526
|
+
// models.dev publishes `deepseek-flash` (DeepSeek V4.1 Flash) in the same
|
|
527
|
+
// `deepseek-flash` family and at the same standard rates as
|
|
528
|
+
// `deepseek-v4-flash`, so it shares that band; a variant that arrives with
|
|
529
|
+
// different standard rates still needs its own row.
|
|
455
530
|
const DEEPSEEK_TEMPORAL_MODELS = new Map([
|
|
456
531
|
['deepseek-v4-flash', { input: '0.44', output: '1.32', cacheRead: '0.014', cacheWrite: '0', effectiveFrom: DEEPSEEK_TEMPORAL_EFFECTIVE_FROM }],
|
|
532
|
+
['deepseek-flash', { input: '0.44', output: '1.32', cacheRead: '0.014', cacheWrite: '0', effectiveFrom: DEEPSEEK_TEMPORAL_EFFECTIVE_FROM }],
|
|
457
533
|
['deepseek-v4-flash-vision-exp', { input: '0.44', output: '1.32', cacheRead: '0.014', cacheWrite: '0', effectiveFrom: DEEPSEEK_VISION_EFFECTIVE_FROM }],
|
|
458
534
|
['deepseek-v4-pro', { input: '1.32', output: '3.96', cacheRead: '0.044', cacheWrite: '0', effectiveFrom: DEEPSEEK_TEMPORAL_EFFECTIVE_FROM }],
|
|
459
535
|
])
|
|
460
536
|
|
|
461
|
-
|
|
537
|
+
// DeepSeek's own peak table excludes Chinese public holidays (`周一至周五(不含中国
|
|
538
|
+
// 法定节假日)`), so the built-in plan has to carry the arrangement itself; otherwise
|
|
539
|
+
// holiday requests are charged at peak rates. The list below is the 2026 arrangement
|
|
540
|
+
// from the State Council notice. Later years ship with a plugin update, and any plan
|
|
541
|
+
// (a built-in copy or a custom one) can be extended by hand or fetched at runtime
|
|
542
|
+
// through POST /api/all-usage/pricing/holidays.
|
|
543
|
+
const CHINA_HOLIDAY_SOURCE_2026 = 'https://www.gov.cn/zhengce/zhengceku/202511/content_7047091.htm'
|
|
544
|
+
const CHINA_HOLIDAYS_2026 = [
|
|
545
|
+
'2026-01-01', '2026-01-02', '2026-01-03', '2026-02-15', '2026-02-16', '2026-02-17',
|
|
546
|
+
'2026-02-18', '2026-02-19', '2026-02-20', '2026-02-21', '2026-02-22', '2026-02-23',
|
|
547
|
+
'2026-04-04', '2026-04-05', '2026-04-06', '2026-05-01', '2026-05-02', '2026-05-03',
|
|
548
|
+
'2026-05-04', '2026-05-05', '2026-06-19', '2026-06-20', '2026-06-21', '2026-09-25',
|
|
549
|
+
'2026-09-26', '2026-09-27', '2026-10-01', '2026-10-02', '2026-10-03', '2026-10-04',
|
|
550
|
+
'2026-10-05', '2026-10-06', '2026-10-07',
|
|
551
|
+
]
|
|
552
|
+
|
|
553
|
+
export function builtinTemporalProfileFor(modelId) {
|
|
462
554
|
const spec = DEEPSEEK_TEMPORAL_MODELS.get(normalizeModelId(modelId))
|
|
463
555
|
if (spec === undefined) return null
|
|
464
556
|
return {
|
|
@@ -469,6 +561,8 @@ function builtinTemporalProfileFor(modelId) {
|
|
|
469
561
|
effectiveFrom: spec.effectiveFrom,
|
|
470
562
|
effectiveUntil: null,
|
|
471
563
|
defaultPlan: null,
|
|
564
|
+
holidays: CHINA_HOLIDAYS_2026,
|
|
565
|
+
holidaysSource: CHINA_HOLIDAY_SOURCE_2026 + ' · 2026 中国法定节假日安排',
|
|
472
566
|
rules: [{ id: 'peak', weekdays: [1, 2, 3, 4, 5], windows: [{ startMinute: 60, endMinute: 240 }, { startMinute: 360, endMinute: 600 }], rates: { input: spec.input, output: spec.output, cacheRead: spec.cacheRead, cacheWrite: spec.cacheWrite } }],
|
|
473
567
|
}],
|
|
474
568
|
}
|
|
@@ -492,6 +586,8 @@ function temporalBand(profile, atMs) {
|
|
|
492
586
|
if (!Number.isFinite(atMs) || atMs < 0) return null
|
|
493
587
|
const policy = temporalPolicyFor(profile, atMs)
|
|
494
588
|
if (policy === null) return null
|
|
589
|
+
// A statutory holiday is a full off-peak day: no rule can claim its instants.
|
|
590
|
+
if (holidayCoversInstant(policy, atMs)) return 'off-peak'
|
|
495
591
|
const matched = matchingTemporalRule(policy, atMs)
|
|
496
592
|
return matched === null ? 'off-peak' : 'peak'
|
|
497
593
|
}
|
|
@@ -520,6 +616,7 @@ function temporalPolicyHash(policy) {
|
|
|
520
616
|
policy.effectiveFrom,
|
|
521
617
|
policy.effectiveUntil,
|
|
522
618
|
policy.defaultPlan,
|
|
619
|
+
policy.holidays,
|
|
523
620
|
policy.rules,
|
|
524
621
|
])
|
|
525
622
|
return createHash('sha256').update(stable).digest('hex')
|
|
@@ -546,9 +643,12 @@ function temporalPlanFor(resolved, pricingAtMs) {
|
|
|
546
643
|
const policy = temporalPolicyFor(profile, at)
|
|
547
644
|
if (policy === null) return { status: 'history-gap', band: null, ruleRates: null, policyId: null, policyHash: null, timezone: null, exemptReason: null }
|
|
548
645
|
const common = { policyId: policy.policyId || null, policyHash: policy.policyHash || null, timezone: policy.timezone === TEMPORAL_TIMEZONE ? TEMPORAL_TIMEZONE : null }
|
|
646
|
+
// A statutory holiday is a full off-peak day; its audit flag is stored with
|
|
647
|
+
// the cost snapshot so a ledger row can explain why it was off-peak.
|
|
648
|
+
if (holidayCoversInstant(policy, at)) return { status: 'applied', band: 'off-peak', ruleRates: policy.defaultPlan ? policy.defaultPlan.rates : null, holiday: true, exemptReason: null, ...common }
|
|
549
649
|
const rule = matchingTemporalRule(policy, at)
|
|
550
|
-
if (rule !== null) return { status: 'applied', band: 'peak', ruleRates: rule.rates, exemptReason: null, ...common }
|
|
551
|
-
return { status: 'applied', band: 'off-peak', ruleRates: policy.defaultPlan ? policy.defaultPlan.rates : null, exemptReason: null, ...common }
|
|
650
|
+
if (rule !== null) return { status: 'applied', band: 'peak', ruleRates: rule.rates, holiday: false, exemptReason: null, ...common }
|
|
651
|
+
return { status: 'applied', band: 'off-peak', ruleRates: policy.defaultPlan ? policy.defaultPlan.rates : null, holiday: false, exemptReason: null, ...common }
|
|
552
652
|
}
|
|
553
653
|
|
|
554
654
|
function normalizeContextTier(raw, fallbackRates, legacySize = null) {
|
|
@@ -836,7 +936,15 @@ function attachTemporalProfile(entry, mapped, identity) {
|
|
|
836
936
|
// fails closed instead of pricing with a plan the user did not write.
|
|
837
937
|
if (entry.temporalPricingInvalid === true) return { invalid: true }
|
|
838
938
|
let profile = isRecord(entry.temporalPricing) ? entry.temporalPricing : null
|
|
839
|
-
|
|
939
|
+
// The editor needs to tell a user-authored plan from the built-in DeepSeek
|
|
940
|
+
// table: only the former can be reset, the latter is re-attached on every
|
|
941
|
+
// resolution and must never be silently frozen into an override.
|
|
942
|
+
const explicit = profile !== null
|
|
943
|
+
let builtin = false
|
|
944
|
+
if (profile === null && DEEPSEEK_OFFICIAL_PROVIDERS.includes(normalizeProvider(entry.providerId))) {
|
|
945
|
+
profile = builtinTemporalProfileFor(entry.modelId)
|
|
946
|
+
builtin = profile !== null
|
|
947
|
+
}
|
|
840
948
|
if (profile === null) return null
|
|
841
949
|
const providerId = normalizeProvider(entry.providerId)
|
|
842
950
|
const identityProvider = normalizeProvider(identity && identity.provider)
|
|
@@ -847,6 +955,8 @@ function attachTemporalProfile(entry, mapped, identity) {
|
|
|
847
955
|
const route = DEEPSEEK_OFFICIAL_PROVIDERS.includes(providerId) && DEEPSEEK_OFFICIAL_PROVIDERS.includes(identityProvider) ? 'official' : mapped !== undefined && mapped !== null ? 'mapped' : 'other'
|
|
848
956
|
return {
|
|
849
957
|
route,
|
|
958
|
+
explicit,
|
|
959
|
+
builtin,
|
|
850
960
|
profile: {
|
|
851
961
|
policies: profile.policies.map((policy) => ({ ...policy, policyHash: temporalPolicyHash(policy) })),
|
|
852
962
|
},
|
|
@@ -912,7 +1022,7 @@ function resolvePricing(identity, rawState) {
|
|
|
912
1022
|
reasoningRateAvailable: entry.reasoningRateAvailable,
|
|
913
1023
|
inputTokenSemantics: mapped && INPUT_SEMANTICS.includes(mapped.inputTokenSemantics) ? mapped.inputTokenSemantics : 'fresh',
|
|
914
1024
|
multiplier: mapped ? mapped.multiplier : '1',
|
|
915
|
-
...(() => { const temporal = attachTemporalProfile(entry, mapped, identity); return temporal === null ? {} : temporal.invalid === true ? { temporalConfigInvalid: true } : { temporalProfile: temporal.profile, temporalRoute: temporal.route } })(),
|
|
1025
|
+
...(() => { const temporal = attachTemporalProfile(entry, mapped, identity); return temporal === null ? {} : temporal.invalid === true ? { temporalConfigInvalid: true } : { temporalProfile: temporal.profile, temporalRoute: temporal.route, temporalExplicit: temporal.explicit === true, temporalBuiltin: temporal.builtin === true } })(),
|
|
916
1026
|
}
|
|
917
1027
|
}
|
|
918
1028
|
const model = models[0] || null
|
|
@@ -1022,6 +1132,7 @@ function calculateCost(values, resolved, pricingAtMs = null, pricingTimeSource =
|
|
|
1022
1132
|
pricingAt,
|
|
1023
1133
|
pricingTimeSource: timeSource,
|
|
1024
1134
|
pricingBand: plan.band,
|
|
1135
|
+
pricingHoliday: plan.holiday === true,
|
|
1025
1136
|
pricingTimezone: plan.timezone,
|
|
1026
1137
|
pricingPolicyId: plan.policyId,
|
|
1027
1138
|
pricingPolicyHash: plan.policyHash,
|
|
@@ -1157,6 +1268,7 @@ function normalizeCostSnapshot(raw) {
|
|
|
1157
1268
|
pricingAt,
|
|
1158
1269
|
pricingTimeSource,
|
|
1159
1270
|
pricingBand,
|
|
1271
|
+
pricingHoliday: raw.pricingHoliday === true,
|
|
1160
1272
|
pricingTimezone,
|
|
1161
1273
|
pricingPolicyId,
|
|
1162
1274
|
pricingPolicyHash,
|
|
@@ -1201,6 +1313,69 @@ async function fetchModelsDevCatalog(fetchImpl = globalThis.fetch, options = {})
|
|
|
1201
1313
|
}
|
|
1202
1314
|
}
|
|
1203
1315
|
|
|
1316
|
+
/** Turn one holiday-cn dataset into the explicit off-day list the policy stores. */
|
|
1317
|
+
export function parseHolidayCalendar(raw, year) {
|
|
1318
|
+
if (!isRecord(raw)) return { ok: false, error: 'holiday-calendar-not-object' }
|
|
1319
|
+
if (raw.year !== undefined && raw.year !== null && Number(raw.year) !== Number(year)) return { ok: false, error: 'holiday-calendar-year-mismatch' }
|
|
1320
|
+
if (!Array.isArray(raw.days)) return { ok: false, error: 'holiday-calendar-days-missing' }
|
|
1321
|
+
const seen = new Set()
|
|
1322
|
+
const dates = []
|
|
1323
|
+
for (const day of raw.days) {
|
|
1324
|
+
if (!isRecord(day) || day.isOffDay !== true) continue
|
|
1325
|
+
const text = normalizeHolidayDate(day.date)
|
|
1326
|
+
if (text === null) return { ok: false, error: 'holiday-calendar-date-invalid' }
|
|
1327
|
+
if (seen.has(text)) continue
|
|
1328
|
+
seen.add(text)
|
|
1329
|
+
dates.push(text)
|
|
1330
|
+
}
|
|
1331
|
+
if (dates.length === 0) return { ok: false, error: 'holiday-calendar-empty' }
|
|
1332
|
+
if (dates.length > MAX_TEMPORAL_HOLIDAYS) return { ok: false, error: 'holiday-calendar-too-large' }
|
|
1333
|
+
dates.sort()
|
|
1334
|
+
const papers = Array.isArray(raw.papers) ? raw.papers.filter((item) => typeof item === 'string' && item.trim() !== '').map((item) => item.trim().slice(0, 512)) : []
|
|
1335
|
+
return { ok: true, calendar: { year: Number(year), dates, paperUrl: papers.length > 0 ? papers[0] : '', fetchedAt: 0 } }
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
/**
|
|
1339
|
+
* Fetch one year of the Chinese holiday arrangement. The result is never applied
|
|
1340
|
+
* automatically: the caller freezes the returned dates into a pricing policy, so
|
|
1341
|
+
* later ledger reconciliation stays reproducible even if the upstream changes.
|
|
1342
|
+
*/
|
|
1343
|
+
export async function fetchHolidayCalendar(year, fetchImpl = globalThis.fetch, options = {}) {
|
|
1344
|
+
const value = Number(year)
|
|
1345
|
+
if (!Number.isSafeInteger(value) || value < 2000 || value > 2100) return { ok: false, error: 'holiday-year-invalid' }
|
|
1346
|
+
if (typeof fetchImpl !== 'function') return { ok: false, error: 'fetch-unavailable' }
|
|
1347
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 15000
|
|
1348
|
+
const bases = Array.isArray(options.bases) && options.bases.length > 0 ? options.bases : HOLIDAY_CALENDAR_BASES
|
|
1349
|
+
let lastError = 'holiday-calendar-unavailable'
|
|
1350
|
+
for (const base of bases) {
|
|
1351
|
+
const url = String(base) + value + '.json'
|
|
1352
|
+
let controller = null
|
|
1353
|
+
let timer = null
|
|
1354
|
+
try {
|
|
1355
|
+
if (typeof AbortController === 'function') {
|
|
1356
|
+
controller = new AbortController()
|
|
1357
|
+
timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
1358
|
+
}
|
|
1359
|
+
const response = await fetchImpl(url, { headers: { accept: 'application/json' }, signal: controller ? controller.signal : undefined })
|
|
1360
|
+
if (!response || response.ok !== true) { lastError = 'http-' + String((response && response.status) || 0); continue }
|
|
1361
|
+
const length = response.headers && typeof response.headers.get === 'function' ? Number(response.headers.get('content-length')) : 0
|
|
1362
|
+
if (Number.isFinite(length) && length > MAX_HOLIDAY_CALENDAR_BYTES) { lastError = 'holiday-calendar-too-large'; continue }
|
|
1363
|
+
const text = await response.text()
|
|
1364
|
+
if (new TextEncoder().encode(text).length > MAX_HOLIDAY_CALENDAR_BYTES) { lastError = 'holiday-calendar-too-large'; continue }
|
|
1365
|
+
let parsed = null
|
|
1366
|
+
try { parsed = JSON.parse(text.replace(/^\uFEFF/, '')) } catch (err) { lastError = 'holiday-calendar-invalid-json'; continue }
|
|
1367
|
+
const result = parseHolidayCalendar(parsed, value)
|
|
1368
|
+
if (!result.ok) { lastError = result.error; continue }
|
|
1369
|
+
return { ok: true, calendar: { ...result.calendar, sourceUrl: url, fetchedAt: Date.now() } }
|
|
1370
|
+
} catch (err) {
|
|
1371
|
+
lastError = 'holiday-calendar-fetch-failed'
|
|
1372
|
+
} finally {
|
|
1373
|
+
if (timer !== null) clearTimeout(timer)
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
return { ok: false, error: lastError }
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1204
1379
|
export {
|
|
1205
1380
|
COST_SCHEMA_VERSION,
|
|
1206
1381
|
COST_STATUSES,
|