@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
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { DEFAULT_ROUTING_PRESET, normalizeRoutingPreset, presetFloor, presetWeights } from './routing-presets.mjs'
|
|
10
10
|
import { liveBenchRow } from './livebench.mjs'
|
|
11
|
+
import { solveCandidateAssignments } from './assignment-solver.mjs'
|
|
11
12
|
|
|
12
13
|
export const OBJECTIVE_WEIGHTS = Object.freeze({
|
|
13
14
|
simple: Object.freeze({ quality: 0.28, cost: 0.45, latency: 0.14, specialty: 0.04, reasoning: 0.07, risk: 0.02 }),
|
|
@@ -194,11 +195,17 @@ function specialtyMatch(model, taskType, liveScores = {}) {
|
|
|
194
195
|
}
|
|
195
196
|
|
|
196
197
|
function qualityForTask(row, taskType) {
|
|
197
|
-
const base = asScore(row?.liveScores?.[taskType]) ?? asScore(row?.liveOverall) ?? row?.metadata?.quality ?? row?.quality ?? 0
|
|
198
|
-
//
|
|
198
|
+
const base = asScore(row?.liveScores?.[taskType]) ?? asScore(row?.liveOverall) ?? row?.metadata?.quality ?? row?.baseQuality ?? row?.quality ?? 0
|
|
199
|
+
// Legacy programmatic scalar bias only. Production feedback uses an
|
|
200
|
+
// independent preferenceAdjustments utility term, never this quality path.
|
|
199
201
|
return row?.qualityBias ? clamp(base + row.qualityBias) : base
|
|
200
202
|
}
|
|
201
203
|
|
|
204
|
+
function qualitySourceForTask(row, taskType) {
|
|
205
|
+
return asScore(row?.liveScores?.[taskType]) !== undefined || asScore(row?.liveOverall) !== undefined
|
|
206
|
+
? 'livebench' : row?.fallbackQualitySource ?? row?.qualitySource ?? 'unknown'
|
|
207
|
+
}
|
|
208
|
+
|
|
202
209
|
function specialtyForTask(row, taskType) {
|
|
203
210
|
return specialtyMatch(row.metadata, taskType, row.liveScores)
|
|
204
211
|
}
|
|
@@ -258,23 +265,9 @@ function normalizedCacheRatios(cacheReadRatio = 0, cacheWriteRatio = 0) {
|
|
|
258
265
|
return { read, write }
|
|
259
266
|
}
|
|
260
267
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
input: (1 - read - write) * Number(pricing.input)
|
|
265
|
-
+ read * Number(pricing.cacheRead)
|
|
266
|
-
+ write * Number(pricing.cacheWrite),
|
|
267
|
-
output: Number(pricing.output),
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function costScore(pricing, maxCost, cacheReadRatio = 0, cacheWriteRatio = 0) {
|
|
272
|
-
if (pricing === null) return 0
|
|
273
|
-
const effective = effectivePricing(pricing, cacheReadRatio, cacheWriteRatio)
|
|
274
|
-
const mean = (effective.input + effective.output) / 2
|
|
275
|
-
if (maxCost <= 0) return mean === 0 ? 1 : 0
|
|
276
|
-
return clamp(1 - mean / maxCost)
|
|
277
|
-
}
|
|
268
|
+
// A preference scale, not a vendor quote. Independent of the configured pool,
|
|
269
|
+
// so adding an irrelevant expensive model cannot flatten every cost penalty.
|
|
270
|
+
export const COST_REFERENCE_PRICING = Object.freeze({ input: 1, output: 5, cacheRead: 1, cacheWrite: 1, currency: 'USD' })
|
|
278
271
|
|
|
279
272
|
export function estimateCost(model, text, outputTokens = 900, pricingOverrides = {}, cacheReadRatio = 0, cacheWriteRatio = 0) {
|
|
280
273
|
const pricing = pricingFor(model, pricingOverrides)
|
|
@@ -679,38 +672,26 @@ function reasoningDecision(row, task) {
|
|
|
679
672
|
}
|
|
680
673
|
}
|
|
681
674
|
|
|
682
|
-
function candidateUtility(row, task, weights,
|
|
675
|
+
function candidateUtility(row, task, weights, cacheReadRatio = 0, cacheWriteRatio = 0, text = '', complexity = 'balanced') {
|
|
683
676
|
const quality = qualityForTask(row, task.type)
|
|
677
|
+
// Subjective satisfaction is an independent, bounded utility term. It
|
|
678
|
+
// must never raise benchmark quality, quality floors or evidence labels.
|
|
679
|
+
const rawPreference = row.preferenceAdjustments?.[task.type]
|
|
680
|
+
const preferenceAdjustment = Number.isFinite(rawPreference) ? clamp(rawPreference, -0.1, 0.1) : 0
|
|
684
681
|
const floor = Number(task.qualityFloor ?? taskQualityFloor('complex', task))
|
|
685
682
|
const qualityGap = Math.max(0, floor - quality)
|
|
686
|
-
// Reuse avoids handoff overhead and is preferable when the same affordable
|
|
687
|
-
// route is suitable for independent, low-risk work packages.
|
|
688
|
-
const duplicatePenalty = 0
|
|
689
|
-
const synthesisPreference = task.purpose === 'synthesis' && /deepseek[- ]?v4[- ]?pro/i.test(row.model) ? 0.025 : 0
|
|
690
683
|
const reasoning = reasoningDecision(row, task)
|
|
691
|
-
const
|
|
684
|
+
const reference = taskCost({ pricing: COST_REFERENCE_PRICING, reasoningKnown: true, reasoningEfforts: [] }, task, text, complexity, cacheReadRatio, cacheWriteRatio)
|
|
685
|
+
const cost = row.pricing === null ? 0 : 1 / (1 + taskCost(row, task, text, complexity, cacheReadRatio, cacheWriteRatio) / Math.max(reference, 1e-12))
|
|
692
686
|
const score = weights.quality * quality
|
|
693
687
|
+ weights.cost * cost
|
|
694
688
|
+ weights.latency * (1 - clamp(row.latency * reasoning.multiplier.latency))
|
|
695
689
|
+ weights.specialty * specialtyForTask(row, task.type)
|
|
696
690
|
+ (weights.reasoning ?? 0) * reasoning.reasoningFit
|
|
697
691
|
- weights.risk * row.risk
|
|
698
|
-
- duplicatePenalty
|
|
699
692
|
- qualityGap * (task.criticality ?? 0.75)
|
|
700
|
-
+
|
|
701
|
-
return { score, floor, qualityGap, ...reasoning }
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
function chooseAssignment(rows, task, weights, maxCost, usedRoutes, preferred, cacheReadRatio = 0, cacheWriteRatio = 0) {
|
|
705
|
-
const ordered = rows
|
|
706
|
-
.map(row => ({ row, decision: candidateUtility(row, task, weights, maxCost, usedRoutes, cacheReadRatio, cacheWriteRatio) }))
|
|
707
|
-
.sort((left, right) => right.decision.score - left.decision.score)
|
|
708
|
-
const feasible = ordered.filter(item => qualityForTask(item.row, task.type) >= item.decision.floor)
|
|
709
|
-
const chosen = (preferred === true ? feasible : feasible.filter(item => !usedRoutes.has(routeKey(item.row.provider, item.row.model))))[0]
|
|
710
|
-
?? feasible[0]
|
|
711
|
-
?? ordered[0]
|
|
712
|
-
if (chosen === undefined) return { row: null, relaxed: true, floor: 0, qualityGap: 1 }
|
|
713
|
-
return { row: chosen.row, relaxed: qualityForTask(chosen.row, task.type) < chosen.decision.floor, floor: chosen.decision.floor, qualityGap: chosen.decision.qualityGap }
|
|
693
|
+
+ preferenceAdjustment
|
|
694
|
+
return { score, floor, qualityGap, preferenceAdjustment, ...reasoning }
|
|
714
695
|
}
|
|
715
696
|
|
|
716
697
|
function taskCost(row, task, text, complexity, cacheReadRatio = 0, cacheWriteRatio = 0) {
|
|
@@ -760,10 +741,14 @@ function dominates(left, right, task, text, complexity, cacheReadRatio, cacheWri
|
|
|
760
741
|
return noWorse && strictlyBetter
|
|
761
742
|
}
|
|
762
743
|
|
|
763
|
-
function
|
|
764
|
-
|
|
744
|
+
function eligibleRowsForTask(rows, task) {
|
|
745
|
+
return task.type === 'vision'
|
|
765
746
|
? rows.filter(row => row.inputModalities.length === 0 || row.inputModalities.includes('image'))
|
|
766
747
|
: rows
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function candidatePool(rows, task, weights, text, complexity, cacheReadRatio, cacheWriteRatio, dependencySensitive = false) {
|
|
751
|
+
const eligibleRows = eligibleRowsForTask(rows, task)
|
|
767
752
|
const floor = Number(task.qualityFloor ?? 0)
|
|
768
753
|
const feasible = eligibleRows.filter(row => qualityForTask(row, task.type) >= floor)
|
|
769
754
|
const source = feasible.length > 0
|
|
@@ -772,109 +757,60 @@ function candidatePool(rows, task, weights, maxCost, text, complexity, cacheRead
|
|
|
772
757
|
const taskWeights = weightsForTask(weights, task)
|
|
773
758
|
const scored = source.map(row => ({
|
|
774
759
|
row,
|
|
775
|
-
decision: candidateUtility(row, task, taskWeights,
|
|
760
|
+
decision: candidateUtility(row, task, taskWeights, cacheReadRatio, cacheWriteRatio, text, complexity),
|
|
776
761
|
cost: taskCost(row, task, text, complexity, cacheReadRatio, cacheWriteRatio),
|
|
777
762
|
}))
|
|
778
|
-
|
|
763
|
+
// Local dominance is unsafe when route identity changes a later handoff.
|
|
764
|
+
// Even without dependencies, a pruning proof must preserve actual utility
|
|
765
|
+
// (including route-specific preferences), not just nominal score axes.
|
|
766
|
+
const frontier = dependencySensitive ? scored : scored.filter(item => !scored.some(other => other !== item
|
|
767
|
+
&& other.decision.score >= item.decision.score
|
|
768
|
+
&& dominates(other.row, item.row, task, text, complexity, cacheReadRatio, cacheWriteRatio)))
|
|
779
769
|
const essential = [
|
|
780
|
-
scored.
|
|
770
|
+
scored.filter(item => item.row.pricing !== null).sort((left, right) => left.cost - right.cost || compareRowsStable(left.row, right.row))[0],
|
|
781
771
|
scored.slice().sort((left, right) => right.decision.score - left.decision.score || compareRowsStable(left.row, right.row))[0],
|
|
782
772
|
scored.slice().sort((left, right) => qualityForTask(right.row, task.type) - qualityForTask(left.row, task.type) || compareRowsStable(left.row, right.row))[0],
|
|
783
773
|
].filter(Boolean)
|
|
784
|
-
const
|
|
774
|
+
const all = [...frontier, ...essential]
|
|
785
775
|
.filter((item, index, all) => all.findIndex(candidate => candidate.row === item.row) === index)
|
|
786
776
|
.sort((left, right) => right.decision.score - left.decision.score || left.cost - right.cost || compareRowsStable(left.row, right.row))
|
|
777
|
+
const anchors = [...new Set(essential)]
|
|
778
|
+
// Reserve anchors before truncation: adding expensive high-score routes
|
|
779
|
+
// must never remove the only budget-feasible, quality-eligible candidate.
|
|
780
|
+
const ordered = [...anchors, ...all.filter(item => !anchors.includes(item))]
|
|
787
781
|
.slice(0, ROUTING_CANDIDATE_LIMIT)
|
|
782
|
+
.sort((left, right) => right.decision.score - left.decision.score || left.cost - right.cost || compareRowsStable(left.row, right.row))
|
|
788
783
|
return {
|
|
789
784
|
options: ordered,
|
|
790
785
|
relaxed: feasible.length === 0,
|
|
791
|
-
pruned: Math.max(0,
|
|
786
|
+
pruned: Math.max(0, scored.length - all.length),
|
|
787
|
+
filtered: Math.max(0, eligibleRows.length - source.length),
|
|
788
|
+
truncated: Math.max(0, all.length - ordered.length),
|
|
789
|
+
dependencySensitive,
|
|
792
790
|
}
|
|
793
791
|
}
|
|
794
792
|
|
|
795
|
-
function
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|| left.switches - right.switches
|
|
805
|
-
|| compareText(stateSignature(left), stateSignature(right))
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
function compareCostStates(left, right) {
|
|
809
|
-
return left.relaxedCount - right.relaxedCount
|
|
810
|
-
|| left.qualityShortfall - right.qualityShortfall
|
|
811
|
-
|| left.cost - right.cost
|
|
812
|
-
|| right.score - left.score
|
|
813
|
-
|| left.switches - right.switches
|
|
814
|
-
|| compareText(stateSignature(left), stateSignature(right))
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
function solveAssignments({ rows, tasks, weights, maxCost, text, complexity, budget, cacheReadRatio, cacheWriteRatio, minimizeCost = false }) {
|
|
818
|
-
const pools = tasks.map(task => candidatePool(rows, task, weights, maxCost, text, complexity, cacheReadRatio, cacheWriteRatio))
|
|
819
|
-
if (pools.some(pool => pool.options.length === 0)) return null
|
|
820
|
-
const suffixMinimum = Array(tasks.length + 1).fill(0)
|
|
821
|
-
for (let index = tasks.length - 1; index >= 0; index -= 1) {
|
|
822
|
-
const costOptions = Number.isFinite(budget)
|
|
823
|
-
? pools[index].options.filter(option => option.row.pricing !== null)
|
|
824
|
-
: pools[index].options
|
|
825
|
-
if (costOptions.length === 0) return null
|
|
826
|
-
suffixMinimum[index] = suffixMinimum[index + 1] + Math.min(...costOptions.map(option => option.cost))
|
|
827
|
-
}
|
|
828
|
-
if (Number.isFinite(budget) && suffixMinimum[0] > budget + 1e-12) return null
|
|
829
|
-
|
|
830
|
-
let states = [{ assignments: [], routesByTask: new Map(), usedRoutes: new Set(), score: 0, cost: 0, switches: 0, relaxedCount: 0, qualityShortfall: 0 }]
|
|
831
|
-
for (let index = 0; index < tasks.length; index += 1) {
|
|
832
|
-
const task = tasks[index]
|
|
833
|
-
const pool = pools[index]
|
|
834
|
-
const expanded = []
|
|
835
|
-
for (const state of states) {
|
|
836
|
-
for (const option of pool.options) {
|
|
837
|
-
if (Number.isFinite(budget) && option.row.pricing === null) continue
|
|
838
|
-
const nextCost = state.cost + option.cost
|
|
839
|
-
if (Number.isFinite(budget) && nextCost + suffixMinimum[index + 1] > budget + 1e-12) continue
|
|
840
|
-
const taskWeights = weightsForTask(weights, task)
|
|
841
|
-
const decision = candidateUtility(option.row, task, taskWeights, maxCost, state.usedRoutes, cacheReadRatio, cacheWriteRatio)
|
|
842
|
-
const route = routeKey(option.row.provider, option.row.model)
|
|
843
|
-
const dependencySwitches = (task.dependsOn ?? []).reduce((count, dependency) => {
|
|
844
|
-
const dependencyRoute = state.routesByTask.get(dependency)
|
|
845
|
-
return count + (dependencyRoute !== undefined && dependencyRoute !== route ? 1 : 0)
|
|
846
|
-
}, 0)
|
|
847
|
-
const handoffPenalty = dependencySwitches * 0.015
|
|
848
|
-
const qualityShortfall = Math.max(0, decision.floor - qualityForTask(option.row, task.type))
|
|
849
|
-
const usedRoutes = new Set(state.usedRoutes)
|
|
850
|
-
usedRoutes.add(route)
|
|
851
|
-
const routesByTask = new Map(state.routesByTask)
|
|
852
|
-
routesByTask.set(task.id, route)
|
|
853
|
-
expanded.push({
|
|
854
|
-
assignments: [...state.assignments, { task, row: option.row, decision: { ...decision, relaxed: qualityShortfall > 0 }, estimatedCost: option.cost, handoffPenalty }],
|
|
855
|
-
routesByTask,
|
|
856
|
-
usedRoutes,
|
|
857
|
-
score: state.score + decision.score - handoffPenalty,
|
|
858
|
-
cost: nextCost,
|
|
859
|
-
switches: state.switches + dependencySwitches,
|
|
860
|
-
relaxedCount: state.relaxedCount + (qualityShortfall > 0 ? 1 : 0),
|
|
861
|
-
qualityShortfall: state.qualityShortfall + qualityShortfall,
|
|
862
|
-
})
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
if (expanded.length === 0) return null
|
|
866
|
-
expanded.sort(minimizeCost ? compareCostStates : compareUtilityStates)
|
|
867
|
-
states = expanded.slice(0, ROUTING_BEAM_WIDTH)
|
|
868
|
-
}
|
|
869
|
-
states.sort(minimizeCost ? compareCostStates : compareUtilityStates)
|
|
793
|
+
function solveAssignments({ rows, tasks, weights, text, complexity, budget, cacheReadRatio, cacheWriteRatio, minimizeCost = false }) {
|
|
794
|
+
const dependencySensitive = tasks.some(task => (task.dependsOn ?? []).length > 0)
|
|
795
|
+
const pools = tasks.map(task => candidatePool(rows, task, weights, text, complexity, cacheReadRatio, cacheWriteRatio, dependencySensitive))
|
|
796
|
+
const result = solveCandidateAssignments({ tasks, pools: pools.map((pool, index) => pool.options.map(option => ({
|
|
797
|
+
route: routeKey(option.row.provider, option.row.model), score: option.decision.score, cost: option.cost,
|
|
798
|
+
qualityShortfall: Math.max(0, option.decision.floor - qualityForTask(option.row, tasks[index].type)),
|
|
799
|
+
pricingKnown: option.row.pricing !== null, payload: option,
|
|
800
|
+
}))), budget, beamWidth: ROUTING_BEAM_WIDTH, minimizeCost })
|
|
801
|
+
if (!result) return null
|
|
870
802
|
return {
|
|
871
|
-
...
|
|
803
|
+
...result,
|
|
804
|
+
assignments: result.choices.map((choice, index) => ({ task: tasks[index], row: choice.payload.row,
|
|
805
|
+
decision: { ...choice.payload.decision, relaxed: choice.qualityShortfall > 0 },
|
|
806
|
+
estimatedCost: choice.cost, handoffPenalty: choice.handoffPenalty })),
|
|
807
|
+
usedRoutes: new Set(result.choices.map(choice => choice.route)),
|
|
872
808
|
candidatePools: pools,
|
|
873
|
-
minimumFeasibleCost:
|
|
809
|
+
minimumFeasibleCost: result.minimumCostLowerBound,
|
|
874
810
|
}
|
|
875
811
|
}
|
|
876
812
|
|
|
877
|
-
export function buildPlan({ text = '', available = [], mode = 'collective', pricing = {}, liveBench = null, liveBenchError = '', budgetUsd = 0, cacheReadRatio = 0, cacheWriteRatio = 0, preset = DEFAULT_ROUTING_PRESET } = {}) {
|
|
813
|
+
export function buildPlan({ text = '', available = [], mode = 'collective', pricing = {}, liveBench = null, liveBenchError = '', dataVersions = null, learning = null, budgetUsd = 0, cacheReadRatio = 0, cacheWriteRatio = 0, preset = DEFAULT_ROUTING_PRESET } = {}) {
|
|
878
814
|
const presetId = normalizeRoutingPreset(preset)
|
|
879
815
|
const firstLine = String(text ?? '').split(/\r?\n/u)[0].trim()
|
|
880
816
|
const transformOnly = /^(?:请|帮我)?(?:总结|概括|翻译|摘要|解释)(?:以下|下列|下面|这份|这些)/u.test(firstLine)
|
|
@@ -894,18 +830,22 @@ export function buildPlan({ text = '', available = [], mode = 'collective', pric
|
|
|
894
830
|
return {
|
|
895
831
|
provider: String(entry.provider ?? ''),
|
|
896
832
|
model: String(entry.model ?? ''),
|
|
833
|
+
benchmarkModel: typeof entry.benchmarkModel === 'string' ? entry.benchmarkModel : undefined,
|
|
897
834
|
reasoningEfforts: [...new Set(reasoningEfforts)],
|
|
898
835
|
defaultReasoningEffort: entry.defaultReasoningEffort === undefined ? undefined : String(entry.defaultReasoningEffort),
|
|
899
836
|
reasoningKnown: entry.reasoningKnown === true
|
|
900
837
|
|| (entry.reasoningKnown === undefined && Array.isArray(entry.reasoningEfforts)),
|
|
901
838
|
quality: asScore(entry.quality),
|
|
902
839
|
qualityBias: Number.isFinite(entry.qualityBias) ? clamp(entry.qualityBias, -0.05, 0.05) : 0,
|
|
840
|
+
preferenceAdjustments: entry.preferenceAdjustments ?? null,
|
|
903
841
|
qualitySource: entry.qualitySource === 'user' ? 'user' : 'route',
|
|
904
842
|
latency: asScore(entry.latency),
|
|
905
843
|
risk: asScore(entry.risk),
|
|
906
844
|
specialties: Array.isArray(entry.specialties) ? entry.specialties.filter(item => typeof item === 'string') : null,
|
|
907
845
|
pricing: normalizePricing({ route: entry.pricing ?? entry.price }).route ?? null,
|
|
908
|
-
pricingSource: entry.pricingSource === 'user' ? 'user' : 'route',
|
|
846
|
+
pricingSource: entry.pricingSource === 'user' ? 'user' : entry.pricingSource === 'dynamic' ? 'dynamic' : 'route',
|
|
847
|
+
pricingVersion: entry.pricingVersion ?? null,
|
|
848
|
+
snapshotAsOf: entry.snapshotAsOf ?? null,
|
|
909
849
|
inputModalities: Array.isArray(entry.inputModalities) ? entry.inputModalities.map(item => String(item).toLowerCase()) : [],
|
|
910
850
|
}
|
|
911
851
|
})
|
|
@@ -922,10 +862,16 @@ export function buildPlan({ text = '', available = [], mode = 'collective', pric
|
|
|
922
862
|
...(route.risk === undefined ? {} : { risk: route.risk }),
|
|
923
863
|
...(route.specialties === null ? {} : { specialties: route.specialties }),
|
|
924
864
|
}
|
|
925
|
-
const live = liveBenchRow(liveBench, route.model)
|
|
865
|
+
const live = liveBenchRow(liveBench, route.benchmarkModel ?? route.model)
|
|
926
866
|
const liveScores = live?.scores ?? {}
|
|
927
|
-
|
|
928
|
-
|
|
867
|
+
// A category average cannot provide evidence for an unmeasured task.
|
|
868
|
+
// Legacy cached category rows have no provenance: use their task scores
|
|
869
|
+
// but do not promote an ambiguous overall to cross-task evidence.
|
|
870
|
+
const liveOverall = live?.overallSource === 'explicit' || (live?.overallSource === undefined && Object.keys(liveScores).length === 0)
|
|
871
|
+
? asScore(live?.overall) : undefined
|
|
872
|
+
const baseQuality = asScore(metadata.quality) ?? 0
|
|
873
|
+
const quality = clamp((asScore(liveScores?.[taskType]) ?? liveOverall ?? baseQuality) + route.qualityBias)
|
|
874
|
+
const fallbackQualitySource = route.quality !== undefined ? route.qualitySource : catalog ? 'catalog-heuristic' : 'unknown'
|
|
929
875
|
const qualitySource = liveOverall !== undefined || asScore(liveScores?.[taskType]) !== undefined
|
|
930
876
|
? 'livebench' : route.quality !== undefined ? route.qualitySource : catalog ? 'catalog-heuristic' : 'unknown'
|
|
931
877
|
const userPrice = normalizedPrices[normalize(`${route.provider}/${route.model}`)] ?? normalizedPrices[normalize(route.model)]
|
|
@@ -940,9 +886,14 @@ export function buildPlan({ text = '', available = [], mode = 'collective', pric
|
|
|
940
886
|
model: route.model,
|
|
941
887
|
metadata,
|
|
942
888
|
quality,
|
|
889
|
+
baseQuality,
|
|
890
|
+
fallbackQualitySource,
|
|
943
891
|
qualityBias: route.qualityBias,
|
|
892
|
+
preferenceAdjustments: route.preferenceAdjustments,
|
|
944
893
|
qualitySource,
|
|
945
894
|
pricingSource,
|
|
895
|
+
pricingVersion: route.pricingVersion,
|
|
896
|
+
snapshotAsOf: route.snapshotAsOf,
|
|
946
897
|
liveScores,
|
|
947
898
|
liveOverall,
|
|
948
899
|
latency: metadata.latency ?? 0.5,
|
|
@@ -960,10 +911,6 @@ export function buildPlan({ text = '', available = [], mode = 'collective', pric
|
|
|
960
911
|
}, text, 900, {}, cacheReadRatio, cacheWriteRatio),
|
|
961
912
|
})
|
|
962
913
|
}
|
|
963
|
-
const maxCost = Math.max(1, ...rows.filter(row => row.pricing !== null).map(row => {
|
|
964
|
-
const effective = effectivePricing(row.pricing, cacheReadRatio, cacheWriteRatio)
|
|
965
|
-
return effective.input + effective.output
|
|
966
|
-
}))
|
|
967
914
|
// Presets tilt each package's weights and quality floor; balanced is a no-op.
|
|
968
915
|
const taskNodes = taskPackages(taskType, text, complexity.band).map(task => presetId === DEFAULT_ROUTING_PRESET ? task : {
|
|
969
916
|
...task,
|
|
@@ -978,7 +925,6 @@ export function buildPlan({ text = '', available = [], mode = 'collective', pric
|
|
|
978
925
|
rows,
|
|
979
926
|
tasks: taskNodes,
|
|
980
927
|
weights,
|
|
981
|
-
maxCost,
|
|
982
928
|
text,
|
|
983
929
|
complexity: complexity.band,
|
|
984
930
|
budget: Number.POSITIVE_INFINITY,
|
|
@@ -990,7 +936,6 @@ export function buildPlan({ text = '', available = [], mode = 'collective', pric
|
|
|
990
936
|
rows,
|
|
991
937
|
tasks: taskNodes,
|
|
992
938
|
weights,
|
|
993
|
-
maxCost,
|
|
994
939
|
text,
|
|
995
940
|
complexity: complexity.band,
|
|
996
941
|
budget,
|
|
@@ -1003,7 +948,6 @@ export function buildPlan({ text = '', available = [], mode = 'collective', pric
|
|
|
1003
948
|
rows,
|
|
1004
949
|
tasks: taskNodes,
|
|
1005
950
|
weights,
|
|
1006
|
-
maxCost,
|
|
1007
951
|
text,
|
|
1008
952
|
complexity: complexity.band,
|
|
1009
953
|
budget: Number.POSITIVE_INFINITY,
|
|
@@ -1017,7 +961,8 @@ export function buildPlan({ text = '', available = [], mode = 'collective', pric
|
|
|
1017
961
|
const usedRoutes = optimized?.usedRoutes ?? new Set()
|
|
1018
962
|
const constraintRelaxed = (optimized?.relaxedCount ?? 0) > 0
|
|
1019
963
|
for (const row of rows) {
|
|
1020
|
-
row.score = candidateUtility(row, taskNodes[0] ?? { type: taskType, qualityFloor: QUALITY_FLOORS[complexity.band] }, weights,
|
|
964
|
+
row.score = candidateUtility(row, taskNodes[0] ?? { type: taskType, qualityFloor: QUALITY_FLOORS[complexity.band] }, weights, cacheReadRatio, cacheWriteRatio, text, complexity.band).score
|
|
965
|
+
if (row.pricing !== null && taskNodes[0]) row.estimatedCost = taskCost(row, taskNodes[0], text, complexity.band, cacheReadRatio, cacheWriteRatio)
|
|
1021
966
|
}
|
|
1022
967
|
rows.sort((left, right) => right.score - left.score || compareRowsStable(left, right))
|
|
1023
968
|
const selectedAssignment = assignments[0]
|
|
@@ -1032,7 +977,7 @@ export function buildPlan({ text = '', available = [], mode = 'collective', pric
|
|
|
1032
977
|
difficulty: task.difficulty,
|
|
1033
978
|
recommended: row?.model ?? '待发现模型',
|
|
1034
979
|
recommendedProvider: row?.provider ?? '',
|
|
1035
|
-
qualitySource: row
|
|
980
|
+
qualitySource: qualitySourceForTask(row, task.type),
|
|
1036
981
|
pricingSource: row?.pricingSource ?? 'unknown',
|
|
1037
982
|
recommendedReasoningEffort: decision?.reasoningEffort,
|
|
1038
983
|
preferredReasoningEffort: decision?.preferredReasoningEffort ?? task.preferredReasoningEffort,
|
|
@@ -1060,23 +1005,24 @@ export function buildPlan({ text = '', available = [], mode = 'collective', pric
|
|
|
1060
1005
|
cacheWriteTokens: tokens.cacheWriteTokens,
|
|
1061
1006
|
outputTokens: tokens.outputTokens,
|
|
1062
1007
|
estimatedCost: row?.pricing === null || row === null ? null : Number(taskEstimate.toFixed(6)),
|
|
1063
|
-
quality: row
|
|
1064
|
-
qualitySource: row
|
|
1008
|
+
quality: qualitySourceForTask(row, task.type) === 'unknown' || row === null ? null : Number(qualityForTask(row, task.type).toFixed(3)),
|
|
1009
|
+
qualitySource: qualitySourceForTask(row, task.type),
|
|
1065
1010
|
pricingSource: row?.pricingSource ?? 'unknown',
|
|
1066
1011
|
handoffPenalty: Number(Number(handoffPenalty ?? 0).toFixed(3)),
|
|
1067
1012
|
}
|
|
1068
1013
|
})
|
|
1069
1014
|
const pricingComplete = assignments.length > 0 && assignments.every(({ row }) => row?.pricing !== null)
|
|
1070
|
-
const totalEstimate = pricingComplete ?
|
|
1015
|
+
const totalEstimate = pricingComplete ? assignments.reduce((sum, { task, row, estimatedCost }) => sum
|
|
1016
|
+
+ (estimatedCost ?? taskCost(row, task, text, complexity.band, cacheReadRatio, cacheWriteRatio)), 0) : null
|
|
1071
1017
|
const baselineRows = assignments.map(({ task }) => {
|
|
1072
|
-
const strongest = rows.reduce((best, row) => qualityForTask(row, task.type) > (best === null ? -1 : qualityForTask(best, task.type)) ? row : best, null)
|
|
1018
|
+
const strongest = eligibleRowsForTask(rows, task).reduce((best, row) => qualityForTask(row, task.type) > (best === null ? -1 : qualityForTask(best, task.type)) ? row : best, null)
|
|
1073
1019
|
return { task, strongest }
|
|
1074
1020
|
})
|
|
1075
1021
|
const baselineCost = baselineRows.every(item => item.strongest?.pricing !== null && item.strongest !== null)
|
|
1076
1022
|
? baselineRows.reduce((sum, { task, strongest }) => sum + taskCost(strongest, task, text, complexity.band, cacheReadRatio, cacheWriteRatio), 0)
|
|
1077
1023
|
: null
|
|
1078
|
-
const qualityEvidenceComplete = assignments.length > 0 && assignments.every(({ row }) => ['livebench', 'route', 'user'].includes(row
|
|
1079
|
-
&& baselineRows.every(({ strongest }) => ['livebench', 'route', 'user'].includes(strongest
|
|
1024
|
+
const qualityEvidenceComplete = assignments.length > 0 && assignments.every(({ row, task }) => ['livebench', 'route', 'user'].includes(qualitySourceForTask(row, task.type)))
|
|
1025
|
+
&& baselineRows.every(({ strongest, task }) => ['livebench', 'route', 'user'].includes(qualitySourceForTask(strongest, task.type)))
|
|
1080
1026
|
const budgetExceeded = Number(budgetUsd) > 0 && totalEstimate !== null ? totalEstimate > Number(budgetUsd) : null
|
|
1081
1027
|
const savings = baselineCost === null || totalEstimate === null || !qualityEvidenceComplete
|
|
1082
1028
|
? null : baselineCost <= 0 ? 0 : clamp((baselineCost - totalEstimate) / baselineCost)
|
|
@@ -1087,7 +1033,7 @@ export function buildPlan({ text = '', available = [], mode = 'collective', pric
|
|
|
1087
1033
|
? unassignableTasks.length > 0
|
|
1088
1034
|
? `图像工作包 ${unassignableTasks.join('、')} 没有可用的图像模型,无法形成完整分配计划。`
|
|
1089
1035
|
: '尚未发现可用模型,保留 Harness 原始模型选择。'
|
|
1090
|
-
: `${complexity.band === 'simple' ? '低复杂度优先成本、响应速度与较低推理开销' : complexity.band === 'balanced' ? '在质量、成本、推理等级、延迟与风险之间平衡' : '
|
|
1036
|
+
: `${complexity.band === 'simple' ? '低复杂度优先成本、响应速度与较低推理开销' : complexity.band === 'balanced' ? '在质量、成本、推理等级、延迟与风险之间平衡' : '高复杂度执行包含推理等级的依赖感知约束分配'};任务类型为 ${taskType},${String(subtasks.length)} 个工作包采用${optimized?.search?.exact ? '保留候选空间内的精确搜索' : '保留候选空间内的有界近似搜索'}。质量分与门槛是未校准的估计,不是答对概率。`
|
|
1091
1037
|
return {
|
|
1092
1038
|
mode,
|
|
1093
1039
|
preset: presetId,
|
|
@@ -1098,16 +1044,33 @@ export function buildPlan({ text = '', available = [], mode = 'collective', pric
|
|
|
1098
1044
|
taskTypes: [...new Set(taskNodes.map(task => task.type).filter(type => type !== 'reasoning'))],
|
|
1099
1045
|
objectiveWeights: weights,
|
|
1100
1046
|
candidates: rows.slice(0, 8).map(row => {
|
|
1101
|
-
const decision = candidateUtility(row, taskNodes[0] ?? { type: taskType, qualityFloor: QUALITY_FLOORS[complexity.band], preferredReasoningEffort: complexity.band === 'simple' ? 'low' : 'medium' }, weights,
|
|
1102
|
-
return { provider: row.provider, model: row.model, score: Number(row.score.toFixed(3)), quality: row.qualitySource === 'unknown' ? null : Number(row.quality.toFixed(3)), qualitySource: row.qualitySource, specialty: Number(row.specialty.toFixed(3)), reasoningEffort: decision.reasoningEffort, preferredReasoningEffort: decision.preferredReasoningEffort, reasoningFit: Number(decision.reasoningFit.toFixed(3)), reasoningKnown: row.reasoningKnown, reasoningEfforts: row.reasoningEfforts, estimatedCost: row.estimatedCost === null ? null : Number(row.estimatedCost.toFixed(6)), inputPrice: row.pricing?.input ?? null, outputPrice: row.pricing?.output ?? null, pricingSource: row.pricingSource }
|
|
1047
|
+
const decision = candidateUtility(row, taskNodes[0] ?? { type: taskType, qualityFloor: QUALITY_FLOORS[complexity.band], preferredReasoningEffort: complexity.band === 'simple' ? 'low' : 'medium' }, weights, cacheReadRatio, cacheWriteRatio, text, complexity.band)
|
|
1048
|
+
return { provider: row.provider, model: row.model, score: Number(row.score.toFixed(3)), preferenceAdjustment: decision.preferenceAdjustment, quality: row.qualitySource === 'unknown' ? null : Number(row.quality.toFixed(3)), qualitySource: row.qualitySource, specialty: Number(row.specialty.toFixed(3)), reasoningEffort: decision.reasoningEffort, preferredReasoningEffort: decision.preferredReasoningEffort, reasoningFit: Number(decision.reasoningFit.toFixed(3)), reasoningKnown: row.reasoningKnown, reasoningEfforts: row.reasoningEfforts, estimatedCost: row.estimatedCost === null ? null : Number(row.estimatedCost.toFixed(6)), inputPrice: row.pricing?.input ?? null, outputPrice: row.pricing?.output ?? null, pricingSource: row.pricingSource, pricingVersion: row.pricingVersion, pricingAsOf: row.snapshotAsOf }
|
|
1103
1049
|
}),
|
|
1104
|
-
selected: selected === null ? null : { provider: selected.provider, model: selected.model, reasoningEffort: selectedAssignment?.decision?.reasoningEffort, estimatedCost: selected.estimatedCost === null ? null : Number(selected.estimatedCost.toFixed(6)), qualitySource: selected.
|
|
1050
|
+
selected: selected === null ? null : { provider: selected.provider, model: selected.model, reasoningEffort: selectedAssignment?.decision?.reasoningEffort, estimatedCost: selected.estimatedCost === null ? null : Number(selected.estimatedCost.toFixed(6)), qualitySource: qualitySourceForTask(selected, selectedAssignment?.task.type ?? taskType), pricingSource: selected.pricingSource },
|
|
1105
1051
|
subtasks,
|
|
1106
1052
|
synthesizer: synthesizer == null ? null : { provider: synthesizer.provider, model: synthesizer.model, reasoningEffort: synthesizerAssignment?.decision?.reasoningEffort },
|
|
1107
|
-
|
|
1053
|
+
// Preserve precision for downstream budget gates; formatting belongs to UI.
|
|
1054
|
+
estimatedCost: totalEstimate,
|
|
1108
1055
|
costBreakdown,
|
|
1109
1056
|
optimization: {
|
|
1110
|
-
|
|
1057
|
+
dataVersions,
|
|
1058
|
+
personalization: learning ? {
|
|
1059
|
+
policyVersion: learning.policyVersion, revision: learning.revision,
|
|
1060
|
+
enabled: learning.enabled, scope: learning.scope, window: learning.window,
|
|
1061
|
+
feedbackCount: learning.feedbackCount, effectiveWeight: learning.effectiveWeight,
|
|
1062
|
+
halfLifeDays: learning.halfLifeDays, priorWeight: learning.priorWeight,
|
|
1063
|
+
maxAdjustment: learning.maxAdjustment, resetAt: learning.resetAt,
|
|
1064
|
+
semantics: 'subjective-utility-not-objective-quality',
|
|
1065
|
+
} : null,
|
|
1066
|
+
solver: 'dependency-safe quality-constrained exact/beam assignment',
|
|
1067
|
+
qualitySemantics: 'uncalibrated-surrogate',
|
|
1068
|
+
costUtility: { method: '1/(1+estimated-task-cost/reference-task-cost)', referencePricing: COST_REFERENCE_PRICING },
|
|
1069
|
+
search: optimized?.search ?? null,
|
|
1070
|
+
optimalityScope: 'retained-candidates-and-surrogate-objective',
|
|
1071
|
+
candidateTruncated: (optimized?.candidatePools ?? []).reduce((sum, pool) => sum + pool.truncated, 0),
|
|
1072
|
+
qualityFiltered: (optimized?.candidatePools ?? []).reduce((sum, pool) => sum + pool.filtered, 0),
|
|
1073
|
+
localParetoDisabled: (optimized?.candidatePools ?? []).some(pool => pool.dependencySensitive),
|
|
1111
1074
|
qualityFloor: QUALITY_FLOORS[complexity.band],
|
|
1112
1075
|
budgetUsd: Number(Number(budgetUsd) > 0 ? Number(budgetUsd) : 0),
|
|
1113
1076
|
cacheReadRatio: normalizedCacheRatios(cacheReadRatio, cacheWriteRatio).read,
|
|
@@ -1124,8 +1087,8 @@ export function buildPlan({ text = '', available = [], mode = 'collective', pric
|
|
|
1124
1087
|
beamWidth: ROUTING_BEAM_WIDTH,
|
|
1125
1088
|
budgetFeasible: budget <= 0 ? (pricingComplete ? true : null) : budgetPlan !== null,
|
|
1126
1089
|
minimumFeasibleCost: minimumFeasibleCost === null ? null : Number(Number(minimumFeasibleCost).toFixed(6)),
|
|
1127
|
-
liveBench: liveBench?.fetchedAt
|
|
1128
|
-
? { source: liveBench.source ?? 'livebench', fetchedAt: liveBench.fetchedAt, models: Object.keys(liveBench.models ?? {}).length, stale: String(liveBenchError).length > 0, error: String(liveBenchError || '') }
|
|
1090
|
+
liveBench: (liveBench?.fetchedAt ?? liveBench?.verifiedAt)
|
|
1091
|
+
? { source: liveBench.source ?? 'livebench', fetchedAt: liveBench.fetchedAt ?? liveBench.verifiedAt, verifiedAt: liveBench.verifiedAt ?? liveBench.fetchedAt, publishedAt: liveBench.publishedAt ?? null, version: liveBench.version ?? null, models: Object.keys(liveBench.models ?? {}).length, stale: String(liveBenchError).length > 0, error: String(liveBenchError || '') }
|
|
1129
1092
|
: { source: 'experimental-baseline', fetchedAt: null, models: 0, stale: false, error: String(liveBenchError || '') },
|
|
1130
1093
|
},
|
|
1131
1094
|
reason,
|
|
@@ -256,6 +256,9 @@ function storedPackage(planned, result, pricing, finishedAt, billingFor = billin
|
|
|
256
256
|
answer: String(result?.answer ?? '').slice(0, MAX_STORED_ANSWER),
|
|
257
257
|
answerTruncated: String(result?.answer ?? '').length > MAX_STORED_ANSWER,
|
|
258
258
|
usage: result?.usage ?? null,
|
|
259
|
+
// Freeze the rates used for this result. Future price updates must not
|
|
260
|
+
// silently rewrite historical spend calculated from usage.
|
|
261
|
+
pricingSnapshot: pricing ? { ...pricing } : null,
|
|
259
262
|
...cost,
|
|
260
263
|
finishedAt,
|
|
261
264
|
review: planned.review ?? null,
|
|
@@ -291,6 +294,8 @@ export function buildRunRecord({ id, createdAt, task, plan, execution, preset =
|
|
|
291
294
|
mode: plan?.mode ?? 'single',
|
|
292
295
|
preset,
|
|
293
296
|
decision: {
|
|
297
|
+
dataVersions: plan?.optimization?.dataVersions ?? null,
|
|
298
|
+
personalization: plan?.optimization?.personalization ?? null,
|
|
294
299
|
selected: plan?.selected ? { provider: plan.selected.provider, model: plan.selected.model } : null,
|
|
295
300
|
reason: String(plan?.reason ?? ''),
|
|
296
301
|
complexity: plan?.complexity ? { band: plan.complexity.band, value: finite(plan.complexity.value) ? Number(plan.complexity.value.toFixed(3)) : null } : null,
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.0 — 2026-10-05
|
|
4
|
+
|
|
5
|
+
Stable release under the npm `latest` tag. Fully quit Harness, including the tray process, and restart after upgrading so both the Host and client load the new version. Public data refresh is opt-in; enabling it does not authorize paid model exploration.
|
|
6
|
+
|
|
7
|
+
- Add opt-in versioned public LiveBench/curated pricing snapshots with TTL, failure backoff, last-good fallback and atomic cross-process promotion; explicit manual prices win. Freeze each run's applied rates and data/policy versions, and disable cleared sources immediately without deleting recoverable snapshots.
|
|
8
|
+
- Replace production review/rating quality bias with task-specific, time-decayed, shrinkage-regularized subjective utility. Preserve objective proxy quality floors; expose learning controls/reset and the local DSH_HOME / last-200-runs scope. Agent rating tools require approval; Desktop ratings bind to the displayed result timestamp.
|
|
9
|
+
- Require explicit score scale for dynamic benchmark mirrors and exact benchmark model identity; expose optional benchmarkModel mapping without claiming verified execution effort. Add controlled synthetic adaptation and Host/Client integration tests; no paid exploration, real-user result collection, universal official pricing scraper or actual quality-cost benefit claim.
|
|
10
|
+
|
|
11
|
+
- Use estimated per-package token cost against a fixed reference pricing scale, not the most expensive configured route; an irrelevant expensive model no longer rescales existing preferences. Remove the model-name-specific synthesis bonus.
|
|
12
|
+
- Protect least-cost known-price candidates before truncating pools. Disable local Pareto pruning for dependency-coupled tasks, where route identity changes later handoff penalties.
|
|
13
|
+
- Solve retained candidate spaces of at most 4,096 combinations exactly; larger spaces use bounded beam search with a protected affordable prefix. Expose exact/approximate search, candidate truncation and the surrogate-objective scope instead of claiming global answer-quality optimality.
|
|
14
|
+
- Correct repeated quality-bias application for unknown models. Explain that benchmark/manual scores and quality floors are not calibrated correctness probabilities.
|
|
15
|
+
- Preserve explicit/derived benchmark provenance, exclude image-ineligible savings baselines, and use unrounded totals for budget gates. Report quality filtering, Pareto pruning, and candidate truncation separately.
|
|
16
|
+
- Add independent exhaustive-oracle tests and a label-matrix offline evaluation tool with validation-selected single-model baselines, random routing, hindsight Oracle, actual recorded costs and rare-expert recall. No paid model calls or real quality-improvement claim.
|
|
17
|
+
- Audit the official LiveBench 2026-06-25 aggregate score/cost release with pinned source hashes, complete-coverage checks and descriptive static Pareto profiles. Add continuous-score, domain-stratified whole-group paired bootstrap tooling and document the missing latest per-question cost/quality matrix. Aggregates are not replayed as question outcomes; no real routing-benefit claim or production routing change.
|
|
18
|
+
|
|
19
|
+
Validation: 558 tests, with 555 passed, 3 existing platform-condition skips and no failures. Strict frozen-lockfile installation, generated-client check and actual-component mock-preview compilation passed. Nine controlled synthetic adaptation checks passed; these are mechanism tests, not real model quality-cost gains. Real paid model calls, user-result collection, execution-effort validation and Harness Desktop end-to-end acceptance were not part of this verification. Research artifacts are in the complete GitHub source checkout, not the npm runtime package; historical experiments retain their original development-version identifiers.
|
|
20
|
+
|
|
3
21
|
## 0.15.0 — 2026-10-03
|
|
4
22
|
|
|
5
23
|
- Reorganize the workbench into task/execution, model configuration, official tools, and budget/security pages. Keep panels mounted so drafts, installation polling, and terminal sessions survive navigation; add keyboard tab navigation and panel-width responsive layouts.
|