@ljwei-stak/dsh-model-router 0.16.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 +182 -50
- package/.dsh-plugin/index.mjs +23 -17
- package/.dsh-plugin/shared/adaptive-feedback.mjs +120 -40
- package/.dsh-plugin/shared/dynamic-data-view.mjs +59 -4
- package/.dsh-plugin/shared/dynamic-data.mjs +161 -83
- package/.dsh-plugin/shared/router-state.mjs +139 -38
- package/.dsh-plugin/shared/run-ledger.mjs +32 -11
- package/CHANGELOG.md +11 -0
- package/INSTALLATION_GUIDE.zh.md +12 -12
- package/README.md +7 -5
- package/README.zh.md +7 -5
- package/docs/ROUTING_ADAPTIVE_LEARNING.zh.md +2 -0
- package/docs/ROUTING_MATURITY.zh.md +76 -0
- package/package.json +2 -1
|
@@ -6,11 +6,15 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export const FEEDBACK_POLICY_VERSION = 'subjective-utility-v1'
|
|
8
8
|
export const FEEDBACK_RUN_WINDOW = 200
|
|
9
|
+
/** Cache invalidation cadence; weights themselves still use exact evaluatedAt. */
|
|
10
|
+
export const FEEDBACK_DECAY_REFRESH_MS = 3_600_000
|
|
9
11
|
|
|
10
12
|
const DAY_MS = 86_400_000
|
|
11
13
|
const finite = value => typeof value === 'number' && Number.isFinite(value)
|
|
12
14
|
const clean = value => typeof value === 'string' ? value.trim() : ''
|
|
13
15
|
const routeKey = (provider, model) => `${provider}\u0000${model}`
|
|
16
|
+
const record = value => value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
17
|
+
const own = (value, key) => record(value) && Object.hasOwn(value, key) ? value[key] : undefined
|
|
14
18
|
// Code-unit ordering avoids machine/browser locale changing cache identity.
|
|
15
19
|
const compare = (left, right) => left === right ? 0 : left < right ? -1 : 1
|
|
16
20
|
|
|
@@ -50,7 +54,7 @@ function completedAt(run, item) {
|
|
|
50
54
|
// Stored packages have their own completion time. A historical run may have
|
|
51
55
|
// only a run-level completion timestamp, then its creation timestamp.
|
|
52
56
|
// Invalid explicitly supplied timestamps are not repaired by a fallback.
|
|
53
|
-
for (const candidate of [item
|
|
57
|
+
for (const candidate of [own(item, 'finishedAt'), own(run, 'finishedAt'), own(run, 'createdAt')]) {
|
|
54
58
|
if (candidate === undefined || candidate === null) continue
|
|
55
59
|
return finite(candidate) && candidate >= 0 ? candidate : null
|
|
56
60
|
}
|
|
@@ -58,29 +62,37 @@ function completedAt(run, item) {
|
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
function evidenceOf(run, item, options) {
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
65
|
+
const reject = reason => ({ event: null, reason })
|
|
66
|
+
const provider = clean(own(item, 'provider'))
|
|
67
|
+
const model = clean(own(item, 'model'))
|
|
68
|
+
if (!provider || !model || provider.includes('\u0000') || model.includes('\u0000')) return reject('invalid-route')
|
|
69
|
+
if (own(item, 'ran') !== true || own(item, 'ok') !== true
|
|
64
70
|
|| item.blocked === true || item.waiting === true || item.paused === true
|
|
65
|
-
|| item.cancelled === true || (item.status !== undefined && !['succeeded', 'fallback'].includes(item.status))) return
|
|
66
|
-
|
|
71
|
+
|| item.cancelled === true || (item.status !== undefined && !['succeeded', 'fallback'].includes(item.status))) return reject('not-successful')
|
|
72
|
+
const rating = own(item, 'rating')
|
|
73
|
+
if (rating !== 1 && rating !== -1) return reject(rating === undefined || rating === null || rating === 0 ? 'unrated' : 'invalid-rating')
|
|
67
74
|
// A CLI default or a different actual model must not train the recommended
|
|
68
75
|
// model. Aliases are not silently equated; a future verified adapter may do so.
|
|
69
|
-
if (item.channel === 'official-cli' && clean(item
|
|
76
|
+
if (item.channel === 'official-cli' && clean(own(item, 'actualModel')) !== model) return reject('cli-model-unverified')
|
|
70
77
|
const finishedAt = completedAt(run, item)
|
|
71
|
-
if (finishedAt === null
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
if (finishedAt === null) return reject('invalid-time')
|
|
79
|
+
if (finishedAt > options.now) return reject('future-time')
|
|
80
|
+
if (finishedAt <= options.resetAt) return reject('before-reset')
|
|
81
|
+
const suppliedRatedAt = own(item, 'ratedAt')
|
|
82
|
+
if (suppliedRatedAt !== undefined && suppliedRatedAt !== null) {
|
|
83
|
+
if (!finite(suppliedRatedAt) || suppliedRatedAt < finishedAt) return reject('invalid-time')
|
|
84
|
+
if (suppliedRatedAt > options.now) return reject('future-time')
|
|
85
|
+
}
|
|
86
|
+
return { reason: null, event: {
|
|
87
|
+
runId: own(run, 'id'),
|
|
88
|
+
packageId: own(item, 'id'),
|
|
77
89
|
provider,
|
|
78
90
|
model,
|
|
79
|
-
domain: clean(item
|
|
80
|
-
rating
|
|
91
|
+
domain: clean(own(item, 'type')) || 'general',
|
|
92
|
+
rating,
|
|
81
93
|
finishedAt,
|
|
82
|
-
ratedAt:
|
|
83
|
-
}
|
|
94
|
+
ratedAt: suppliedRatedAt ?? finishedAt,
|
|
95
|
+
} }
|
|
84
96
|
}
|
|
85
97
|
|
|
86
98
|
/**
|
|
@@ -112,35 +124,72 @@ export function buildFeedbackProfile(runs, options = {}) {
|
|
|
112
124
|
resetAt: settings.resetAt,
|
|
113
125
|
window: 'last-200-runs',
|
|
114
126
|
adjustments: {},
|
|
127
|
+
cellStats: {},
|
|
128
|
+
exclusionReasons: {},
|
|
129
|
+
evaluatedAt: settings.now,
|
|
130
|
+
decayRefreshMs: FEEDBACK_DECAY_REFRESH_MS,
|
|
131
|
+
decayEpoch: null,
|
|
132
|
+
evidenceRevision: '',
|
|
133
|
+
revisionSemantics: 'evidence-settings-and-hourly-decay-cache-identity-not-security-hash',
|
|
115
134
|
revision: '',
|
|
116
135
|
}
|
|
136
|
+
const exclude = reason => {
|
|
137
|
+
profile.ignoredCount += 1
|
|
138
|
+
profile.exclusionReasons[reason] = (profile.exclusionReasons[reason] ?? 0) + 1
|
|
139
|
+
}
|
|
117
140
|
const latest = new Map()
|
|
118
141
|
if (settings.enabled) {
|
|
119
142
|
for (const run of (Array.isArray(runs) ? runs : []).slice(-FEEDBACK_RUN_WINDOW)) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
143
|
+
if (!record(run) || (run.packages !== undefined && !Array.isArray(own(run, 'packages')))) {
|
|
144
|
+
exclude('invalid-record')
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
for (const item of Array.isArray(own(run, 'packages')) ? run.packages : []) {
|
|
148
|
+
if (!record(item)) {
|
|
149
|
+
exclude('invalid-record')
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
if (!clean(own(run, 'id')) || !clean(own(item, 'id'))) {
|
|
153
|
+
exclude('missing-identity')
|
|
123
154
|
continue
|
|
124
155
|
}
|
|
125
156
|
const identity = JSON.stringify([run.id, item.id])
|
|
126
|
-
if (latest.has(identity))
|
|
157
|
+
if (latest.has(identity)) exclude('superseded')
|
|
127
158
|
latest.set(identity, { run, item })
|
|
128
159
|
}
|
|
129
160
|
}
|
|
130
161
|
}
|
|
131
|
-
const events = [
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
162
|
+
const events = []
|
|
163
|
+
for (const { run, item } of latest.values()) {
|
|
164
|
+
const result = evidenceOf(run, item, settings)
|
|
165
|
+
if (result.event) events.push(result.event)
|
|
166
|
+
else exclude(result.reason)
|
|
167
|
+
}
|
|
168
|
+
events.sort((left, right) => compare(JSON.stringify([left.runId, left.packageId]), JSON.stringify([right.runId, right.packageId])))
|
|
136
169
|
const cells = new Map()
|
|
137
170
|
for (const event of events) {
|
|
138
171
|
const weight = 2 ** (-(settings.now - event.finishedAt) / (settings.halfLifeDays * DAY_MS))
|
|
139
172
|
const key = routeKey(event.provider, event.model)
|
|
140
173
|
const domains = cells.get(key) ?? new Map()
|
|
141
|
-
const cell = domains.get(event.domain) ?? { sum: 0, weight: 0
|
|
174
|
+
const cell = domains.get(event.domain) ?? { sum: 0, weight: 0, count: 0, positive: 0, negative: 0,
|
|
175
|
+
weightSquaredSum: 0, maxWeight: 0, scaledWeightSum: 0, scaledWeightSquares: 0 }
|
|
142
176
|
cell.sum += weight * event.rating
|
|
143
177
|
cell.weight += weight
|
|
178
|
+
cell.count += 1
|
|
179
|
+
if (event.rating === 1) cell.positive += 1
|
|
180
|
+
else cell.negative += 1
|
|
181
|
+
cell.weightSquaredSum += weight * weight
|
|
182
|
+
// Rescaling prevents old tiny weights squared from underflowing Kish ESS.
|
|
183
|
+
if (weight > cell.maxWeight) {
|
|
184
|
+
const scale = cell.maxWeight / weight
|
|
185
|
+
cell.scaledWeightSum = cell.scaledWeightSum * scale + 1
|
|
186
|
+
cell.scaledWeightSquares = cell.scaledWeightSquares * scale * scale + 1
|
|
187
|
+
cell.maxWeight = weight
|
|
188
|
+
} else if (weight > 0) {
|
|
189
|
+
const scaled = weight / cell.maxWeight
|
|
190
|
+
cell.scaledWeightSum += scaled
|
|
191
|
+
cell.scaledWeightSquares += scaled * scaled
|
|
192
|
+
}
|
|
144
193
|
domains.set(event.domain, cell)
|
|
145
194
|
cells.set(key, domains)
|
|
146
195
|
profile.feedbackCount += 1
|
|
@@ -149,11 +198,31 @@ export function buildFeedbackProfile(runs, options = {}) {
|
|
|
149
198
|
}
|
|
150
199
|
// Object.fromEntries safely creates own keys even for opaque identifiers
|
|
151
200
|
// such as '__proto__'; no task text or answer is copied into this profile.
|
|
152
|
-
|
|
153
|
-
.map(([key, domains]) => [key,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
profile.
|
|
201
|
+
const sortedCells = [...cells.entries()].sort(([left], [right]) => compare(left, right))
|
|
202
|
+
.map(([key, domains]) => [key, [...domains.entries()].sort(([left], [right]) => compare(left, right))])
|
|
203
|
+
const adjustment = cell => Math.max(-settings.maxAdjustment, Math.min(settings.maxAdjustment,
|
|
204
|
+
settings.maxAdjustment * cell.sum / (settings.priorWeight + cell.weight)))
|
|
205
|
+
profile.adjustments = Object.fromEntries(sortedCells.map(([key, domains]) => [key,
|
|
206
|
+
Object.fromEntries(domains.map(([domain, cell]) => [domain, adjustment(cell)]))]))
|
|
207
|
+
profile.cellStats = Object.fromEntries(sortedCells.map(([key, domains]) => [key,
|
|
208
|
+
Object.fromEntries(domains.map(([domain, cell]) => [domain, {
|
|
209
|
+
feedbackCount: cell.count, positiveCount: cell.positive, negativeCount: cell.negative,
|
|
210
|
+
weightedRatingSum: cell.sum, effectiveWeight: cell.weight, weightSquaredSum: cell.weightSquaredSum,
|
|
211
|
+
// Keep machine output within the mathematical bound despite ULP rounding.
|
|
212
|
+
effectiveSampleSize: cell.scaledWeightSquares > 0
|
|
213
|
+
? Math.min(cell.count, Math.max(0, cell.scaledWeightSum ** 2 / cell.scaledWeightSquares)) : 0,
|
|
214
|
+
shrinkage: cell.weight / (settings.priorWeight + cell.weight), adjustment: adjustment(cell),
|
|
215
|
+
semantics: 'observed-feedback-weight-diagnostics-not-confidence-or-correctness',
|
|
216
|
+
}]))]))
|
|
217
|
+
const { now: evaluatedAt, ...evidenceSettings } = settings
|
|
218
|
+
const identityEvents = events.map(({ ratedAt, ...event }) => event)
|
|
219
|
+
profile.evidenceRevision = cacheRevision({ settings: evidenceSettings, events: identityEvents })
|
|
220
|
+
profile.decayEpoch = events.length > 0 && settings.maxAdjustment > 0
|
|
221
|
+
? Math.floor(evaluatedAt / FEEDBACK_DECAY_REFRESH_MS) : null
|
|
222
|
+
// A decision cache identity, intentionally not the hash of every continuously
|
|
223
|
+
// changing weight. Empty evidence and audit-only re-rating do not erase plans;
|
|
224
|
+
// active preferences invalidate once/hour so long-term decay keeps updating.
|
|
225
|
+
profile.revision = cacheRevision({ evidenceRevision: profile.evidenceRevision, decayEpoch: profile.decayEpoch })
|
|
157
226
|
return profile
|
|
158
227
|
}
|
|
159
228
|
|
|
@@ -163,18 +232,25 @@ export function buildFeedbackProfile(runs, options = {}) {
|
|
|
163
232
|
* profile. The planner must consume the domain-specific adjustment explicitly.
|
|
164
233
|
*/
|
|
165
234
|
export function applyFeedbackProfile(routes, profile) {
|
|
166
|
-
if (!Array.isArray(routes) || profile
|
|
167
|
-
|| typeof profile.adjustments !== 'object' || Array.isArray(profile.adjustments)) return routes
|
|
235
|
+
if (!Array.isArray(routes) || !record(profile) || typeof profile.enabled !== 'boolean') return routes
|
|
168
236
|
return routes.map(route => {
|
|
169
|
-
if (!route
|
|
237
|
+
if (!record(route)) return route
|
|
238
|
+
let base = route
|
|
239
|
+
if (Object.hasOwn(route, 'preferenceAdjustments') || Object.hasOwn(route, 'preferenceAdjustmentMeta')) {
|
|
240
|
+
base = { ...route }
|
|
241
|
+
delete base.preferenceAdjustments
|
|
242
|
+
delete base.preferenceAdjustmentMeta
|
|
243
|
+
}
|
|
244
|
+
if (profile.enabled !== true || !record(profile.adjustments)) return base
|
|
170
245
|
const key = routeKey(clean(route.provider), clean(route.model))
|
|
171
|
-
if (!Object.hasOwn(profile.adjustments, key)) return
|
|
246
|
+
if (!Object.hasOwn(profile.adjustments, key)) return base
|
|
172
247
|
const value = profile.adjustments[key]
|
|
173
|
-
if (!
|
|
174
|
-
const
|
|
175
|
-
|
|
248
|
+
if (!record(value)) return base
|
|
249
|
+
const limit = finite(profile.maxAdjustment) && profile.maxAdjustment >= 0 && profile.maxAdjustment <= 0.1 ? profile.maxAdjustment : 0.1
|
|
250
|
+
const adjustments = Object.fromEntries(Object.entries(value).filter(([, bias]) => finite(bias) && Math.abs(bias) <= limit))
|
|
251
|
+
if (!Object.keys(adjustments).length) return base
|
|
176
252
|
return {
|
|
177
|
-
...
|
|
253
|
+
...base,
|
|
178
254
|
preferenceAdjustments: adjustments,
|
|
179
255
|
preferenceAdjustmentMeta: {
|
|
180
256
|
policyVersion: profile.policyVersion,
|
|
@@ -186,6 +262,10 @@ export function applyFeedbackProfile(routes, profile) {
|
|
|
186
262
|
halfLifeDays: profile.halfLifeDays,
|
|
187
263
|
maxAdjustment: profile.maxAdjustment,
|
|
188
264
|
window: profile.window,
|
|
265
|
+
cellStats: Object.hasOwn(profile.cellStats ?? {}, key) ? profile.cellStats[key] : {},
|
|
266
|
+
evaluatedAt: profile.evaluatedAt,
|
|
267
|
+
decayRefreshMs: profile.decayRefreshMs,
|
|
268
|
+
revisionSemantics: profile.revisionSemantics,
|
|
189
269
|
},
|
|
190
270
|
}
|
|
191
271
|
})
|
|
@@ -2,7 +2,53 @@
|
|
|
2
2
|
const DEFAULT_TTL_MS = 86_400_000
|
|
3
3
|
const currentTime = value => typeof value === 'function' ? value() : value ?? Date.now()
|
|
4
4
|
const exactRouteKey = route => `${String(route?.provider ?? '')}\0${String(route?.model ?? '')}`
|
|
5
|
-
const
|
|
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
|
+
}
|
|
6
52
|
|
|
7
53
|
/** Explicit user prices override the curated public feed, including stale last-good data. */
|
|
8
54
|
export function applyPricingSnapshot(routes, snapshot) {
|
|
@@ -25,15 +71,24 @@ function sourceSummary(data, kind, enabled, ttlMs, at) {
|
|
|
25
71
|
const snapshot = enabled ? data?.[kind] : null
|
|
26
72
|
const status = enabled ? data?.status?.[kind] : null
|
|
27
73
|
const verifiedAt = Number.isFinite(snapshot?.verifiedAt) ? snapshot.verifiedAt : null
|
|
28
|
-
const expired = verifiedAt !== null && at - verifiedAt > ttlMs
|
|
29
|
-
const
|
|
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)
|
|
30
80
|
return {
|
|
31
81
|
status: !enabled || status?.disabled ? 'disabled' : !snapshot ? (error ? 'error' : 'missing')
|
|
32
82
|
: expired || error ? 'stale' : 'fresh',
|
|
33
83
|
verifiedAt, publishedAt: snapshot?.publishedAt ?? null,
|
|
34
84
|
version: snapshot?.version ?? null, source: snapshot?.source ?? null,
|
|
35
|
-
endpoint: snapshot?.endpoint
|
|
85
|
+
endpoint: publicEndpointMetadata(snapshot?.endpoint), configuredEndpoint: publicEndpointMetadata(status?.endpoint),
|
|
36
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),
|
|
37
92
|
...(kind === 'liveBench' ? { modelCount: Object.keys(snapshot?.models ?? {}).length }
|
|
38
93
|
: { rowCount: Object.keys(snapshot?.prices ?? {}).length }),
|
|
39
94
|
}
|