@ljwei-stak/dsh-model-router 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
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 publicError = value => value === 'snapshot-rollback-rejected' ? value : value ? 'refresh-failed' : ''
6
+
7
+ /** Explicit user prices override the curated public feed, including stale last-good data. */
8
+ export function applyPricingSnapshot(routes, snapshot) {
9
+ if (snapshot?.kind !== 'pricing' || !snapshot.prices || typeof snapshot.prices !== 'object') return routes
10
+ return (Array.isArray(routes) ? routes : []).map(route => {
11
+ if (route?.pricingSource === 'user') return route
12
+ const entry = snapshot.prices[exactRouteKey(route)]
13
+ if (!entry) return route
14
+ return {
15
+ ...route,
16
+ pricing: { input: entry.input, output: entry.output, currency: 'USD',
17
+ ...(entry.cacheRead === undefined ? {} : { cacheRead: entry.cacheRead }),
18
+ ...(entry.cacheWrite === undefined ? {} : { cacheWrite: entry.cacheWrite }) },
19
+ pricingSource: 'dynamic', pricingVersion: snapshot.version, snapshotAsOf: entry.asOf,
20
+ }
21
+ })
22
+ }
23
+
24
+ function sourceSummary(data, kind, enabled, ttlMs, at) {
25
+ const snapshot = enabled ? data?.[kind] : null
26
+ const status = enabled ? data?.status?.[kind] : null
27
+ const verifiedAt = Number.isFinite(snapshot?.verifiedAt) ? snapshot.verifiedAt : null
28
+ const expired = verifiedAt !== null && at - verifiedAt > ttlMs
29
+ const error = publicError(status?.error)
30
+ return {
31
+ status: !enabled || status?.disabled ? 'disabled' : !snapshot ? (error ? 'error' : 'missing')
32
+ : expired || error ? 'stale' : 'fresh',
33
+ verifiedAt, publishedAt: snapshot?.publishedAt ?? null,
34
+ version: snapshot?.version ?? null, source: snapshot?.source ?? null,
35
+ endpoint: snapshot?.endpoint ?? null, configuredEndpoint: status?.endpoint ?? null,
36
+ error: enabled ? error : '',
37
+ ...(kind === 'liveBench' ? { modelCount: Object.keys(snapshot?.models ?? {}).length }
38
+ : { rowCount: Object.keys(snapshot?.prices ?? {}).length }),
39
+ }
40
+ }
41
+
42
+ /** Age means source verification age, not the most recent attempted download. */
43
+ export function dynamicDataSummary(data, { enabled = false, ttlMs = DEFAULT_TTL_MS, now = Date.now } = {}) {
44
+ const at = currentTime(now)
45
+ const ttl = Number.isFinite(ttlMs) && ttlMs >= 0 ? ttlMs : DEFAULT_TTL_MS
46
+ return {
47
+ enabled: enabled === true,
48
+ liveBench: sourceSummary(data, 'liveBench', enabled === true, ttl, at),
49
+ pricing: sourceSummary(data, 'pricing', enabled === true, ttl, at),
50
+ revision: enabled === true ? String(data?.revision ?? '') : '',
51
+ }
52
+ }
53
+
54
+ /** No prompts, credentials, private endpoints, raw errors or price fetching enter the Client. */
55
+ export function routingData(data, enabled = false) {
56
+ return {
57
+ liveBench: enabled === true ? data?.liveBench ?? null : null,
58
+ liveBenchError: enabled === true ? publicError(data?.status?.liveBench?.error) : '',
59
+ dataVersions: {
60
+ liveBench: enabled === true ? data?.liveBench?.version ?? null : null,
61
+ pricing: enabled === true ? data?.pricing?.version ?? null : null,
62
+ revision: enabled === true ? String(data?.revision ?? '') : '',
63
+ },
64
+ }
65
+ }
@@ -0,0 +1,294 @@
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 { isIP } from 'node:net'
13
+ import { fetchLiveBenchSnapshot } from './livebench.mjs'
14
+ export { applyPricingSnapshot, dynamicDataSummary, routingData } 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 ROW_FIELDS = new Set(['provider', 'model', 'input', 'output', 'cacheRead', 'cacheWrite', 'currency', 'unit', 'asOf', 'sourceUrl'])
22
+ const PRICE_FIELDS = new Set(['schemaVersion', 'kind', 'version', 'publishedAt', 'rows'])
23
+ const SCORE_FIELDS = new Set(['reasoning', 'code', 'math', 'research', 'writing', 'vision', 'summarization', 'classification'])
24
+ const object = value => value && typeof value === 'object' && !Array.isArray(value)
25
+ const finite = value => typeof value === 'number' && Number.isFinite(value)
26
+ const emptyData = () => ({ liveBench: null, pricing: null, status: {}, revision: '' })
27
+
28
+ function stable(value) {
29
+ if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`
30
+ if (object(value)) return `{${Object.keys(value).filter(key => value[key] !== undefined).sort()
31
+ .map(key => `${JSON.stringify(key)}:${stable(value[key])}`).join(',')}}`
32
+ return JSON.stringify(value)
33
+ }
34
+
35
+ const hash = value => createHash('sha256').update(stable(value)).digest('hex')
36
+ function dataOf(state) {
37
+ const data = state?.dynamicData
38
+ return object(data) ? { liveBench: data.liveBench ?? null, pricing: data.pricing ?? null,
39
+ status: object(data.status) ? { ...data.status } : {}, revision: String(data.revision ?? '') } : emptyData()
40
+ }
41
+
42
+ function revisionOf(data) {
43
+ return hash({ liveBench: data.liveBench?.hash ?? null, pricing: data.pricing?.hash ?? null, status: data.status })
44
+ }
45
+
46
+ function publicUrl(value) {
47
+ if (typeof value !== 'string' || value.length > 2048) throw new Error('invalid-public-url')
48
+ const url = new URL(value)
49
+ const host = url.hostname.toLowerCase()
50
+ if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash
51
+ || (url.port && url.port !== '443') || isIP(host) || host.startsWith('[')
52
+ || !/^(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/u.test(host)
53
+ || /(?:^|\.)(?:localhost|local|internal|lan|home|test|invalid)$/u.test(host)) throw new Error('invalid-public-url')
54
+ return url.toString()
55
+ }
56
+
57
+ function identifier(value, max = 240) {
58
+ if (typeof value !== 'string' || value.trim() !== value || !value || value.length > max
59
+ || /[\u0000-\u001f\u007f]/u.test(value)) throw new Error('invalid-identifier')
60
+ return value
61
+ }
62
+
63
+ function timestamp(value, at) {
64
+ if (!Number.isSafeInteger(value) || value < Date.UTC(2000, 0, 1)
65
+ || value > at + MAX_CLOCK_SKEW_MS) throw new Error('invalid-timestamp')
66
+ return value
67
+ }
68
+
69
+ function boundedPrice(value) {
70
+ if (!finite(value) || value < 0 || value > 1_000_000) throw new Error('invalid-price')
71
+ return value
72
+ }
73
+
74
+ function pricingSnapshot(payload, endpoint, at) {
75
+ if (!object(payload) || Object.keys(payload).some(key => !PRICE_FIELDS.has(key))
76
+ || payload.schemaVersion !== 1 || payload.kind !== 'pricing'
77
+ || !Array.isArray(payload.rows) || payload.rows.length === 0 || payload.rows.length > MAX_ROWS) throw new Error('invalid-price-snapshot')
78
+ const version = identifier(payload.version, 160)
79
+ const publishedAt = timestamp(payload.publishedAt, at)
80
+ const prices = {}
81
+ let verifiedAt = Number.POSITIVE_INFINITY
82
+ for (const row of payload.rows) {
83
+ if (!object(row) || Object.keys(row).some(key => !ROW_FIELDS.has(key))
84
+ || row.currency !== 'USD' || row.unit !== 'per-million-tokens') throw new Error('unsupported-price-schema')
85
+ const provider = identifier(row.provider, 160)
86
+ const model = identifier(row.model)
87
+ const key = `${provider}\0${model}`
88
+ if (Object.hasOwn(prices, key)) throw new Error('duplicate-route')
89
+ const asOf = timestamp(row.asOf, at)
90
+ if (asOf > publishedAt) throw new Error('verification-after-publication')
91
+ verifiedAt = Math.min(verifiedAt, asOf)
92
+ prices[key] = { provider, model, input: boundedPrice(row.input), output: boundedPrice(row.output),
93
+ ...(row.cacheRead === undefined ? {} : { cacheRead: boundedPrice(row.cacheRead) }),
94
+ ...(row.cacheWrite === undefined ? {} : { cacheWrite: boundedPrice(row.cacheWrite) }),
95
+ currency: 'USD', unit: 'per-million-tokens', asOf, sourceUrl: publicUrl(row.sourceUrl) }
96
+ }
97
+ const snapshot = { kind: 'pricing', source: 'curated-pricing-json', endpoint, verifiedAt, publishedAt, version, prices }
98
+ return { ...snapshot, hash: hash({ kind: snapshot.kind, version, publishedAt, prices }) }
99
+ }
100
+
101
+ function benchmarkSnapshot(payload, endpoint, at) {
102
+ if (!object(payload) || !object(payload.models) || Object.keys(payload.models).length === 0
103
+ || Object.keys(payload.models).length > MAX_ROWS) throw new Error('empty-benchmark')
104
+ const models = {}
105
+ for (const [id, row] of Object.entries(payload.models)) {
106
+ // The existing adapter supplies normalized model ids, not provider aliases.
107
+ if (!/^[a-z0-9]{1,240}$/u.test(id) || !object(row) || !object(row.scores)
108
+ || !finite(row.overall) || row.overall < 0 || row.overall > 1) throw new Error('invalid-benchmark-row')
109
+ const model = identifier(row.model)
110
+ if (model.toLowerCase().replace(/[^a-z0-9]+/gu, '') !== id) throw new Error('invalid-benchmark-model-id')
111
+ const scores = {}
112
+ for (const [task, score] of Object.entries(row.scores)) {
113
+ if (!SCORE_FIELDS.has(task) || !finite(score) || score < 0 || score > 1) throw new Error('invalid-benchmark-score')
114
+ scores[task] = score
115
+ }
116
+ if (row.overallSource !== 'explicit' && row.overallSource !== 'derived') throw new Error('invalid-benchmark-provenance')
117
+ if (row.rank !== undefined && (!finite(row.rank) || row.rank < 0)) throw new Error('invalid-benchmark-rank')
118
+ models[id] = { model, overall: row.overall, overallSource: row.overallSource, scores,
119
+ ...(row.rank === undefined ? {} : { rank: row.rank }) }
120
+ }
121
+ const source = identifier(payload.source ?? 'livebench-json-mirror', 160)
122
+ const release = /^livebench:(20\d{2}-\d{2}-\d{2})$/u.exec(source)?.[1]
123
+ let publishedAt = null
124
+ if (release) {
125
+ publishedAt = Date.parse(`${release}T00:00:00.000Z`)
126
+ if (!Number.isSafeInteger(publishedAt) || new Date(publishedAt).toISOString().slice(0, 10) !== release
127
+ || publishedAt > at + MAX_CLOCK_SKEW_MS) throw new Error('invalid-benchmark-release')
128
+ } else if (payload.publishedAt !== undefined) {
129
+ publishedAt = timestamp(payload.publishedAt, at)
130
+ }
131
+ const contentHash = hash({ source, publishedAt, models })
132
+ return { kind: 'liveBench', source, endpoint, verifiedAt: at, publishedAt, exactModelMatch: true,
133
+ version: release ?? contentHash, hash: contentHash, models }
134
+ }
135
+
136
+ function mayPromote(previous, candidate) {
137
+ if (!previous) return true
138
+ // If the feed supplies no publication metadata, hash identity is NOT time order.
139
+ if (finite(previous.publishedAt) && finite(candidate.publishedAt) && candidate.publishedAt < previous.publishedAt) return false
140
+ if (candidate.kind === 'pricing') {
141
+ if (finite(previous.verifiedAt) && candidate.verifiedAt < previous.verifiedAt) return false
142
+ for (const [key, entry] of Object.entries(candidate.prices)) {
143
+ if (finite(previous.prices?.[key]?.asOf) && entry.asOf < previous.prices[key].asOf) return false
144
+ }
145
+ if (previous.publishedAt === candidate.publishedAt && previous.hash !== candidate.hash) return false
146
+ if (previous.version === candidate.version && previous.hash !== candidate.hash) return false
147
+ }
148
+ return true
149
+ }
150
+
151
+ async function limitedText(response) {
152
+ const length = Number(response.headers?.get?.('content-length'))
153
+ if (Number.isFinite(length) && length > MAX_PAYLOAD_BYTES) throw new Error('payload-too-large')
154
+ if (!response.body?.getReader) {
155
+ const text = await response.text()
156
+ if (Buffer.byteLength(text, 'utf8') > MAX_PAYLOAD_BYTES) throw new Error('payload-too-large')
157
+ return text
158
+ }
159
+ const reader = response.body.getReader()
160
+ const decoder = new TextDecoder()
161
+ let size = 0
162
+ let text = ''
163
+ try {
164
+ for (;;) {
165
+ const chunk = await reader.read()
166
+ if (chunk.done) return text + decoder.decode()
167
+ size += chunk.value.byteLength
168
+ if (size > MAX_PAYLOAD_BYTES) throw new Error('payload-too-large')
169
+ text += decoder.decode(chunk.value, { stream: true })
170
+ }
171
+ } catch (error) {
172
+ await reader.cancel().catch(() => {})
173
+ throw error
174
+ } finally { reader.releaseLock() }
175
+ }
176
+
177
+ function publicFetch(fetchImpl) {
178
+ return async (input, options = {}) => {
179
+ const endpoint = publicUrl(String(input))
180
+ const response = await fetchImpl(endpoint, {
181
+ method: 'GET', headers: { accept: 'application/json,text/csv,text/html' },
182
+ credentials: 'omit', redirect: 'error', signal: options.signal,
183
+ })
184
+ if (!response?.ok || response.redirected) throw new Error('public-fetch-failed')
185
+ if (response.url && publicUrl(response.url) !== endpoint) throw new Error('public-fetch-redirected')
186
+ return { ok: true, status: response.status, headers: response.headers,
187
+ text: () => limitedText(response) }
188
+ }
189
+ }
190
+
191
+ async function withTimeout(run) {
192
+ const controller = new AbortController()
193
+ let timer
194
+ let rejectTimeout
195
+ const timeout = new Promise((_, reject) => { rejectTimeout = reject })
196
+ timer = setTimeout(() => { controller.abort(); rejectTimeout(new Error('refresh-timeout')) }, TIMEOUT_MS)
197
+ try { return await Promise.race([run(controller.signal), timeout]) }
198
+ finally { clearTimeout(timer) }
199
+ }
200
+
201
+ function due(data, kind, endpoint, ttlMs, at, force) {
202
+ const status = data.status?.[kind]
203
+ if (force || status?.endpoint !== endpoint || !finite(status?.lastAttemptAt)) return true
204
+ const delay = status.error ? Math.min(ttlMs, 300_000) : ttlMs
205
+ return at - status.lastAttemptAt >= delay
206
+ }
207
+
208
+ /**
209
+ * Store API: read() -> state, update(change) -> change's return value; change
210
+ * mutates the fresh state under the caller's interprocess/atomic write lock.
211
+ */
212
+ export function createDynamicDataRefresher({ store, fetchImpl = globalThis.fetch, now = Date.now,
213
+ fetchBenchmark = fetchLiveBenchSnapshot } = {}) {
214
+ if (typeof store?.read !== 'function' || typeof store?.update !== 'function') throw new Error('dynamic-data-store-required')
215
+ const flights = new Map()
216
+ const refresh = async (options = {}) => {
217
+ if (options.enabled !== true) return emptyData()
218
+ const ttlMs = finite(options.ttlMs) && options.ttlMs >= 0 ? options.ttlMs : DEFAULT_TTL_MS
219
+ const liveBenchEndpoint = options.liveBenchEndpoint ?? 'https://livebench.ai'
220
+ const pricingSnapshotEndpoint = options.pricingSnapshotEndpoint ?? ''
221
+ // Never reserve, persist, hash or return an unchecked raw endpoint: a bad
222
+ // URL can contain credentials even though no network request is made.
223
+ const sources = [['liveBench', liveBenchEndpoint], ['pricing', pricingSnapshotEndpoint]].map(([kind, raw]) => {
224
+ if (typeof raw === 'string' && raw.trim() === '') return { kind, endpoint: '', disabled: true, invalid: false }
225
+ try { return { kind, endpoint: publicUrl(raw), disabled: false, invalid: false } }
226
+ catch { return { kind, endpoint: '', disabled: false, invalid: true } }
227
+ })
228
+ const key = stable([sources, ttlMs, options.force === true])
229
+ if (flights.has(key)) return flights.get(key)
230
+ const operation = (async () => {
231
+ const initial = dataOf(await store.read())
232
+ const requests = sources.filter(source => !source.disabled)
233
+ .filter(({ kind, endpoint }) => due(initial, kind, endpoint, ttlMs, now(), options.force === true))
234
+ await Promise.all(requests.map(async ({ kind, endpoint, invalid }) => {
235
+ const at = now()
236
+ const requestId = randomUUID()
237
+ // Reserve before network I/O. Another process or source change wins by
238
+ // replacing this token; a late completion then cannot mutate its data.
239
+ const reserved = await store.update(state => {
240
+ const data = dataOf(state)
241
+ if (!due(data, kind, endpoint, ttlMs, at, options.force === true)) return false
242
+ data.status[kind] = { ...(data.status[kind] ?? {}), endpoint,
243
+ lastAttemptAt: at, requestId, error: '' }
244
+ data.revision = revisionOf(data)
245
+ state.dynamicData = data
246
+ return true
247
+ })
248
+ if (!reserved) return
249
+ let candidate
250
+ let error = ''
251
+ try {
252
+ if (invalid) throw new Error('invalid-public-url')
253
+ if (typeof fetchImpl !== 'function') throw new Error('fetch-unavailable')
254
+ const fetchPublic = publicFetch(fetchImpl)
255
+ candidate = await withTimeout(async signal => {
256
+ if (kind === 'liveBench') {
257
+ const payload = await fetchBenchmark({ endpoint, fetchImpl: fetchPublic, timeoutMs: TIMEOUT_MS, strict: true })
258
+ return benchmarkSnapshot(payload, endpoint, now())
259
+ }
260
+ const response = await fetchPublic(endpoint, { signal })
261
+ return pricingSnapshot(JSON.parse(await response.text()), endpoint, now())
262
+ })
263
+ } catch { error = 'refresh-failed' }
264
+ await store.update(state => {
265
+ const data = dataOf(state)
266
+ if (data.status[kind]?.requestId !== requestId) return false
267
+ if (candidate && !mayPromote(data[kind], candidate)) error = 'snapshot-rollback-rejected'
268
+ if (candidate && !error) data[kind] = candidate
269
+ data.status[kind] = { ...data.status[kind], error,
270
+ ...(candidate && !error ? { lastSuccessAt: now() } : {}) }
271
+ delete data.status[kind].requestId
272
+ data.revision = revisionOf(data)
273
+ state.dynamicData = data
274
+ return true
275
+ })
276
+ }))
277
+ const result = dataOf(await store.read())
278
+ // A user clearing a source disables its use immediately, without erasing
279
+ // recoverable last-good snapshots from disk.
280
+ for (const { kind, disabled } of sources) {
281
+ if (disabled) {
282
+ result[kind] = null
283
+ result.status[kind] = { disabled: true, endpoint: '', error: '' }
284
+ }
285
+ }
286
+ result.revision = revisionOf(result)
287
+ return result
288
+ })()
289
+ flights.set(key, operation)
290
+ try { return await operation }
291
+ finally { if (flights.get(key) === operation) flights.delete(key) }
292
+ }
293
+ return { refresh }
294
+ }
@@ -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
@@ -23,7 +23,7 @@ const TASK_ALIASES = Object.freeze({
23
23
  const CATEGORY_TO_TASK = Object.freeze({
24
24
  reasoning: 'reasoning',
25
25
  coding: 'code',
26
- 'agentic coding': 'code',
26
+ // Agentic Coding is a different task family, not silently a Coding score.
27
27
  mathematics: 'math',
28
28
  'data analysis': 'research',
29
29
  language: 'writing',
@@ -38,10 +38,12 @@ function normalized(value) {
38
38
  return String(value ?? '').toLowerCase().replace(/[^a-z0-9]+/g, '')
39
39
  }
40
40
 
41
- function asScore(value) {
41
+ function asScore(value, scale) {
42
+ if (value === null || value === undefined || String(value).trim() === '') return undefined
42
43
  const number = Number(value)
43
44
  if (!Number.isFinite(number)) return undefined
44
- return clamp(number > 1 ? number / 100 : number)
45
+ if (number < 0 || number > (scale ?? 100)) return undefined
46
+ return scale === undefined ? (number > 1 ? number / 100 : number) : number / scale
45
47
  }
46
48
 
47
49
  function modelRows(payload) {
@@ -59,13 +61,13 @@ function rowName(row) {
59
61
  return String(row.model ?? row.model_name ?? row.name ?? row.id ?? row.slug ?? '')
60
62
  }
61
63
 
62
- function rowScores(row) {
64
+ function rowScores(row, scale) {
63
65
  const source = row?.scores ?? row?.categories ?? row?.benchmark ?? row
64
66
  const scores = {}
65
67
  if (source === null || typeof source !== 'object') return scores
66
68
  for (const [task, aliases] of Object.entries(TASK_ALIASES)) {
67
69
  for (const alias of aliases) {
68
- const score = asScore(source[alias])
70
+ const score = asScore(source[alias], scale)
69
71
  if (score !== undefined) {
70
72
  scores[task] = score
71
73
  break
@@ -77,22 +79,27 @@ function rowScores(row) {
77
79
 
78
80
  /** Normalize a provider response into `{ models: Record<normalizedId, row> }`. */
79
81
  export function normalizeLiveBenchPayload(payload, fetchedAt = Date.now(), source = 'livebench') {
82
+ const scale = payload?.scoreScale === 1 || payload?.scoreScale === 100 ? payload.scoreScale : undefined
80
83
  const models = {}
81
84
  for (const row of modelRows(payload)) {
82
85
  const id = normalized(rowName(row))
83
86
  if (id === '') continue
84
- const scores = rowScores(row)
87
+ const scores = rowScores(row, scale)
85
88
  const values = Object.values(scores)
86
- const overall = asScore(row?.overall ?? row?.score ?? row?.livebench_score)
89
+ const suppliedOverall = asScore(row?.overall ?? row?.score ?? row?.livebench_score, scale)
90
+ const overall = suppliedOverall
87
91
  ?? (values.length > 0 ? values.reduce((sum, value) => sum + value, 0) / values.length : undefined)
88
92
  if (overall === undefined) continue
93
+ if (models[id]) throw new Error('LiveBench model identifiers collide after normalization')
89
94
  models[id] = {
95
+ model: rowName(row),
90
96
  overall: Number(clamp(overall).toFixed(4)),
97
+ overallSource: suppliedOverall !== undefined && row?.overallSource !== 'derived' ? 'explicit' : 'derived',
91
98
  scores,
92
99
  rank: Number.isFinite(Number(row?.rank)) ? Number(row.rank) : undefined,
93
100
  }
94
101
  }
95
- return { source, fetchedAt, models }
102
+ return { source, fetchedAt, models, ...(payload?.publishedAt === undefined ? {} : { publishedAt: payload.publishedAt }) }
96
103
  }
97
104
 
98
105
  /** Parse a small RFC-4180-compatible CSV without adding a runtime dependency. */
@@ -134,13 +141,22 @@ export function parseCsv(text) {
134
141
  return rows.slice(1).map(cells => Object.fromEntries(headers.map((header, index) => [header, cells[index] ?? ''])))
135
142
  }
136
143
 
137
- function mean(values) {
138
- const numbers = values.map(asScore).filter(value => value !== undefined)
144
+ function mean(values, percent = false) {
145
+ const numbers = values.map(value => percent
146
+ ? (String(value ?? '').trim() !== '' && Number.isFinite(Number(value)) && Number(value) >= 0 && Number(value) <= 100 ? Number(value) / 100 : undefined)
147
+ : asScore(value)).filter(value => value !== undefined)
139
148
  return numbers.length === 0 ? undefined : numbers.reduce((sum, value) => sum + value, 0) / numbers.length
140
149
  }
141
150
 
142
- function officialCsvPayload(csv, categories = {}) {
151
+ function officialCsvPayload(csv, categories = {}, percent = false, strict = false) {
143
152
  const rows = parseCsv(csv)
153
+ if (strict) for (const row of rows) {
154
+ if (!row.model) throw new Error('LiveBench row has no model')
155
+ for (const [column, value] of Object.entries(row)) {
156
+ if (column === 'model' || String(value).trim() === '') continue
157
+ if (!Number.isFinite(Number(value)) || Number(value) < 0 || Number(value) > 100) throw new Error('LiveBench CSV score is outside 0..100')
158
+ }
159
+ }
144
160
  const categoryColumns = new Map()
145
161
  for (const [label, columns] of Object.entries(categories ?? {})) {
146
162
  const task = CATEGORY_TO_TASK[String(label).trim().toLowerCase()]
@@ -149,16 +165,18 @@ function officialCsvPayload(csv, categories = {}) {
149
165
  const models = rows.map(row => {
150
166
  const scores = {}
151
167
  for (const [task, columns] of categoryColumns.entries()) {
152
- const score = mean(columns.map(column => row[column]))
168
+ const score = mean(columns.map(column => row[column]), percent)
153
169
  if (score !== undefined) scores[task] = score
154
170
  }
155
171
  // A mirror may already provide normalized task columns, so preserve them.
156
172
  for (const [task, aliases] of Object.entries(TASK_ALIASES)) {
157
173
  if (scores[task] !== undefined) continue
158
- const score = mean(aliases.map(alias => row[alias]))
174
+ const score = mean(aliases.map(alias => row[alias]), percent)
159
175
  if (score !== undefined) scores[task] = score
160
176
  }
161
- return { model: row.model, scores, overall: mean(Object.values(row).slice(1)) }
177
+ const suppliedOverall = mean([row.overall ?? row.score ?? row.livebench_score], percent)
178
+ return { model: row.model, scores, overall: suppliedOverall ?? mean(Object.values(row).slice(1), percent),
179
+ overallSource: suppliedOverall === undefined ? 'derived' : 'explicit' }
162
180
  })
163
181
  return { models }
164
182
  }
@@ -194,7 +212,7 @@ async function fetchWithTimeout(fetchImpl, url, signal) {
194
212
  return response
195
213
  }
196
214
 
197
- async function officialSnapshot({ endpoint, fetchImpl, signal, fetchedAt }) {
215
+ async function officialSnapshot({ endpoint, fetchImpl, signal, fetchedAt, strict }) {
198
216
  const base = new URL(endpoint).origin
199
217
  let releases = [DEFAULT_RELEASE]
200
218
  try {
@@ -219,7 +237,7 @@ async function officialSnapshot({ endpoint, fetchImpl, signal, fetchedAt }) {
219
237
  const categoryResponse = await fetchWithTimeout(fetchImpl, absoluteUrl(base, `categories_${token}.json`), signal)
220
238
  const table = (await readResponse(tableResponse)).text
221
239
  const categories = JSON.parse((await readResponse(categoryResponse)).text)
222
- return normalizeLiveBenchPayload(officialCsvPayload(table, categories), fetchedAt, `livebench:${release}`)
240
+ return normalizeLiveBenchPayload(officialCsvPayload(table, categories, true, strict), fetchedAt, `livebench:${release}`)
223
241
  } catch (error) {
224
242
  lastError = error
225
243
  }
@@ -235,6 +253,7 @@ export async function fetchLiveBenchSnapshot({
235
253
  endpoint = 'https://livebench.ai',
236
254
  fetchImpl = globalThis.fetch,
237
255
  timeoutMs = 8000,
256
+ strict = false,
238
257
  } = {}) {
239
258
  if (typeof fetchImpl !== 'function') throw new Error('fetch is unavailable')
240
259
  const controller = new AbortController()
@@ -245,14 +264,29 @@ export async function fetchLiveBenchSnapshot({
245
264
  const parsed = new URL(rawEndpoint)
246
265
  if ((parsed.hostname === 'livebench.ai' || parsed.hostname === 'www.livebench.ai')
247
266
  && (parsed.pathname === '' || parsed.pathname === '/')) {
248
- return await officialSnapshot({ endpoint: parsed.toString(), fetchImpl, signal: controller.signal, fetchedAt })
267
+ return await officialSnapshot({ endpoint: parsed.toString(), fetchImpl, signal: controller.signal, fetchedAt, strict })
249
268
  }
250
269
  const url = parsed.toString().replace('{release}', DEFAULT_RELEASE)
251
270
  const response = await fetchWithTimeout(fetchImpl, url, controller.signal)
252
271
  const { type, text } = await readResponse(response)
253
272
  const isCsv = type.includes('csv') || /\.csv(?:$|\?)/i.test(parsed.pathname)
254
- if (isCsv) return normalizeLiveBenchPayload(officialCsvPayload(text), fetchedAt, 'livebench-csv-mirror')
255
- return normalizeLiveBenchPayload(JSON.parse(text), fetchedAt, 'livebench-json-mirror')
273
+ if (isCsv) {
274
+ if (strict) throw new Error('Dynamic custom LiveBench feeds require JSON with explicit scoreScale')
275
+ return normalizeLiveBenchPayload(officialCsvPayload(text), fetchedAt, 'livebench-csv-mirror')
276
+ }
277
+ const payload = JSON.parse(text)
278
+ if (strict) {
279
+ if (![1, 100].includes(payload?.scoreScale) || modelRows(payload).length === 0) throw new Error('LiveBench feed needs explicit scoreScale and nonempty models')
280
+ for (const row of modelRows(payload)) {
281
+ if (!rowName(row) || rowName(row).trim() !== rowName(row)) throw new Error('LiveBench model identifier is invalid')
282
+ const scores = row?.scores ?? row?.categories ?? row?.benchmark ?? row
283
+ const values = [row.overall ?? row.score ?? row.livebench_score,
284
+ ...Object.values(TASK_ALIASES).flatMap(aliases => aliases.map(alias => scores?.[alias]))]
285
+ .filter(value => value !== undefined && value !== null && String(value).trim() !== '')
286
+ if (!values.length || values.some(value => typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > payload.scoreScale)) throw new Error('LiveBench score is invalid for declared scale')
287
+ }
288
+ }
289
+ return normalizeLiveBenchPayload(payload, fetchedAt, 'livebench-json-mirror')
256
290
  } finally {
257
291
  clearTimeout(timer)
258
292
  }
@@ -260,5 +294,8 @@ export async function fetchLiveBenchSnapshot({
260
294
 
261
295
  /** Return a model's benchmark row using the same normalization as the adapter. */
262
296
  export function liveBenchRow(snapshot, model) {
263
- return snapshot?.models?.[normalized(model)] ?? null
297
+ const row = snapshot?.models?.[normalized(model)] ?? null
298
+ // Dynamic snapshots require explicit model identity. Old programmatic
299
+ // snapshots retain their previous normalization-only compatibility.
300
+ return snapshot?.exactModelMatch === true && row?.model !== model ? null : row
264
301
  }
@@ -22,7 +22,7 @@ function nonnegative(value, label) {
22
22
  function normalizeProfile(entry, index) {
23
23
  const label = `第 ${index + 1} 个模型`
24
24
  if (!entry || typeof entry !== 'object' || Array.isArray(entry)) throw new Error(`${label} 必须是对象`)
25
- const allowed = new Set(['provider', 'model', 'quality', 'pricing', 'specialties', 'cliModel', 'execution', 'billing', 'subscription', 'apiRoute'])
25
+ const allowed = new Set(['provider', 'model', 'benchmarkModel', 'quality', 'pricing', 'specialties', 'cliModel', 'execution', 'billing', 'subscription', 'apiRoute'])
26
26
  const unknown = Object.keys(entry).find(key => !allowed.has(key))
27
27
  if (unknown) throw new Error(`${label} 含不支持的字段 ${unknown};不要在这里填写密钥或命令`)
28
28
  const provider = id(entry.provider)
@@ -31,6 +31,11 @@ function normalizeProfile(entry, index) {
31
31
  throw new Error(`${label} 需要模型目录中的准确 provider 和 model`)
32
32
  }
33
33
  const profile = { provider, model }
34
+ if (entry.benchmarkModel !== undefined) {
35
+ const benchmarkModel = id(entry.benchmarkModel)
36
+ if (!benchmarkModel || benchmarkModel.length > 240 || /[\u0000-\u001f]/u.test(benchmarkModel)) throw new Error(`${label} 的 benchmarkModel 需要准确的基准模型名称`)
37
+ profile.benchmarkModel = benchmarkModel
38
+ }
34
39
  if (entry.quality !== undefined) {
35
40
  const value = nonnegative(entry.quality, `${label} 的 quality`)
36
41
  if (value > 100) throw new Error(`${label} 的 quality 应在 0 到 100 之间`)
@@ -129,6 +134,7 @@ export function applyModelProfiles(routes, profiles) {
129
134
  if (!profile) return route
130
135
  return {
131
136
  ...route,
137
+ ...(profile.benchmarkModel === undefined ? {} : { benchmarkModel: profile.benchmarkModel }),
132
138
  ...(profile.quality === undefined ? {} : { quality: profile.quality, qualitySource: 'user' }),
133
139
  ...(profile.pricing === undefined ? {} : { pricing: { ...profile.pricing }, pricingSource: 'user' }),
134
140
  ...(profile.specialties === undefined ? {} : { specialties: [...profile.specialties] }),
@@ -78,7 +78,9 @@ const optionalRouteText = (value, subject) => {
78
78
  const rateRequestCodec = strictCodec(`${OFFICIAL_TOOLS_REMOTE_PACKAGE}#RateRequest`, value => {
79
79
  const request = plainObject(value, 'rate request')
80
80
  if (!['up', 'down', 'clear'].includes(request.rating)) throw new TypeError('rating must be up, down or clear')
81
- return { runId: idText(request.runId, 'runId'), packageId: idText(request.packageId, 'packageId'), rating: request.rating }
81
+ if (request.expectedFinishedAt !== undefined && (!Number.isFinite(request.expectedFinishedAt) || request.expectedFinishedAt < 0)) throw new TypeError('expectedFinishedAt must be a non-negative timestamp')
82
+ return { runId: idText(request.runId, 'runId'), packageId: idText(request.packageId, 'packageId'), rating: request.rating,
83
+ ...(request.expectedFinishedAt === undefined ? {} : { expectedFinishedAt: request.expectedFinishedAt }) }
82
84
  })
83
85
 
84
86
  const rerunRequestCodec = strictCodec(`${OFFICIAL_TOOLS_REMOTE_PACKAGE}#RerunRequest`, value => {