@ljwei-stak/dsh-model-router 0.15.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.
- package/.dsh-plugin/client.js +556 -167
- package/.dsh-plugin/index.mjs +115 -20
- package/.dsh-plugin/shared/adaptive-feedback.mjs +192 -0
- package/.dsh-plugin/shared/assignment-solver.mjs +86 -0
- package/.dsh-plugin/shared/dynamic-data-view.mjs +65 -0
- package/.dsh-plugin/shared/dynamic-data.mjs +294 -0
- package/.dsh-plugin/shared/harness-plan.mjs +5 -2
- package/.dsh-plugin/shared/livebench.mjs +57 -20
- package/.dsh-plugin/shared/model-profiles.mjs +7 -1
- package/.dsh-plugin/shared/official-tools-remote.mjs +3 -1
- package/.dsh-plugin/shared/router-state.mjs +2 -1
- package/.dsh-plugin/shared/router.mjs +115 -152
- package/.dsh-plugin/shared/run-ledger.mjs +5 -0
- package/CHANGELOG.md +18 -0
- package/INSTALLATION_GUIDE.zh.md +21 -15
- package/README.md +12 -6
- package/README.zh.md +12 -6
- package/docs/ROUTING_ADAPTIVE_LEARNING.zh.md +151 -0
- package/docs/ROUTING_DERIVATION_EXPERIMENTS.zh.md +684 -0
- package/docs/ROUTING_LIVEBENCH_DATA_SEARCH.zh.md +74 -0
- package/docs/ROUTING_LIVEBENCH_VALIDATION.zh.md +154 -0
- package/docs/ROUTING_RESEARCH.zh.md +59 -0
- package/docs/ROUTING_SELFTEST_STEPS.zh.md +192 -0
- package/docs/WORKBENCH_USER_GUIDE.zh.md +25 -3
- package/package.json +18 -2
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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)
|
|
255
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 => {
|
|
@@ -39,7 +39,7 @@ const LOCK_STALE_MS = 15_000
|
|
|
39
39
|
const LOCK_TIMEOUT_MS = 10_000
|
|
40
40
|
|
|
41
41
|
function emptyState() {
|
|
42
|
-
return { version: STATE_VERSION, onboarding: { completedAt: null }, health: null, quota: {}, authFailures: {}, runs: [], archivedSpending: sanitizeArchivedSpending(), notices: [], terminalSessions: [] }
|
|
42
|
+
return { version: STATE_VERSION, onboarding: { completedAt: null }, health: null, quota: {}, authFailures: {}, runs: [], dynamicData: { liveBench: null, pricing: null, status: {}, revision: '' }, archivedSpending: sanitizeArchivedSpending(), notices: [], terminalSessions: [] }
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
function sanitizeNotices(value) {
|
|
@@ -65,6 +65,7 @@ function sanitize(value, maxRuns) {
|
|
|
65
65
|
authFailures: Object.fromEntries(Object.entries(value.authFailures && typeof value.authFailures === 'object' && !Array.isArray(value.authFailures) ? value.authFailures : {})
|
|
66
66
|
.filter(([, entry]) => Number.isFinite(entry?.at))),
|
|
67
67
|
runs: Array.isArray(value.runs) ? value.runs.filter(run => run && typeof run.id === 'string') : [],
|
|
68
|
+
dynamicData: value.dynamicData && typeof value.dynamicData === 'object' && !Array.isArray(value.dynamicData) ? value.dynamicData : { liveBench: null, pricing: null, status: {}, revision: '' },
|
|
68
69
|
archivedSpending: sanitizeArchivedSpending(value.archivedSpending),
|
|
69
70
|
notices: sanitizeNotices(value.notices),
|
|
70
71
|
terminalSessions: sanitizeTerminalSessions(value.terminalSessions),
|