@ljwei-stak/dsh-model-router 0.16.0 → 0.16.2
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 +314 -80
- 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/latest-versions.mjs +3 -21
- package/.dsh-plugin/shared/official-tools-runtime.mjs +188 -41
- package/.dsh-plugin/shared/router-state.mjs +139 -38
- package/.dsh-plugin/shared/run-ledger.mjs +32 -11
- package/.dsh-plugin/shared/version-order.mjs +30 -0
- package/CHANGELOG.md +20 -0
- package/INSTALLATION_GUIDE.zh.md +12 -12
- package/README.md +9 -5
- package/README.zh.md +9 -5
- package/docs/ROUTING_ADAPTIVE_LEARNING.zh.md +2 -0
- package/docs/ROUTING_MATURITY.zh.md +76 -0
- package/package.json +3 -1
package/.dsh-plugin/index.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { createRouterState } from './shared/router-state.mjs'
|
|
15
15
|
import { applyFeedbackProfile, buildFeedbackProfile } from './shared/adaptive-feedback.mjs'
|
|
16
16
|
import { applyPricingSnapshot, createDynamicDataRefresher, dynamicDataSummary, routingData } from './shared/dynamic-data.mjs'
|
|
17
|
+
import { projectDynamicData } from './shared/dynamic-data-view.mjs'
|
|
17
18
|
import {
|
|
18
19
|
budgetCheck, buildRunRecord, formatUsd, buildTeamRunRecord, buildToolRunRecord, billingOf, mergeRerun,
|
|
19
20
|
spending, storedResults, actualCost, teamExecutionResults,
|
|
@@ -180,8 +181,14 @@ export function routerStateStore() {
|
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
async function savedState() {
|
|
183
|
-
try {
|
|
184
|
-
|
|
184
|
+
try {
|
|
185
|
+
const saved = await routerStateStore().read()
|
|
186
|
+
const storageAvailable = !(saved.notices ?? []).some(notice => notice.kind === 'state-recovery-pending')
|
|
187
|
+
return { ...saved, storageAvailable, historyVerified: storageAvailable && saved.historyIncomplete !== true }
|
|
188
|
+
} catch {
|
|
189
|
+
return { onboarding: { completedAt: null }, health: null, runs: [], storageAvailable: false, historyVerified: false,
|
|
190
|
+
notices: [{ kind: 'state-read-failed', at: Date.now(), message: '执行记录暂时无法读取,费用与学习摘要尚未核验;请恢复存储连接后重试。' }] }
|
|
191
|
+
}
|
|
185
192
|
}
|
|
186
193
|
|
|
187
194
|
const NOTICE_DAYS = 7
|
|
@@ -344,6 +351,9 @@ function budgetSettings(config) {
|
|
|
344
351
|
|
|
345
352
|
function budgetFor(config, saved, estimateUsd) {
|
|
346
353
|
const settings = budgetSettings(config)
|
|
354
|
+
if ((saved.storageAvailable === false || saved.historyVerified === false) && (settings.dailyLimitUsd > 0 || settings.monthlyLimitUsd > 0)) {
|
|
355
|
+
throw new Error('无法读取费用历史,不能核验日/月预算;请恢复存储连接后再执行。')
|
|
356
|
+
}
|
|
347
357
|
const spent = spending(saved.runs, undefined, saved.archivedSpending)
|
|
348
358
|
return { ...budgetCheck({ estimateUsd, spent, ...settings }), spent, ...settings }
|
|
349
359
|
}
|
|
@@ -395,18 +405,10 @@ async function routingSavedState(config) {
|
|
|
395
405
|
try { refreshedData = await dynamicRefresher.refresh(options) } catch { /* state/read errors use the normal fallback */ }
|
|
396
406
|
}
|
|
397
407
|
const saved = await savedState()
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
saved.dynamicData = { ...saved.dynamicData, status: { ...saved.dynamicData.status } }
|
|
403
|
-
for (const [kind, endpoint] of [['pricing', options.pricingSnapshotEndpoint], ['liveBench', options.liveBenchEndpoint]]) {
|
|
404
|
-
if (!text(endpoint)) {
|
|
405
|
-
saved.dynamicData[kind] = null
|
|
406
|
-
saved.dynamicData.status[kind] = { disabled: true, endpoint: '', error: '' }
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
408
|
+
// Config can change while a public request is in flight. Reproject against
|
|
409
|
+
// the current settings, not the options captured before awaiting refresh.
|
|
410
|
+
// Cached source A must not masquerade as a newly configured source B.
|
|
411
|
+
saved.dynamicData = projectDynamicData(refreshedData ?? saved.dynamicData, dynamicOptions(config))
|
|
410
412
|
saved.routingLearning = buildFeedbackProfile(saved.runs, feedbackOptions(config))
|
|
411
413
|
return saved
|
|
412
414
|
}
|
|
@@ -1100,11 +1102,15 @@ export async function ledgerSummary(config = {}, { limit = 30 } = {}) {
|
|
|
1100
1102
|
return {
|
|
1101
1103
|
runs: saved.runs.slice(-limit).reverse().map(run => ({
|
|
1102
1104
|
...run,
|
|
1103
|
-
task: run.task.slice(0, 2_000),
|
|
1104
|
-
packages: run.packages.
|
|
1105
|
+
task: text(run.task).slice(0, 2_000),
|
|
1106
|
+
packages: (Array.isArray(run.packages) ? run.packages : []).filter(item => item && typeof item === 'object')
|
|
1107
|
+
.map(item => ({ ...item, answer: text(item.answer).slice(0, 1_500) })),
|
|
1105
1108
|
})),
|
|
1106
1109
|
spent,
|
|
1107
|
-
budget: { ...settings, ...budgetCheck({ estimateUsd: null, spent, ...settings }) },
|
|
1110
|
+
budget: { ...settings, ...budgetCheck({ estimateUsd: null, spent, ...settings }), historyVerified: saved.historyVerified !== false },
|
|
1111
|
+
storageAvailable: saved.storageAvailable !== false,
|
|
1112
|
+
storageNotice: saved.storageAvailable === false ? '执行记录存储暂不可用;下方费用与学习摘要尚未核验,不能当作零花费或没有反馈。'
|
|
1113
|
+
: saved.historyVerified === false ? '此前执行记录已损坏或不完整,当前摘要只包含可读记录;不能视为完整费用历史,请从备份恢复后再核验预算。' : '',
|
|
1108
1114
|
// Legacy scalar quality biases are intentionally no longer learned.
|
|
1109
1115
|
biases: {},
|
|
1110
1116
|
learning: saved.routingLearning,
|
|
@@ -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
|
}
|