@ljwei-stak/dsh-model-router 0.15.0 → 0.16.1

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.
@@ -0,0 +1,120 @@
1
+ /** Public, browser-safe projections of dynamic benchmark/pricing snapshots. */
2
+ const DEFAULT_TTL_MS = 86_400_000
3
+ const currentTime = value => typeof value === 'function' ? value() : value ?? Date.now()
4
+ const exactRouteKey = route => `${String(route?.provider ?? '')}\0${String(route?.model ?? '')}`
5
+ const publicErrors = new Set(['snapshot-rollback-rejected', 'refresh-failed', 'storage-failed', 'refresh-interrupted'])
6
+ const publicError = value => publicErrors.has(value) ? value : value ? 'refresh-failed' : ''
7
+
8
+ /** Same public URL contract on Host and Client; validates syntax, not DNS routing. */
9
+ export function publicDynamicEndpoint(value) {
10
+ if (typeof value !== 'string' || value.length > 2048) throw new Error('invalid-public-url')
11
+ const url = new URL(value)
12
+ const host = url.hostname.toLowerCase()
13
+ if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash
14
+ || (url.port && url.port !== '443') || host.startsWith('[')
15
+ || !/^(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/u.test(host)
16
+ || /(?:^|\.)(?:localhost|local|internal|lan|home|test|invalid)$/u.test(host)) throw new Error('invalid-public-url')
17
+ return url.toString()
18
+ }
19
+
20
+ const publicEndpointMetadata = value => { try { return publicDynamicEndpoint(value) } catch { return null } }
21
+
22
+ /** Mask stopped or unmatched sources; an old A snapshot must never impersonate new B. */
23
+ export function projectDynamicData(data, { enabled = false, liveBenchEndpoint = 'https://livebench.ai', pricingSnapshotEndpoint = '' } = {}) {
24
+ if (enabled !== true) return { liveBench: null, pricing: null, status: {}, revision: '' }
25
+ const result = { liveBench: data?.liveBench ?? null, pricing: data?.pricing ?? null,
26
+ status: { ...(data?.status ?? {}) }, revision: String(data?.revision ?? '') }
27
+ for (const [kind, raw] of [['liveBench', liveBenchEndpoint], ['pricing', pricingSnapshotEndpoint]]) {
28
+ if (typeof raw === 'string' && raw.trim() === '') {
29
+ result[kind] = null
30
+ result.status[kind] = { disabled: true, endpoint: '', error: '' }
31
+ continue
32
+ }
33
+ let endpoint
34
+ try { endpoint = publicDynamicEndpoint(raw) }
35
+ catch {
36
+ result[kind] = null
37
+ result.status[kind] = { endpoint: '', error: 'refresh-failed' }
38
+ continue
39
+ }
40
+ const snapshot = result[kind]
41
+ const status = result.status[kind]
42
+ if (snapshot && snapshot.endpoint !== endpoint) {
43
+ result[kind] = null
44
+ result.status[kind] = { ...(status?.endpoint === endpoint ? status : {}), endpoint,
45
+ lastGoodEndpoint: publicEndpointMetadata(snapshot.endpoint), error: status?.endpoint === endpoint ? publicError(status?.error) : '' }
46
+ } else if (status?.endpoint !== endpoint) {
47
+ result.status[kind] = { endpoint, error: '' }
48
+ }
49
+ }
50
+ return result
51
+ }
52
+
53
+ /** Explicit user prices override the curated public feed, including stale last-good data. */
54
+ export function applyPricingSnapshot(routes, snapshot) {
55
+ if (snapshot?.kind !== 'pricing' || !snapshot.prices || typeof snapshot.prices !== 'object') return routes
56
+ return (Array.isArray(routes) ? routes : []).map(route => {
57
+ if (route?.pricingSource === 'user') return route
58
+ const entry = snapshot.prices[exactRouteKey(route)]
59
+ if (!entry) return route
60
+ return {
61
+ ...route,
62
+ pricing: { input: entry.input, output: entry.output, currency: 'USD',
63
+ ...(entry.cacheRead === undefined ? {} : { cacheRead: entry.cacheRead }),
64
+ ...(entry.cacheWrite === undefined ? {} : { cacheWrite: entry.cacheWrite }) },
65
+ pricingSource: 'dynamic', pricingVersion: snapshot.version, snapshotAsOf: entry.asOf,
66
+ }
67
+ })
68
+ }
69
+
70
+ function sourceSummary(data, kind, enabled, ttlMs, at) {
71
+ const snapshot = enabled ? data?.[kind] : null
72
+ const status = enabled ? data?.status?.[kind] : null
73
+ const verifiedAt = Number.isFinite(snapshot?.verifiedAt) ? snapshot.verifiedAt : null
74
+ const expired = verifiedAt !== null && (verifiedAt > at + 60_000 || at - verifiedAt > ttlMs)
75
+ const deadline = Number.isFinite(status?.requestDeadlineAt) ? status.requestDeadlineAt
76
+ : Number.isFinite(status?.lastAttemptAt) ? status.lastAttemptAt + 13_000 : null
77
+ const interrupted = Boolean(status?.requestId) && deadline !== null && at >= deadline
78
+ const pending = Boolean(status?.requestId) && !interrupted
79
+ const error = interrupted ? 'refresh-interrupted' : publicError(status?.error)
80
+ return {
81
+ status: !enabled || status?.disabled ? 'disabled' : !snapshot ? (error ? 'error' : 'missing')
82
+ : expired || error ? 'stale' : 'fresh',
83
+ verifiedAt, publishedAt: snapshot?.publishedAt ?? null,
84
+ version: snapshot?.version ?? null, source: snapshot?.source ?? null,
85
+ endpoint: publicEndpointMetadata(snapshot?.endpoint), configuredEndpoint: publicEndpointMetadata(status?.endpoint),
86
+ error: enabled ? error : '',
87
+ refreshing: enabled && pending, pending: enabled && pending,
88
+ lastAttemptAt: Number.isFinite(status?.lastAttemptAt) ? status.lastAttemptAt : null,
89
+ lastSuccessAt: Number.isFinite(status?.lastSuccessAt) ? status.lastSuccessAt : null,
90
+ requestDeadlineAt: pending ? deadline : null,
91
+ lastGoodEndpoint: publicEndpointMetadata(status?.lastGoodEndpoint),
92
+ ...(kind === 'liveBench' ? { modelCount: Object.keys(snapshot?.models ?? {}).length }
93
+ : { rowCount: Object.keys(snapshot?.prices ?? {}).length }),
94
+ }
95
+ }
96
+
97
+ /** Age means source verification age, not the most recent attempted download. */
98
+ export function dynamicDataSummary(data, { enabled = false, ttlMs = DEFAULT_TTL_MS, now = Date.now } = {}) {
99
+ const at = currentTime(now)
100
+ const ttl = Number.isFinite(ttlMs) && ttlMs >= 0 ? ttlMs : DEFAULT_TTL_MS
101
+ return {
102
+ enabled: enabled === true,
103
+ liveBench: sourceSummary(data, 'liveBench', enabled === true, ttl, at),
104
+ pricing: sourceSummary(data, 'pricing', enabled === true, ttl, at),
105
+ revision: enabled === true ? String(data?.revision ?? '') : '',
106
+ }
107
+ }
108
+
109
+ /** No prompts, credentials, private endpoints, raw errors or price fetching enter the Client. */
110
+ export function routingData(data, enabled = false) {
111
+ return {
112
+ liveBench: enabled === true ? data?.liveBench ?? null : null,
113
+ liveBenchError: enabled === true ? publicError(data?.status?.liveBench?.error) : '',
114
+ dataVersions: {
115
+ liveBench: enabled === true ? data?.liveBench?.version ?? null : null,
116
+ pricing: enabled === true ? data?.pricing?.version ?? null : null,
117
+ revision: enabled === true ? String(data?.revision ?? '') : '',
118
+ },
119
+ }
120
+ }
@@ -0,0 +1,372 @@
1
+ /**
2
+ * Opt-in public-data refresh, never an API/model call. Pricing is a user-chosen
3
+ * curated JSON feed with official documentation citations, NOT a universal
4
+ * scraper or an independently authenticated official price service.
5
+ *
6
+ * Whole snapshots are validated before atomic promotion; a request reservation
7
+ * prevents late responses from replacing another process's newer request.
8
+ * Domain-name checks are not DNS-rebinding isolation: deployments must apply
9
+ * their own outbound network policy if untrusted users can configure endpoints.
10
+ */
11
+ import { createHash, randomUUID } from 'node:crypto'
12
+ import { fetchLiveBenchSnapshot } from './livebench.mjs'
13
+ import { projectDynamicData, publicDynamicEndpoint } from './dynamic-data-view.mjs'
14
+ export { applyPricingSnapshot, dynamicDataSummary, routingData, projectDynamicData, publicDynamicEndpoint } from './dynamic-data-view.mjs'
15
+
16
+ const DEFAULT_TTL_MS = 86_400_000
17
+ const TIMEOUT_MS = 8000
18
+ const MAX_PAYLOAD_BYTES = 4 * 1024 * 1024
19
+ const MAX_ROWS = 2000
20
+ const MAX_CLOCK_SKEW_MS = 60_000
21
+ const RESERVATION_GRACE_MS = 5000
22
+ const ROW_FIELDS = new Set(['provider', 'model', 'input', 'output', 'cacheRead', 'cacheWrite', 'currency', 'unit', 'asOf', 'sourceUrl'])
23
+ const PRICE_FIELDS = new Set(['schemaVersion', 'kind', 'version', 'publishedAt', 'rows'])
24
+ const SCORE_FIELDS = new Set(['reasoning', 'code', 'math', 'research', 'writing', 'vision', 'summarization', 'classification'])
25
+ const object = value => value && typeof value === 'object' && !Array.isArray(value)
26
+ const finite = value => typeof value === 'number' && Number.isFinite(value)
27
+ const emptyData = () => ({ liveBench: null, pricing: null, status: {}, revision: '' })
28
+
29
+ function stable(value) {
30
+ if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`
31
+ if (object(value)) return `{${Object.keys(value).filter(key => value[key] !== undefined).sort()
32
+ .map(key => `${JSON.stringify(key)}:${stable(value[key])}`).join(',')}}`
33
+ return JSON.stringify(value)
34
+ }
35
+
36
+ const hash = value => createHash('sha256').update(stable(value)).digest('hex')
37
+ function dataOf(state) {
38
+ const data = state?.dynamicData
39
+ return object(data) ? { liveBench: data.liveBench ?? null, pricing: data.pricing ?? null,
40
+ status: object(data.status) ? { ...data.status } : {}, revision: String(data.revision ?? '') } : emptyData()
41
+ }
42
+
43
+ function revisionOf(data) {
44
+ return hash({ liveBench: data.liveBench?.hash ?? null, pricing: data.pricing?.hash ?? null, status: data.status })
45
+ }
46
+
47
+ const publicUrl = publicDynamicEndpoint
48
+
49
+ function identifier(value, max = 240) {
50
+ if (typeof value !== 'string' || value.trim() !== value || !value || value.length > max
51
+ || /[\u0000-\u001f\u007f]/u.test(value)) throw new Error('invalid-identifier')
52
+ return value
53
+ }
54
+
55
+ function timestamp(value, at) {
56
+ if (!Number.isSafeInteger(value) || value < Date.UTC(2000, 0, 1)
57
+ || value > at + MAX_CLOCK_SKEW_MS) throw new Error('invalid-timestamp')
58
+ return value
59
+ }
60
+
61
+ function boundedPrice(value) {
62
+ if (!finite(value) || value < 0 || value > 1_000_000) throw new Error('invalid-price')
63
+ return value
64
+ }
65
+
66
+ function pricingSnapshot(payload, endpoint, at) {
67
+ if (!object(payload) || Object.keys(payload).some(key => !PRICE_FIELDS.has(key))
68
+ || payload.schemaVersion !== 1 || payload.kind !== 'pricing'
69
+ || !Array.isArray(payload.rows) || payload.rows.length === 0 || payload.rows.length > MAX_ROWS) throw new Error('invalid-price-snapshot')
70
+ const version = identifier(payload.version, 160)
71
+ const publishedAt = timestamp(payload.publishedAt, at)
72
+ const prices = {}
73
+ let verifiedAt = Number.POSITIVE_INFINITY
74
+ for (const row of payload.rows) {
75
+ if (!object(row) || Object.keys(row).some(key => !ROW_FIELDS.has(key))
76
+ || row.currency !== 'USD' || row.unit !== 'per-million-tokens') throw new Error('unsupported-price-schema')
77
+ const provider = identifier(row.provider, 160)
78
+ const model = identifier(row.model)
79
+ const key = `${provider}\0${model}`
80
+ if (Object.hasOwn(prices, key)) throw new Error('duplicate-route')
81
+ const asOf = timestamp(row.asOf, at)
82
+ if (asOf > publishedAt) throw new Error('verification-after-publication')
83
+ verifiedAt = Math.min(verifiedAt, asOf)
84
+ prices[key] = { provider, model, input: boundedPrice(row.input), output: boundedPrice(row.output),
85
+ ...(row.cacheRead === undefined ? {} : { cacheRead: boundedPrice(row.cacheRead) }),
86
+ ...(row.cacheWrite === undefined ? {} : { cacheWrite: boundedPrice(row.cacheWrite) }),
87
+ currency: 'USD', unit: 'per-million-tokens', asOf, sourceUrl: publicUrl(row.sourceUrl) }
88
+ }
89
+ const snapshot = { kind: 'pricing', source: 'curated-pricing-json', endpoint, verifiedAt, publishedAt, version, prices }
90
+ return { ...snapshot, hash: hash({ kind: snapshot.kind, version, publishedAt, prices }) }
91
+ }
92
+
93
+ function benchmarkSnapshot(payload, endpoint, at) {
94
+ if (!object(payload) || !object(payload.models) || Object.keys(payload.models).length === 0
95
+ || Object.keys(payload.models).length > MAX_ROWS) throw new Error('empty-benchmark')
96
+ const models = {}
97
+ for (const [id, row] of Object.entries(payload.models)) {
98
+ // The existing adapter supplies normalized model ids, not provider aliases.
99
+ if (!/^[a-z0-9]{1,240}$/u.test(id) || !object(row) || !object(row.scores)
100
+ || !finite(row.overall) || row.overall < 0 || row.overall > 1) throw new Error('invalid-benchmark-row')
101
+ const model = identifier(row.model)
102
+ if (model.toLowerCase().replace(/[^a-z0-9]+/gu, '') !== id) throw new Error('invalid-benchmark-model-id')
103
+ const scores = {}
104
+ for (const [task, score] of Object.entries(row.scores)) {
105
+ if (!SCORE_FIELDS.has(task) || !finite(score) || score < 0 || score > 1) throw new Error('invalid-benchmark-score')
106
+ scores[task] = score
107
+ }
108
+ if (row.overallSource !== 'explicit' && row.overallSource !== 'derived') throw new Error('invalid-benchmark-provenance')
109
+ if (row.rank !== undefined && (!finite(row.rank) || row.rank < 0)) throw new Error('invalid-benchmark-rank')
110
+ models[id] = { model, overall: row.overall, overallSource: row.overallSource, scores,
111
+ ...(row.rank === undefined ? {} : { rank: row.rank }) }
112
+ }
113
+ const source = identifier(payload.source ?? 'livebench-json-mirror', 160)
114
+ const release = /^livebench:(20\d{2}-\d{2}-\d{2})$/u.exec(source)?.[1]
115
+ let publishedAt = null
116
+ if (release) {
117
+ publishedAt = Date.parse(`${release}T00:00:00.000Z`)
118
+ if (!Number.isSafeInteger(publishedAt) || new Date(publishedAt).toISOString().slice(0, 10) !== release
119
+ || publishedAt > at + MAX_CLOCK_SKEW_MS) throw new Error('invalid-benchmark-release')
120
+ } else if (payload.publishedAt !== undefined) {
121
+ publishedAt = timestamp(payload.publishedAt, at)
122
+ }
123
+ const contentHash = hash({ source, publishedAt, models })
124
+ return { kind: 'liveBench', source, endpoint, verifiedAt: at, publishedAt, exactModelMatch: true,
125
+ version: release ?? contentHash, hash: contentHash, models }
126
+ }
127
+
128
+ function mayPromote(previous, candidate) {
129
+ if (!previous) return true
130
+ // If the feed supplies no publication metadata, hash identity is NOT time order.
131
+ if (finite(previous.publishedAt) && finite(candidate.publishedAt) && candidate.publishedAt < previous.publishedAt) return false
132
+ if (candidate.kind === 'pricing') {
133
+ if (finite(previous.verifiedAt) && candidate.verifiedAt < previous.verifiedAt) return false
134
+ for (const [key, entry] of Object.entries(candidate.prices)) {
135
+ if (finite(previous.prices?.[key]?.asOf) && entry.asOf < previous.prices[key].asOf) return false
136
+ }
137
+ if (previous.publishedAt === candidate.publishedAt && previous.hash !== candidate.hash) return false
138
+ if (previous.version === candidate.version && previous.hash !== candidate.hash) return false
139
+ }
140
+ return true
141
+ }
142
+
143
+ function cancelBody(body) {
144
+ try { Promise.resolve(body?.cancel?.()).catch(() => {}) } catch { /* best effort; never block rejection */ }
145
+ }
146
+
147
+ async function limitedText(response, signal) {
148
+ const length = Number(response.headers?.get?.('content-length'))
149
+ if (Number.isFinite(length) && length > MAX_PAYLOAD_BYTES) {
150
+ cancelBody(response.body)
151
+ throw new Error('payload-too-large')
152
+ }
153
+ if (!response.body?.getReader) {
154
+ const text = await response.text()
155
+ if (Buffer.byteLength(text, 'utf8') > MAX_PAYLOAD_BYTES) throw new Error('payload-too-large')
156
+ return text
157
+ }
158
+ const reader = response.body.getReader()
159
+ const decoder = new TextDecoder('utf-8', { fatal: true })
160
+ let size = 0
161
+ let text = ''
162
+ const cancelReader = () => { try { Promise.resolve(reader.cancel()).catch(() => {}) } catch {} }
163
+ signal?.addEventListener('abort', cancelReader, { once: true })
164
+ try {
165
+ for (;;) {
166
+ if (signal?.aborted) throw new Error('refresh-aborted')
167
+ const chunk = await reader.read()
168
+ if (signal?.aborted) throw new Error('refresh-aborted')
169
+ if (chunk.done) return text + decoder.decode()
170
+ size += chunk.value.byteLength
171
+ if (size > MAX_PAYLOAD_BYTES) throw new Error('payload-too-large')
172
+ text += decoder.decode(chunk.value, { stream: true })
173
+ }
174
+ } catch (error) {
175
+ cancelReader()
176
+ throw error
177
+ } finally { signal?.removeEventListener('abort', cancelReader); reader.releaseLock() }
178
+ }
179
+
180
+ function publicFetch(fetchImpl, outerSignal) {
181
+ return async (input, options = {}) => {
182
+ const endpoint = publicUrl(String(input))
183
+ const controller = new AbortController()
184
+ const signals = [...new Set([outerSignal, options.signal].filter(Boolean))]
185
+ const abort = () => controller.abort(new Error('refresh-aborted'))
186
+ const cleanup = () => { for (const signal of signals) signal.removeEventListener('abort', abort) }
187
+ for (const signal of signals) {
188
+ if (signal.aborted) abort()
189
+ else signal.addEventListener('abort', abort, { once: true })
190
+ }
191
+ controller.signal.addEventListener('abort', cleanup, { once: true })
192
+ let response
193
+ try {
194
+ if (controller.signal.aborted) throw new Error('refresh-aborted')
195
+ response = await fetchImpl(endpoint, {
196
+ method: 'GET', headers: { accept: 'application/json,text/csv,text/html' },
197
+ credentials: 'omit', redirect: 'error', signal: controller.signal,
198
+ })
199
+ if (controller.signal.aborted || !response?.ok || response.redirected) {
200
+ throw new Error('public-fetch-failed')
201
+ }
202
+ if (response.url && publicUrl(response.url) !== endpoint) {
203
+ throw new Error('public-fetch-redirected')
204
+ }
205
+ return { ok: true, status: response.status, headers: response.headers,
206
+ text: async () => { try { return await limitedText(response, controller.signal) } finally { cleanup() } } }
207
+ } catch (error) { cancelBody(response?.body); cleanup(); throw error }
208
+ }
209
+ }
210
+
211
+ async function withTimeout(run, controller, timeoutMs) {
212
+ let rejectAborted
213
+ const aborted = new Promise((_, reject) => { rejectAborted = reject })
214
+ const onAbort = () => rejectAborted(new Error('refresh-aborted'))
215
+ controller.signal.addEventListener('abort', onAbort, { once: true })
216
+ const timer = setTimeout(() => controller.abort(new Error('refresh-timeout')), timeoutMs)
217
+ try {
218
+ if (controller.signal.aborted) throw new Error('refresh-aborted')
219
+ return await Promise.race([run(controller.signal), aborted])
220
+ } finally { clearTimeout(timer); controller.signal.removeEventListener('abort', onAbort) }
221
+ }
222
+
223
+ function due(data, kind, endpoint, ttlMs, at, force) {
224
+ const status = data.status?.[kind]
225
+ if (force || status?.endpoint !== endpoint || !finite(status?.lastAttemptAt)) return true
226
+ if (at < status.lastAttemptAt) return true // a corrected clock must not freeze refresh forever
227
+ if (status.requestId) {
228
+ const deadline = finite(status.requestDeadlineAt) ? status.requestDeadlineAt : status.lastAttemptAt + TIMEOUT_MS + RESERVATION_GRACE_MS
229
+ return at >= deadline
230
+ }
231
+ const delay = status.error ? Math.min(ttlMs, 300_000) : ttlMs
232
+ return at - status.lastAttemptAt >= delay
233
+ }
234
+
235
+ /**
236
+ * Store API: read() -> state, update(change) -> change's return value; change
237
+ * mutates the fresh state under the caller's interprocess/atomic write lock.
238
+ */
239
+ export function createDynamicDataRefresher({ store, fetchImpl = globalThis.fetch, now = Date.now,
240
+ fetchBenchmark = fetchLiveBenchSnapshot, timeoutMs = TIMEOUT_MS } = {}) {
241
+ if (typeof store?.read !== 'function' || typeof store?.update !== 'function') throw new Error('dynamic-data-store-required')
242
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > TIMEOUT_MS) throw new Error('invalid-refresh-timeout')
243
+ const flights = new Map()
244
+ const active = new Map()
245
+ const sourceStates = new Map()
246
+ const generationOf = kind => sourceStates.get(kind)?.generation ?? 0
247
+ const refresh = async (options = {}) => {
248
+ if (options.enabled !== true) {
249
+ for (const kind of ['liveBench', 'pricing']) sourceStates.set(kind, { fingerprint: '', generation: generationOf(kind) + 1 })
250
+ const pending = [...active.values()]
251
+ for (const request of pending) request.controller.abort()
252
+ if (pending.length) {
253
+ try { await store.update(state => {
254
+ const data = dataOf(state)
255
+ for (const request of pending) if (data.status[request.kind]?.requestId === request.requestId) {
256
+ data.status[request.kind] = { ...data.status[request.kind], disabled: true, endpoint: '', error: '' }
257
+ delete data.status[request.kind].requestId
258
+ delete data.status[request.kind].requestDeadlineAt
259
+ }
260
+ data.revision = revisionOf(data)
261
+ state.dynamicData = data
262
+ }) } catch { /* disabled projection is safe even if cancellation cannot be persisted */ }
263
+ }
264
+ return emptyData()
265
+ }
266
+ const ttlMs = finite(options.ttlMs) && options.ttlMs >= 0 ? options.ttlMs : DEFAULT_TTL_MS
267
+ const liveBenchEndpoint = options.liveBenchEndpoint ?? 'https://livebench.ai'
268
+ const pricingSnapshotEndpoint = options.pricingSnapshotEndpoint ?? ''
269
+ // Never reserve, persist, hash or return an unchecked raw endpoint: a bad
270
+ // URL can contain credentials even though no network request is made.
271
+ const sources = [['liveBench', liveBenchEndpoint], ['pricing', pricingSnapshotEndpoint]].map(([kind, raw]) => {
272
+ if (typeof raw === 'string' && raw.trim() === '') return { kind, endpoint: '', disabled: true, invalid: false }
273
+ try { return { kind, endpoint: publicUrl(raw), disabled: false, invalid: false } }
274
+ catch { return { kind, endpoint: '', disabled: false, invalid: true } }
275
+ })
276
+ for (const source of sources) {
277
+ const fingerprint = stable(source)
278
+ if (fingerprint !== sourceStates.get(source.kind)?.fingerprint) {
279
+ sourceStates.set(source.kind, { fingerprint, generation: generationOf(source.kind) + 1 })
280
+ for (const request of active.values()) if (request.kind === source.kind) request.controller.abort()
281
+ }
282
+ }
283
+ const generations = Object.fromEntries(sources.map(source => [source.kind, generationOf(source.kind)]))
284
+ const key = stable([sources, generations, ttlMs, options.force === true])
285
+ if (flights.has(key)) return flights.get(key)
286
+ const operation = (async () => {
287
+ const initial = dataOf(await store.read())
288
+ const storageFailures = new Set()
289
+ await Promise.all(sources.filter(source => source.disabled && (!initial.status[source.kind]?.disabled || initial.status[source.kind]?.requestId))
290
+ .map(async ({ kind }) => {
291
+ try { await store.update(state => {
292
+ if (generationOf(kind) !== generations[kind]) return
293
+ const data = dataOf(state)
294
+ data.status[kind] = { ...(data.status[kind] ?? {}), disabled: true, endpoint: '', error: '' }
295
+ delete data.status[kind].requestId
296
+ delete data.status[kind].requestDeadlineAt
297
+ data.revision = revisionOf(data)
298
+ state.dynamicData = data
299
+ }) } catch { storageFailures.add(kind) }
300
+ }))
301
+ const requests = sources.filter(source => !source.disabled)
302
+ .filter(({ kind, endpoint }) => due(initial, kind, endpoint, ttlMs, now(), options.force === true))
303
+ const settled = await Promise.allSettled(requests.map(async ({ kind, endpoint, invalid }) => {
304
+ const requestId = randomUUID()
305
+ const controller = new AbortController()
306
+ const request = { kind, endpoint, requestId, controller }
307
+ active.set(requestId, request)
308
+ try {
309
+ // Reserve before network I/O. Another process or source change wins by
310
+ // replacing this token; a late completion then cannot mutate its data.
311
+ const reserved = await store.update(state => {
312
+ if (generationOf(kind) !== generations[kind] || controller.signal.aborted) return false
313
+ const data = dataOf(state)
314
+ const at = now()
315
+ if (!due(data, kind, endpoint, ttlMs, at, options.force === true)) return false
316
+ data.status[kind] = { ...(data.status[kind] ?? {}), disabled: false, endpoint,
317
+ lastAttemptAt: at, requestId, requestDeadlineAt: at + timeoutMs + RESERVATION_GRACE_MS }
318
+ data.revision = revisionOf(data)
319
+ state.dynamicData = data
320
+ return true
321
+ })
322
+ if (!reserved) return
323
+ let candidate
324
+ let error = ''
325
+ try {
326
+ if (invalid) throw new Error('invalid-public-url')
327
+ if (typeof fetchImpl !== 'function') throw new Error('fetch-unavailable')
328
+ candidate = await withTimeout(async signal => {
329
+ const fetchPublic = publicFetch(fetchImpl, signal)
330
+ if (kind === 'liveBench') {
331
+ const payload = await fetchBenchmark({ endpoint, fetchImpl: fetchPublic, timeoutMs, signal, strict: true })
332
+ return benchmarkSnapshot(payload, endpoint, now())
333
+ }
334
+ const response = await fetchPublic(endpoint, { signal })
335
+ return pricingSnapshot(JSON.parse(await response.text()), endpoint, now())
336
+ }, controller, timeoutMs)
337
+ } catch { error = 'refresh-failed' }
338
+ await store.update(state => {
339
+ const data = dataOf(state)
340
+ if (data.status[kind]?.requestId !== requestId) return false
341
+ if (candidate && !mayPromote(data[kind], candidate)) error = 'snapshot-rollback-rejected'
342
+ if (candidate && !error) data[kind] = candidate
343
+ data.status[kind] = { ...data.status[kind], error,
344
+ ...(candidate && !error ? { lastSuccessAt: now() } : {}) }
345
+ delete data.status[kind].requestId
346
+ delete data.status[kind].requestDeadlineAt
347
+ data.revision = revisionOf(data)
348
+ state.dynamicData = data
349
+ return true
350
+ })
351
+ } finally { active.delete(requestId) }
352
+ }))
353
+ for (let index = 0; index < settled.length; index += 1) if (settled[index].status === 'rejected') storageFailures.add(requests[index].kind)
354
+ const result = dataOf(await store.read())
355
+ for (const kind of storageFailures) {
356
+ const endpoint = sources.find(source => source.kind === kind)?.endpoint ?? ''
357
+ result.status[kind] = { ...(result.status[kind]?.endpoint === endpoint ? result.status[kind] : {}),
358
+ endpoint, error: 'storage-failed' }
359
+ delete result.status[kind].requestId
360
+ delete result.status[kind].requestDeadlineAt
361
+ }
362
+ // A user clearing a source disables its use immediately, without erasing
363
+ // recoverable last-good snapshots from disk.
364
+ result.revision = revisionOf(result)
365
+ return projectDynamicData(result, options)
366
+ })()
367
+ flights.set(key, operation)
368
+ try { return await operation }
369
+ finally { if (flights.get(key) === operation) flights.delete(key) }
370
+ }
371
+ return { refresh }
372
+ }
@@ -73,7 +73,7 @@ function annotate(channel) {
73
73
  */
74
74
  export function createPlanFromRoutes(task, availableRoutes, {
75
75
  mode = 'single', budgetUsd = 0, installedToolIds = [], runnableToolIds = [],
76
- pricing = {}, liveBench = null, cacheReadRatio = 0, cacheWriteRatio = 0,
76
+ pricing = {}, liveBench = null, liveBenchError = '', dataVersions = null, learning = null, cacheReadRatio = 0, cacheWriteRatio = 0,
77
77
  directProvider = '', directModel = '', preset = 'balanced', loggedOutToolIds = [],
78
78
  } = {}) {
79
79
  const taskText = clean(task)
@@ -107,6 +107,9 @@ export function createPlanFromRoutes(task, availableRoutes, {
107
107
  budgetUsd: Math.max(0, Number.isFinite(budgetUsd) ? budgetUsd : 0),
108
108
  pricing,
109
109
  liveBench,
110
+ liveBenchError,
111
+ dataVersions,
112
+ learning,
110
113
  cacheReadRatio,
111
114
  cacheWriteRatio,
112
115
  preset,
@@ -126,7 +129,7 @@ export function createPlanFromRoutes(task, availableRoutes, {
126
129
  ? '部分路线尚未配置该供应商的美元输入/输出单价,无法计算可靠的总费用与节省比例;请在模型价格设置中补齐。'
127
130
  : '费用按已提供的美元单价和估计 token 数计算,不是供应商账单,也不是硬性支出上限。',
128
131
  qualityNotice: plan.optimization.qualityEvidenceComplete
129
- ? '模型质量使用已提供评分或基准数据估计,仍需实际任务验证。'
132
+ ? '模型质量使用已提供评分或基准数据估计;分数与门槛不是答对概率,也不保证实际质量,仍需独立任务验证。'
130
133
  : '部分模型质量缺少可核验评分;目录启发式只供选择参考,质量门槛和节省比例无法保证。',
131
134
  modalityNotice: needsImage
132
135
  ? plan.unassignableTasks.length > 0