@ljwei-stak/model-router-galgame 0.4.10 → 0.4.12
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 +202 -49
- package/.dsh-plugin/shared/router.mjs +235 -49
- package/README.md +164 -9
- package/README.zh.md +158 -9
- package/cordis.patch.yml +2 -0
- package/package.json +1 -1
package/.dsh-plugin/client.js
CHANGED
|
@@ -59760,7 +59760,7 @@ var OBJECTIVE_WEIGHTS = Object.freeze({
|
|
|
59760
59760
|
balanced: Object.freeze({ quality: 0.45, cost: 0.3, latency: 0.1, specialty: 0.1, risk: 0.05 }),
|
|
59761
59761
|
complex: Object.freeze({ quality: 0.55, cost: 0.16, latency: 0.06, specialty: 0.16, risk: 0.07 })
|
|
59762
59762
|
});
|
|
59763
|
-
var QUALITY_FLOORS = Object.freeze({ simple: 0.
|
|
59763
|
+
var QUALITY_FLOORS = Object.freeze({ simple: 0.75, balanced: 0.78, complex: 0.82 });
|
|
59764
59764
|
var MODEL_ROUTER_SETTINGS_NAMESPACE = "model-router";
|
|
59765
59765
|
var DEFAULT_ROUTER_SETTINGS = Object.freeze({
|
|
59766
59766
|
pricing: Object.freeze({}),
|
|
@@ -59958,48 +59958,176 @@ function taskQualityFloor(band, task) {
|
|
|
59958
59958
|
}
|
|
59959
59959
|
function taskPackages(taskType, text6, band) {
|
|
59960
59960
|
if (band !== "complex") {
|
|
59961
|
-
const task = { name: "\u76F4\u63A5\u56DE\u7B54\u4E0E\u5FC5\u8981\u6821\u9A8C", type: taskType, purpose: "execution", criticality: 0.65 };
|
|
59961
|
+
const task = { id: "execution", name: "\u76F4\u63A5\u56DE\u7B54\u4E0E\u5FC5\u8981\u6821\u9A8C", type: taskType, purpose: "execution", criticality: 0.65, dependsOn: [] };
|
|
59962
59962
|
return [{ ...task, qualityFloor: taskQualityFloor(band, task) }];
|
|
59963
59963
|
}
|
|
59964
59964
|
const value = String(text6 ?? "");
|
|
59965
59965
|
const packages = [
|
|
59966
|
-
{ name: "\u95EE\u9898\u5EFA\u6A21\u4E0E\u7EA6\u675F\u63D0\u53D6", type: "reasoning", purpose: "analysis", criticality: 0.92 }
|
|
59966
|
+
{ id: "analysis", name: "\u95EE\u9898\u5EFA\u6A21\u4E0E\u7EA6\u675F\u63D0\u53D6", type: "reasoning", purpose: "analysis", criticality: 0.92, dependsOn: [] }
|
|
59967
59967
|
];
|
|
59968
59968
|
const domains = [...new Set([...detectTaskTypes(text6), taskType].filter((type) => type !== "general"))];
|
|
59969
59969
|
for (const type of domains.length > 0 ? domains : [taskType]) {
|
|
59970
59970
|
packages.push({
|
|
59971
|
+
id: `execution-${type}`,
|
|
59971
59972
|
name: `${TASK_TYPE_LABELS[type] ?? type}\u65B9\u5411\u5904\u7406`,
|
|
59972
59973
|
type,
|
|
59973
59974
|
purpose: "execution",
|
|
59974
|
-
criticality: domains.length > 1 ? 0.8 : 0.78
|
|
59975
|
+
criticality: domains.length > 1 ? 0.8 : 0.78,
|
|
59976
|
+
dependsOn: ["analysis"]
|
|
59975
59977
|
});
|
|
59976
59978
|
}
|
|
59977
59979
|
if (/(测试|验证|评估|对比|benchmark|test|verify|audit)/i.test(value)) {
|
|
59978
|
-
packages.push({
|
|
59980
|
+
packages.push({
|
|
59981
|
+
id: "verification",
|
|
59982
|
+
name: "\u9A8C\u8BC1\u3001\u53CD\u4F8B\u4E0E\u98CE\u9669\u5BA1\u67E5",
|
|
59983
|
+
type: "reasoning",
|
|
59984
|
+
purpose: "verification",
|
|
59985
|
+
criticality: 0.88,
|
|
59986
|
+
dependsOn: packages.filter((task) => task.purpose === "execution").map((task) => task.id)
|
|
59987
|
+
});
|
|
59979
59988
|
}
|
|
59980
|
-
packages.push({
|
|
59989
|
+
packages.push({
|
|
59990
|
+
id: "synthesis",
|
|
59991
|
+
name: "\u7ED3\u679C\u6821\u9A8C\u4E0E\u6574\u5408",
|
|
59992
|
+
type: "reasoning",
|
|
59993
|
+
purpose: "synthesis",
|
|
59994
|
+
criticality: 1,
|
|
59995
|
+
dependsOn: packages.filter((task) => task.purpose !== "analysis").map((task) => task.id)
|
|
59996
|
+
});
|
|
59981
59997
|
return packages.map((task) => ({ ...task, qualityFloor: taskQualityFloor(band, task) }));
|
|
59982
59998
|
}
|
|
59999
|
+
var SYNTHESIS_WEIGHTS = Object.freeze({ quality: 0.7, cost: 0.1, latency: 0.04, specialty: 0.1, risk: 0.06 });
|
|
60000
|
+
var ROUTING_BEAM_WIDTH = 256;
|
|
60001
|
+
var ROUTING_CANDIDATE_LIMIT = 12;
|
|
60002
|
+
function weightsForTask(weights, task) {
|
|
60003
|
+
return task.purpose === "synthesis" ? SYNTHESIS_WEIGHTS : weights;
|
|
60004
|
+
}
|
|
60005
|
+
function compareText(left, right) {
|
|
60006
|
+
const a2 = String(left);
|
|
60007
|
+
const b3 = String(right);
|
|
60008
|
+
return a2 < b3 ? -1 : a2 > b3 ? 1 : 0;
|
|
60009
|
+
}
|
|
60010
|
+
function compareRowsStable(left, right) {
|
|
60011
|
+
return compareText(routeKey(left.provider, left.model), routeKey(right.provider, right.model));
|
|
60012
|
+
}
|
|
59983
60013
|
function candidateUtility(row, task, weights, maxCost, usedRoutes, cacheReadRatio = 0, cacheWriteRatio = 0) {
|
|
59984
60014
|
const quality = qualityForTask(row, task.type);
|
|
59985
60015
|
const floor = Number(task.qualityFloor ?? taskQualityFloor("complex", task));
|
|
59986
60016
|
const qualityGap = Math.max(0, floor - quality);
|
|
59987
60017
|
const duplicatePenalty = usedRoutes.has(routeKey(row.provider, row.model)) ? 0.08 : 0;
|
|
60018
|
+
const synthesisPreference = task.purpose === "synthesis" && /deepseek[- ]?v4[- ]?pro/i.test(row.model) ? 0.025 : 0;
|
|
59988
60019
|
const cost = costScore(row.pricing, maxCost, cacheReadRatio, cacheWriteRatio);
|
|
59989
|
-
const score = weights.quality * quality + weights.cost * cost + weights.latency * (1 - clamp(row.latency)) + weights.specialty * specialtyForTask(row, task.type) - weights.risk * row.risk - duplicatePenalty - qualityGap * (task.criticality ?? 0.75);
|
|
60020
|
+
const score = weights.quality * quality + weights.cost * cost + weights.latency * (1 - clamp(row.latency)) + weights.specialty * specialtyForTask(row, task.type) - weights.risk * row.risk - duplicatePenalty - qualityGap * (task.criticality ?? 0.75) + synthesisPreference;
|
|
59990
60021
|
return { score, floor, qualityGap };
|
|
59991
60022
|
}
|
|
59992
|
-
function chooseAssignment(rows, task, weights, maxCost, usedRoutes, preferred, cacheReadRatio = 0, cacheWriteRatio = 0) {
|
|
59993
|
-
const ordered = rows.map((row) => ({ row, decision: candidateUtility(row, task, weights, maxCost, usedRoutes, cacheReadRatio, cacheWriteRatio) })).sort((left, right) => right.decision.score - left.decision.score);
|
|
59994
|
-
const feasible = ordered.filter((item) => qualityForTask(item.row, task.type) >= item.decision.floor);
|
|
59995
|
-
const chosen = (preferred === true ? feasible : feasible.filter((item) => !usedRoutes.has(routeKey(item.row.provider, item.row.model))))[0] ?? feasible[0] ?? ordered[0];
|
|
59996
|
-
if (chosen === void 0) return { row: null, relaxed: true, floor: 0, qualityGap: 1 };
|
|
59997
|
-
return { row: chosen.row, relaxed: qualityForTask(chosen.row, task.type) < chosen.decision.floor, floor: chosen.decision.floor, qualityGap: chosen.decision.qualityGap };
|
|
59998
|
-
}
|
|
59999
60023
|
function taskCost(row, task, text6, complexity, cacheReadRatio = 0, cacheWriteRatio = 0) {
|
|
60000
60024
|
const tokens = taskTokenBudget(text6, task, complexity, cacheReadRatio, cacheWriteRatio);
|
|
60001
60025
|
return row === null ? 0 : ((tokens.inputTokens - tokens.cacheReadTokens - tokens.cacheWriteTokens) * row.pricing.input + tokens.cacheReadTokens * row.pricing.cacheRead + tokens.cacheWriteTokens * row.pricing.cacheWrite + tokens.outputTokens * row.pricing.output) / 1e6;
|
|
60002
60026
|
}
|
|
60027
|
+
function dominates(left, right, task, text6, complexity, cacheReadRatio, cacheWriteRatio) {
|
|
60028
|
+
const leftValues = {
|
|
60029
|
+
quality: qualityForTask(left, task.type),
|
|
60030
|
+
cost: taskCost(left, task, text6, complexity, cacheReadRatio, cacheWriteRatio),
|
|
60031
|
+
latency: clamp(left.latency),
|
|
60032
|
+
specialty: specialtyForTask(left, task.type),
|
|
60033
|
+
risk: clamp(left.risk)
|
|
60034
|
+
};
|
|
60035
|
+
const rightValues = {
|
|
60036
|
+
quality: qualityForTask(right, task.type),
|
|
60037
|
+
cost: taskCost(right, task, text6, complexity, cacheReadRatio, cacheWriteRatio),
|
|
60038
|
+
latency: clamp(right.latency),
|
|
60039
|
+
specialty: specialtyForTask(right, task.type),
|
|
60040
|
+
risk: clamp(right.risk)
|
|
60041
|
+
};
|
|
60042
|
+
const noWorse = leftValues.quality >= rightValues.quality && leftValues.cost <= rightValues.cost && leftValues.latency <= rightValues.latency && leftValues.specialty >= rightValues.specialty && leftValues.risk <= rightValues.risk;
|
|
60043
|
+
const strictlyBetter = leftValues.quality > rightValues.quality || leftValues.cost < rightValues.cost || leftValues.latency < rightValues.latency || leftValues.specialty > rightValues.specialty || leftValues.risk < rightValues.risk;
|
|
60044
|
+
return noWorse && strictlyBetter;
|
|
60045
|
+
}
|
|
60046
|
+
function candidatePool(rows, task, weights, maxCost, text6, complexity, cacheReadRatio, cacheWriteRatio) {
|
|
60047
|
+
const floor = Number(task.qualityFloor ?? 0);
|
|
60048
|
+
const feasible = rows.filter((row) => qualityForTask(row, task.type) >= floor);
|
|
60049
|
+
const source = feasible.length > 0 ? feasible : rows.slice().sort((left, right) => qualityForTask(right, task.type) - qualityForTask(left, task.type) || compareRowsStable(left, right)).slice(0, 3);
|
|
60050
|
+
const taskWeights = weightsForTask(weights, task);
|
|
60051
|
+
const scored = source.map((row) => ({
|
|
60052
|
+
row,
|
|
60053
|
+
decision: candidateUtility(row, task, taskWeights, maxCost, /* @__PURE__ */ new Set(), cacheReadRatio, cacheWriteRatio),
|
|
60054
|
+
cost: taskCost(row, task, text6, complexity, cacheReadRatio, cacheWriteRatio)
|
|
60055
|
+
}));
|
|
60056
|
+
const frontier = scored.filter((item) => !source.some((other) => other !== item.row && dominates(other, item.row, task, text6, complexity, cacheReadRatio, cacheWriteRatio)));
|
|
60057
|
+
const essential = [
|
|
60058
|
+
scored.slice().sort((left, right) => left.cost - right.cost || compareRowsStable(left.row, right.row))[0],
|
|
60059
|
+
scored.slice().sort((left, right) => right.decision.score - left.decision.score || compareRowsStable(left.row, right.row))[0],
|
|
60060
|
+
scored.slice().sort((left, right) => qualityForTask(right.row, task.type) - qualityForTask(left.row, task.type) || compareRowsStable(left.row, right.row))[0]
|
|
60061
|
+
].filter(Boolean);
|
|
60062
|
+
const ordered = [...frontier, ...essential].filter((item, index2, all3) => all3.findIndex((candidate) => candidate.row === item.row) === index2).sort((left, right) => right.decision.score - left.decision.score || left.cost - right.cost || compareRowsStable(left.row, right.row)).slice(0, ROUTING_CANDIDATE_LIMIT);
|
|
60063
|
+
return {
|
|
60064
|
+
options: ordered,
|
|
60065
|
+
relaxed: feasible.length === 0,
|
|
60066
|
+
pruned: Math.max(0, rows.length - ordered.length)
|
|
60067
|
+
};
|
|
60068
|
+
}
|
|
60069
|
+
function stateSignature(state) {
|
|
60070
|
+
return state.assignments.map((assignment) => routeKey(assignment.row?.provider, assignment.row?.model)).join("|");
|
|
60071
|
+
}
|
|
60072
|
+
function compareUtilityStates(left, right) {
|
|
60073
|
+
return left.relaxedCount - right.relaxedCount || left.qualityShortfall - right.qualityShortfall || right.score - left.score || left.cost - right.cost || left.switches - right.switches || compareText(stateSignature(left), stateSignature(right));
|
|
60074
|
+
}
|
|
60075
|
+
function compareCostStates(left, right) {
|
|
60076
|
+
return left.relaxedCount - right.relaxedCount || left.qualityShortfall - right.qualityShortfall || left.cost - right.cost || right.score - left.score || left.switches - right.switches || compareText(stateSignature(left), stateSignature(right));
|
|
60077
|
+
}
|
|
60078
|
+
function solveAssignments({ rows, tasks, weights, maxCost, text: text6, complexity, budget, cacheReadRatio, cacheWriteRatio, minimizeCost = false }) {
|
|
60079
|
+
const pools = tasks.map((task) => candidatePool(rows, task, weights, maxCost, text6, complexity, cacheReadRatio, cacheWriteRatio));
|
|
60080
|
+
if (pools.some((pool) => pool.options.length === 0)) return null;
|
|
60081
|
+
const suffixMinimum = Array(tasks.length + 1).fill(0);
|
|
60082
|
+
for (let index2 = tasks.length - 1; index2 >= 0; index2 -= 1) {
|
|
60083
|
+
suffixMinimum[index2] = suffixMinimum[index2 + 1] + Math.min(...pools[index2].options.map((option2) => option2.cost));
|
|
60084
|
+
}
|
|
60085
|
+
if (Number.isFinite(budget) && suffixMinimum[0] > budget + 1e-12) return null;
|
|
60086
|
+
let states = [{ assignments: [], routesByTask: /* @__PURE__ */ new Map(), usedRoutes: /* @__PURE__ */ new Set(), score: 0, cost: 0, switches: 0, relaxedCount: 0, qualityShortfall: 0 }];
|
|
60087
|
+
for (let index2 = 0; index2 < tasks.length; index2 += 1) {
|
|
60088
|
+
const task = tasks[index2];
|
|
60089
|
+
const pool = pools[index2];
|
|
60090
|
+
const expanded = [];
|
|
60091
|
+
for (const state of states) {
|
|
60092
|
+
for (const option2 of pool.options) {
|
|
60093
|
+
const nextCost = state.cost + option2.cost;
|
|
60094
|
+
if (Number.isFinite(budget) && nextCost + suffixMinimum[index2 + 1] > budget + 1e-12) continue;
|
|
60095
|
+
const taskWeights = weightsForTask(weights, task);
|
|
60096
|
+
const decision = candidateUtility(option2.row, task, taskWeights, maxCost, state.usedRoutes, cacheReadRatio, cacheWriteRatio);
|
|
60097
|
+
const route = routeKey(option2.row.provider, option2.row.model);
|
|
60098
|
+
const dependencySwitches = (task.dependsOn ?? []).reduce((count, dependency) => {
|
|
60099
|
+
const dependencyRoute = state.routesByTask.get(dependency);
|
|
60100
|
+
return count + (dependencyRoute !== void 0 && dependencyRoute !== route ? 1 : 0);
|
|
60101
|
+
}, 0);
|
|
60102
|
+
const handoffPenalty = dependencySwitches * 0.015;
|
|
60103
|
+
const qualityShortfall = Math.max(0, decision.floor - qualityForTask(option2.row, task.type));
|
|
60104
|
+
const usedRoutes = new Set(state.usedRoutes);
|
|
60105
|
+
usedRoutes.add(route);
|
|
60106
|
+
const routesByTask = new Map(state.routesByTask);
|
|
60107
|
+
routesByTask.set(task.id, route);
|
|
60108
|
+
expanded.push({
|
|
60109
|
+
assignments: [...state.assignments, { task, row: option2.row, decision: { ...decision, relaxed: qualityShortfall > 0 }, estimatedCost: option2.cost, handoffPenalty }],
|
|
60110
|
+
routesByTask,
|
|
60111
|
+
usedRoutes,
|
|
60112
|
+
score: state.score + decision.score - handoffPenalty,
|
|
60113
|
+
cost: nextCost,
|
|
60114
|
+
switches: state.switches + dependencySwitches,
|
|
60115
|
+
relaxedCount: state.relaxedCount + (qualityShortfall > 0 ? 1 : 0),
|
|
60116
|
+
qualityShortfall: state.qualityShortfall + qualityShortfall
|
|
60117
|
+
});
|
|
60118
|
+
}
|
|
60119
|
+
}
|
|
60120
|
+
if (expanded.length === 0) return null;
|
|
60121
|
+
expanded.sort(minimizeCost ? compareCostStates : compareUtilityStates);
|
|
60122
|
+
states = expanded.slice(0, ROUTING_BEAM_WIDTH);
|
|
60123
|
+
}
|
|
60124
|
+
states.sort(minimizeCost ? compareCostStates : compareUtilityStates);
|
|
60125
|
+
return {
|
|
60126
|
+
...states[0],
|
|
60127
|
+
candidatePools: pools,
|
|
60128
|
+
minimumFeasibleCost: suffixMinimum[0]
|
|
60129
|
+
};
|
|
60130
|
+
}
|
|
60003
60131
|
function buildPlan({ text: text6 = "", available = [], mode = "collective", pricing = {}, liveBench = null, liveBenchError = "", budgetUsd = 0, cacheReadRatio = 0, cacheWriteRatio = 0 } = {}) {
|
|
60004
60132
|
const complexity = assessComplexity(text6);
|
|
60005
60133
|
const taskType = classifyTask(text6);
|
|
@@ -60048,48 +60176,65 @@ function buildPlan({ text: text6 = "", available = [], mode = "collective", pric
|
|
|
60048
60176
|
});
|
|
60049
60177
|
}
|
|
60050
60178
|
const taskNodes = taskPackages(taskType, text6, complexity.band);
|
|
60051
|
-
const usedRoutes = /* @__PURE__ */ new Set();
|
|
60052
|
-
let constraintRelaxed = false;
|
|
60053
|
-
const assignments = [];
|
|
60054
|
-
for (const task of taskNodes) {
|
|
60055
|
-
const preferred = task.purpose === "synthesis" ? rows.find((row2) => /deepseek[- ]?v4[- ]?pro/i.test(row2.model)) ?? rows.find((row2) => /deepseek/i.test(row2.model)) : null;
|
|
60056
|
-
const selectedAssignment = preferred === null ? chooseAssignment(rows, task, weights, maxCost, usedRoutes, false, cacheReadRatio, cacheWriteRatio) : { row: preferred, relaxed: qualityForTask(preferred, task.type) < task.qualityFloor, floor: task.qualityFloor, qualityGap: Math.max(0, task.qualityFloor - qualityForTask(preferred, task.type)) };
|
|
60057
|
-
const row = selectedAssignment.row;
|
|
60058
|
-
if (row !== null) {
|
|
60059
|
-
row.score = candidateUtility(row, task, weights, maxCost, usedRoutes, cacheReadRatio, cacheWriteRatio).score;
|
|
60060
|
-
usedRoutes.add(routeKey(row.provider, row.model));
|
|
60061
|
-
}
|
|
60062
|
-
constraintRelaxed || (constraintRelaxed = selectedAssignment.relaxed);
|
|
60063
|
-
assignments.push({ task, row, decision: selectedAssignment });
|
|
60064
|
-
}
|
|
60065
60179
|
const budget = Number(budgetUsd);
|
|
60066
|
-
const
|
|
60067
|
-
|
|
60068
|
-
|
|
60069
|
-
|
|
60070
|
-
|
|
60071
|
-
|
|
60072
|
-
|
|
60073
|
-
|
|
60074
|
-
|
|
60075
|
-
|
|
60076
|
-
|
|
60077
|
-
|
|
60078
|
-
|
|
60180
|
+
const utilityPlan = solveAssignments({
|
|
60181
|
+
rows,
|
|
60182
|
+
tasks: taskNodes,
|
|
60183
|
+
weights,
|
|
60184
|
+
maxCost,
|
|
60185
|
+
text: text6,
|
|
60186
|
+
complexity: complexity.band,
|
|
60187
|
+
budget: Number.POSITIVE_INFINITY,
|
|
60188
|
+
cacheReadRatio,
|
|
60189
|
+
cacheWriteRatio
|
|
60190
|
+
});
|
|
60191
|
+
const budgetPlan = budget > 0 ? solveAssignments({
|
|
60192
|
+
rows,
|
|
60193
|
+
tasks: taskNodes,
|
|
60194
|
+
weights,
|
|
60195
|
+
maxCost,
|
|
60196
|
+
text: text6,
|
|
60197
|
+
complexity: complexity.band,
|
|
60198
|
+
budget,
|
|
60199
|
+
cacheReadRatio,
|
|
60200
|
+
cacheWriteRatio
|
|
60201
|
+
}) : null;
|
|
60202
|
+
const minimumCostPlan = budget > 0 && budgetPlan === null ? solveAssignments({
|
|
60203
|
+
rows,
|
|
60204
|
+
tasks: taskNodes,
|
|
60205
|
+
weights,
|
|
60206
|
+
maxCost,
|
|
60207
|
+
text: text6,
|
|
60208
|
+
complexity: complexity.band,
|
|
60209
|
+
budget: Number.POSITIVE_INFINITY,
|
|
60210
|
+
cacheReadRatio,
|
|
60211
|
+
cacheWriteRatio,
|
|
60212
|
+
minimizeCost: true
|
|
60213
|
+
}) : null;
|
|
60214
|
+
const optimized = budget > 0 ? budgetPlan ?? minimumCostPlan ?? utilityPlan : utilityPlan;
|
|
60215
|
+
const assignments = optimized?.assignments ?? [];
|
|
60216
|
+
const usedRoutes = optimized?.usedRoutes ?? /* @__PURE__ */ new Set();
|
|
60217
|
+
const constraintRelaxed = (optimized?.relaxedCount ?? 0) > 0;
|
|
60218
|
+
for (const row of rows) {
|
|
60219
|
+
row.score = candidateUtility(row, taskNodes[0] ?? { type: taskType, qualityFloor: QUALITY_FLOORS[complexity.band] }, weights, maxCost, /* @__PURE__ */ new Set(), cacheReadRatio, cacheWriteRatio).score;
|
|
60220
|
+
}
|
|
60221
|
+
rows.sort((left, right) => right.score - left.score || compareRowsStable(left, right));
|
|
60079
60222
|
const selected = assignments[0]?.row ?? rows[0] ?? null;
|
|
60080
60223
|
const synthesizer = assignments.at(-1)?.row ?? rows.find((row) => /deepseek/i.test(row.model)) ?? rows[0];
|
|
60081
60224
|
const subtasks = assignments.map(({ task, row }) => ({
|
|
60225
|
+
id: task.id,
|
|
60082
60226
|
name: task.name,
|
|
60083
60227
|
type: task.type,
|
|
60084
60228
|
recommended: row?.model ?? "\u5F85\u53D1\u73B0\u6A21\u578B",
|
|
60085
60229
|
recommendedProvider: row?.provider ?? "",
|
|
60086
60230
|
purpose: task.purpose,
|
|
60087
60231
|
criticality: task.criticality,
|
|
60088
|
-
qualityFloor: Number(task.qualityFloor.toFixed(3))
|
|
60232
|
+
qualityFloor: Number(task.qualityFloor.toFixed(3)),
|
|
60233
|
+
dependsOn: [...task.dependsOn ?? []]
|
|
60089
60234
|
}));
|
|
60090
|
-
const costBreakdown = assignments.map(({ task, row }, index2) => {
|
|
60235
|
+
const costBreakdown = assignments.map(({ task, row, estimatedCost, handoffPenalty }, index2) => {
|
|
60091
60236
|
const tokens = taskTokenBudget(text6, task, complexity.band, cacheReadRatio, cacheWriteRatio);
|
|
60092
|
-
const
|
|
60237
|
+
const taskEstimate = estimatedCost ?? taskCost(row, task, text6, complexity.band, cacheReadRatio, cacheWriteRatio);
|
|
60093
60238
|
return {
|
|
60094
60239
|
stage: index2 + 1,
|
|
60095
60240
|
purpose: task.purpose,
|
|
@@ -60099,8 +60244,9 @@ function buildPlan({ text: text6 = "", available = [], mode = "collective", pric
|
|
|
60099
60244
|
cacheReadTokens: tokens.cacheReadTokens,
|
|
60100
60245
|
cacheWriteTokens: tokens.cacheWriteTokens,
|
|
60101
60246
|
outputTokens: tokens.outputTokens,
|
|
60102
|
-
estimatedCost: Number(
|
|
60103
|
-
quality: Number((row === null ? 0 : qualityForTask(row, task.type)).toFixed(3))
|
|
60247
|
+
estimatedCost: Number(taskEstimate.toFixed(6)),
|
|
60248
|
+
quality: Number((row === null ? 0 : qualityForTask(row, task.type)).toFixed(3)),
|
|
60249
|
+
handoffPenalty: Number(Number(handoffPenalty ?? 0).toFixed(3))
|
|
60104
60250
|
};
|
|
60105
60251
|
});
|
|
60106
60252
|
const totalEstimate = costBreakdown.reduce((sum, row) => sum + row.estimatedCost, 0);
|
|
@@ -60111,7 +60257,9 @@ function buildPlan({ text: text6 = "", available = [], mode = "collective", pric
|
|
|
60111
60257
|
}, 0);
|
|
60112
60258
|
const budgetExceeded = Number(budgetUsd) > 0 && totalEstimate > Number(budgetUsd);
|
|
60113
60259
|
const savings = baselineCost <= 0 ? 0 : clamp((baselineCost - totalEstimate) / baselineCost);
|
|
60114
|
-
const
|
|
60260
|
+
const paretoPruned = (optimized?.candidatePools ?? []).reduce((sum, pool) => sum + pool.pruned, 0);
|
|
60261
|
+
const minimumFeasibleCost = optimized?.minimumFeasibleCost ?? minimumCostPlan?.cost ?? 0;
|
|
60262
|
+
const reason = selected === null ? "\u5C1A\u672A\u53D1\u73B0\u53EF\u7528\u6A21\u578B\uFF0C\u4FDD\u7559 Harness \u539F\u59CB\u6A21\u578B\u9009\u62E9\u3002" : `${complexity.band === "simple" ? "\u4F4E\u590D\u6742\u5EA6\u4F18\u5148\u6210\u672C\u4E0E\u54CD\u5E94\u901F\u5EA6" : complexity.band === "balanced" ? "\u5728\u8D28\u91CF\u3001\u6210\u672C\u3001\u5EF6\u8FDF\u4E0E\u98CE\u9669\u4E4B\u95F4\u5E73\u8861" : "\u9AD8\u590D\u6742\u5EA6\u6267\u884C\u4F9D\u8D56\u611F\u77E5\u7684\u5168\u5C40\u7EA6\u675F\u5206\u914D"}\uFF1B\u4EFB\u52A1\u7C7B\u578B\u4E3A ${taskType}\uFF0C\u5DF2\u5BF9 ${String(subtasks.length)} \u4E2A\u5DE5\u4F5C\u5305\u8FDB\u884C Pareto \u526A\u679D\u548C\u6709\u754C\u7EC4\u5408\u641C\u7D22\u3002`;
|
|
60115
60263
|
return {
|
|
60116
60264
|
mode,
|
|
60117
60265
|
complexity: { value: Number(complexity.value.toFixed(3)), band: complexity.band },
|
|
@@ -60125,7 +60273,7 @@ function buildPlan({ text: text6 = "", available = [], mode = "collective", pric
|
|
|
60125
60273
|
estimatedCost: Number(totalEstimate.toFixed(6)),
|
|
60126
60274
|
costBreakdown,
|
|
60127
60275
|
optimization: {
|
|
60128
|
-
solver: "quality-constrained
|
|
60276
|
+
solver: "pareto-pruned quality-constrained beam assignment",
|
|
60129
60277
|
qualityFloor: QUALITY_FLOORS[complexity.band],
|
|
60130
60278
|
budgetUsd: Number(Number(budgetUsd) > 0 ? Number(budgetUsd) : 0),
|
|
60131
60279
|
cacheReadRatio: normalizedCacheRatios(cacheReadRatio, cacheWriteRatio).read,
|
|
@@ -60135,6 +60283,11 @@ function buildPlan({ text: text6 = "", available = [], mode = "collective", pric
|
|
|
60135
60283
|
baselineAllStrongCost: Number(baselineCost.toFixed(6)),
|
|
60136
60284
|
estimatedSavings: Number(savings.toFixed(4)),
|
|
60137
60285
|
distinctRoutes: usedRoutes.size,
|
|
60286
|
+
handoffCount: optimized?.switches ?? 0,
|
|
60287
|
+
paretoPruned,
|
|
60288
|
+
beamWidth: ROUTING_BEAM_WIDTH,
|
|
60289
|
+
budgetFeasible: budget <= 0 || budgetPlan !== null,
|
|
60290
|
+
minimumFeasibleCost: Number(Number(minimumFeasibleCost).toFixed(6)),
|
|
60138
60291
|
liveBench: liveBench?.fetchedAt ? { source: liveBench.source ?? "livebench", fetchedAt: liveBench.fetchedAt, models: Object.keys(liveBench.models ?? {}).length, stale: String(liveBenchError).length > 0, error: String(liveBenchError || "") } : { source: "experimental-baseline", fetchedAt: null, models: 0, stale: false, error: String(liveBenchError || "") }
|
|
60139
60292
|
},
|
|
60140
60293
|
reason,
|
|
@@ -61587,7 +61740,7 @@ var gal_scene_default = { version: 1, settings: { stageW: 1920, stageH: 1080, sh
|
|
|
61587
61740
|
var name = "gal-view";
|
|
61588
61741
|
var PROJECT_URL = "https://github.com/ljwei-stak/deepseek-harness";
|
|
61589
61742
|
var RELEASES_URL = `${PROJECT_URL}/releases`;
|
|
61590
|
-
var PLUGIN_VERSION = "0.4.
|
|
61743
|
+
var PLUGIN_VERSION = "0.4.12";
|
|
61591
61744
|
function createUpdateApi() {
|
|
61592
61745
|
const bridge = globalThis.deepSeekHarnessDesktop;
|
|
61593
61746
|
const openExternal = (url) => {
|
|
@@ -15,7 +15,7 @@ export const OBJECTIVE_WEIGHTS = Object.freeze({
|
|
|
15
15
|
})
|
|
16
16
|
|
|
17
17
|
/** Quality floor for a task node before a cost-saving substitution is allowed. */
|
|
18
|
-
export const QUALITY_FLOORS = Object.freeze({ simple: 0.
|
|
18
|
+
export const QUALITY_FLOORS = Object.freeze({ simple: 0.75, balanced: 0.78, complex: 0.82 })
|
|
19
19
|
|
|
20
20
|
/** Host settings namespace used by the manual pricing editor. */
|
|
21
21
|
export const MODEL_ROUTER_SETTINGS_NAMESPACE = 'model-router'
|
|
@@ -349,34 +349,69 @@ function taskQualityFloor(band, task) {
|
|
|
349
349
|
|
|
350
350
|
function taskPackages(taskType, text, band) {
|
|
351
351
|
if (band !== 'complex') {
|
|
352
|
-
const task = { name: '直接回答与必要校验', type: taskType, purpose: 'execution', criticality: 0.65 }
|
|
352
|
+
const task = { id: 'execution', name: '直接回答与必要校验', type: taskType, purpose: 'execution', criticality: 0.65, dependsOn: [] }
|
|
353
353
|
return [{ ...task, qualityFloor: taskQualityFloor(band, task) }]
|
|
354
354
|
}
|
|
355
355
|
const value = String(text ?? '')
|
|
356
356
|
const packages = [
|
|
357
|
-
{ name: '问题建模与约束提取', type: 'reasoning', purpose: 'analysis', criticality: 0.92 },
|
|
357
|
+
{ id: 'analysis', name: '问题建模与约束提取', type: 'reasoning', purpose: 'analysis', criticality: 0.92, dependsOn: [] },
|
|
358
358
|
]
|
|
359
359
|
const domains = [...new Set([...(detectTaskTypes(text)), taskType].filter(type => type !== 'general'))]
|
|
360
360
|
for (const type of domains.length > 0 ? domains : [taskType]) {
|
|
361
361
|
packages.push({
|
|
362
|
+
id: `execution-${type}`,
|
|
362
363
|
name: `${TASK_TYPE_LABELS[type] ?? type}方向处理`,
|
|
363
364
|
type,
|
|
364
365
|
purpose: 'execution',
|
|
365
366
|
criticality: domains.length > 1 ? 0.80 : 0.78,
|
|
367
|
+
dependsOn: ['analysis'],
|
|
366
368
|
})
|
|
367
369
|
}
|
|
368
370
|
if (/(测试|验证|评估|对比|benchmark|test|verify|audit)/i.test(value)) {
|
|
369
|
-
packages.push({
|
|
371
|
+
packages.push({
|
|
372
|
+
id: 'verification',
|
|
373
|
+
name: '验证、反例与风险审查',
|
|
374
|
+
type: 'reasoning',
|
|
375
|
+
purpose: 'verification',
|
|
376
|
+
criticality: 0.88,
|
|
377
|
+
dependsOn: packages.filter(task => task.purpose === 'execution').map(task => task.id),
|
|
378
|
+
})
|
|
370
379
|
}
|
|
371
|
-
packages.push({
|
|
380
|
+
packages.push({
|
|
381
|
+
id: 'synthesis',
|
|
382
|
+
name: '结果校验与整合',
|
|
383
|
+
type: 'reasoning',
|
|
384
|
+
purpose: 'synthesis',
|
|
385
|
+
criticality: 1,
|
|
386
|
+
dependsOn: packages.filter(task => task.purpose !== 'analysis').map(task => task.id),
|
|
387
|
+
})
|
|
372
388
|
return packages.map(task => ({ ...task, qualityFloor: taskQualityFloor(band, task) }))
|
|
373
389
|
}
|
|
374
390
|
|
|
391
|
+
const SYNTHESIS_WEIGHTS = Object.freeze({ quality: 0.70, cost: 0.10, latency: 0.04, specialty: 0.10, risk: 0.06 })
|
|
392
|
+
const ROUTING_BEAM_WIDTH = 256
|
|
393
|
+
const ROUTING_CANDIDATE_LIMIT = 12
|
|
394
|
+
|
|
395
|
+
function weightsForTask(weights, task) {
|
|
396
|
+
return task.purpose === 'synthesis' ? SYNTHESIS_WEIGHTS : weights
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function compareText(left, right) {
|
|
400
|
+
const a = String(left)
|
|
401
|
+
const b = String(right)
|
|
402
|
+
return a < b ? -1 : a > b ? 1 : 0
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function compareRowsStable(left, right) {
|
|
406
|
+
return compareText(routeKey(left.provider, left.model), routeKey(right.provider, right.model))
|
|
407
|
+
}
|
|
408
|
+
|
|
375
409
|
function candidateUtility(row, task, weights, maxCost, usedRoutes, cacheReadRatio = 0, cacheWriteRatio = 0) {
|
|
376
410
|
const quality = qualityForTask(row, task.type)
|
|
377
411
|
const floor = Number(task.qualityFloor ?? taskQualityFloor('complex', task))
|
|
378
412
|
const qualityGap = Math.max(0, floor - quality)
|
|
379
413
|
const duplicatePenalty = usedRoutes.has(routeKey(row.provider, row.model)) ? 0.08 : 0
|
|
414
|
+
const synthesisPreference = task.purpose === 'synthesis' && /deepseek[- ]?v4[- ]?pro/i.test(row.model) ? 0.025 : 0
|
|
380
415
|
const cost = costScore(row.pricing, maxCost, cacheReadRatio, cacheWriteRatio)
|
|
381
416
|
const score = weights.quality * quality
|
|
382
417
|
+ weights.cost * cost
|
|
@@ -385,6 +420,7 @@ function candidateUtility(row, task, weights, maxCost, usedRoutes, cacheReadRati
|
|
|
385
420
|
- weights.risk * row.risk
|
|
386
421
|
- duplicatePenalty
|
|
387
422
|
- qualityGap * (task.criticality ?? 0.75)
|
|
423
|
+
+ synthesisPreference
|
|
388
424
|
return { score, floor, qualityGap }
|
|
389
425
|
}
|
|
390
426
|
|
|
@@ -410,6 +446,140 @@ function taskCost(row, task, text, complexity, cacheReadRatio = 0, cacheWriteRat
|
|
|
410
446
|
+ (tokens.outputTokens * row.pricing.output)) / 1_000_000
|
|
411
447
|
}
|
|
412
448
|
|
|
449
|
+
function dominates(left, right, task, text, complexity, cacheReadRatio, cacheWriteRatio) {
|
|
450
|
+
const leftValues = {
|
|
451
|
+
quality: qualityForTask(left, task.type),
|
|
452
|
+
cost: taskCost(left, task, text, complexity, cacheReadRatio, cacheWriteRatio),
|
|
453
|
+
latency: clamp(left.latency),
|
|
454
|
+
specialty: specialtyForTask(left, task.type),
|
|
455
|
+
risk: clamp(left.risk),
|
|
456
|
+
}
|
|
457
|
+
const rightValues = {
|
|
458
|
+
quality: qualityForTask(right, task.type),
|
|
459
|
+
cost: taskCost(right, task, text, complexity, cacheReadRatio, cacheWriteRatio),
|
|
460
|
+
latency: clamp(right.latency),
|
|
461
|
+
specialty: specialtyForTask(right, task.type),
|
|
462
|
+
risk: clamp(right.risk),
|
|
463
|
+
}
|
|
464
|
+
const noWorse = leftValues.quality >= rightValues.quality
|
|
465
|
+
&& leftValues.cost <= rightValues.cost
|
|
466
|
+
&& leftValues.latency <= rightValues.latency
|
|
467
|
+
&& leftValues.specialty >= rightValues.specialty
|
|
468
|
+
&& leftValues.risk <= rightValues.risk
|
|
469
|
+
const strictlyBetter = leftValues.quality > rightValues.quality
|
|
470
|
+
|| leftValues.cost < rightValues.cost
|
|
471
|
+
|| leftValues.latency < rightValues.latency
|
|
472
|
+
|| leftValues.specialty > rightValues.specialty
|
|
473
|
+
|| leftValues.risk < rightValues.risk
|
|
474
|
+
return noWorse && strictlyBetter
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function candidatePool(rows, task, weights, maxCost, text, complexity, cacheReadRatio, cacheWriteRatio) {
|
|
478
|
+
const floor = Number(task.qualityFloor ?? 0)
|
|
479
|
+
const feasible = rows.filter(row => qualityForTask(row, task.type) >= floor)
|
|
480
|
+
const source = feasible.length > 0
|
|
481
|
+
? feasible
|
|
482
|
+
: rows.slice().sort((left, right) => qualityForTask(right, task.type) - qualityForTask(left, task.type) || compareRowsStable(left, right)).slice(0, 3)
|
|
483
|
+
const taskWeights = weightsForTask(weights, task)
|
|
484
|
+
const scored = source.map(row => ({
|
|
485
|
+
row,
|
|
486
|
+
decision: candidateUtility(row, task, taskWeights, maxCost, new Set(), cacheReadRatio, cacheWriteRatio),
|
|
487
|
+
cost: taskCost(row, task, text, complexity, cacheReadRatio, cacheWriteRatio),
|
|
488
|
+
}))
|
|
489
|
+
const frontier = scored.filter(item => !source.some(other => other !== item.row && dominates(other, item.row, task, text, complexity, cacheReadRatio, cacheWriteRatio)))
|
|
490
|
+
const essential = [
|
|
491
|
+
scored.slice().sort((left, right) => left.cost - right.cost || compareRowsStable(left.row, right.row))[0],
|
|
492
|
+
scored.slice().sort((left, right) => right.decision.score - left.decision.score || compareRowsStable(left.row, right.row))[0],
|
|
493
|
+
scored.slice().sort((left, right) => qualityForTask(right.row, task.type) - qualityForTask(left.row, task.type) || compareRowsStable(left.row, right.row))[0],
|
|
494
|
+
].filter(Boolean)
|
|
495
|
+
const ordered = [...frontier, ...essential]
|
|
496
|
+
.filter((item, index, all) => all.findIndex(candidate => candidate.row === item.row) === index)
|
|
497
|
+
.sort((left, right) => right.decision.score - left.decision.score || left.cost - right.cost || compareRowsStable(left.row, right.row))
|
|
498
|
+
.slice(0, ROUTING_CANDIDATE_LIMIT)
|
|
499
|
+
return {
|
|
500
|
+
options: ordered,
|
|
501
|
+
relaxed: feasible.length === 0,
|
|
502
|
+
pruned: Math.max(0, rows.length - ordered.length),
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function stateSignature(state) {
|
|
507
|
+
return state.assignments.map(assignment => routeKey(assignment.row?.provider, assignment.row?.model)).join('|')
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function compareUtilityStates(left, right) {
|
|
511
|
+
return left.relaxedCount - right.relaxedCount
|
|
512
|
+
|| left.qualityShortfall - right.qualityShortfall
|
|
513
|
+
|| right.score - left.score
|
|
514
|
+
|| left.cost - right.cost
|
|
515
|
+
|| left.switches - right.switches
|
|
516
|
+
|| compareText(stateSignature(left), stateSignature(right))
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function compareCostStates(left, right) {
|
|
520
|
+
return left.relaxedCount - right.relaxedCount
|
|
521
|
+
|| left.qualityShortfall - right.qualityShortfall
|
|
522
|
+
|| left.cost - right.cost
|
|
523
|
+
|| right.score - left.score
|
|
524
|
+
|| left.switches - right.switches
|
|
525
|
+
|| compareText(stateSignature(left), stateSignature(right))
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function solveAssignments({ rows, tasks, weights, maxCost, text, complexity, budget, cacheReadRatio, cacheWriteRatio, minimizeCost = false }) {
|
|
529
|
+
const pools = tasks.map(task => candidatePool(rows, task, weights, maxCost, text, complexity, cacheReadRatio, cacheWriteRatio))
|
|
530
|
+
if (pools.some(pool => pool.options.length === 0)) return null
|
|
531
|
+
const suffixMinimum = Array(tasks.length + 1).fill(0)
|
|
532
|
+
for (let index = tasks.length - 1; index >= 0; index -= 1) {
|
|
533
|
+
suffixMinimum[index] = suffixMinimum[index + 1] + Math.min(...pools[index].options.map(option => option.cost))
|
|
534
|
+
}
|
|
535
|
+
if (Number.isFinite(budget) && suffixMinimum[0] > budget + 1e-12) return null
|
|
536
|
+
|
|
537
|
+
let states = [{ assignments: [], routesByTask: new Map(), usedRoutes: new Set(), score: 0, cost: 0, switches: 0, relaxedCount: 0, qualityShortfall: 0 }]
|
|
538
|
+
for (let index = 0; index < tasks.length; index += 1) {
|
|
539
|
+
const task = tasks[index]
|
|
540
|
+
const pool = pools[index]
|
|
541
|
+
const expanded = []
|
|
542
|
+
for (const state of states) {
|
|
543
|
+
for (const option of pool.options) {
|
|
544
|
+
const nextCost = state.cost + option.cost
|
|
545
|
+
if (Number.isFinite(budget) && nextCost + suffixMinimum[index + 1] > budget + 1e-12) continue
|
|
546
|
+
const taskWeights = weightsForTask(weights, task)
|
|
547
|
+
const decision = candidateUtility(option.row, task, taskWeights, maxCost, state.usedRoutes, cacheReadRatio, cacheWriteRatio)
|
|
548
|
+
const route = routeKey(option.row.provider, option.row.model)
|
|
549
|
+
const dependencySwitches = (task.dependsOn ?? []).reduce((count, dependency) => {
|
|
550
|
+
const dependencyRoute = state.routesByTask.get(dependency)
|
|
551
|
+
return count + (dependencyRoute !== undefined && dependencyRoute !== route ? 1 : 0)
|
|
552
|
+
}, 0)
|
|
553
|
+
const handoffPenalty = dependencySwitches * 0.015
|
|
554
|
+
const qualityShortfall = Math.max(0, decision.floor - qualityForTask(option.row, task.type))
|
|
555
|
+
const usedRoutes = new Set(state.usedRoutes)
|
|
556
|
+
usedRoutes.add(route)
|
|
557
|
+
const routesByTask = new Map(state.routesByTask)
|
|
558
|
+
routesByTask.set(task.id, route)
|
|
559
|
+
expanded.push({
|
|
560
|
+
assignments: [...state.assignments, { task, row: option.row, decision: { ...decision, relaxed: qualityShortfall > 0 }, estimatedCost: option.cost, handoffPenalty }],
|
|
561
|
+
routesByTask,
|
|
562
|
+
usedRoutes,
|
|
563
|
+
score: state.score + decision.score - handoffPenalty,
|
|
564
|
+
cost: nextCost,
|
|
565
|
+
switches: state.switches + dependencySwitches,
|
|
566
|
+
relaxedCount: state.relaxedCount + (qualityShortfall > 0 ? 1 : 0),
|
|
567
|
+
qualityShortfall: state.qualityShortfall + qualityShortfall,
|
|
568
|
+
})
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (expanded.length === 0) return null
|
|
572
|
+
expanded.sort(minimizeCost ? compareCostStates : compareUtilityStates)
|
|
573
|
+
states = expanded.slice(0, ROUTING_BEAM_WIDTH)
|
|
574
|
+
}
|
|
575
|
+
states.sort(minimizeCost ? compareCostStates : compareUtilityStates)
|
|
576
|
+
return {
|
|
577
|
+
...states[0],
|
|
578
|
+
candidatePools: pools,
|
|
579
|
+
minimumFeasibleCost: suffixMinimum[0],
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
413
583
|
export function buildPlan({ text = '', available = [], mode = 'collective', pricing = {}, liveBench = null, liveBenchError = '', budgetUsd = 0, cacheReadRatio = 0, cacheWriteRatio = 0 } = {}) {
|
|
414
584
|
const complexity = assessComplexity(text)
|
|
415
585
|
const taskType = classifyTask(text)
|
|
@@ -454,50 +624,57 @@ export function buildPlan({ text = '', available = [], mode = 'collective', pric
|
|
|
454
624
|
})
|
|
455
625
|
}
|
|
456
626
|
const taskNodes = taskPackages(taskType, text, complexity.band)
|
|
457
|
-
const usedRoutes = new Set()
|
|
458
|
-
let constraintRelaxed = false
|
|
459
|
-
const assignments = []
|
|
460
|
-
for (const task of taskNodes) {
|
|
461
|
-
const preferred = task.purpose === 'synthesis'
|
|
462
|
-
? rows.find(row => /deepseek[- ]?v4[- ]?pro/i.test(row.model))
|
|
463
|
-
?? rows.find(row => /deepseek/i.test(row.model))
|
|
464
|
-
: null
|
|
465
|
-
const selectedAssignment = preferred === null
|
|
466
|
-
? chooseAssignment(rows, task, weights, maxCost, usedRoutes, false, cacheReadRatio, cacheWriteRatio)
|
|
467
|
-
: { row: preferred, relaxed: qualityForTask(preferred, task.type) < task.qualityFloor, floor: task.qualityFloor, qualityGap: Math.max(0, task.qualityFloor - qualityForTask(preferred, task.type)) }
|
|
468
|
-
const row = selectedAssignment.row
|
|
469
|
-
if (row !== null) {
|
|
470
|
-
row.score = candidateUtility(row, task, weights, maxCost, usedRoutes, cacheReadRatio, cacheWriteRatio).score
|
|
471
|
-
usedRoutes.add(routeKey(row.provider, row.model))
|
|
472
|
-
}
|
|
473
|
-
constraintRelaxed ||= selectedAssignment.relaxed
|
|
474
|
-
assignments.push({ task, row, decision: selectedAssignment })
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// A user budget is a hard secondary constraint. If the first utility pass
|
|
478
|
-
// exceeds it, progressively replace the least-critical non-synthesis stage
|
|
479
|
-
// with the cheapest candidate that still satisfies its quality floor.
|
|
480
627
|
const budget = Number(budgetUsd)
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
628
|
+
const utilityPlan = solveAssignments({
|
|
629
|
+
rows,
|
|
630
|
+
tasks: taskNodes,
|
|
631
|
+
weights,
|
|
632
|
+
maxCost,
|
|
633
|
+
text,
|
|
634
|
+
complexity: complexity.band,
|
|
635
|
+
budget: Number.POSITIVE_INFINITY,
|
|
636
|
+
cacheReadRatio,
|
|
637
|
+
cacheWriteRatio,
|
|
638
|
+
})
|
|
639
|
+
const budgetPlan = budget > 0
|
|
640
|
+
? solveAssignments({
|
|
641
|
+
rows,
|
|
642
|
+
tasks: taskNodes,
|
|
643
|
+
weights,
|
|
644
|
+
maxCost,
|
|
645
|
+
text,
|
|
646
|
+
complexity: complexity.band,
|
|
647
|
+
budget,
|
|
648
|
+
cacheReadRatio,
|
|
649
|
+
cacheWriteRatio,
|
|
650
|
+
})
|
|
651
|
+
: null
|
|
652
|
+
const minimumCostPlan = budget > 0 && budgetPlan === null
|
|
653
|
+
? solveAssignments({
|
|
654
|
+
rows,
|
|
655
|
+
tasks: taskNodes,
|
|
656
|
+
weights,
|
|
657
|
+
maxCost,
|
|
658
|
+
text,
|
|
659
|
+
complexity: complexity.band,
|
|
660
|
+
budget: Number.POSITIVE_INFINITY,
|
|
661
|
+
cacheReadRatio,
|
|
662
|
+
cacheWriteRatio,
|
|
663
|
+
minimizeCost: true,
|
|
664
|
+
})
|
|
665
|
+
: null
|
|
666
|
+
const optimized = budget > 0 ? (budgetPlan ?? minimumCostPlan ?? utilityPlan) : utilityPlan
|
|
667
|
+
const assignments = optimized?.assignments ?? []
|
|
668
|
+
const usedRoutes = optimized?.usedRoutes ?? new Set()
|
|
669
|
+
const constraintRelaxed = (optimized?.relaxedCount ?? 0) > 0
|
|
670
|
+
for (const row of rows) {
|
|
671
|
+
row.score = candidateUtility(row, taskNodes[0] ?? { type: taskType, qualityFloor: QUALITY_FLOORS[complexity.band] }, weights, maxCost, new Set(), cacheReadRatio, cacheWriteRatio).score
|
|
494
672
|
}
|
|
495
|
-
|
|
496
|
-
for (const assignment of assignments) if (assignment.row !== null) usedRoutes.add(routeKey(assignment.row.provider, assignment.row.model))
|
|
497
|
-
rows.sort((a, b) => b.score - a.score)
|
|
673
|
+
rows.sort((left, right) => right.score - left.score || compareRowsStable(left, right))
|
|
498
674
|
const selected = assignments[0]?.row ?? rows[0] ?? null
|
|
499
675
|
const synthesizer = assignments.at(-1)?.row ?? rows.find(row => /deepseek/i.test(row.model)) ?? rows[0]
|
|
500
676
|
const subtasks = assignments.map(({ task, row }) => ({
|
|
677
|
+
id: task.id,
|
|
501
678
|
name: task.name,
|
|
502
679
|
type: task.type,
|
|
503
680
|
recommended: row?.model ?? '待发现模型',
|
|
@@ -505,10 +682,11 @@ export function buildPlan({ text = '', available = [], mode = 'collective', pric
|
|
|
505
682
|
purpose: task.purpose,
|
|
506
683
|
criticality: task.criticality,
|
|
507
684
|
qualityFloor: Number(task.qualityFloor.toFixed(3)),
|
|
685
|
+
dependsOn: [...(task.dependsOn ?? [])],
|
|
508
686
|
}))
|
|
509
|
-
const costBreakdown = assignments.map(({ task, row }, index) => {
|
|
687
|
+
const costBreakdown = assignments.map(({ task, row, estimatedCost, handoffPenalty }, index) => {
|
|
510
688
|
const tokens = taskTokenBudget(text, task, complexity.band, cacheReadRatio, cacheWriteRatio)
|
|
511
|
-
const
|
|
689
|
+
const taskEstimate = estimatedCost ?? taskCost(row, task, text, complexity.band, cacheReadRatio, cacheWriteRatio)
|
|
512
690
|
return {
|
|
513
691
|
stage: index + 1,
|
|
514
692
|
purpose: task.purpose,
|
|
@@ -518,8 +696,9 @@ export function buildPlan({ text = '', available = [], mode = 'collective', pric
|
|
|
518
696
|
cacheReadTokens: tokens.cacheReadTokens,
|
|
519
697
|
cacheWriteTokens: tokens.cacheWriteTokens,
|
|
520
698
|
outputTokens: tokens.outputTokens,
|
|
521
|
-
estimatedCost: Number(
|
|
699
|
+
estimatedCost: Number(taskEstimate.toFixed(6)),
|
|
522
700
|
quality: Number((row === null ? 0 : qualityForTask(row, task.type)).toFixed(3)),
|
|
701
|
+
handoffPenalty: Number(Number(handoffPenalty ?? 0).toFixed(3)),
|
|
523
702
|
}
|
|
524
703
|
})
|
|
525
704
|
const totalEstimate = costBreakdown.reduce((sum, row) => sum + row.estimatedCost, 0)
|
|
@@ -533,9 +712,11 @@ export function buildPlan({ text = '', available = [], mode = 'collective', pric
|
|
|
533
712
|
}, 0)
|
|
534
713
|
const budgetExceeded = Number(budgetUsd) > 0 && totalEstimate > Number(budgetUsd)
|
|
535
714
|
const savings = baselineCost <= 0 ? 0 : clamp((baselineCost - totalEstimate) / baselineCost)
|
|
715
|
+
const paretoPruned = (optimized?.candidatePools ?? []).reduce((sum, pool) => sum + pool.pruned, 0)
|
|
716
|
+
const minimumFeasibleCost = optimized?.minimumFeasibleCost ?? minimumCostPlan?.cost ?? 0
|
|
536
717
|
const reason = selected === null
|
|
537
718
|
? '尚未发现可用模型,保留 Harness 原始模型选择。'
|
|
538
|
-
: `${complexity.band === 'simple' ? '低复杂度优先成本与响应速度' : complexity.band === 'balanced' ? '在质量、成本、延迟与风险之间平衡' : '
|
|
719
|
+
: `${complexity.band === 'simple' ? '低复杂度优先成本与响应速度' : complexity.band === 'balanced' ? '在质量、成本、延迟与风险之间平衡' : '高复杂度执行依赖感知的全局约束分配'};任务类型为 ${taskType},已对 ${String(subtasks.length)} 个工作包进行 Pareto 剪枝和有界组合搜索。`
|
|
539
720
|
return {
|
|
540
721
|
mode,
|
|
541
722
|
complexity: { value: Number(complexity.value.toFixed(3)), band: complexity.band },
|
|
@@ -549,7 +730,7 @@ export function buildPlan({ text = '', available = [], mode = 'collective', pric
|
|
|
549
730
|
estimatedCost: Number(totalEstimate.toFixed(6)),
|
|
550
731
|
costBreakdown,
|
|
551
732
|
optimization: {
|
|
552
|
-
solver: 'quality-constrained
|
|
733
|
+
solver: 'pareto-pruned quality-constrained beam assignment',
|
|
553
734
|
qualityFloor: QUALITY_FLOORS[complexity.band],
|
|
554
735
|
budgetUsd: Number(Number(budgetUsd) > 0 ? Number(budgetUsd) : 0),
|
|
555
736
|
cacheReadRatio: normalizedCacheRatios(cacheReadRatio, cacheWriteRatio).read,
|
|
@@ -559,6 +740,11 @@ export function buildPlan({ text = '', available = [], mode = 'collective', pric
|
|
|
559
740
|
baselineAllStrongCost: Number(baselineCost.toFixed(6)),
|
|
560
741
|
estimatedSavings: Number(savings.toFixed(4)),
|
|
561
742
|
distinctRoutes: usedRoutes.size,
|
|
743
|
+
handoffCount: optimized?.switches ?? 0,
|
|
744
|
+
paretoPruned,
|
|
745
|
+
beamWidth: ROUTING_BEAM_WIDTH,
|
|
746
|
+
budgetFeasible: budget <= 0 || budgetPlan !== null,
|
|
747
|
+
minimumFeasibleCost: Number(Number(minimumFeasibleCost).toFixed(6)),
|
|
562
748
|
liveBench: liveBench?.fetchedAt
|
|
563
749
|
? { source: liveBench.source ?? 'livebench', fetchedAt: liveBench.fetchedAt, models: Object.keys(liveBench.models ?? {}).length, stale: String(liveBenchError).length > 0, error: String(liveBenchError || '') }
|
|
564
750
|
: { source: 'experimental-baseline', fetchedAt: null, models: 0, stale: false, error: String(liveBenchError || '') },
|
package/README.md
CHANGED
|
@@ -20,24 +20,179 @@ This plugin installs on an original DeepSeek Harness checkout. It adds a cost-aw
|
|
|
20
20
|
|
|
21
21
|
## Installation
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
### Requirements
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
- DeepSeek Harness / DSH Desktop 0.4.8 or newer.
|
|
26
|
+
- Node.js 22.19 or newer (the current DSH Desktop release uses Node 24).
|
|
27
|
+
- At least one LLM provider configured in Harness.
|
|
28
|
+
- A network connection to the npm registry for the first installation.
|
|
29
|
+
|
|
30
|
+
### Recommended: install the published npm package
|
|
31
|
+
|
|
32
|
+
The package is public and already includes the official `@liustack/modlens@3.25.4` bundle. You do not need to install Docker, Python, a ModLens server, or a second ModLens package.
|
|
33
|
+
|
|
34
|
+
1. Open PowerShell in the DSH Desktop checkout:
|
|
35
|
+
|
|
36
|
+
```powershell
|
|
37
|
+
cd F:\DeepSeek_harness\DSH-Desktop
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
2. Use the official npm registry. This avoids a temporary 404 when a mirror has not synchronized a newly published package:
|
|
41
|
+
|
|
42
|
+
```powershell
|
|
43
|
+
pnpm config set registry https://registry.npmjs.org/
|
|
44
|
+
pnpm config get registry
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The second command should print `https://registry.npmjs.org/`.
|
|
48
|
+
|
|
49
|
+
3. Install into the profile you use:
|
|
50
|
+
|
|
51
|
+
```powershell
|
|
52
|
+
# Query the newest version that is actually visible on npm.
|
|
53
|
+
$routerVersion = npm view @ljwei-stak/model-router-galgame version --registry=https://registry.npmjs.org/
|
|
54
|
+
$routerVersion
|
|
55
|
+
|
|
56
|
+
# Web profile
|
|
57
|
+
pnpm dsh plugin --profile web add "@ljwei-stak/model-router-galgame@$routerVersion"
|
|
58
|
+
|
|
59
|
+
# Desktop profile (use this when the desktop application runs the desktop profile)
|
|
60
|
+
pnpm dsh plugin --profile desktop add "@ljwei-stak/model-router-galgame@$routerVersion"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The public registry may lag behind a just-created release. After publication,
|
|
64
|
+
the same commands automatically use the newest visible version. If npm reports
|
|
65
|
+
`No matching version found`, do not guess a version: rerun the query and use the
|
|
66
|
+
version it prints.
|
|
67
|
+
|
|
68
|
+
If you do not want to change the global pnpm registry, add this option to the `add` command instead:
|
|
69
|
+
|
|
70
|
+
```powershell
|
|
71
|
+
$routerVersion = npm view @ljwei-stak/model-router-galgame version --registry=https://registry.npmjs.org/
|
|
72
|
+
pnpm dsh plugin --profile web add --registry=https://registry.npmjs.org "@ljwei-stak/model-router-galgame@$routerVersion"
|
|
28
73
|
```
|
|
29
74
|
|
|
30
|
-
|
|
75
|
+
4. Verify that the router and its ModLens dependency are present:
|
|
76
|
+
|
|
77
|
+
```powershell
|
|
78
|
+
pnpm dsh --profile web --dump-config | Select-String "model-router-galgame|modlens"
|
|
79
|
+
```
|
|
31
80
|
|
|
32
|
-
|
|
81
|
+
The output should contain:
|
|
33
82
|
|
|
34
83
|
```text
|
|
35
|
-
|
|
84
|
+
@ljwei-stak/model-router-galgame
|
|
85
|
+
@liustack/modlens
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Do not add `@liustack/modlens` separately after installing this package. The
|
|
89
|
+
router already includes the official ModLens bundle. If you previously added
|
|
90
|
+
ModLens separately to the same profile, remove that standalone dependency first,
|
|
91
|
+
then reinstall the router:
|
|
92
|
+
|
|
93
|
+
```powershell
|
|
94
|
+
pnpm dsh plugin --profile web remove @liustack/modlens
|
|
95
|
+
pnpm dsh plugin --profile web add "@ljwei-stak/model-router-galgame@$routerVersion"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
5. Stop any already-running DSH process, then start the selected profile:
|
|
99
|
+
|
|
100
|
+
```powershell
|
|
101
|
+
pnpm dsh web
|
|
102
|
+
# or, for the desktop profile:
|
|
103
|
+
pnpm dsh --profile desktop
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Do not start a second process on the same profile. If port 3080 is already in
|
|
107
|
+
use, or startup says `task-board ledger is already owned by process ...`, close
|
|
108
|
+
the existing DSH process before retrying. You can use another port only after
|
|
109
|
+
the old process has released its profile lock:
|
|
110
|
+
|
|
111
|
+
```powershell
|
|
112
|
+
pnpm dsh web --no-open --port 3081
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Verify ModLens
|
|
116
|
+
|
|
117
|
+
Run the diagnostic from the installed Web profile (the command is forwarded to
|
|
118
|
+
the profile's installed binary):
|
|
119
|
+
|
|
120
|
+
```powershell
|
|
121
|
+
pnpm dsh plugin --profile web exec modlens doctor
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Configure the vision provider in the DSH settings page or in `C:\Users\<your-user>\.modlens\config.json`. Then create a conversation, upload an image, and ask the model to transcribe or explain it. Text-only models appear with a `(modlens vision)` entry when a compatible upstream route is available.
|
|
125
|
+
|
|
126
|
+
### Update
|
|
127
|
+
|
|
128
|
+
To install the newest version visible on npm:
|
|
129
|
+
|
|
130
|
+
```powershell
|
|
131
|
+
$routerVersion = npm view @ljwei-stak/model-router-galgame version --registry=https://registry.npmjs.org/
|
|
132
|
+
pnpm dsh plugin --profile web add "@ljwei-stak/model-router-galgame@$routerVersion"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
For a reproducible deployment, replace `$routerVersion` with a concrete version
|
|
136
|
+
that you have verified with `npm view` (for example `0.4.12`):
|
|
137
|
+
|
|
138
|
+
```powershell
|
|
139
|
+
pnpm dsh plugin --profile web add @ljwei-stak/model-router-galgame@0.4.12
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
You can also ask pnpm to update an already-installed package within its declared
|
|
143
|
+
version range:
|
|
144
|
+
|
|
145
|
+
```powershell
|
|
146
|
+
pnpm dsh plugin --profile web update @ljwei-stak/model-router-galgame
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Restart DSH after updating. Repeat the same command with `--profile desktop` for the desktop profile.
|
|
150
|
+
|
|
151
|
+
### Clean reinstall after a loader or profile error
|
|
152
|
+
|
|
153
|
+
If startup reports `duplicate loader entry id: modlens`, it means the same
|
|
154
|
+
ModLens bundle was installed separately and is also included by the router.
|
|
155
|
+
Stop DSH, remove the standalone ModLens dependency, and add the router again:
|
|
156
|
+
|
|
157
|
+
```powershell
|
|
158
|
+
cd F:\DeepSeek_harness\DSH-Desktop
|
|
159
|
+
pnpm dsh plugin --profile web remove @liustack/modlens
|
|
160
|
+
$routerVersion = npm view @ljwei-stak/model-router-galgame version --registry=https://registry.npmjs.org/
|
|
161
|
+
pnpm dsh plugin --profile web add "@ljwei-stak/model-router-galgame@$routerVersion"
|
|
162
|
+
pnpm dsh --profile web --dump-config | Select-String "model-router-galgame|modlens"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The dump should show one `modlens` row and one `model-router-galgame` row. If the
|
|
166
|
+
error is `EADDRINUSE` on port 3080, or `task-board ledger is already owned by
|
|
167
|
+
process ...`, an old DSH process is still running; close that process before
|
|
168
|
+
starting another instance, or choose another port:
|
|
169
|
+
|
|
170
|
+
```powershell
|
|
171
|
+
pnpm dsh web --no-open --port 3081
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Local development installation
|
|
175
|
+
|
|
176
|
+
To test the checkout in `F:\DeepSeek_harness` without downloading from npm:
|
|
177
|
+
|
|
178
|
+
```powershell
|
|
179
|
+
cd F:\DeepSeek_harness\DSH-Desktop
|
|
180
|
+
pnpm dsh plugin --profile web add F:\DeepSeek_harness\model-router-galgame
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The local path form is only for development. The installed package is still named `@ljwei-stak/model-router-galgame` after the package is published.
|
|
184
|
+
|
|
185
|
+
### Remove the plugin
|
|
186
|
+
|
|
187
|
+
```powershell
|
|
188
|
+
cd F:\DeepSeek_harness\DSH-Desktop
|
|
189
|
+
pnpm dsh plugin --profile web remove @ljwei-stak/model-router-galgame
|
|
190
|
+
pnpm dsh plugin --profile desktop remove @ljwei-stak/model-router-galgame
|
|
36
191
|
```
|
|
37
192
|
|
|
38
|
-
|
|
193
|
+
Removing the router also removes its profile layer. It does not delete your provider credentials or `C:\Users\<your-user>\.modlens\config.json`.
|
|
39
194
|
|
|
40
|
-
|
|
195
|
+
If no model is available, native model selection remains intact and the conversation is not blocked. For the full provider setup and troubleshooting guide, see [INSTALLATION_GUIDE.zh.md](INSTALLATION_GUIDE.zh.md) and [MODLENS_DEPLOYMENT.md](MODLENS_DEPLOYMENT.md).
|
|
41
196
|
|
|
42
197
|
## Commands
|
|
43
198
|
|
package/README.zh.md
CHANGED
|
@@ -20,24 +20,173 @@
|
|
|
20
20
|
|
|
21
21
|
## 安装
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
### 前置条件
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
- DeepSeek Harness / DSH Desktop 0.4.8 或更高版本。
|
|
26
|
+
- Node.js 22.19 或更高版本(当前 DSH Desktop 使用 Node 24)。
|
|
27
|
+
- Harness 中至少配置一个 LLM provider。
|
|
28
|
+
- 首次安装需要能够访问 npm registry。
|
|
29
|
+
|
|
30
|
+
### 推荐方式:安装已发布的 npm 包
|
|
31
|
+
|
|
32
|
+
这个公开包已经把官方 `@liustack/modlens@3.25.4` 作为依赖和 DSH bundle 一起打包。无需安装 Docker、Python、8000 端口服务,也无需再单独安装 ModLens。
|
|
33
|
+
|
|
34
|
+
1. 在 PowerShell 中进入 DSH Desktop 目录:
|
|
35
|
+
|
|
36
|
+
```powershell
|
|
37
|
+
cd F:\DeepSeek_harness\DSH-Desktop
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
2. 使用官方 npm 源。如果使用国内镜像,刚发布的新包可能暂时返回 404:
|
|
41
|
+
|
|
42
|
+
```powershell
|
|
43
|
+
pnpm config set registry https://registry.npmjs.org/
|
|
44
|
+
pnpm config get registry
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
第二条命令应输出 `https://registry.npmjs.org/`。
|
|
48
|
+
|
|
49
|
+
3. 安装到你实际使用的 profile:
|
|
50
|
+
|
|
51
|
+
```powershell
|
|
52
|
+
# 查询 npm registry 当前实际可见的最新版本
|
|
53
|
+
$routerVersion = npm view @ljwei-stak/model-router-galgame version --registry=https://registry.npmjs.org/
|
|
54
|
+
$routerVersion
|
|
55
|
+
|
|
56
|
+
# Web profile
|
|
57
|
+
pnpm dsh plugin --profile web add "@ljwei-stak/model-router-galgame@$routerVersion"
|
|
58
|
+
|
|
59
|
+
# Desktop profile(桌面程序使用 desktop profile 时执行)
|
|
60
|
+
pnpm dsh plugin --profile desktop add "@ljwei-stak/model-router-galgame@$routerVersion"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
公开 registry 可能会在新版本发布后短暂延迟同步;同步完成后,同样的命令会自动使用
|
|
64
|
+
最新可见版本。如果 npm 提示 `No matching version found`,不要猜版本号;重新执行查询,
|
|
65
|
+
并使用它实际输出的版本。
|
|
66
|
+
|
|
67
|
+
如果不想修改全局 pnpm 源,可以只在安装命令中指定:
|
|
68
|
+
|
|
69
|
+
```powershell
|
|
70
|
+
$routerVersion = npm view @ljwei-stak/model-router-galgame version --registry=https://registry.npmjs.org/
|
|
71
|
+
pnpm dsh plugin --profile web add --registry=https://registry.npmjs.org "@ljwei-stak/model-router-galgame@$routerVersion"
|
|
28
72
|
```
|
|
29
73
|
|
|
30
|
-
|
|
74
|
+
4. 检查路由器和它的 ModLens 依赖是否已加入 profile:
|
|
75
|
+
|
|
76
|
+
```powershell
|
|
77
|
+
pnpm dsh --profile web --dump-config | Select-String "model-router-galgame|modlens"
|
|
78
|
+
```
|
|
31
79
|
|
|
32
|
-
|
|
80
|
+
输出中应包含:
|
|
33
81
|
|
|
34
82
|
```text
|
|
35
|
-
|
|
83
|
+
@ljwei-stak/model-router-galgame
|
|
84
|
+
@liustack/modlens
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
安装本插件后不要再单独添加 `@liustack/modlens`。本插件已经包含官方 ModLens
|
|
88
|
+
bundle。如果以前在同一个 profile 中单独安装过 ModLens,先删除那条独立依赖,
|
|
89
|
+
再重新安装路由器:
|
|
90
|
+
|
|
91
|
+
```powershell
|
|
92
|
+
pnpm dsh plugin --profile web remove @liustack/modlens
|
|
93
|
+
pnpm dsh plugin --profile web add "@ljwei-stak/model-router-galgame@$routerVersion"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
5. 关闭已经运行的 DSH,再启动对应 profile:
|
|
97
|
+
|
|
98
|
+
```powershell
|
|
99
|
+
pnpm dsh web
|
|
100
|
+
# 或 desktop profile:
|
|
101
|
+
pnpm dsh --profile desktop
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
不要在同一个 profile 上重复启动两个 DSH 进程。如果出现 `EADDRINUSE`,或启动时
|
|
105
|
+
提示 `task-board ledger is already owned by process ...`,先关闭旧 DSH 进程再重试。
|
|
106
|
+
只有旧进程释放 profile 锁后,才适合换端口启动:
|
|
107
|
+
|
|
108
|
+
```powershell
|
|
109
|
+
pnpm dsh web --no-open --port 3081
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 检查 ModLens
|
|
113
|
+
|
|
114
|
+
在已安装的 Web profile 中运行:
|
|
115
|
+
|
|
116
|
+
```powershell
|
|
117
|
+
pnpm dsh plugin --profile web exec modlens doctor
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
然后在 DSH 设置页或 `C:\Users\<你的用户名>\.modlens\config.json` 中配置视觉引擎。新建对话、上传图片并要求模型转录或解释图片即可。存在兼容的上游路由时,纯文本模型会显示对应的 `(modlens vision)` 条目。
|
|
121
|
+
|
|
122
|
+
### 更新插件
|
|
123
|
+
|
|
124
|
+
安装 npm 当前可见的最新版本:
|
|
125
|
+
|
|
126
|
+
```powershell
|
|
127
|
+
$routerVersion = npm view @ljwei-stak/model-router-galgame version --registry=https://registry.npmjs.org/
|
|
128
|
+
pnpm dsh plugin --profile web add "@ljwei-stak/model-router-galgame@$routerVersion"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
如果需要可复现部署,请把 `$routerVersion` 替换为通过 `npm view` 确认过的具体版本号
|
|
132
|
+
(例如 `0.4.12`):
|
|
133
|
+
|
|
134
|
+
```powershell
|
|
135
|
+
pnpm dsh plugin --profile web add @ljwei-stak/model-router-galgame@0.4.12
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
已经安装过插件时,也可以让 pnpm 在当前版本范围内更新:
|
|
139
|
+
|
|
140
|
+
```powershell
|
|
141
|
+
pnpm dsh plugin --profile web update @ljwei-stak/model-router-galgame
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
更新后请重启 DSH;Desktop profile 使用同样的命令并把 profile 改为 `desktop`。
|
|
145
|
+
|
|
146
|
+
### 出现加载器或 profile 错误时重新安装
|
|
147
|
+
|
|
148
|
+
如果启动时报 `duplicate loader entry id: modlens`,表示 ModLens 被单独安装过,
|
|
149
|
+
又被本插件内置 bundle 再加载了一次。先关闭 DSH,再删除单独安装的 ModLens,
|
|
150
|
+
然后重新安装本插件:
|
|
151
|
+
|
|
152
|
+
```powershell
|
|
153
|
+
cd F:\DeepSeek_harness\DSH-Desktop
|
|
154
|
+
pnpm dsh plugin --profile web remove @liustack/modlens
|
|
155
|
+
$routerVersion = npm view @ljwei-stak/model-router-galgame version --registry=https://registry.npmjs.org/
|
|
156
|
+
pnpm dsh plugin --profile web add "@ljwei-stak/model-router-galgame@$routerVersion"
|
|
157
|
+
pnpm dsh --profile web --dump-config | Select-String "model-router-galgame|modlens"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
输出应只显示一条 `modlens` 行和一条 `model-router-galgame` 行。如果报
|
|
161
|
+
`EADDRINUSE` 且端口为 3080,或报 `task-board ledger is already owned by process ...`,
|
|
162
|
+
说明旧的 DSH 进程仍在运行;先关闭旧进程,或换一个端口启动:
|
|
163
|
+
|
|
164
|
+
```powershell
|
|
165
|
+
pnpm dsh web --no-open --port 3081
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### 本地源码安装
|
|
169
|
+
|
|
170
|
+
如果要测试 `F:\DeepSeek_harness` 中的源码,不从 npm 下载:
|
|
171
|
+
|
|
172
|
+
```powershell
|
|
173
|
+
cd F:\DeepSeek_harness\DSH-Desktop
|
|
174
|
+
pnpm dsh plugin --profile web add F:\DeepSeek_harness\model-router-galgame
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
本地路径方式只用于开发测试;正式发布后推荐使用上面的 scoped npm 包名。
|
|
178
|
+
|
|
179
|
+
### 卸载插件
|
|
180
|
+
|
|
181
|
+
```powershell
|
|
182
|
+
cd F:\DeepSeek_harness\DSH-Desktop
|
|
183
|
+
pnpm dsh plugin --profile web remove @ljwei-stak/model-router-galgame
|
|
184
|
+
pnpm dsh plugin --profile desktop remove @ljwei-stak/model-router-galgame
|
|
36
185
|
```
|
|
37
186
|
|
|
38
|
-
|
|
187
|
+
卸载只会移除插件的 profile 层,不会删除 provider 凭据,也不会删除 `C:\Users\<你的用户名>\.modlens\config.json`。
|
|
39
188
|
|
|
40
|
-
|
|
189
|
+
没有可用模型时,插件仍保留 Harness 原生模型选择,不会阻塞对话。完整的 provider 配置和故障排查见 [INSTALLATION_GUIDE.zh.md](INSTALLATION_GUIDE.zh.md) 与 [MODLENS_DEPLOYMENT.md](MODLENS_DEPLOYMENT.md)。
|
|
41
190
|
|
|
42
191
|
## 命令
|
|
43
192
|
|
package/cordis.patch.yml
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
- insert:
|
|
4
4
|
- id: modlens
|
|
5
5
|
name: '@liustack/modlens'
|
|
6
|
+
# The package dependency is transitive from the profile's point of view,
|
|
7
|
+
# so this bundle entry is needed when the router is installed by itself.
|
|
6
8
|
- id: model-router-galgame
|
|
7
9
|
name: '@ljwei-stak/model-router-galgame'
|
|
8
10
|
|