@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.
@@ -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 { return await routerStateStore().read() }
184
- catch { return { onboarding: { completedAt: null }, health: null, runs: [] } }
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
- // The refresher's projection excludes actively disabled sources while
399
- // preserving their last-good snapshots on disk for recovery.
400
- if (refreshedData) saved.dynamicData = refreshedData
401
- if (saved.dynamicData) {
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.map(item => ({ ...item, answer: item.answer.slice(0, 1_500) })),
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.finishedAt, run.finishedAt, run.createdAt]) {
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 provider = clean(item.provider)
62
- const model = clean(item.model)
63
- if (!provider || !model || provider.includes('\u0000') || model.includes('\u0000') || item.ran !== true || item.ok !== true
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 null
66
- if (item.rating !== 1 && item.rating !== -1) return null
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.actualModel) !== model) return null
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 || finishedAt > options.now || finishedAt <= options.resetAt) return null
72
- if (item.ratedAt !== undefined && item.ratedAt !== null
73
- && (!finite(item.ratedAt) || item.ratedAt < 0 || item.ratedAt > options.now)) return null
74
- return {
75
- runId: run.id,
76
- packageId: item.id,
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.type) || 'general',
80
- rating: item.rating,
91
+ domain: clean(own(item, 'type')) || 'general',
92
+ rating,
81
93
  finishedAt,
82
- ratedAt: item.ratedAt ?? finishedAt,
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
- for (const item of Array.isArray(run?.packages) ? run.packages : []) {
121
- if (!item || typeof item !== 'object' || !clean(run?.id) || !clean(item.id)) {
122
- profile.ignoredCount += 1
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)) profile.ignoredCount += 1
157
+ if (latest.has(identity)) exclude('superseded')
127
158
  latest.set(identity, { run, item })
128
159
  }
129
160
  }
130
161
  }
131
- const events = [...latest.values()].map(({ run, item }) => evidenceOf(run, item, settings))
132
- .filter(event => {
133
- if (!event) profile.ignoredCount += 1
134
- return event !== null
135
- }).sort((left, right) => compare(JSON.stringify([left.runId, left.packageId]), JSON.stringify([right.runId, right.packageId])))
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
- profile.adjustments = Object.fromEntries([...cells.entries()].sort(([left], [right]) => compare(left, right))
153
- .map(([key, domains]) => [key, Object.fromEntries([...domains.entries()].sort(([left], [right]) => compare(left, right))
154
- .map(([domain, cell]) => [domain, Math.max(-settings.maxAdjustment, Math.min(settings.maxAdjustment,
155
- settings.maxAdjustment * cell.sum / (settings.priorWeight + cell.weight)))]))]))
156
- profile.revision = cacheRevision({ settings, events })
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?.enabled !== true || !profile.adjustments
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 || typeof route !== 'object') return 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 route
246
+ if (!Object.hasOwn(profile.adjustments, key)) return base
172
247
  const value = profile.adjustments[key]
173
- if (!value || typeof value !== 'object' || Array.isArray(value)) return route
174
- const adjustments = Object.fromEntries(Object.entries(value).filter(([, bias]) => finite(bias) && Math.abs(bias) <= 0.1))
175
- if (!Object.keys(adjustments).length) return route
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
- ...route,
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 publicError = value => value === 'snapshot-rollback-rejected' ? value : value ? 'refresh-failed' : ''
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 error = publicError(status?.error)
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 ?? null, configuredEndpoint: status?.endpoint ?? null,
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
  }