@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.
- package/.dsh-plugin/client.js +701 -180
- package/.dsh-plugin/index.mjs +126 -25
- package/.dsh-plugin/shared/adaptive-feedback.mjs +272 -0
- package/.dsh-plugin/shared/assignment-solver.mjs +86 -0
- package/.dsh-plugin/shared/dynamic-data-view.mjs +120 -0
- package/.dsh-plugin/shared/dynamic-data.mjs +372 -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 +140 -38
- package/.dsh-plugin/shared/router.mjs +115 -152
- package/.dsh-plugin/shared/run-ledger.mjs +37 -11
- package/CHANGELOG.md +29 -0
- package/INSTALLATION_GUIDE.zh.md +21 -15
- package/README.md +14 -6
- package/README.zh.md +14 -6
- package/docs/ROUTING_ADAPTIVE_LEARNING.zh.md +153 -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_MATURITY.zh.md +76 -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 +19 -2
|
@@ -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
|