@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.
@@ -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.64, balanced: 0.72, complex: 0.78 });
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({ name: "\u9A8C\u8BC1\u3001\u53CD\u4F8B\u4E0E\u98CE\u9669\u5BA1\u67E5", type: "reasoning", purpose: "verification", criticality: 0.88 });
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({ name: "\u7ED3\u679C\u6821\u9A8C\u4E0E\u6574\u5408", type: "reasoning", purpose: "synthesis", criticality: 1 });
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 totalFor = () => assignments.reduce((sum, assignment) => sum + taskCost(assignment.row, assignment.task, text6, complexity.band, cacheReadRatio, cacheWriteRatio), 0);
60067
- if (budget > 0 && totalFor() > budget) {
60068
- const movable = assignments.filter((assignment) => assignment.task.purpose !== "synthesis").sort((left, right) => (left.task.criticality ?? 0) - (right.task.criticality ?? 0));
60069
- for (const assignment of movable) {
60070
- if (totalFor() <= budget) break;
60071
- const alternatives = rows.filter((row) => row !== assignment.row && qualityForTask(row, assignment.task.type) >= assignment.task.qualityFloor).sort((left, right) => taskCost(left, assignment.task, text6, complexity.band, cacheReadRatio, cacheWriteRatio) - taskCost(right, assignment.task, text6, complexity.band, cacheReadRatio, cacheWriteRatio));
60072
- const replacement = alternatives.find((row) => taskCost(row, assignment.task, text6, complexity.band, cacheReadRatio, cacheWriteRatio) < taskCost(assignment.row, assignment.task, text6, complexity.band, cacheReadRatio, cacheWriteRatio));
60073
- if (replacement !== void 0) assignment.row = replacement;
60074
- }
60075
- }
60076
- usedRoutes.clear();
60077
- for (const assignment of assignments) if (assignment.row !== null) usedRoutes.add(routeKey(assignment.row.provider, assignment.row.model));
60078
- rows.sort((a2, b3) => b3.score - a2.score);
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 estimatedCost = taskCost(row, task, text6, complexity.band, cacheReadRatio, cacheWriteRatio);
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(estimatedCost.toFixed(6)),
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 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\u6309\u5173\u952E\u5EA6\u8BBE\u7F6E\u8D28\u91CF\u4E0B\u9650\uFF0C\u518D\u5728\u53EF\u884C\u5019\u9009\u4E2D\u6700\u5C0F\u5316\u8D39\u7528"}\uFF1B\u4EFB\u52A1\u7C7B\u578B\u4E3A ${taskType}\uFF0C\u5DF2\u5BF9 ${String(subtasks.length)} \u4E2A\u5DE5\u4F5C\u5305\u8FDB\u884C\u7EA6\u675F\u6307\u6D3E\u3002`;
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 greedy assignment with diversity penalty",
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.10";
61743
+ var PLUGIN_VERSION = "0.4.14";
61591
61744
  function createUpdateApi() {
61592
61745
  const bridge = globalThis.deepSeekHarnessDesktop;
61593
61746
  const openExternal = (url) => {
@@ -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
- return { kind: 'error', text: '用法:/router mode collective、/router mode single 或 /router plan' }
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
- state.plan = buildPlan({
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
+ }