@ljwei-stak/model-router-galgame 0.4.10 → 0.4.14
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/index.mjs +65 -3
- package/.dsh-plugin/shared/approval-gate.mjs +108 -0
- package/.dsh-plugin/shared/router.mjs +235 -49
- package/.dsh-plugin/shared/web-routing.mjs +90 -0
- package/README.md +505 -22
- package/README.zh.md +461 -22
- package/cordis.patch.yml +17 -0
- package/package.json +7 -2
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.14";
|
|
61591
61744
|
function createUpdateApi() {
|
|
61592
61745
|
const bridge = globalThis.deepSeekHarnessDesktop;
|
|
61593
61746
|
const openExternal = (url) => {
|
package/.dsh-plugin/index.mjs
CHANGED
|
@@ -15,6 +15,17 @@ import {
|
|
|
15
15
|
modLensUpstream,
|
|
16
16
|
routeThroughModLens,
|
|
17
17
|
} from './shared/modlens-routing.mjs'
|
|
18
|
+
import {
|
|
19
|
+
approvalGateStatus,
|
|
20
|
+
approvalSafetyContext,
|
|
21
|
+
decorateApprovalReason,
|
|
22
|
+
isApprovalGateReason,
|
|
23
|
+
} from './shared/approval-gate.mjs'
|
|
24
|
+
import {
|
|
25
|
+
webCapabilityForPlan,
|
|
26
|
+
webCapabilityStatus,
|
|
27
|
+
webInstruction,
|
|
28
|
+
} from './shared/web-routing.mjs'
|
|
18
29
|
|
|
19
30
|
let settingsRuntimePromise
|
|
20
31
|
let routerSettings = { ...DEFAULT_ROUTER_SETTINGS }
|
|
@@ -213,6 +224,17 @@ function stageMessage(plan, step) {
|
|
|
213
224
|
}
|
|
214
225
|
}
|
|
215
226
|
|
|
227
|
+
function webMessage(plan) {
|
|
228
|
+
const text = webInstruction(plan?.web)
|
|
229
|
+
if (text === '') return null
|
|
230
|
+
return {
|
|
231
|
+
id: newMessageId(),
|
|
232
|
+
role: 'user',
|
|
233
|
+
content: [{ type: 'text', text }],
|
|
234
|
+
source: { kind: 'plugin', plugin: name, form: 'web-capability', summary: '联网与可见浏览器策略' },
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
216
238
|
/**
|
|
217
239
|
* Persona is a final-answer context only. It is intentionally a separate
|
|
218
240
|
* message so the collaboration stages and their audit records remain free of
|
|
@@ -268,6 +290,7 @@ function analysisMessage(plan) {
|
|
|
268
290
|
`缓存计费比例:读取 ${Math.round(Number(plan.optimization?.cacheReadRatio ?? 0) * 100)}%,写入 ${Math.round(Number(plan.optimization?.cacheWriteRatio ?? 0) * 100)}%(未填写时按普通输入计费)`,
|
|
269
291
|
Number(plan.optimization?.budgetUsd ?? 0) > 0 ? `预算上限:$${Number(plan.optimization.budgetUsd).toFixed(6)};${plan.optimization.budgetExceeded ? '仍超预算,已在质量下限内尽量压缩' : '满足预算约束'}` : '',
|
|
270
292
|
`LiveBench:${plan.optimization?.liveBench?.fetchedAt ? `快照于 ${new Date(Number(plan.optimization.liveBench.fetchedAt)).toISOString()}${plan.optimization.liveBench.stale ? '(本次刷新失败,沿用上次快照)' : ''}` : '未完成联网核验,使用实验基线'}`,
|
|
293
|
+
plan.web?.needsWeb ? `联网策略:${plan.web.directBrowser ? 'Ego Browser 可见窗口优先' : 'ModSearch 搜索/抓取,失败时 Ego Browser 窗口兜底'};反爬处理:人工接管后继续` : '',
|
|
271
294
|
String(plan.reason ?? ''),
|
|
272
295
|
].filter(Boolean).join('\n')
|
|
273
296
|
return {
|
|
@@ -432,6 +455,27 @@ export function apply(ctx) {
|
|
|
432
455
|
routerSettingsPromise = registerRouterSettings(ctx)
|
|
433
456
|
}
|
|
434
457
|
const scheduleOpenCodeRepair = createOpenCodeRepairScheduler(ctx)
|
|
458
|
+
|
|
459
|
+
// dsh-approval-gate owns the actual decision. The router adds auditable
|
|
460
|
+
// stage/route context before that waterfall so multi-task escalations are
|
|
461
|
+
// visible to the gate's Flash classifier and human reviewer. The request
|
|
462
|
+
// object is borrowed by the Host approval service for this dispatch only.
|
|
463
|
+
ctx.on('approval/request', (request, next) => {
|
|
464
|
+
if (!isApprovalGateReason(request?.reason)) return next()
|
|
465
|
+
const state = request?.agent === undefined ? null : stateFor(request.agent)
|
|
466
|
+
const context = approvalSafetyContext(state, state?.lastStep)
|
|
467
|
+
const decorated = decorateApprovalReason(request.reason, context)
|
|
468
|
+
if (decorated !== request.reason) {
|
|
469
|
+
try {
|
|
470
|
+
request.reason = decorated
|
|
471
|
+
} catch {
|
|
472
|
+
// Some hosts freeze event payloads. In that case the gate still
|
|
473
|
+
// receives the original reason and remains fully fail-safe.
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return next()
|
|
477
|
+
}, { prepend: true })
|
|
478
|
+
|
|
435
479
|
ctx.commands.register({
|
|
436
480
|
name: 'router',
|
|
437
481
|
description: 'switch Model Router mode or inspect the latest routing plan',
|
|
@@ -440,7 +484,7 @@ export function apply(ctx) {
|
|
|
440
484
|
// the whole composer submission; the GAL client sends this command
|
|
441
485
|
// without image bytes so the attachment remains available for the next
|
|
442
486
|
// user turn.
|
|
443
|
-
input: { hint: 'mode collective|single | plan', images: true },
|
|
487
|
+
input: { hint: 'mode collective|single | plan | safety', images: true },
|
|
444
488
|
recordInput: true,
|
|
445
489
|
handler: ({ agent, rawInput }) => {
|
|
446
490
|
const state = stateFor(agent)
|
|
@@ -458,7 +502,13 @@ export function apply(ctx) {
|
|
|
458
502
|
if (value === 'plan' || value === '') {
|
|
459
503
|
return { kind: 'success', text: state.plan === null ? '还没有可展示的路由方案。' : JSON.stringify(state.plan) }
|
|
460
504
|
}
|
|
461
|
-
|
|
505
|
+
if (value === 'safety' || value === 'approval') {
|
|
506
|
+
return { kind: 'success', text: JSON.stringify({ ...approvalGateStatus(ctx), context: approvalSafetyContext(state, state.lastStep) }) }
|
|
507
|
+
}
|
|
508
|
+
if (value === 'web' || value === 'network') {
|
|
509
|
+
return { kind: 'success', text: JSON.stringify({ ...webCapabilityStatus(ctx), context: webCapabilityForPlan(state.taskText, state.plan) }) }
|
|
510
|
+
}
|
|
511
|
+
return { kind: 'error', text: '用法:/router mode collective、/router mode single、/router plan、/router safety 或 /router web' }
|
|
462
512
|
},
|
|
463
513
|
})
|
|
464
514
|
|
|
@@ -515,7 +565,7 @@ export function apply(ctx) {
|
|
|
515
565
|
await routerSettingsPromise
|
|
516
566
|
state.taskText = inputText(messages)
|
|
517
567
|
const liveBench = await liveBenchFor(ctx, state)
|
|
518
|
-
|
|
568
|
+
const plan = buildPlan({
|
|
519
569
|
text: state.taskText,
|
|
520
570
|
available,
|
|
521
571
|
mode: state.mode,
|
|
@@ -526,6 +576,16 @@ export function apply(ctx) {
|
|
|
526
576
|
cacheReadRatio: routerSettings.cacheReadRatio,
|
|
527
577
|
cacheWriteRatio: routerSettings.cacheWriteRatio,
|
|
528
578
|
})
|
|
579
|
+
state.plan = {
|
|
580
|
+
...plan,
|
|
581
|
+
web: webCapabilityForPlan(state.taskText, plan),
|
|
582
|
+
safety: {
|
|
583
|
+
...approvalGateStatus(ctx),
|
|
584
|
+
...approvalSafetyContext({ ...state, plan }, Number(step)),
|
|
585
|
+
hardCategories: ['deletion', 'credential', 'remote', 'system', 'bulk'],
|
|
586
|
+
failSafe: true,
|
|
587
|
+
},
|
|
588
|
+
}
|
|
529
589
|
state.collaboration = shouldCollaborate(state.plan, available)
|
|
530
590
|
? { lastStep: 0, queuedStep: null }
|
|
531
591
|
: null
|
|
@@ -538,11 +598,13 @@ export function apply(ctx) {
|
|
|
538
598
|
const currentStep = Number.isFinite(Number(step)) ? Number(step) : state.lastStep + 1
|
|
539
599
|
const stageContext = stageMessage(state.plan, currentStep)
|
|
540
600
|
const analysisContext = currentStep === 1 ? analysisMessage(state.plan) : null
|
|
601
|
+
const webContext = currentStep === 1 ? webMessage(state.plan) : null
|
|
541
602
|
const hasPersona = proposed.messages.some(message => message?.content?.some(block => isPersonaPrompt(block?.text)))
|
|
542
603
|
if (hasPersona) state.personaInjected = true
|
|
543
604
|
const personaContext = state.personaInjected ? null : personaMessage(state, agent, currentStep)
|
|
544
605
|
const additions = []
|
|
545
606
|
if (analysisContext !== null && !hasStageMarker(proposed.messages, currentStep)) additions.push(analysisContext)
|
|
607
|
+
if (webContext !== null && !proposed.messages.some(message => message?.content?.some(block => block?.text === webContext.content[0].text))) additions.push(webContext)
|
|
546
608
|
if (personaContext !== null) {
|
|
547
609
|
additions.push(personaContext)
|
|
548
610
|
state.personaInjected = true
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compatibility bridge for dsh-approval-gate.
|
|
3
|
+
*
|
|
4
|
+
* The approval-gate package owns the approval waterfall, Flash judgement,
|
|
5
|
+
* learning, audit files, snapshots and UI. This module only produces a small,
|
|
6
|
+
* deterministic safety context for the current Model Router work package.
|
|
7
|
+
* Keeping the bridge stateless makes it safe when the gate is installed by
|
|
8
|
+
* another profile layer as well as when it is bundled by this plugin.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const APPROVAL_GATE_PACKAGE = 'dsh-approval-gate'
|
|
12
|
+
export const APPROVAL_GATE_VERSION = '0.5.0'
|
|
13
|
+
|
|
14
|
+
const ESCALATION_RE = /escalate\s+sandbox\s+to\s+([^\s:]+):?\s*([\s\S]*)/i
|
|
15
|
+
|
|
16
|
+
function clean(value, max = 240) {
|
|
17
|
+
return String(value ?? '').replace(/[\r\n]+/g, ' ').trim().slice(0, max)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function stageFor(state, step) {
|
|
21
|
+
const plan = state?.plan
|
|
22
|
+
const tasks = Array.isArray(plan?.subtasks) ? plan.subtasks : []
|
|
23
|
+
if (tasks.length === 0) return null
|
|
24
|
+
const index = Math.max(0, Number(step || state?.lastStep || 1) - 1)
|
|
25
|
+
return tasks[index] ?? tasks[tasks.length - 1] ?? null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Return the router safety facts that are relevant to an approval request.
|
|
30
|
+
* `bulk` is a candidate supplied as context; dsh-approval-gate still performs
|
|
31
|
+
* the authoritative category decision and fail-safe human handoff.
|
|
32
|
+
*/
|
|
33
|
+
export function approvalSafetyContext(state, step) {
|
|
34
|
+
const plan = state?.plan
|
|
35
|
+
const tasks = Array.isArray(plan?.subtasks) ? plan.subtasks : []
|
|
36
|
+
const stage = stageFor(state, step)
|
|
37
|
+
const collective = state?.mode === 'collective'
|
|
38
|
+
const multiTask = collective && plan?.complexity?.band === 'complex' && tasks.length >= 3
|
|
39
|
+
return {
|
|
40
|
+
mode: state?.mode ?? 'collective',
|
|
41
|
+
complexity: plan?.complexity?.band ?? 'unknown',
|
|
42
|
+
stage: stage?.id ?? null,
|
|
43
|
+
stagePurpose: stage?.purpose ?? null,
|
|
44
|
+
stageType: stage?.type ?? null,
|
|
45
|
+
stageIndex: tasks.length === 0 ? 0 : Math.max(1, Number(step || state?.lastStep || 1)),
|
|
46
|
+
stageCount: tasks.length,
|
|
47
|
+
multiTask,
|
|
48
|
+
candidateCategory: multiTask ? 'bulk' : 'neutral',
|
|
49
|
+
selectedRoute: plan?.selected?.provider && plan?.selected?.model
|
|
50
|
+
? `${plan.selected.provider}/${plan.selected.model}`
|
|
51
|
+
: null,
|
|
52
|
+
activeRoute: state?.lastTarget?.provider && state?.lastTarget?.model
|
|
53
|
+
? `${state.lastTarget.provider}/${state.lastTarget.model}`
|
|
54
|
+
: null,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Decorate the justification consumed by dsh-approval-gate. The original
|
|
60
|
+
* escalation prefix remains intact, so the target plugin can parse it. The
|
|
61
|
+
* marker is deliberately plain text because the target plugin's Flash model
|
|
62
|
+
* judges only the justification string.
|
|
63
|
+
*/
|
|
64
|
+
export function decorateApprovalReason(reason, context) {
|
|
65
|
+
const raw = String(reason ?? '')
|
|
66
|
+
const match = raw.match(ESCALATION_RE)
|
|
67
|
+
if (!match || context === null || context === undefined) return raw
|
|
68
|
+
const mode = clean(match[1], 64)
|
|
69
|
+
const justification = clean(match[2], 500)
|
|
70
|
+
const stage = context.stage ? `${context.stage} ${context.stageIndex}/${context.stageCount}` : 'unknown'
|
|
71
|
+
const route = context.activeRoute || context.selectedRoute || 'unassigned'
|
|
72
|
+
const marker = [
|
|
73
|
+
'[model-router safety context]',
|
|
74
|
+
`mode=${context.mode}`,
|
|
75
|
+
`complexity=${context.complexity}`,
|
|
76
|
+
`stage=${stage}`,
|
|
77
|
+
`purpose=${context.stagePurpose || 'unknown'}`,
|
|
78
|
+
`route=${route}`,
|
|
79
|
+
`task_count=${context.stageCount || 0}`,
|
|
80
|
+
`risk_candidate=${context.candidateCategory}`,
|
|
81
|
+
context.multiTask ? 'multi_task_review=required' : 'multi_task_review=not_applicable',
|
|
82
|
+
].join('; ')
|
|
83
|
+
return `escalate sandbox to ${mode}: ${justification || 'router stage requires sandbox escalation'} ${marker}`.trim()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function isApprovalGateReason(reason) {
|
|
87
|
+
return ESCALATION_RE.test(String(reason ?? ''))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function approvalGateStatus(ctx) {
|
|
91
|
+
let approval = false
|
|
92
|
+
let permissionPresets = false
|
|
93
|
+
try {
|
|
94
|
+
approval = Boolean(ctx?.get?.('approval'))
|
|
95
|
+
permissionPresets = Boolean(ctx?.get?.('permissionPresets'))
|
|
96
|
+
} catch {
|
|
97
|
+
approval = false
|
|
98
|
+
permissionPresets = false
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
package: APPROVAL_GATE_PACKAGE,
|
|
102
|
+
version: APPROVAL_GATE_VERSION,
|
|
103
|
+
bundled: true,
|
|
104
|
+
approvalServiceDetected: approval,
|
|
105
|
+
permissionPresetsDetected: permissionPresets,
|
|
106
|
+
policy: 'hard-risk-human-review',
|
|
107
|
+
}
|
|
108
|
+
}
|