dsh-all-usage 1.1.14 → 1.1.16
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 +24 -0
- package/README.md +29 -23
- 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 +176 -6
- package/package.json +3 -3
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. */
|
|
@@ -463,7 +534,23 @@ const DEEPSEEK_TEMPORAL_MODELS = new Map([
|
|
|
463
534
|
['deepseek-v4-pro', { input: '1.32', output: '3.96', cacheRead: '0.044', cacheWrite: '0', effectiveFrom: DEEPSEEK_TEMPORAL_EFFECTIVE_FROM }],
|
|
464
535
|
])
|
|
465
536
|
|
|
466
|
-
|
|
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) {
|
|
467
554
|
const spec = DEEPSEEK_TEMPORAL_MODELS.get(normalizeModelId(modelId))
|
|
468
555
|
if (spec === undefined) return null
|
|
469
556
|
return {
|
|
@@ -474,6 +561,8 @@ function builtinTemporalProfileFor(modelId) {
|
|
|
474
561
|
effectiveFrom: spec.effectiveFrom,
|
|
475
562
|
effectiveUntil: null,
|
|
476
563
|
defaultPlan: null,
|
|
564
|
+
holidays: CHINA_HOLIDAYS_2026,
|
|
565
|
+
holidaysSource: CHINA_HOLIDAY_SOURCE_2026 + ' · 2026 中国法定节假日安排',
|
|
477
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 } }],
|
|
478
567
|
}],
|
|
479
568
|
}
|
|
@@ -497,6 +586,8 @@ function temporalBand(profile, atMs) {
|
|
|
497
586
|
if (!Number.isFinite(atMs) || atMs < 0) return null
|
|
498
587
|
const policy = temporalPolicyFor(profile, atMs)
|
|
499
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'
|
|
500
591
|
const matched = matchingTemporalRule(policy, atMs)
|
|
501
592
|
return matched === null ? 'off-peak' : 'peak'
|
|
502
593
|
}
|
|
@@ -525,6 +616,7 @@ function temporalPolicyHash(policy) {
|
|
|
525
616
|
policy.effectiveFrom,
|
|
526
617
|
policy.effectiveUntil,
|
|
527
618
|
policy.defaultPlan,
|
|
619
|
+
policy.holidays,
|
|
528
620
|
policy.rules,
|
|
529
621
|
])
|
|
530
622
|
return createHash('sha256').update(stable).digest('hex')
|
|
@@ -551,9 +643,12 @@ function temporalPlanFor(resolved, pricingAtMs) {
|
|
|
551
643
|
const policy = temporalPolicyFor(profile, at)
|
|
552
644
|
if (policy === null) return { status: 'history-gap', band: null, ruleRates: null, policyId: null, policyHash: null, timezone: null, exemptReason: null }
|
|
553
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 }
|
|
554
649
|
const rule = matchingTemporalRule(policy, at)
|
|
555
|
-
if (rule !== null) return { status: 'applied', band: 'peak', ruleRates: rule.rates, exemptReason: null, ...common }
|
|
556
|
-
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 }
|
|
557
652
|
}
|
|
558
653
|
|
|
559
654
|
function normalizeContextTier(raw, fallbackRates, legacySize = null) {
|
|
@@ -841,7 +936,15 @@ function attachTemporalProfile(entry, mapped, identity) {
|
|
|
841
936
|
// fails closed instead of pricing with a plan the user did not write.
|
|
842
937
|
if (entry.temporalPricingInvalid === true) return { invalid: true }
|
|
843
938
|
let profile = isRecord(entry.temporalPricing) ? entry.temporalPricing : null
|
|
844
|
-
|
|
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
|
+
}
|
|
845
948
|
if (profile === null) return null
|
|
846
949
|
const providerId = normalizeProvider(entry.providerId)
|
|
847
950
|
const identityProvider = normalizeProvider(identity && identity.provider)
|
|
@@ -852,6 +955,8 @@ function attachTemporalProfile(entry, mapped, identity) {
|
|
|
852
955
|
const route = DEEPSEEK_OFFICIAL_PROVIDERS.includes(providerId) && DEEPSEEK_OFFICIAL_PROVIDERS.includes(identityProvider) ? 'official' : mapped !== undefined && mapped !== null ? 'mapped' : 'other'
|
|
853
956
|
return {
|
|
854
957
|
route,
|
|
958
|
+
explicit,
|
|
959
|
+
builtin,
|
|
855
960
|
profile: {
|
|
856
961
|
policies: profile.policies.map((policy) => ({ ...policy, policyHash: temporalPolicyHash(policy) })),
|
|
857
962
|
},
|
|
@@ -917,7 +1022,7 @@ function resolvePricing(identity, rawState) {
|
|
|
917
1022
|
reasoningRateAvailable: entry.reasoningRateAvailable,
|
|
918
1023
|
inputTokenSemantics: mapped && INPUT_SEMANTICS.includes(mapped.inputTokenSemantics) ? mapped.inputTokenSemantics : 'fresh',
|
|
919
1024
|
multiplier: mapped ? mapped.multiplier : '1',
|
|
920
|
-
...(() => { 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 } })(),
|
|
921
1026
|
}
|
|
922
1027
|
}
|
|
923
1028
|
const model = models[0] || null
|
|
@@ -1027,6 +1132,7 @@ function calculateCost(values, resolved, pricingAtMs = null, pricingTimeSource =
|
|
|
1027
1132
|
pricingAt,
|
|
1028
1133
|
pricingTimeSource: timeSource,
|
|
1029
1134
|
pricingBand: plan.band,
|
|
1135
|
+
pricingHoliday: plan.holiday === true,
|
|
1030
1136
|
pricingTimezone: plan.timezone,
|
|
1031
1137
|
pricingPolicyId: plan.policyId,
|
|
1032
1138
|
pricingPolicyHash: plan.policyHash,
|
|
@@ -1162,6 +1268,7 @@ function normalizeCostSnapshot(raw) {
|
|
|
1162
1268
|
pricingAt,
|
|
1163
1269
|
pricingTimeSource,
|
|
1164
1270
|
pricingBand,
|
|
1271
|
+
pricingHoliday: raw.pricingHoliday === true,
|
|
1165
1272
|
pricingTimezone,
|
|
1166
1273
|
pricingPolicyId,
|
|
1167
1274
|
pricingPolicyHash,
|
|
@@ -1206,6 +1313,69 @@ async function fetchModelsDevCatalog(fetchImpl = globalThis.fetch, options = {})
|
|
|
1206
1313
|
}
|
|
1207
1314
|
}
|
|
1208
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
|
+
|
|
1209
1379
|
export {
|
|
1210
1380
|
COST_SCHEMA_VERSION,
|
|
1211
1381
|
COST_STATUSES,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-all-usage",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.16",
|
|
4
4
|
"description": "DeepSeek Harness usage dashboard with model, provider, workspace, cache, balance, and CSV insights",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dsh": {
|
|
33
33
|
"compatibility": {
|
|
34
|
-
"runtime": ">=0.1.1-rc.1 <0.1.5-0 || >=0.1.5-rc.1 <0.1.6-0 || >=0.1.7-rc.2 <0.1.8-0",
|
|
35
|
-
"verified": ["0.1.7-rc.2", "0.1.5-rc.2", "0.1.5-rc.1", "0.1.1-rc.2", "0.1.1-rc.1"]
|
|
34
|
+
"runtime": ">=0.1.1-rc.1 <0.1.5-0 || >=0.1.5-rc.1 <0.1.6-0 || >=0.1.7-rc.2 <0.1.8-0 || >=0.2.0-rc.2 <0.2.1-0",
|
|
35
|
+
"verified": ["0.2.0-rc.2", "0.1.7-rc.2", "0.1.5-rc.2", "0.1.5-rc.1", "0.1.1-rc.2", "0.1.1-rc.1"]
|
|
36
36
|
},
|
|
37
37
|
"bundle": {
|
|
38
38
|
"patch": "./cordis.patch.yml"
|