@ljwei-stak/dsh-model-router 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dsh-plugin/client.js +556 -167
- package/.dsh-plugin/index.mjs +115 -20
- package/.dsh-plugin/shared/adaptive-feedback.mjs +192 -0
- package/.dsh-plugin/shared/assignment-solver.mjs +86 -0
- package/.dsh-plugin/shared/dynamic-data-view.mjs +65 -0
- package/.dsh-plugin/shared/dynamic-data.mjs +294 -0
- package/.dsh-plugin/shared/harness-plan.mjs +5 -2
- package/.dsh-plugin/shared/livebench.mjs +57 -20
- package/.dsh-plugin/shared/model-profiles.mjs +7 -1
- package/.dsh-plugin/shared/official-tools-remote.mjs +3 -1
- package/.dsh-plugin/shared/router-state.mjs +2 -1
- package/.dsh-plugin/shared/router.mjs +115 -152
- package/.dsh-plugin/shared/run-ledger.mjs +5 -0
- package/CHANGELOG.md +18 -0
- package/INSTALLATION_GUIDE.zh.md +21 -15
- package/README.md +12 -6
- package/README.zh.md +12 -6
- package/docs/ROUTING_ADAPTIVE_LEARNING.zh.md +151 -0
- package/docs/ROUTING_DERIVATION_EXPERIMENTS.zh.md +684 -0
- package/docs/ROUTING_LIVEBENCH_DATA_SEARCH.zh.md +74 -0
- package/docs/ROUTING_LIVEBENCH_VALIDATION.zh.md +154 -0
- package/docs/ROUTING_RESEARCH.zh.md +59 -0
- package/docs/ROUTING_SELFTEST_STEPS.zh.md +192 -0
- package/docs/WORKBENCH_USER_GUIDE.zh.md +25 -3
- package/package.json +18 -2
package/.dsh-plugin/index.mjs
CHANGED
|
@@ -12,9 +12,11 @@ import {
|
|
|
12
12
|
DEFAULT_COOLDOWN_MINUTES, billingOverview, createQuotaTracker, detectQuotaExhaustion, parseQuotaPatterns, vendorKey,
|
|
13
13
|
} from './shared/subscription-billing.mjs'
|
|
14
14
|
import { createRouterState } from './shared/router-state.mjs'
|
|
15
|
+
import { applyFeedbackProfile, buildFeedbackProfile } from './shared/adaptive-feedback.mjs'
|
|
16
|
+
import { applyPricingSnapshot, createDynamicDataRefresher, dynamicDataSummary, routingData } from './shared/dynamic-data.mjs'
|
|
15
17
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
budgetCheck, buildRunRecord, formatUsd, buildTeamRunRecord, buildToolRunRecord, billingOf, mergeRerun,
|
|
19
|
+
spending, storedResults, actualCost, teamExecutionResults,
|
|
18
20
|
} from './shared/run-ledger.mjs'
|
|
19
21
|
import { routeBoundaries } from './shared/security-boundaries.mjs'
|
|
20
22
|
import { createPlanFromRoutes } from './shared/harness-plan.mjs'
|
|
@@ -62,6 +64,15 @@ export const Config = z.object({
|
|
|
62
64
|
overBudgetAction: z.union(['downgrade', 'pause']).default('downgrade').volatile(),
|
|
63
65
|
reviewMode: z.union(['off', 'sample', 'always']).default('off').volatile(),
|
|
64
66
|
reviewSampleRate: z.number().min(0).max(1).default(0.2).volatile(),
|
|
67
|
+
feedbackLearningEnabled: z.boolean().default(true).volatile(),
|
|
68
|
+
feedbackHalfLifeDays: z.number().min(1).max(3650).default(30).volatile(),
|
|
69
|
+
feedbackPriorWeight: z.number().min(1).max(1_000_000).default(3).volatile(),
|
|
70
|
+
feedbackMaxAdjustment: z.number().min(0).max(0.1).default(0.04).volatile(),
|
|
71
|
+
feedbackResetAt: z.number().min(0).default(0).volatile(),
|
|
72
|
+
dynamicDataEnabled: z.boolean().default(false).volatile(),
|
|
73
|
+
liveBenchEndpoint: z.string().max(2048).default('https://livebench.ai').volatile(),
|
|
74
|
+
pricingSnapshotEndpoint: z.string().max(2048).default('').volatile(),
|
|
75
|
+
dynamicDataTtlMinutes: z.number().min(1).max(10_080).default(1440).volatile(),
|
|
65
76
|
allowManualReassign: z.boolean().default(true).volatile(),
|
|
66
77
|
confirmUnsandboxedCli: z.boolean().default(true).volatile(),
|
|
67
78
|
subscriptionCooldownMinutes: z.number().step(1).min(1).max(10_080).default(DEFAULT_COOLDOWN_MINUTES).volatile(),
|
|
@@ -350,13 +361,70 @@ export function billingForResult(result) {
|
|
|
350
361
|
}
|
|
351
362
|
|
|
352
363
|
function routePricing(routes, item) {
|
|
353
|
-
|
|
364
|
+
const route = routes.find(route => route.provider === item?.provider && route.model === item?.model)
|
|
365
|
+
return route?.pricing ? { ...route.pricing, source: route.pricingSource ?? 'route', version: route.pricingVersion ?? null, asOf: route.snapshotAsOf ?? null } : null
|
|
354
366
|
}
|
|
355
367
|
|
|
356
|
-
|
|
357
|
-
|
|
368
|
+
function feedbackOptions(config) {
|
|
369
|
+
return {
|
|
370
|
+
enabled: valueOf(config, 'feedbackLearningEnabled', true) !== false,
|
|
371
|
+
halfLifeDays: valueOf(config, 'feedbackHalfLifeDays', 30),
|
|
372
|
+
priorWeight: valueOf(config, 'feedbackPriorWeight', 3),
|
|
373
|
+
maxAdjustment: valueOf(config, 'feedbackMaxAdjustment', 0.04),
|
|
374
|
+
resetAt: valueOf(config, 'feedbackResetAt', 0),
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function dynamicOptions(config) {
|
|
379
|
+
return {
|
|
380
|
+
enabled: valueOf(config, 'dynamicDataEnabled', false) === true,
|
|
381
|
+
liveBenchEndpoint: valueOf(config, 'liveBenchEndpoint', 'https://livebench.ai'),
|
|
382
|
+
pricingSnapshotEndpoint: valueOf(config, 'pricingSnapshotEndpoint', ''),
|
|
383
|
+
ttlMs: boundedInteger(valueOf(config, 'dynamicDataTtlMinutes', 1440), 1440, 1, 10_080) * 60_000,
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
let dynamicRefresher = null
|
|
388
|
+
async function routingSavedState(config) {
|
|
389
|
+
const options = dynamicOptions(config)
|
|
390
|
+
let refreshedData = null
|
|
391
|
+
if (options.enabled) {
|
|
392
|
+
dynamicRefresher ??= createDynamicDataRefresher({ store: routerStateStore() })
|
|
393
|
+
// A public-data failure must never turn into a paid probe or prevent
|
|
394
|
+
// ordinary routing. The refresher preserves last-good snapshots.
|
|
395
|
+
try { refreshedData = await dynamicRefresher.refresh(options) } catch { /* state/read errors use the normal fallback */ }
|
|
396
|
+
}
|
|
397
|
+
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
|
+
}
|
|
410
|
+
saved.routingLearning = buildFeedbackProfile(saved.runs, feedbackOptions(config))
|
|
411
|
+
return saved
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function planDataOptions(saved, config) {
|
|
415
|
+
return {
|
|
416
|
+
...routingData(saved.dynamicData, dynamicOptions(config).enabled),
|
|
417
|
+
learning: saved.routingLearning ?? buildFeedbackProfile(saved.runs, feedbackOptions(config)),
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** Manual exact-route profiles take precedence over public price snapshots.
|
|
422
|
+
* Only explicit subjective feedback affects utility; reviews stay separate. */
|
|
423
|
+
async function routesWithLearning(ctx, config, signal, runs, dynamicData = null, feedbackProfile = null) {
|
|
358
424
|
const discovered = await discoverConfiguredRoutes(ctx, signal)
|
|
359
|
-
|
|
425
|
+
const profiled = configuredRoutesWithProfiles(discovered, config)
|
|
426
|
+
const priced = applyPricingSnapshot(profiled, dynamicOptions(config).enabled ? dynamicData?.pricing : null)
|
|
427
|
+
return applyFeedbackProfile(priced, feedbackProfile ?? buildFeedbackProfile(runs, feedbackOptions(config)))
|
|
360
428
|
}
|
|
361
429
|
|
|
362
430
|
/** Produce a route recommendation and work packages compatible with official Agent Teams. */
|
|
@@ -366,9 +434,9 @@ export async function createRoutePlan(ctx, task, config = {}, options = {}) {
|
|
|
366
434
|
const mode = options.mode === 'team' ? 'team' : 'single'
|
|
367
435
|
const configuredBudget = valueOf(config, 'budgetUsd', DEFAULT_ROUTER_SETTINGS.budgetUsd)
|
|
368
436
|
const budgetUsd = Math.max(0, finiteNumber(options.budgetUsd, finiteNumber(configuredBudget, 0)))
|
|
369
|
-
const saved = await
|
|
437
|
+
const saved = await routingSavedState(config)
|
|
370
438
|
const [availableRoutes, installed] = await Promise.all([
|
|
371
|
-
routesWithLearning(ctx, config, options.signal, saved.runs),
|
|
439
|
+
routesWithLearning(ctx, config, options.signal, saved.runs, saved.dynamicData, saved.routingLearning),
|
|
372
440
|
options.skipToolProbe === true
|
|
373
441
|
? Promise.resolve(Array.isArray(options.installedToolIds) ? options.installedToolIds : [])
|
|
374
442
|
: installedToolIds(),
|
|
@@ -381,6 +449,7 @@ export async function createRoutePlan(ctx, task, config = {}, options = {}) {
|
|
|
381
449
|
? (Array.isArray(options.loggedOutToolIds) ? options.loggedOutToolIds : [])
|
|
382
450
|
: loggedOutIds(await currentHealth())
|
|
383
451
|
const plan = createPlanFromRoutes(taskText, availableRoutes, {
|
|
452
|
+
...planDataOptions(saved, config),
|
|
384
453
|
mode, budgetUsd, installedToolIds: installed,
|
|
385
454
|
runnableToolIds: installed.filter(id => executable.has(id)),
|
|
386
455
|
preset: routingPresetOf(config, options.preset),
|
|
@@ -599,8 +668,8 @@ export async function planAssignment(ctx, task, config = {}, options = {}) {
|
|
|
599
668
|
if (problem) throw new Error(problem)
|
|
600
669
|
const direct = text(options.provider) || text(options.model)
|
|
601
670
|
if (direct && (!text(options.provider) || !text(options.model))) throw new Error('指定模型需要同时提供 provider 和 model。')
|
|
602
|
-
const saved = await
|
|
603
|
-
const routes = await routesWithLearning(ctx, config, options.signal, saved.runs)
|
|
671
|
+
const saved = await routingSavedState(config)
|
|
672
|
+
const routes = await routesWithLearning(ctx, config, options.signal, saved.runs, saved.dynamicData, saved.routingLearning)
|
|
604
673
|
if (direct && !routes.some(route => route.provider === text(options.provider) && route.model === text(options.model))) {
|
|
605
674
|
throw new Error(`模型 ${text(options.provider)}/${text(options.model)} 不在 Harness 模型目录中;请在模型页添加后重试,或改用自动路由。`)
|
|
606
675
|
}
|
|
@@ -618,10 +687,12 @@ export async function planAssignment(ctx, task, config = {}, options = {}) {
|
|
|
618
687
|
const preset = routingPresetOf(config, options.preset)
|
|
619
688
|
const planWith = (presetId, budgetUsd) => direct
|
|
620
689
|
? createPlanFromRoutes(taskText, routes, {
|
|
690
|
+
...planDataOptions(saved, config),
|
|
621
691
|
mode: 'direct', directProvider: options.provider, directModel: options.model,
|
|
622
692
|
installedToolIds: installed, runnableToolIds, preset: presetId, loggedOutToolIds: loggedOut,
|
|
623
693
|
})
|
|
624
694
|
: createPlanFromRoutes(taskText, routes, {
|
|
695
|
+
...planDataOptions(saved, config),
|
|
625
696
|
mode: options.planMode === 'team' ? 'team' : 'single',
|
|
626
697
|
budgetUsd, installedToolIds: installed, runnableToolIds, preset: presetId, loggedOutToolIds: loggedOut,
|
|
627
698
|
})
|
|
@@ -741,7 +812,8 @@ export async function rerunRecordedStep(ctx, config = {}, { runId, packageId, pr
|
|
|
741
812
|
if (choice && !['api', 'subscription', 'cancel'].includes(choice)) throw new Error('subscriptionChoice 只能是 api、subscription 或 cancel')
|
|
742
813
|
if (choice && !target.paused) throw new Error('该步骤没有在等待订阅失败的确认;直接重跑即可。')
|
|
743
814
|
if (choice === 'cancel') return { run: await cancelPausedStep(run.id, target.id), execution: null, budget: null, cancelled: true }
|
|
744
|
-
const
|
|
815
|
+
const refreshed = await routingSavedState(config)
|
|
816
|
+
const routes = await routesWithLearning(ctx, config, signal, refreshed.runs, refreshed.dynamicData, refreshed.routingLearning)
|
|
745
817
|
let override = null
|
|
746
818
|
if (text(provider) || text(model)) {
|
|
747
819
|
if (!text(provider) || !text(model)) throw new Error('改派需要同时提供 provider 和 model')
|
|
@@ -954,7 +1026,8 @@ export async function planToolRun(ctx, config, args = {}, { signal } = {}) {
|
|
|
954
1026
|
if (text(args.provider) || text(args.model)) {
|
|
955
1027
|
if (!text(args.provider) || !text(args.model)) throw new Error('provider 和 model 必须同时提供')
|
|
956
1028
|
if (toolForProvider(args.provider)?.id !== toolId) throw new Error('所选模型供应商与官方 CLI 工具不匹配')
|
|
957
|
-
|
|
1029
|
+
const refreshed = await routingSavedState(config)
|
|
1030
|
+
routes = await routesWithLearning(ctx, config, signal, refreshed.runs, refreshed.dynamicData, refreshed.routingLearning)
|
|
958
1031
|
route = routes.find(item => item.provider === text(args.provider) && item.model === text(args.model)) ?? null
|
|
959
1032
|
if (!route) throw new Error('所选 provider/model 不在官方模型目录中')
|
|
960
1033
|
modelId = route.cliModel && toolId !== 'zcode'
|
|
@@ -1000,19 +1073,28 @@ export async function recordToolRun({ toolId, task, provider, model, cliModel =
|
|
|
1000
1073
|
}
|
|
1001
1074
|
|
|
1002
1075
|
/** Store a user rating (+1 useful, -1 not useful, 0 clears) for one result. */
|
|
1003
|
-
export async function rateRecordedResult({ runId, packageId, rating } = {}) {
|
|
1076
|
+
export async function rateRecordedResult({ runId, packageId, rating, expectedFinishedAt } = {}, { origin = 'local-explicit-input' } = {}) {
|
|
1004
1077
|
const value = rating === 'up' || rating === 1 ? 1 : rating === 'down' || rating === -1 ? -1 : rating === 'clear' || rating === 0 ? null : undefined
|
|
1005
1078
|
if (value === undefined) throw new Error('rating 只能是 up、down 或 clear')
|
|
1006
1079
|
return routerStateStore().updateRun(text(runId), current => {
|
|
1007
1080
|
const item = current.packages.find(entry => entry.id === text(packageId))
|
|
1008
1081
|
if (!item) throw new Error(`运行记录中没有工作包 ${text(packageId)}`)
|
|
1082
|
+
if (expectedFinishedAt !== undefined && (!Number.isFinite(expectedFinishedAt) || expectedFinishedAt !== item.finishedAt)) {
|
|
1083
|
+
throw new Error('结果已更新,请刷新执行记录后再评价。')
|
|
1084
|
+
}
|
|
1085
|
+
if (value !== null && (item.ran !== true || item.ok !== true || item.paused || item.waiting || item.blocked)) {
|
|
1086
|
+
throw new Error('只能评价已成功执行的结果;失败和暂停不自动视为不满意。')
|
|
1087
|
+
}
|
|
1009
1088
|
item.rating = value
|
|
1089
|
+
item.ratedAt = Date.now()
|
|
1090
|
+
item.feedbackPolicyVersion = 'subjective-utility-v1'
|
|
1091
|
+
item.feedbackOrigin = ['desktop-ui', 'approved-tool'].includes(origin) ? origin : 'local-explicit-input'
|
|
1010
1092
|
})
|
|
1011
1093
|
}
|
|
1012
1094
|
|
|
1013
1095
|
/** Everything the workbench shows: recent runs, spending, budget, learned biases and settings. */
|
|
1014
1096
|
export async function ledgerSummary(config = {}, { limit = 30 } = {}) {
|
|
1015
|
-
const saved = await
|
|
1097
|
+
const saved = await routingSavedState(config)
|
|
1016
1098
|
const spent = spending(saved.runs, undefined, saved.archivedSpending)
|
|
1017
1099
|
const settings = budgetSettings(config)
|
|
1018
1100
|
return {
|
|
@@ -1023,7 +1105,14 @@ export async function ledgerSummary(config = {}, { limit = 30 } = {}) {
|
|
|
1023
1105
|
})),
|
|
1024
1106
|
spent,
|
|
1025
1107
|
budget: { ...settings, ...budgetCheck({ estimateUsd: null, spent, ...settings }) },
|
|
1026
|
-
biases
|
|
1108
|
+
// Legacy scalar quality biases are intentionally no longer learned.
|
|
1109
|
+
biases: {},
|
|
1110
|
+
learning: saved.routingLearning,
|
|
1111
|
+
dynamicData: dynamicDataSummary(saved.dynamicData, { ...dynamicOptions(config), now: Date.now() }),
|
|
1112
|
+
routingData: {
|
|
1113
|
+
...routingData(saved.dynamicData, dynamicOptions(config).enabled),
|
|
1114
|
+
pricingSnapshot: dynamicOptions(config).enabled ? saved.dynamicData?.pricing ?? null : null,
|
|
1115
|
+
},
|
|
1027
1116
|
onboarding: saved.onboarding,
|
|
1028
1117
|
settings: {
|
|
1029
1118
|
preset: routingPresetOf(config),
|
|
@@ -1308,7 +1397,7 @@ export function routerRemoteServices(ctx, config, { terminals = null } = {}) {
|
|
|
1308
1397
|
},
|
|
1309
1398
|
completeOnboarding: async () => routerStateStore().completeOnboarding(Date.now()),
|
|
1310
1399
|
ledger: () => ledgerSummary(config),
|
|
1311
|
-
rate: request => rateRecordedResult(request),
|
|
1400
|
+
rate: request => rateRecordedResult(request, { origin: 'desktop-ui' }),
|
|
1312
1401
|
rerun: request => rerunRecordedStep(ctx, config, {
|
|
1313
1402
|
runId: request?.runId, packageId: request?.packageId, provider: request?.provider, model: request?.model,
|
|
1314
1403
|
confirmOverBudget: request?.confirmOverBudget === true, confirmWrite: request?.confirmWrite === true,
|
|
@@ -1337,6 +1426,10 @@ export function apply(ctx, config = {}) {
|
|
|
1337
1426
|
ctx.on('tools/pre-execute', async (exec, next) => {
|
|
1338
1427
|
const decision = await next()
|
|
1339
1428
|
if (decision.kind !== 'allow') return decision
|
|
1429
|
+
if (exec.name === 'model_router_rate') {
|
|
1430
|
+
return { kind: 'ask', reason: 'Confirm the user\'s subjective rating; agents must not self-rate their results',
|
|
1431
|
+
displayReason: { en: 'Save this as your rating and adjust future recommendations?', zh: '确认这是你对该结果的评价,并用于调整以后的推荐?Agent 不能代替你给自己评分。' } }
|
|
1432
|
+
}
|
|
1340
1433
|
if (exec.name === 'model_router_tool_install') {
|
|
1341
1434
|
const requested = getOfficialTool(text(exec.arguments?.tool))
|
|
1342
1435
|
const label = requested?.label ?? '官方 CLI'
|
|
@@ -1645,15 +1738,16 @@ function registerOfficialToolModels(ctx, config) {
|
|
|
1645
1738
|
}))
|
|
1646
1739
|
ctx.tools.register(defineTool({
|
|
1647
1740
|
name: 'model_router_rate',
|
|
1648
|
-
description: 'Record the user\'s rating of one recorded result (up
|
|
1741
|
+
description: 'Record only the user\'s explicitly stated rating of one recorded result (up, down, clear). Requires approval; never self-rate. Feedback adjusts bounded local subjective utility, not objective quality.',
|
|
1649
1742
|
parameters: {
|
|
1650
1743
|
runId: { type: 'string', required: true, description: 'runId returned by model_router_execute.' },
|
|
1651
1744
|
packageId: { type: 'string', required: true, description: 'Work package id.' },
|
|
1652
1745
|
rating: { type: 'string', required: true, enum: ['up', 'down', 'clear'], description: 'The user\'s rating.' },
|
|
1746
|
+
expectedFinishedAt: { type: 'number', description: 'Completion timestamp of the displayed result; rejects stale feedback after a rerun.' },
|
|
1653
1747
|
},
|
|
1654
1748
|
output: JSON_OUTPUT,
|
|
1655
1749
|
async execute(args) {
|
|
1656
|
-
const run = await rateRecordedResult(args)
|
|
1750
|
+
const run = await rateRecordedResult(args, { origin: 'approved-tool' })
|
|
1657
1751
|
return jsonValue({ ok: true, runId: run.id, packageId: args.packageId, rating: args.rating })
|
|
1658
1752
|
},
|
|
1659
1753
|
}))
|
|
@@ -1672,8 +1766,8 @@ function registerOfficialToolModels(ctx, config) {
|
|
|
1672
1766
|
const { cwd, root, sandboxMode } = await sessionWorkspace(ctx, exec)
|
|
1673
1767
|
const mode = args.mode === 'workspace-write' ? 'workspace-write' : 'read-only'
|
|
1674
1768
|
if (mode === 'workspace-write' && sandboxMode === 'read-only') throw new Error('当前 Harness 会话为只读模式,不能请求可编辑团队执行')
|
|
1675
|
-
const
|
|
1676
|
-
const routes =
|
|
1769
|
+
const saved = await routingSavedState(config)
|
|
1770
|
+
const [routes, installed] = await Promise.all([routesWithLearning(ctx, config, exec.signal, saved.runs, saved.dynamicData, saved.routingLearning), installedToolIds()])
|
|
1677
1771
|
const readiness = await Promise.all(installed.map(id => officialToolReadiness(id, cwd)))
|
|
1678
1772
|
const supported = new Set(readiness.filter(item => item.ready).map(item => item.id))
|
|
1679
1773
|
const capabilities = new Map(officialToolExecutionCapabilities().map(item => [item.id, item]))
|
|
@@ -1688,6 +1782,7 @@ function registerOfficialToolModels(ctx, config) {
|
|
|
1688
1782
|
const configuredBudget = valueOf(config, 'budgetUsd', DEFAULT_ROUTER_SETTINGS.budgetUsd)
|
|
1689
1783
|
const budgetUsd = Math.max(0, finiteNumber(args.budgetUsd, finiteNumber(configuredBudget, 0)))
|
|
1690
1784
|
const plan = createPlanFromRoutes(args.task, executableRoutes, {
|
|
1785
|
+
...planDataOptions(saved, config),
|
|
1691
1786
|
mode: 'team', budgetUsd, installedToolIds: installed,
|
|
1692
1787
|
runnableToolIds: installed.filter(id => supported.has(id)),
|
|
1693
1788
|
preset: routingPresetOf(config),
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded, local subjective-utility personalization. This is not a learned
|
|
3
|
+
* answer-correctness probability, a bandit policy, or an unbiased evaluation.
|
|
4
|
+
* Only explicit human feedback on an executed, successful result is evidence.
|
|
5
|
+
* Model reviews and missing feedback are deliberately not user satisfaction.
|
|
6
|
+
*/
|
|
7
|
+
export const FEEDBACK_POLICY_VERSION = 'subjective-utility-v1'
|
|
8
|
+
export const FEEDBACK_RUN_WINDOW = 200
|
|
9
|
+
|
|
10
|
+
const DAY_MS = 86_400_000
|
|
11
|
+
const finite = value => typeof value === 'number' && Number.isFinite(value)
|
|
12
|
+
const clean = value => typeof value === 'string' ? value.trim() : ''
|
|
13
|
+
const routeKey = (provider, model) => `${provider}\u0000${model}`
|
|
14
|
+
// Code-unit ordering avoids machine/browser locale changing cache identity.
|
|
15
|
+
const compare = (left, right) => left === right ? 0 : left < right ? -1 : 1
|
|
16
|
+
|
|
17
|
+
function boundedOption(options, key, fallback, minimum, maximum = Infinity) {
|
|
18
|
+
const value = options[key] === undefined ? fallback : options[key]
|
|
19
|
+
if (!finite(value) || value < minimum || value > maximum) {
|
|
20
|
+
throw new RangeError(`${key} must be a finite number in [${minimum}, ${maximum}]`)
|
|
21
|
+
}
|
|
22
|
+
return value
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizedOptions(options) {
|
|
26
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) throw new TypeError('options must be an object')
|
|
27
|
+
const enabled = options.enabled === undefined ? true : options.enabled
|
|
28
|
+
if (typeof enabled !== 'boolean') throw new TypeError('enabled must be boolean')
|
|
29
|
+
return {
|
|
30
|
+
enabled,
|
|
31
|
+
now: boundedOption(options, 'now', Date.now(), 0),
|
|
32
|
+
halfLifeDays: boundedOption(options, 'halfLifeDays', 30, 1, 3650),
|
|
33
|
+
priorWeight: boundedOption(options, 'priorWeight', 3, 1, 1_000_000),
|
|
34
|
+
maxAdjustment: boundedOption(options, 'maxAdjustment', 0.04, 0, 0.1),
|
|
35
|
+
resetAt: boundedOption(options, 'resetAt', 0, 0),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** FNV-1a over JS UTF-16 code units: a short cache identity, NOT a security hash. */
|
|
40
|
+
function cacheRevision(value) {
|
|
41
|
+
const serialized = JSON.stringify(value)
|
|
42
|
+
let hash = 0x811c9dc5
|
|
43
|
+
for (let index = 0; index < serialized.length; index += 1) {
|
|
44
|
+
hash = Math.imul(hash ^ serialized.charCodeAt(index), 0x01000193) >>> 0
|
|
45
|
+
}
|
|
46
|
+
return `${FEEDBACK_POLICY_VERSION}:${hash.toString(16).padStart(8, '0')}`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function completedAt(run, item) {
|
|
50
|
+
// Stored packages have their own completion time. A historical run may have
|
|
51
|
+
// only a run-level completion timestamp, then its creation timestamp.
|
|
52
|
+
// Invalid explicitly supplied timestamps are not repaired by a fallback.
|
|
53
|
+
for (const candidate of [item.finishedAt, run.finishedAt, run.createdAt]) {
|
|
54
|
+
if (candidate === undefined || candidate === null) continue
|
|
55
|
+
return finite(candidate) && candidate >= 0 ? candidate : null
|
|
56
|
+
}
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
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
|
|
64
|
+
|| 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
|
|
67
|
+
// A CLI default or a different actual model must not train the recommended
|
|
68
|
+
// 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
|
|
70
|
+
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,
|
|
77
|
+
provider,
|
|
78
|
+
model,
|
|
79
|
+
domain: clean(item.type) || 'general',
|
|
80
|
+
rating: item.rating,
|
|
81
|
+
finishedAt,
|
|
82
|
+
ratedAt: item.ratedAt ?? finishedAt,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build a pure profile from the newest 200 runs under one local DSH home.
|
|
88
|
+
* Last occurrence of the same run/package replaces earlier occurrences BEFORE
|
|
89
|
+
* validation: a clear, retry, or invalid latest result removes old evidence.
|
|
90
|
+
* Existing metadata without ran/ok/identifiers/timestamps is not guessed.
|
|
91
|
+
*
|
|
92
|
+
* In one exact provider/model/domain cell:
|
|
93
|
+
* w_i = 2 ** (-(now - completedAt_i) / halfLifeMs)
|
|
94
|
+
* bias = maxAdjustment * sum(w_i * rating_i) / (priorWeight + sum(w_i))
|
|
95
|
+
* Ratings are revised in place, never extra independent observations. Decay
|
|
96
|
+
* uses result completion, so re-rating an old result cannot refresh its age or
|
|
97
|
+
* bypass resetAt. No cross-domain/model-version/user backoff is performed.
|
|
98
|
+
*/
|
|
99
|
+
export function buildFeedbackProfile(runs, options = {}) {
|
|
100
|
+
const settings = normalizedOptions(options)
|
|
101
|
+
const profile = {
|
|
102
|
+
enabled: settings.enabled,
|
|
103
|
+
scope: 'local-dsh-home',
|
|
104
|
+
policyVersion: FEEDBACK_POLICY_VERSION,
|
|
105
|
+
feedbackCount: 0,
|
|
106
|
+
ignoredCount: 0,
|
|
107
|
+
effectiveWeight: 0,
|
|
108
|
+
updatedAt: null,
|
|
109
|
+
halfLifeDays: settings.halfLifeDays,
|
|
110
|
+
priorWeight: settings.priorWeight,
|
|
111
|
+
maxAdjustment: settings.maxAdjustment,
|
|
112
|
+
resetAt: settings.resetAt,
|
|
113
|
+
window: 'last-200-runs',
|
|
114
|
+
adjustments: {},
|
|
115
|
+
revision: '',
|
|
116
|
+
}
|
|
117
|
+
const latest = new Map()
|
|
118
|
+
if (settings.enabled) {
|
|
119
|
+
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
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
const identity = JSON.stringify([run.id, item.id])
|
|
126
|
+
if (latest.has(identity)) profile.ignoredCount += 1
|
|
127
|
+
latest.set(identity, { run, item })
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
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])))
|
|
136
|
+
const cells = new Map()
|
|
137
|
+
for (const event of events) {
|
|
138
|
+
const weight = 2 ** (-(settings.now - event.finishedAt) / (settings.halfLifeDays * DAY_MS))
|
|
139
|
+
const key = routeKey(event.provider, event.model)
|
|
140
|
+
const domains = cells.get(key) ?? new Map()
|
|
141
|
+
const cell = domains.get(event.domain) ?? { sum: 0, weight: 0 }
|
|
142
|
+
cell.sum += weight * event.rating
|
|
143
|
+
cell.weight += weight
|
|
144
|
+
domains.set(event.domain, cell)
|
|
145
|
+
cells.set(key, domains)
|
|
146
|
+
profile.feedbackCount += 1
|
|
147
|
+
profile.effectiveWeight += weight
|
|
148
|
+
profile.updatedAt = Math.max(profile.updatedAt ?? 0, event.ratedAt)
|
|
149
|
+
}
|
|
150
|
+
// Object.fromEntries safely creates own keys even for opaque identifiers
|
|
151
|
+
// 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 })
|
|
157
|
+
return profile
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Add separate preference metadata; never overwrite benchmark quality/source.
|
|
162
|
+
* Unknown/catalog quality remains unknown/catalog evidence after applying this
|
|
163
|
+
* profile. The planner must consume the domain-specific adjustment explicitly.
|
|
164
|
+
*/
|
|
165
|
+
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
|
|
168
|
+
return routes.map(route => {
|
|
169
|
+
if (!route || typeof route !== 'object') return route
|
|
170
|
+
const key = routeKey(clean(route.provider), clean(route.model))
|
|
171
|
+
if (!Object.hasOwn(profile.adjustments, key)) return route
|
|
172
|
+
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
|
|
176
|
+
return {
|
|
177
|
+
...route,
|
|
178
|
+
preferenceAdjustments: adjustments,
|
|
179
|
+
preferenceAdjustmentMeta: {
|
|
180
|
+
policyVersion: profile.policyVersion,
|
|
181
|
+
revision: profile.revision,
|
|
182
|
+
scope: profile.scope,
|
|
183
|
+
feedbackCount: profile.feedbackCount,
|
|
184
|
+
effectiveWeight: profile.effectiveWeight,
|
|
185
|
+
updatedAt: profile.updatedAt,
|
|
186
|
+
halfLifeDays: profile.halfLifeDays,
|
|
187
|
+
maxAdjustment: profile.maxAdjustment,
|
|
188
|
+
window: profile.window,
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic budget-constrained DAG assignment, independent of model names.
|
|
3
|
+
* Exact enumeration is bounded by the retained Cartesian candidate space;
|
|
4
|
+
* larger spaces use a beam with a protected least-cost feasible prefix.
|
|
5
|
+
* Optimality concerns this surrogate objective, never actual answer quality.
|
|
6
|
+
*/
|
|
7
|
+
const EPSILON = 1e-12
|
|
8
|
+
const compareText = (a, b) => a < b ? -1 : a > b ? 1 : 0
|
|
9
|
+
const signature = state => JSON.stringify(state.choices.map(option => option.route))
|
|
10
|
+
|
|
11
|
+
function compareStates(left, right, minimizeCost = false) {
|
|
12
|
+
return left.relaxedCount - right.relaxedCount
|
|
13
|
+
|| left.qualityShortfall - right.qualityShortfall
|
|
14
|
+
|| (minimizeCost ? left.cost - right.cost : right.score - left.score)
|
|
15
|
+
|| (minimizeCost ? right.score - left.score : left.cost - right.cost)
|
|
16
|
+
|| left.switches - right.switches
|
|
17
|
+
|| compareText(signature(left), signature(right))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function solveCandidateAssignments({ tasks = [], pools = [], budget = Infinity, beamWidth = 256, exactLimit = 4096, handoffPenalty = 0.015, minimizeCost = false } = {}) {
|
|
21
|
+
if (!tasks.length || tasks.length !== pools.length) return null
|
|
22
|
+
const preceding = new Set()
|
|
23
|
+
for (const task of tasks) {
|
|
24
|
+
if (!task.id || preceding.has(task.id) || (task.dependsOn ?? []).some(id => !preceding.has(id))) {
|
|
25
|
+
throw new RangeError('Assignment tasks must have unique ids and known dependencies in topological order')
|
|
26
|
+
}
|
|
27
|
+
preceding.add(task.id)
|
|
28
|
+
}
|
|
29
|
+
const constrained = Number.isFinite(budget)
|
|
30
|
+
const options = pools.map(pool => (Array.isArray(pool) ? pool : []).filter(option =>
|
|
31
|
+
Number.isFinite(option.cost) && option.cost >= 0 && Number.isFinite(option.score)
|
|
32
|
+
&& (!constrained || option.pricingKnown === true)))
|
|
33
|
+
if (options.some(pool => pool.length === 0)) return null
|
|
34
|
+
const suffixMinimum = Array(tasks.length + 1).fill(0)
|
|
35
|
+
let searchSpace = 1
|
|
36
|
+
let searchSpaceCapped = false
|
|
37
|
+
for (let index = tasks.length - 1; index >= 0; index--) {
|
|
38
|
+
suffixMinimum[index] = suffixMinimum[index + 1] + Math.min(...options[index].map(option => option.cost))
|
|
39
|
+
const product = searchSpace * options[index].length
|
|
40
|
+
if (product > Number.MAX_SAFE_INTEGER) searchSpaceCapped = true
|
|
41
|
+
searchSpace = Math.min(Number.MAX_SAFE_INTEGER, product)
|
|
42
|
+
}
|
|
43
|
+
if (constrained && suffixMinimum[0] > budget + EPSILON) return null
|
|
44
|
+
const exact = !searchSpaceCapped && searchSpace <= Math.max(1, exactLimit)
|
|
45
|
+
const width = Math.max(1, Math.floor(beamWidth) || 256)
|
|
46
|
+
const search = { method: exact ? 'exact' : 'beam', exact, searchSpace, searchSpaceCapped, expandedStates: 0, beamPruned: 0 }
|
|
47
|
+
let states = [{ choices: [], routesByTask: new Map(), score: 0, cost: 0, switches: 0, relaxedCount: 0, qualityShortfall: 0 }]
|
|
48
|
+
for (let index = 0; index < tasks.length; index++) {
|
|
49
|
+
const task = tasks[index]
|
|
50
|
+
const expanded = []
|
|
51
|
+
for (const state of states) {
|
|
52
|
+
for (const option of options[index]) {
|
|
53
|
+
const cost = state.cost + option.cost
|
|
54
|
+
if (constrained && cost + suffixMinimum[index + 1] > budget + EPSILON) continue
|
|
55
|
+
const switches = (task.dependsOn ?? []).reduce((count, dependency) => {
|
|
56
|
+
const route = state.routesByTask.get(dependency)
|
|
57
|
+
return count + (route !== undefined && route !== option.route ? 1 : 0)
|
|
58
|
+
}, 0)
|
|
59
|
+
const penalty = switches * handoffPenalty
|
|
60
|
+
const shortfall = Math.max(0, Number(option.qualityShortfall) || 0)
|
|
61
|
+
const routesByTask = new Map(state.routesByTask)
|
|
62
|
+
routesByTask.set(task.id, option.route)
|
|
63
|
+
expanded.push({ choices: [...state.choices, { ...option, handoffPenalty: penalty }], routesByTask,
|
|
64
|
+
score: state.score + option.score - penalty, cost, switches: state.switches + switches,
|
|
65
|
+
relaxedCount: state.relaxedCount + (shortfall > 0 ? 1 : 0), qualityShortfall: state.qualityShortfall + shortfall })
|
|
66
|
+
search.expandedStates++
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!expanded.length) return null
|
|
70
|
+
expanded.sort((left, right) => compareStates(left, right, minimizeCost))
|
|
71
|
+
if (!exact && expanded.length > width) {
|
|
72
|
+
// A high-utility prefix must not crowd out every affordable completion.
|
|
73
|
+
// This also works at width=1; quality relaxation remains lexicographic.
|
|
74
|
+
states = expanded.slice(0, width)
|
|
75
|
+
if (constrained && index < tasks.length - 1) {
|
|
76
|
+
const cheapest = expanded.reduce((best, state) => compareStates(state, best, true) < 0 ? state : best)
|
|
77
|
+
if (!states.includes(cheapest)) states[width - 1] = cheapest
|
|
78
|
+
}
|
|
79
|
+
search.beamPruned += expanded.length - states.length
|
|
80
|
+
} else states = expanded
|
|
81
|
+
}
|
|
82
|
+
states.sort((left, right) => compareStates(left, right, minimizeCost))
|
|
83
|
+
const best = states[0]
|
|
84
|
+
const { routesByTask, ...result } = best
|
|
85
|
+
return { ...result, minimumCostLowerBound: suffixMinimum[0], search }
|
|
86
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
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 publicError = value => value === 'snapshot-rollback-rejected' ? value : value ? 'refresh-failed' : ''
|
|
6
|
+
|
|
7
|
+
/** Explicit user prices override the curated public feed, including stale last-good data. */
|
|
8
|
+
export function applyPricingSnapshot(routes, snapshot) {
|
|
9
|
+
if (snapshot?.kind !== 'pricing' || !snapshot.prices || typeof snapshot.prices !== 'object') return routes
|
|
10
|
+
return (Array.isArray(routes) ? routes : []).map(route => {
|
|
11
|
+
if (route?.pricingSource === 'user') return route
|
|
12
|
+
const entry = snapshot.prices[exactRouteKey(route)]
|
|
13
|
+
if (!entry) return route
|
|
14
|
+
return {
|
|
15
|
+
...route,
|
|
16
|
+
pricing: { input: entry.input, output: entry.output, currency: 'USD',
|
|
17
|
+
...(entry.cacheRead === undefined ? {} : { cacheRead: entry.cacheRead }),
|
|
18
|
+
...(entry.cacheWrite === undefined ? {} : { cacheWrite: entry.cacheWrite }) },
|
|
19
|
+
pricingSource: 'dynamic', pricingVersion: snapshot.version, snapshotAsOf: entry.asOf,
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function sourceSummary(data, kind, enabled, ttlMs, at) {
|
|
25
|
+
const snapshot = enabled ? data?.[kind] : null
|
|
26
|
+
const status = enabled ? data?.status?.[kind] : null
|
|
27
|
+
const verifiedAt = Number.isFinite(snapshot?.verifiedAt) ? snapshot.verifiedAt : null
|
|
28
|
+
const expired = verifiedAt !== null && at - verifiedAt > ttlMs
|
|
29
|
+
const error = publicError(status?.error)
|
|
30
|
+
return {
|
|
31
|
+
status: !enabled || status?.disabled ? 'disabled' : !snapshot ? (error ? 'error' : 'missing')
|
|
32
|
+
: expired || error ? 'stale' : 'fresh',
|
|
33
|
+
verifiedAt, publishedAt: snapshot?.publishedAt ?? null,
|
|
34
|
+
version: snapshot?.version ?? null, source: snapshot?.source ?? null,
|
|
35
|
+
endpoint: snapshot?.endpoint ?? null, configuredEndpoint: status?.endpoint ?? null,
|
|
36
|
+
error: enabled ? error : '',
|
|
37
|
+
...(kind === 'liveBench' ? { modelCount: Object.keys(snapshot?.models ?? {}).length }
|
|
38
|
+
: { rowCount: Object.keys(snapshot?.prices ?? {}).length }),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Age means source verification age, not the most recent attempted download. */
|
|
43
|
+
export function dynamicDataSummary(data, { enabled = false, ttlMs = DEFAULT_TTL_MS, now = Date.now } = {}) {
|
|
44
|
+
const at = currentTime(now)
|
|
45
|
+
const ttl = Number.isFinite(ttlMs) && ttlMs >= 0 ? ttlMs : DEFAULT_TTL_MS
|
|
46
|
+
return {
|
|
47
|
+
enabled: enabled === true,
|
|
48
|
+
liveBench: sourceSummary(data, 'liveBench', enabled === true, ttl, at),
|
|
49
|
+
pricing: sourceSummary(data, 'pricing', enabled === true, ttl, at),
|
|
50
|
+
revision: enabled === true ? String(data?.revision ?? '') : '',
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** No prompts, credentials, private endpoints, raw errors or price fetching enter the Client. */
|
|
55
|
+
export function routingData(data, enabled = false) {
|
|
56
|
+
return {
|
|
57
|
+
liveBench: enabled === true ? data?.liveBench ?? null : null,
|
|
58
|
+
liveBenchError: enabled === true ? publicError(data?.status?.liveBench?.error) : '',
|
|
59
|
+
dataVersions: {
|
|
60
|
+
liveBench: enabled === true ? data?.liveBench?.version ?? null : null,
|
|
61
|
+
pricing: enabled === true ? data?.pricing?.version ?? null : null,
|
|
62
|
+
revision: enabled === true ? String(data?.revision ?? '') : '',
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
}
|