dsh-auto-model-routing 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +15 -10
  3. package/README.zh.md +15 -9
  4. package/docs/feedback-evaluation.md +20 -3
  5. package/docs/host-capability-report.md +2 -0
  6. package/docs/jev-policy-v2-validation.md +3 -1
  7. package/docs/magpie-live-acceptance.md +2 -0
  8. package/docs/policy-v2-development.md +5 -3
  9. package/docs/policy-v2-real-evaluation-plan.md +2 -0
  10. package/lib/client/control.d.ts +2 -2
  11. package/lib/client/control.js +10 -20
  12. package/lib/client/index.d.ts +9 -2
  13. package/lib/client/index.js +97 -12
  14. package/lib/client/label.d.ts +8 -11
  15. package/lib/client/label.js +51 -29
  16. package/lib/client/settings-section.d.ts +2 -2
  17. package/lib/client/settings-section.js +7 -69
  18. package/lib/client/strategy-details.js +3 -4
  19. package/lib/client.js +316 -489
  20. package/lib/context.d.ts +1 -3
  21. package/lib/context.js +3 -4
  22. package/lib/effort.d.ts +2 -10
  23. package/lib/effort.js +1 -31
  24. package/lib/endpoints.d.ts +11 -9
  25. package/lib/endpoints.js +44 -42
  26. package/lib/feedback.d.ts +77 -6
  27. package/lib/feedback.js +34 -3
  28. package/lib/index.d.ts +4 -0
  29. package/lib/index.js +54 -89
  30. package/lib/jev.d.ts +17 -29
  31. package/lib/jev.js +6 -34
  32. package/lib/policy-host.d.ts +13 -8
  33. package/lib/policy-host.js +98 -47
  34. package/lib/policy-store.d.ts +9 -17
  35. package/lib/policy-store.js +28 -15
  36. package/lib/routing.d.ts +20 -0
  37. package/lib/routing.js +51 -0
  38. package/lib/rubric.json +15 -6
  39. package/lib/settings-contract.d.ts +5 -21
  40. package/lib/settings-contract.js +3 -2
  41. package/lib/settings-host.d.ts +4 -8
  42. package/lib/settings-host.js +9 -9
  43. package/lib/settings-validation.d.ts +1 -7
  44. package/lib/settings-validation.js +0 -51
  45. package/lib/settings.d.ts +3 -8
  46. package/lib/settings.js +18 -177
  47. package/lib/state.d.ts +2 -4
  48. package/lib/state.js +2 -7
  49. package/lib/strategies/claude.d.ts +21 -0
  50. package/lib/strategies/claude.js +8 -0
  51. package/lib/strategies/codex.d.ts +21 -0
  52. package/lib/strategies/codex.js +9 -0
  53. package/lib/strategies/index.d.ts +22 -0
  54. package/lib/strategies/index.js +85 -0
  55. package/lib/strategies/types.d.ts +14 -0
  56. package/lib/strategies/types.js +1 -0
  57. package/lib/subagent-tool.d.ts +7 -7
  58. package/lib/subagent-tool.js +33 -76
  59. package/lib/support.d.ts +24 -0
  60. package/lib/support.js +36 -0
  61. package/lib/usage-report.d.ts +21 -2
  62. package/lib/usage-report.js +86 -22
  63. package/package.json +19 -7
  64. package/skills/auto-model-routing-report/SKILL.md +9 -3
  65. package/skills/subagent-model-routing/SKILL.md +14 -30
  66. package/lib/client/route-picker.d.ts +0 -21
  67. package/lib/client/route-picker.js +0 -75
  68. package/lib/judgements.d.ts +0 -41
  69. package/lib/judgements.js +0 -55
  70. package/lib/policy-v2-rubric.json +0 -21
  71. package/lib/policy-v2.d.ts +0 -20
  72. package/lib/policy-v2.js +0 -53
  73. package/lib/router.d.ts +0 -68
  74. package/lib/router.js +0 -153
  75. package/scripts/label-report.mjs +0 -50
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0
4
+
5
+ - **Breaking — one AUTO strategy.** The opt-in legacy four-tier chain is removed: the tier router and its rubric, the per-family `mainEfforts` / `childRoutes` mappings and their settings tabs, the reply-bar tier menu and `label` parameter, `judgements.jsonl` writes, `scripts/label-report.mjs` and the `eval/run.mjs` baseline. Default AUTO behavior (main effort, child capability routing, exact-child binding, Magpie transports, caps, allowlists, fork refusal) is unchanged.
6
+ - **Breaking — no strategy versions.** The `policy` setting and the `policyVersion` / `mappingVersion` / `rubricVersion` markers are gone, as is the `version` field on child records and feedback events and the usage report's `schema`. New feedback snapshots record the strategy family (`strategy`). The rule file is now `rubric.json` (same bytes).
7
+ - **Family strategies.** Everything about the Claude and GPT families (supported main models, transport prefixes, effort starting points, capability slots, accepted providers) lives in `src/strategies/`; a new family is one strategy file plus registration. The registry rejects conflicting strategies at load.
8
+ - **Old data stays readable.** Retired `policy`, `mainEfforts` and `childRoutes` keys are ignored, listed as `deprecated` and removed on the next settings save; submitting them is rejected. Child records written by 0.4.x restore without a version check (only the three known historical keys are tolerated, then dropped). Old feedback rows and events stay readable; the usage report keeps them in their original historical group. `judgements.jsonl` is kept read-only for the report.
9
+ - **Rollback.** Pin the published 0.4.2 package: in `dsh.yaml`, set this entry to `source: remote`, `spec: dsh-auto-model-routing@0.4.2`, `version: 0.4.2`, run `dsh build` and restart the Host. Data boundary: child records rewritten by 0.5.0 and feedback rows written by 0.5.0 are not readable by 0.4.2 (fail closed; recreate those child sessions), and settings saved under 0.5.0 no longer contain the retired mapping keys. Back up `state/auto-model-routing/` and the plugin `config.json` before upgrading if you may roll back.
10
+ - The Jev decision deadline now defaults to 5000 ms (was 3000 ms) and can be set up to 8000 ms (was 4500 ms); an explicitly saved value is kept. The `route_subagent` tool deadline is 10 s so it outlasts the longest decision.
11
+ - The package build now clears `lib/` before compiling, so deleted or renamed modules never ship from a stale output directory.
12
+
13
+ ## 0.4.2
14
+
15
+ - AUTO now handles Claude and GPT model families only: selecting any other main model switches the session to MANUAL instead of failing the turn in the effort mapper.
16
+ - Support the renamed `dsh-magpie-connector` Magpie route as an exact policy transport (replacing `dsh-magpie`); `dsh-magpie-connect` remains supported.
17
+ - Fix the missing feedback entry on turns whose request configuration did not change: the main decision is marked applied from the effective request header instead of waiting for a new one.
18
+ - Record two implicit feedback facts as separate ledger events keyed by session and decision: reply exposure and a later switch to MANUAL. No message text is stored; routing is unchanged.
19
+ - The usage report adds a mutually exclusive implicit-signal breakdown per comparison group, with a fixed caveat that implicit no-objection is not the same as appropriate.
20
+ - Sync the AUTO-selected effort through the official selection and simplify the picker display.
21
+
3
22
  ## 0.4.1
4
23
 
5
24
  - Add the on-demand `auto-model-routing-report` skill and read-only `lib/usage-report.js`: joins policy-v2 decisions to actual per-call usage by message ID, grouped by version × model × depth, with coverage gaps, sample thresholds and fixed caveats. Observational only; never writes, spawns or uses the network, and never prints message text, paths or session IDs.
package/README.md CHANGED
@@ -55,7 +55,8 @@ Restart DSH and refresh the Web page. In **Settings → Auto model routing**, ch
55
55
 
56
56
  ### Main model stays fixed; children are selected separately
57
57
 
58
- - **Main chat:** `lower / baseline / higher` moves at most one supported effort step below/at/above the model's verified recommended starting point. The cap defaults to `xhigh`; AUTO never selects `max`. It does not change the main provider/model or write global model defaults.
58
+ - **Main chat:** `lower / baseline / higher` moves at most one supported effort step below/at/above the model's verified recommended starting point. The cap defaults to `xhigh`; AUTO never selects `max`. It keeps the main provider/model and synchronizes the selected effort through official `selectModel`, which also saves global defaults.
59
+ - **Claude/GPT families only:** AUTO manages models whose id (after any transport prefix) starts with `claude-` or `gpt-`. Selecting any other main model — DeepSeek, GLM, Grok, Gemini, Kimi, Qwen, … — switches the session to MANUAL and locks the chip: it shows an outlined info icon, and hovering the whole button explains the support scope with an issue/PR link to the repository. Switching back to a Claude/GPT model restores a clickable MANUAL chip; AUTO is never re-enabled automatically.
59
60
  - **Fresh children:** capability is `bounded / analytical / frontier`; effort is judged separately. Claude slots are Haiku / Sonnet / Opus 5.5; GPT slots are Luna / Sol / Sol. The current catalog, supported efforts and official child allowlist must all permit the route. Magpie uses its exact provider and full family-prefixed model IDs, not a fallback to local transports.
60
61
  - **Explicit choices win:** specifying a model or effort leaves only the other dimension to judge. Specifying both skips Jev and validates the route. Missing task scope stops delegation rather than guessing.
61
62
  - **Official creation binds the route:** fresh `subagent` and fresh `spawn_teammate` paths bind an exact child identity before its first request. `route_subagent` is an optional preview—not proof of application and not a user constraint to copy into a subsequent call. Fork and unbound creation paths are unsupported; there is no silent substitution.
@@ -74,16 +75,21 @@ Preset model menus expose documented versions, not live availability guarantees.
74
75
 
75
76
  **Certainty is not the selected probability.** Choice certainty is `(pmax − 1/n) / (1 − 1/n)`: with three candidates, a 70% top probability gives 55% certainty. The panel shows the service's returned certainty and full candidate distribution separately, including returned zeroes; missing values are marked unavailable. Neither number is task success rate.
76
77
 
78
+ ### Magpie transports and picker display
79
+
80
+ AUTO accepts both exact transports, `dsh-magpie-connector` and `dsh-magpie-connect`. They are not aliases: full model ids, live model capabilities, supported effort/caps, the current allowlist and the parent's captured allowlist all retain their exact transport identity. Families without a registered strategy need an explicit model and a verifiable effort. With a MANUAL root session the preview returns `routed: false` without judging or state writes. Already bound children replay their saved route.
81
+
82
+ AUTO applies the selected effort to the current main request and awaits official `selectModel` on the same provider/model when the effort changes. This also updates global defaults; the official picker shows one effort value. MANUAL requests pass through unchanged and bound children never write the main selection. A selection-sync failure is logged while the current request retains its selected effort. Magpie menu grouping still requires ohmydsh's managed five-package Host extension; stock rc.2 keeps a flat list. See [Magpie setup and display requirements](<../dsh-magpie-connector/README.md#model-menu-groups-and-effort>); local tests do not establish deployment or live GUI acceptance.
83
+
77
84
  ## Good to know
78
85
 
79
86
  - **Failures are visible.** Main judgment failure disables AUTO with a reason; child judgment/route failure stops dispatch. No local classifier, automatic retry, upgrade or cross-service fallback.
80
87
  - **MANUAL stops adjustments, not history.** It does not restore an earlier effort. The official current selection remains in control; existing bound children keep their route while the plugin is enabled.
81
88
  - **Data leaves the machine.** AUTO sends shape-redacted task text (up to 8000 characters) and short previous-turn excerpts to your chosen service. Redaction is best effort, not a guarantee that all secrets are removed. Test Connection sends only a fixed public sample and can incur a small charge.
82
89
  - **Keys use official DSH credentials**, never plugin configuration. Launch-environment keys take precedence and are read-only. Service changes clear key drafts; save the service before managing its separate key. Changing a custom endpoint does not create a new credential namespace—review the key before trusting a new host.
83
- - **Local state:** configuration is under `$DSH_HOME/plugins/dsh-auto-model-routing/`; mode, child routes and feedback are under `$DSH_HOME/state/auto-model-routing/`. Feedback stores resource metadata, IDs, versions, returned distributions and timestamps—not task/reply text. Request event IDs allow later usage correlation; costs are not fabricated when unavailable. See [feedback and evaluation](docs/feedback-evaluation.md).
90
+ - **Local state:** configuration is under `$DSH_HOME/plugins/dsh-auto-model-routing/`; mode, child routes and feedback are under `$DSH_HOME/state/auto-model-routing/`. Feedback stores resource metadata, IDs, the strategy family, returned distributions and timestamps—not task/reply text. Request event IDs allow later usage correlation; costs are not fabricated when unavailable. See [feedback and evaluation](docs/feedback-evaluation.md).
84
91
  - **No automatic learning.** Human allocation feedback is observational. It does not change routing or prove quality/cost benefits. Old messages without decision bindings are not backfilled; old records with missing evaluation fields remain unknown.
85
- - **Upgrade:** existing AUTO sessions use the new policy without a version switch or rewriting their stored mode. New chats still default to MANUAL unless configured otherwise. Historical four-tier mappings remain only for explicit `policy: "legacy"` rollback in plugin configuration; they are not the current policy or feedback menu.
86
-
92
+ - **Upgrade to 0.5.0:** there is one AUTO strategy and no version setting. Existing sessions keep their stored mode; new chats still default to MANUAL unless configured otherwise. Retired `policy` / `mainEfforts` / `childRoutes` keys in the plugin configuration are ignored, shown as no longer effective in Settings, and removed on the next save. Child decisions and feedback recorded by 0.4.x stay readable. To go back, pin the published `dsh-auto-model-routing@0.4.2` (see [CHANGELOG](CHANGELOG.md) for the data boundary).
87
93
  ## Architecture
88
94
  <!-- section: architecture -->
89
95
 
@@ -91,17 +97,16 @@ Preset model menus expose documented versions, not live availability guarantees.
91
97
 
92
98
  Main failures disable AUTO; child failures stop dispatch. Explicit model/effort constraints are preserved. The diagram shows the AUTO paths; MANUAL makes no new judgments and fork is unsupported. Feedback does not automatically alter routing.
93
99
 
94
- Entry: [index.ts](src/index.ts). Enforcement: [policy-host.ts](src/policy-host.ts). Actual rubric: [policy-v2-rubric.json](src/policy-v2-rubric.json). Model slots: [policy-v2.ts](src/policy-v2.ts). UI: [client](src/client/index.ts). Coverage and limitations: [development verification](docs/policy-v2-development.md), [real Magpie acceptance](docs/magpie-live-acceptance.md).
100
+ Entry: [index.ts](src/index.ts). Enforcement: [policy-host.ts](src/policy-host.ts). Actual rubric: [rubric.json](src/rubric.json). Family strategies (supported models, transports, effort starting points, capability slots): [strategies](src/strategies/index.ts); routing: [routing.ts](src/routing.ts). UI: [client](src/client/index.ts). Coverage and limitations (historical records): [development verification](docs/policy-v2-development.md), [real Magpie acceptance](docs/magpie-live-acceptance.md).
95
101
 
96
102
  <details>
97
- <summary>Earlier four-tier UI demos (legacy rollback only)</summary>
103
+ <summary>Earlier four-tier UI demos (removed in 0.5.0)</summary>
98
104
 
99
- These isolated-host recordings use mock Jev/model responses and show the previous UI, not the current default policy.
105
+ These isolated-host recordings use mock Jev/model responses and show a UI that no longer exists.
100
106
 
101
- ![Legacy four-tier routing settings](docs/settings.gif)
107
+ ![Earlier four-tier routing settings](docs/settings.gif)
102
108
 
103
109
  </details>
104
-
105
110
  ## Development
106
111
 
107
112
  ```sh
@@ -110,7 +115,7 @@ npm run typecheck
110
115
  npm run build
111
116
  ```
112
117
 
113
- Tests use the official AgentLoop with fake LLM streams and mock/loopback Jev responses, not paid model calls. `scripts/label-report.mjs` reports **legacy tier labels only**. To observe AUTO decisions and their actual token usage, ask the agent to use the `auto-model-routing-report` skill (on demand only; it runs the read-only `lib/usage-report.js`). It is observational, not a savings measurement. Evaluation schema and deduplication rules are in [feedback-evaluation.md](docs/feedback-evaluation.md).
118
+ Tests use the official AgentLoop with fake LLM streams and mock/loopback Jev responses, not paid model calls. To observe AUTO decisions and their actual token usage, ask the agent to use the `auto-model-routing-report` skill (on demand only; it runs the read-only `lib/usage-report.js`). It is observational, not a savings measurement. Evaluation schema and deduplication rules are in [feedback-evaluation.md](docs/feedback-evaluation.md).
114
119
 
115
120
  Development stays in the source monorepo; this repository is its release mirror. The package builds and loads independently using its declared dependencies, bundled skill and official DSH services; no monorepo checkout or monorepo instruction file is required. External design references are optional background, not runtime inputs.
116
121
 
package/README.zh.md CHANGED
@@ -55,7 +55,8 @@ dsh plugin --profile web add prgrmrwy/dsh-auto-model-routing
55
55
 
56
56
  ### 主模型不换,子任务独立选配
57
57
 
58
- - **主会话**:`lower / baseline / higher` 相对于具体模型已验证的推荐起点,在受支持序列降低一档、保持或提高一档。缺省上限 `xhigh`,自动不选 `max`;不修改主 provider/model,也不改写全局模型默认值。
58
+ - **主会话**:`lower / baseline / higher` 相对于具体模型已验证的推荐起点,在受支持序列降低一档、保持或提高一档。缺省上限 `xhigh`,自动不选 `max`;主 provider/model 不变;通过官方 `selectModel` 同步选定 effort,同时保存全局默认值。
59
+ - **仅支持 Claude / GPT 系列**:AUTO 只管理模型名(去掉 transport 前缀后)以 `claude-` 或 `gpt-` 开头的主模型。切换到其他主模型(DeepSeek、GLM、Grok、Gemini、Kimi、Qwen 等)会自动落为 MANUAL 并锁定芯片:芯片显示描边 info 图标,悬停整个按钮说明支持范围并提供仓库的 issue/PR 链接。切回 Claude / GPT 系列后芯片恢复可点击且保持 MANUAL,不会自动重新启用 AUTO。
59
60
  - **fresh 子任务**:能力需求为 `bounded / analytical / frontier`,推理投入独立判断。Claude 能力槽为 Haiku / Sonnet / Opus 5.5;GPT 为 Luna / Sol / Sol。实际路由必须同时满足当前目录、支持的 effort 和官方子智能体允许列表。Magpie 保留真实 provider 与家族前缀完整模型 ID,不回退到本地入口。
60
61
  - **尊重显式选择**:只指定模型或投入时,仅判断另一维;两者都指定则不请求 Jev,只校验路由。缺少任务目标或范围时停止委派,不猜。
61
62
  - **官方创建绑定路由**:fresh `subagent` 和 fresh `spawn_teammate` 在子任务首次请求前绑定精确身份。`route_subagent` 只是可选预览,不代表已应用,也不能把推荐复制成用户约束。fork 和未绑定创建路径暂不支持,不静默替换。
@@ -74,15 +75,21 @@ dsh plugin --profile web add prgrmrwy/dsh-auto-model-routing
74
75
 
75
76
  **判定确定度不等于选中概率**:Choice 公式为 `(pmax − 1/n) / (1 − 1/n)`,三个候选中最高概率 70% 对应确定度 55%。面板分别显示服务返回的确定度和完整分布,保留返回的 0%,缺值标未提供。两者都不是任务成功率。
76
77
 
78
+ ### Magpie transport 与选择器展示
79
+
80
+ AUTO 接受 `dsh-magpie-connector`、`dsh-magpie-connect` 两个精确 transport,不互为 alias:完整 model id、实时模型能力、支持 effort/cap、当前允许列表与父 captured allowlist 都按原 transport 校验。没有注册策略的家族需显式 model 及可验证的 effort。根会话为 MANUAL 时预览直接返回 `routed: false`,不判断、不写状态。已绑定子实例恢复原路线。
81
+
82
+ AUTO 将选定 effort 应用于本次主请求;effort 改变时,等待官方 `selectModel` 同步同一 provider/model,并允许保存全局默认值。官方选择器只显示一个 effort。MANUAL 请求原样通过,已绑定子会话不写主选择;同步失败会记录告警,本次请求仍保留选定 effort。Magpie 菜单分组仍需 ohmydsh 受管五包 Host 扩展,stock rc.2 保持 flat 列表。见 [Magpie 配置与展示前提](<../dsh-magpie-connector/README.zh.md#模型菜单分组与-effort>);本地测试不等于已部署或 live GUI 验收。
83
+
77
84
  ## 须知
78
85
 
79
86
  - **失败可见**:主判断失败会停用 AUTO 并显示原因,子判断或路由失败则停止派发。没有本地分类器、自动重试、自动升级或跨服务降级。
80
87
  - **MANUAL 停止调整,不复原历史值**:使用官方当前选择;只要插件启用,已绑定子任务仍保留原路线。
81
88
  - **任务会外发**:AUTO 将按形态脱敏的任务文本(最多 8000 字符)及上一轮少量片段发送给所选服务。脱敏尽力而为,不保证去除全部秘密。测试连接只发送固定公开样例,可能产生少量费用。
82
89
  - **key 经官方 DSH 凭据服务保存**,不写入插件配置。启动环境 key 优先且只读;切换服务清空 key 草稿,先保存服务再管理新 key。修改自定义端点不会产生新凭据空间,换地址前请确认专用 key 的信任边界。
83
- - **本地状态**:配置在 `$DSH_HOME/plugins/dsh-auto-model-routing/`,模式、子路由及反馈在 `$DSH_HOME/state/auto-model-routing/`。反馈只保存资源元数据、身份、版本、返回分布及时间,不保存任务或回复正文。请求事件序号供后续关联用量,缺失成本不会编造。见[反馈评估说明](docs/feedback-evaluation.md)。
90
+ - **本地状态**:配置在 `$DSH_HOME/plugins/dsh-auto-model-routing/`,模式、子路由及反馈在 `$DSH_HOME/state/auto-model-routing/`。反馈只保存资源元数据、身份、策略家族、返回分布及时间,不保存任务或回复正文。请求事件序号供后续关联用量,缺失成本不会编造。见[反馈评估说明](docs/feedback-evaluation.md)。
84
91
  - **不自动学习**:人工反馈仅作观察,不改变路由,也不能直接证明质量提升或节省。无决策绑定的旧消息不回填,旧记录缺少的评估字段保持未知。
85
- - **升级兼容**:已有 AUTO 会话直接使用新策略,无需版本开关,也不重写会话模式。新会话缺省仍为 MANUAL,除非你改过设置。旧四档映射仅供配置中显式 `policy: "legacy"` 回滚,不是当前策略或反馈菜单。
92
+ - **升级到 0.5.0**:只有一套 AUTO 策略,没有版本设置。已有会话保留原模式;新会话缺省仍为 MANUAL,除非你改过设置。插件配置中残留的 `policy`、`mainEfforts`、`childRoutes` 会被忽略,设置页提示不再生效,下次保存时移除。0.4.x 记录的子任务决策与反馈仍可读取。如需退回,固定已发布的 `dsh-auto-model-routing@0.4.2`(数据边界见 [CHANGELOG](CHANGELOG.md))。
86
93
 
87
94
  ## 架构
88
95
  <!-- section: architecture -->
@@ -91,17 +98,16 @@ dsh plugin --profile web add prgrmrwy/dsh-auto-model-routing
91
98
 
92
99
  主判断失败停用 AUTO,子失败停止派发;保留显式 model/effort。图示为 AUTO 路径,MANUAL 不做新判断,fork 暂不支持。反馈不会自动改变路由。
93
100
 
94
- 入口:[index.ts](src/index.ts)。执行接入:[policy-host.ts](src/policy-host.ts)。实际判据:[policy-v2-rubric.json](src/policy-v2-rubric.json)。模型槽:[policy-v2.ts](src/policy-v2.ts)。界面:[client](src/client/index.ts)。覆盖边界:[开发验证](docs/policy-v2-development.md)、[真实 Magpie 验收](docs/magpie-live-acceptance.md)。
101
+ 入口:[index.ts](src/index.ts)。执行接入:[policy-host.ts](src/policy-host.ts)。实际判据:[rubric.json](src/rubric.json)。家族策略(支持的模型、transport、effort 起点、能力槽):[strategies](src/strategies/index.ts);路由计算:[routing.ts](src/routing.ts)。界面:[client](src/client/index.ts)。覆盖边界(历史记录):[开发验证](docs/policy-v2-development.md)、[真实 Magpie 验收](docs/magpie-live-acceptance.md)。
95
102
 
96
103
  <details>
97
- <summary>旧四档界面演示(仅供 legacy 回滚参考)</summary>
104
+ <summary>早期四档界面演示(已于 0.5.0 删除)</summary>
98
105
 
99
- 下列隔离宿主录屏使用模拟 Jev/模型响应,展示旧界面,不代表当前默认策略。
106
+ 下列隔离宿主录屏使用模拟 Jev/模型响应,展示的界面已不存在。
100
107
 
101
- ![旧版四档路由设置](docs/settings.gif)
108
+ ![早期四档路由设置](docs/settings.gif)
102
109
 
103
110
  </details>
104
-
105
111
  ## 开发验证
106
112
 
107
113
  ```sh
@@ -110,7 +116,7 @@ npm run typecheck
110
116
  npm run build
111
117
  ```
112
118
 
113
- 测试使用官方 AgentLoop、假 LLM 流和 mock/回环 Jev,不产生真实模型费用。`scripts/label-report.mjs` **仅汇总旧四档标注**。如需观察 AUTO 决策及实际 token 用量,请明确让 agent 使用 `auto-model-routing-report` skill(仅按需调用,运行只读的 `lib/usage-report.js`);它是观察统计,不衡量节省;新数据结构与去重口径见[feedback-evaluation.md](docs/feedback-evaluation.md)。
119
+ 测试使用官方 AgentLoop、假 LLM 流和 mock/回环 Jev,不产生真实模型费用。如需观察 AUTO 决策及实际 token 用量,请明确让 agent 使用 `auto-model-routing-report` skill(仅按需调用,运行只读的 `lib/usage-report.js`);它是观察统计,不衡量节省;新数据结构与去重口径见[feedback-evaluation.md](docs/feedback-evaluation.md)。
114
120
 
115
121
  继续在主仓开发,本仓库仅作发布镜像。插件使用自身声明的依赖、包内 skill 和官方 DSH 服务即可独立构建与加载,不要求检出主仓,也不要求安装主仓环境指令。外部设计链接仅供背景阅读,不是运行输入。
116
122
 
@@ -3,16 +3,33 @@
3
3
  ## 记录
4
4
  `$DSH_HOME/state/auto-model-routing/allocation-feedback.jsonl` 是本地append-only记录,无会话正文和key。
5
5
 
6
- 绑定行使用sessionId/messageId/decisionId、depth、实际model/effort,新增boundAt与不可变snapshot:decidedAt/appliedAt、policyVersion/mappingVersion、decisionProvider/decisionModel、判断ms、返回的confidence/probabilities、实际provider、requestSeq及可得时requestTurn。requestSeq是同session请求头事件,可与该会话历史关联actual request及usage;绑定并非完成任务或用量已知的证明。
6
+ 绑定行使用sessionId/messageId/decisionId、depth、实际model/effort,新增boundAt与不可变snapshot:decidedAt/appliedAt、strategy(实际主模型所属策略家族id,无匹配家族时省略)、decisionProvider/decisionModel、判断ms、返回的confidence/probabilities、实际provider、requestSeq及可得时requestTurn。0.5.0之前写入的快照带policyVersion/mappingVersion而无strategy,仍原样可读,缺失的strategy保持未知,不补造;`label`端点返回时去掉这两个历史键。requestSeq是同session请求头事件,可与该会话历史关联actual request及usage;绑定并非完成任务或用量已知的证明。
7
7
 
8
- 反馈事件为`type:feedback, version:2`,带sessionId/messageId/decisionId、at、feedback。每次修改或撤销独立追加;null表示撤销。读取时按追加顺序应用到既有精确绑定。历史完整记录仍兼容,原快照不可被后续同身份行替换;旧记录没有时间或快照则未知,禁止用迁移时刻或最新配置补造。
8
+ 反馈事件为`type:feedback`,带sessionId/messageId/decisionId、at、feedback;新事件不带version,带`version:2`的旧事件照常读取,其它未知键按损坏行跳过。每次修改或撤销独立追加;null表示撤销。读取时按追加顺序应用到既有精确绑定。历史完整记录仍兼容,原快照不可被后续同身份行替换;旧记录没有时间或快照则未知,禁止用迁移时刻或最新配置补造。
9
9
 
10
10
  ## 评估
11
11
  - 评估单位为(sessionId, decisionId),不是每个assistant/message,避免同轮多条工具消息导致重复计数。先按消息取最后事件,再按决策取最后反馈;历史无时间以追加顺序作本文件内顺序,跨文件不可猜。
12
12
  - appropriate/too-high/too-low分别统计;unknown不是满意,null撤销不计有效评价;未评价不能计为合适。多条消息评价冲突也应单独报告。
13
- - 按策略版本/映射版本/服务模型及depth/实际路由分组;旧缺字段独立unknown组,不混进新策略精确对照。
13
+ - 按策略家族(strategy)及depth/实际路由分组;0.5.0之前的快照按原policyVersion/mappingVersion组成历史组(标记historical),无快照的记录独立unknown组;历史组与家族组不合并比较。
14
14
  - 以requestSeq和消息ID关联实际usage/延迟/返工证据;拿不到价格或token就未知。用户主观反馈不等于正确性oracle,适合率也不能单独证明质量或节省。
15
15
  - 按需观察报表见包内 skill `auto-model-routing-report`(`lib/usage-report.js`,只读);仍不做自动阈值调整、自动训练或换模重试。
16
16
 
17
+ ## 隐式事实与分布(2026-10-26)
18
+
19
+ 运行时只追加 `{type:'fact', sessionId, decisionId, fact, at}`(0.5.0之前的事实行带 `version:1`,照常读取)。`fact` 仅为 `exposed` 或 `manual-switch`;按决策/类型保留首次事实,时间由 Host 生成,不收正文。曝光要求回复操作栏在前台、聚焦页面连续完整可见 3 秒;切 MANUAL 在清除运行态之前取当前主决策,只在切换成功后记录。写入失败只记录日志,不阻断主线。
20
+
21
+ 回合结束与继续不重复写账本,按需报告从已有日志推导。决策所在回合取最后一次关联调用的实际回合号,不是归属的用户回合。第一个后续用户回合的开始时间为继续时间;曝光时间必须早于继续时间,否则视为事后翻看,不算有效曝光。没有继续时曝光仍有效,但不能得到隐式无异议。
22
+
23
+ 每个比较子组的 `implicit.counts` 五类互斥,合计等于决策数:
24
+ 1. `explicit`:有显式评价;`implicit.explicit` 分类别,冲突单列。
25
+ 2. `negative`:无显式评价,且用户中止了决策最后一次关联调用所在回合,或切换 MANUAL;`implicit.negative` 分原因,两原因可重叠。
26
+ 3. `implicit-ok`:有效曝光、正常完成且用户继续,无负向事实。只表示这些行为同时出现,不证明资源分配合适。
27
+ 4. `inconclusive`:有效曝光但其它条件不足,包括未继续、结束未知或父会话取消。
28
+ 5. `unobserved`:没有有效曝光。
29
+
30
+ `implicit.exposedExplicit` 是有效曝光且有显式评价的参照计数。曝光不证明读过;用户中止或切 MANUAL 也可能因为换任务,不证明策略有错。旧数据不回填曝光,未知不能当正例。隐式信号、显式评价和将来的机器评审均应分开,不自动调策略。
31
+
32
+ D012 修复后,实际配置不变、不产生新 `request/header` 的回合也会绑定决策。`snapshot.requestSeq` 指向当时生效的请求头(可能来自更早回合),`requestTurn` 是应用时的当前回合。上线后记录覆盖率会提高;这是补缺口,不是策略改善,不能直接比较修复前后的适合率。
33
+
17
34
  ## 确定度
18
35
  UI称“判定确定度”,Choice公式为`(pmax - 1/n)/(1 - 1/n)`;n为该维候选数。三选一最高概率70%得到55%,不等于选中概率或任务成功率。插件直接展示服务返回值,分布独立保留,不从四舍五入后的百分比反推。官方定义:https://docs.typesafe.ai/confidence.md 。
@@ -1,5 +1,7 @@
1
1
  # AUTO 宿主能力核验(官方 0.2.0-rc.2)
2
2
 
3
+ > 历史记录:命名、路径与行为以记录时的 0.4.x 版本为准;0.5.0 起只有一套 AUTO 策略,规则文件为 `src/rubric.json`,家族策略见 `src/strategies/`。
4
+
3
5
  ## 结论
4
6
 
5
7
  **官方入口足以让插件在普通子会话、fork、teammate 的首次模型请求前读取精确子身份和本轮任务,并改写实际 provider/model/effort;不需要改宿主或 compat。** 这证明的是宿主接入能力,不是现有 AUTO 已支持这些路径。
@@ -1,5 +1,7 @@
1
1
  # Jev policy-v2 验证(2026-10-24)
2
2
 
3
+ > 历史记录:命名、路径与行为以记录时的 0.4.x 版本为准;0.5.0 起只有一套 AUTO 策略,规则文件为 `src/rubric.json`,家族策略见 `src/strategies/`。
4
+
3
5
  ## 结论
4
6
 
5
7
  官方接口和真实 `typesafe/jev-1.13` 均支持一次请求中独立返回 capability 与 depth。10 次合成任务请求全部返回合法的双 Choice;预先定义的 development / holdout 各 5 例均符合预期,不调整 rubric、不重试。**这只证明接口与小样本分类,不证明实际子模型质量、成本收益或默认推广条件。**
@@ -26,7 +28,7 @@
26
28
  | hold-constraints | holdout | frontier | higher |
27
29
  | hold-unknown | holdout | insufficient_context | baseline |
28
30
 
29
- [测试输入及独立预期](../eval/policy-v2-cases.json)、[问题定义](../src/policy-v2-rubric.json)、[有预算保护的实测脚本](../eval/policy-v2-live.mjs)。depth 从同一任务证据独立判断相对适配模型的推荐投入,不引用另一个尚未返回的 capability 答案。两者都指定时不判;只指定一个维度时仅询问另一个。
31
+ [测试输入及独立预期](../eval/cases.json)、[问题定义](../src/rubric.json)、[有预算保护的实测脚本](../eval/live.mjs)(当时文件名分别带 `policy-v2-` 前缀)。depth 从同一任务证据独立判断相对适配模型的推荐投入,不引用另一个尚未返回的 capability 答案。两者都指定时不判;只指定一个维度时仅询问另一个。
30
32
 
31
33
  实测脚本先保留每次费用预算再请求,失败也计一次;预留每次 65536 input tokens 远高于合成输入。未知 usage.cost 停止,不记零。尚有 10 次授权余量,不默认消耗,也不因生成一个新结果文件就获得新预算。跨多次运行的全局预算需由执行者核对,脚本不是账户级额度控制。
32
34
 
@@ -1,5 +1,7 @@
1
1
  # Magpie 真实最小验收与本地部署
2
2
 
3
+ > 历史记录:命名、路径与行为以记录时的 0.4.x 版本为准;0.5.0 起只有一套 AUTO 策略,规则文件为 `src/rubric.json`,家族策略见 `src/strategies/`。
4
+
3
5
  2026-10-25。用户授权按订阅最小必要调用完成验收与本地部署重启,不授权全局默认切换或发布。
4
6
 
5
7
  ## 实际结果
@@ -1,5 +1,7 @@
1
1
  # policy-v2 开发与验收记录
2
2
 
3
+ > 历史记录:命名、路径与行为以记录时的 0.4.x 版本为准;0.5.0 起只有一套 AUTO 策略,规则文件为 `src/rubric.json`,家族策略见 `src/strategies/`。
4
+
3
5
  ## 状态
4
6
 
5
7
  2026-10-25 用户在最小真实 Magpie 验收后追加授权:AUTO 默认升级,不再要求版本开关。默认配置为内部 policy-v2;显式 legacy 仅供回滚。旧四档配置保留但不参与新策略,MANUAL 不变。已构建部署、通过分离控制进程重启现有3080服务,并在实际GUI验证无版本选择、显示 AUTO 已使用新策略。真实质量/返工/成本对照仍未完成,不宣称性能收益。
@@ -7,7 +9,7 @@
7
9
  ## 实现入口
8
10
 
9
11
  - [Jev 契约](../src/jev.ts):复用认证、deadline、body 上限、错误分类;原 tier 问题不变。新主请求只问 depth;子请求一次问 capability/depth,显式约束只问未指定维度。
10
- - [独立映射](../src/policy-v2.ts):版本化能力槽,effort 推荐起点按受支持序列最多移动一档,受 cap 限制,不自动使用 max。映射是初始基线,Codex analytical/frontier 都是 Sol,未虚构第三模型。
12
+ - [独立映射](../src/routing.ts)(当时为 `src/policy-v2.ts`):版本化能力槽,effort 推荐起点按受支持序列最多移动一档,受 cap 限制,不自动使用 max。映射是初始基线,Codex analytical/frontier 都是 Sol,未虚构第三模型。
11
13
  - [宿主接入](../src/policy-host.ts):官方 `tools/execute` → AsyncLocalStorage → `agent/created` 绑定精确 child ID 和 parent/root;不依赖 prompt 相似度。`agent/pre-step` 获取首次任务并决策,`agent/request` 应用;`request/header` 记录匹配证据。
12
14
  - [持久化](../src/policy-store.ts):每 child 哈希路径、严格 schema、owner-only 原子替换,只有身份/版本/约束/路由/状态,不含任务原文。缺失、损坏、未完成恢复不默默继承;未完成决策不能用后续消息重新分类。
13
15
  - [工具](../src/subagent-tool.ts):policy-v2 下 optional preview,仅 recommended;普通创建工具负责精确应用,不把预览冒充 applied。
@@ -18,7 +20,7 @@
18
20
 
19
21
  | 路径/行为 | 证据 | 结果/边界 |
20
22
  |---|---|---|
21
- | 官方 ordinary fresh continuable 工具与 provider | [整链脚本](../scripts/policy-v2-host-probe.mjs)加载官方 0.2.0-rc.2、真实生产插件,fake Jev/adapter | 实际请求 model/effort 匹配;全宿主关闭重建后恢复,不重判 |
23
+ | 官方 ordinary fresh continuable 工具与 provider | [整链脚本](../scripts/host-probe-children.mjs)(当时为 `policy-v2-host-probe.mjs`)加载官方 0.2.0-rc.2、真实生产插件,fake Jev/adapter | 实际请求 model/effort 匹配;全宿主关闭重建后恢复,不重判 |
22
24
  | 官方 fresh teammate 工具与服务 | 同上 | 创建、后续消息和全宿主恢复通过,不重选 |
23
25
  | 并发创建与显式模型/effort | [宿主测试](../test/policy-host.integration.test.ts)官方 Tools/Agents,测试创建工具 | 并发精确身份隔离;两者指定零判断;普通/teammate 官方 provider 的并发压力未测 |
24
26
  | fork / teammate fork | tools wrapper 可提前拒绝 | unsupported,真实历史兼容未验证,不擅自转 fresh |
@@ -41,7 +43,7 @@
41
43
  重跑整链:先 package build;在原临时官方 prefix 的基础上,精确安装 `@deepseek-ai/dsh-tool-subagent@0.2.0-rc.2`、`@deepseek-ai/dsh-experimental-tool-agent-team@0.2.0-rc.2`、`@deepseek-ai/dsh-launch-environment@0.2.0-rc.2`(`--save-exact --ignore-scripts`),然后:
42
44
 
43
45
  ```sh
44
- node packages/auto-model-routing/scripts/policy-v2-host-probe.mjs "$PROBE_HOME"
46
+ node packages/auto-model-routing/scripts/host-probe-children.mjs "$PROBE_HOME"
45
47
  ```
46
48
 
47
49
  官方 standing tool 的 `modelSelectionSettings:true` 要求 preset-scoped context;隔离脚本没有伪造该 composition,使用 standing fixed-schema 工具 + 合成 allowlist。因此显式选择与父快照另用 host tests 验证,真实 preset/GUI 组合不记为通过。
@@ -1,5 +1,7 @@
1
1
  # policy-v2 真实任务对照(计划;完整对照未执行)
2
2
 
3
+ > 历史记录:命名、路径与行为以记录时的 0.4.x 版本为准;0.5.0 起只有一套 AUTO 策略,规则文件为 `src/rubric.json`,家族策略见 `src/strategies/`。
4
+
3
5
  ## 目的与门槛
4
6
 
5
7
  下一步不是直接部署,而是在独立评审修复后验证真实子模型质量、耗时与总开销。现有 10 个 Jev 分类样例不是执行任务基准,不能把其标签命中率当任务成功率。不得消耗剩余 Jev 授权来调用其它模型;2026-10-25 用户已授权以最小必要订阅调用验收并本地部署,不再按美元预算审批;仅经 Magpie。当前真实最小验收已完成,完整对照仍未完成,见 [实测记录](magpie-live-acceptance.md)。
@@ -16,13 +16,13 @@ export declare const panelOpenAfter: (event: PanelEvent) => boolean;
16
16
  export interface Decision {
17
17
  id: number;
18
18
  source: 'main' | 'subagent';
19
- tier: string;
19
+ tier?: string;
20
20
  effort: string;
21
21
  model?: string;
22
22
  confidence?: number;
23
23
  probabilities?: Record<string, number>;
24
24
  ms: number;
25
- depth?: string;
25
+ depth: string;
26
26
  capability?: string;
27
27
  capabilityConfidence?: number;
28
28
  capabilityProbabilities?: Record<string, number>;
@@ -6,7 +6,6 @@ export async function callRouting(rpc, sessionId, mode) {
6
6
  }
7
7
  export const chipOrder = -10;
8
8
  export const panelOpenAfter = (event) => event === 'enter' || event === 'focus';
9
- const TIER_ORDER = ['simple', 'standard', 'complex', 'escalated'];
10
9
  const pct = (v) => `${Math.round(v * 100)}%`;
11
10
  export const TOAST_MS = 3000;
12
11
  const spreadText = (order, values) => {
@@ -15,25 +14,16 @@ const spreadText = (order, values) => {
15
14
  };
16
15
  /** Shared by the persistent hover panel and transient decision hint. */
17
16
  export function toastText(d) {
18
- if (d.depth)
19
- return [
20
- `${d.source === 'main' ? '主会话' : '子任务'} · ${d.status === 'applied' ? '已应用' : d.status === 'failed' ? '未应用' : d.status === 'unsupported' ? '不支持调整' : '已判断/待应用'}`,
21
- ...(d.capability ? [`能力 ${d.capability} · 判定确定度 ${d.capabilityConfidence === undefined ? '未提供' : pct(d.capabilityConfidence)}`] : []),
22
- ...(d.capability ? [`能力分布:${spreadText(['bounded', 'analytical', 'frontier', 'insufficient_context'], d.capabilityProbabilities)}`] : []),
23
- `投入 ${d.depth} · 判定确定度 ${d.confidence === undefined ? '未提供' : pct(d.confidence)}`,
24
- `投入分布:${spreadText(['lower', 'baseline', 'higher'], d.probabilities)}`,
25
- `${d.provider ?? ''} / ${d.model ?? '待确定'} · ${d.effort}`,
26
- `${d.decisionProvider ?? '未知服务'} / ${d.decisionModel ?? '未知模型'} · ${(d.ms / 1000).toFixed(2)}s`,
27
- '确定度 = (最高概率 − 1/n) ÷ (1 − 1/n),n 为候选数;不是任务成功率',
28
- ];
29
- const head = [
30
- d.source === 'main' ? '主会话判断' : '子会话判断',
31
- `${d.tier} → ${d.model ? `${d.model} · ` : ''}${d.effort}`,
32
- `${(d.ms / 1000).toFixed(2)}s`,
33
- ].join(' · ');
34
- const spread = TIER_ORDER.filter(t => d.probabilities?.[t] !== undefined && Math.round(d.probabilities[t] * 100) > 0)
35
- .map(t => `${t} ${pct(d.probabilities[t])}`).join(' / ');
36
- return spread ? [head, spread] : [head];
17
+ return [
18
+ `${d.source === 'main' ? '主会话' : '子任务'} · ${d.status === 'applied' ? '已应用' : d.status === 'failed' ? '未应用' : d.status === 'unsupported' ? '不支持调整' : '已判断/待应用'}`,
19
+ ...(d.capability ? [`能力 ${d.capability} · 判定确定度 ${d.capabilityConfidence === undefined ? '未提供' : pct(d.capabilityConfidence)}`] : []),
20
+ ...(d.capability ? [`能力分布:${spreadText(['bounded', 'analytical', 'frontier', 'insufficient_context'], d.capabilityProbabilities)}`] : []),
21
+ `投入 ${d.depth} · 判定确定度 ${d.confidence === undefined ? '未提供' : pct(d.confidence)}`,
22
+ `投入分布:${spreadText(['lower', 'baseline', 'higher'], d.probabilities)}`,
23
+ `${d.provider ?? ''} / ${d.model ?? '待确定'} · ${d.effort}`,
24
+ `${d.decisionProvider ?? '未知服务'} / ${d.decisionModel ?? '未知模型'} · ${(d.ms / 1000).toFixed(2)}s`,
25
+ '确定度 = (最高概率 − 1/n) ÷ (1 − 1/n),n 为候选数;不是任务成功率',
26
+ ];
37
27
  }
38
28
  /** A hint is shown only for a decision id not seen before (it is hidden TOAST_MS later by the chip). */
39
29
  export function nextToast(seen, d) {
@@ -1,9 +1,13 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
2
  import { type Decision, type Rpc, type Toast } from './control.js';
3
3
  import { type JevCategory } from '../hints.js';
4
+ interface AutoSupport {
5
+ supported: boolean;
6
+ provider?: string;
7
+ model?: string;
8
+ }
4
9
  interface View {
5
10
  mode: 'AUTO' | 'MANUAL';
6
- policy?: string;
7
11
  depth?: string;
8
12
  effort?: string;
9
13
  reason?: string;
@@ -13,14 +17,17 @@ interface View {
13
17
  };
14
18
  decision?: Decision;
15
19
  decisionToast?: boolean;
20
+ autoSupport?: AutoSupport;
16
21
  }
22
+ type SubscribeSelection = (sessionId: string, onChange: () => void) => () => void;
17
23
  interface Props {
18
24
  sessionId: string;
19
25
  rpc: Rpc;
26
+ subscribeSelection?: SubscribeSelection;
20
27
  }
21
28
  export declare const name = "dsh-auto-model-routing/client";
22
29
  export declare const inject: string[];
23
- export declare function RoutingChip({ sessionId, rpc, initialView, initiallyOpen, initialToast }: Props & {
30
+ export declare function RoutingChip({ sessionId, rpc, initialView, initiallyOpen, initialToast, subscribeSelection }: Props & {
24
31
  initialView?: View;
25
32
  initiallyOpen?: boolean;
26
33
  initialToast?: Toast;
@@ -2,13 +2,17 @@ import { createElement as h, useEffect, useRef, useState } from 'react';
2
2
  import { createPortal } from 'react-dom';
3
3
  import { callRouting, chipOrder, nextToast, panelOpenAfter, TOAST_MS, toastText } from './control.js';
4
4
  import { CATEGORY_HINTS } from '../hints.js';
5
- import { IconGaugeOutlineRegular, Menu, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives';
5
+ import { IconInfoOutlineRegular, IconGaugeOutlineRegular, Menu, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives';
6
6
  import { LabelButton } from './label.js';
7
7
  import * as settingsUi from '@deepseek-ai/dsh-client-ui-primitives';
8
8
  import { RoutingSettings } from './settings-section.js';
9
+ import { registry } from '../strategies/index.js';
10
+ /** Supported families as the user sees them. */
11
+ const FAMILIES = registry.labels().join(' / ');
9
12
  import { installRoutingNavIcon } from './nav-icon.js';
10
13
  export const name = 'dsh-auto-model-routing/client';
11
14
  export const inject = ['slots', 'connection'];
15
+ const REPO_URL = 'https://github.com/prgrmrwy/dsh-auto-model-routing';
12
16
  // Same look as the official model picker trigger (dsh-client-ui-model-selection), values read from its bundle.
13
17
  const CSS = `
14
18
  .amr-root{position:relative;min-width:0;display:inline-flex}
@@ -17,6 +21,11 @@ const CSS = `
17
21
  .amr-trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary))}
18
22
  .amr-trigger:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}
19
23
  .amr-icon{flex:none;color:var(--dsw-alias-label-caption)}
24
+ .amr-trigger[data-locked=true]{color:var(--dsw-alias-label-dimmed);cursor:default}
25
+ .amr-trigger[data-locked=true]:hover{background:0 0}
26
+ .amr-info{color:var(--dsw-alias-label-caption)}
27
+ .amr-card a{color:var(--dsw-alias-state-business-primary);text-decoration:none;overflow-wrap:anywhere}
28
+ .amr-card a:hover{text-decoration:underline}
20
29
  .amr-trigger[data-disabled=true],.amr-trigger[data-disabled=true] .amr-icon{color:var(--dsw-alias-state-error-primary)}
21
30
  .amr-panel{position:absolute;z-index:1100;bottom:100%;left:0;padding-bottom:6px}
22
31
  .amr-card{width:280px;padding:10px 12px;border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg,#fff);color:var(--dsw-alias-label-primary);box-shadow:var(--dsw-elevation-prominent);font-size:12px;line-height:18px}
@@ -28,9 +37,9 @@ const CSS = `
28
37
  `;
29
38
  // Left-right swap arrows at the picker chevron's size; colour follows currentColor.
30
39
  function SwapIcon() {
31
- return h('svg', { className: 'amr-icon', width: 14, height: 14, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true }, h('path', { d: 'M3 5.5h9.5M10 3l2.5 2.5L10 8' }), h('path', { d: 'M13 10.5H3.5M6 8l-2.5 2.5L6 13' }));
40
+ return h('svg', { className: 'amr-icon amr-swap', width: 14, height: 14, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true }, h('path', { d: 'M3 5.5h9.5M10 3l2.5 2.5L10 8' }), h('path', { d: 'M13 10.5H3.5M6 8l-2.5 2.5L6 13' }));
32
41
  }
33
- export function RoutingChip({ sessionId, rpc, initialView, initiallyOpen = false, initialToast }) {
42
+ export function RoutingChip({ sessionId, rpc, initialView, initiallyOpen = false, initialToast, subscribeSelection }) {
34
43
  const [view, setView] = useState(initialView);
35
44
  const [open, setOpen] = useState(initiallyOpen);
36
45
  const [busy, setBusy] = useState(false);
@@ -77,6 +86,28 @@ export function RoutingChip({ sessionId, rpc, initialView, initiallyOpen = false
77
86
  const hide = setTimeout(() => setToast(undefined), TOAST_MS);
78
87
  return () => clearTimeout(hide);
79
88
  }, [toast]);
89
+ const refreshRef = useRef(() => { });
90
+ // Optional acceleration from the official picker store; retried on every state read so a late service still binds.
91
+ const subscription = useRef(undefined);
92
+ const subscribeRef = useRef(undefined);
93
+ subscribeRef.current = subscribeSelection;
94
+ const ensureSubscription = () => {
95
+ const subscribe = subscribeRef.current;
96
+ if (!subscribe || subscription.current?.sessionId === sessionId)
97
+ return;
98
+ subscription.current?.stop();
99
+ subscription.current = undefined;
100
+ let handle;
101
+ try {
102
+ const stop = subscribe(sessionId, () => { if (handle)
103
+ clearTimeout(handle); handle = setTimeout(() => refreshRef.current(), 80); });
104
+ subscription.current = { sessionId, stop: () => { stop(); if (handle)
105
+ clearTimeout(handle); } };
106
+ }
107
+ catch {
108
+ subscription.current = undefined;
109
+ }
110
+ };
80
111
  useEffect(() => {
81
112
  let active = true;
82
113
  primed.current = false;
@@ -87,11 +118,20 @@ export function RoutingChip({ sessionId, rpc, initialView, initiallyOpen = false
87
118
  viewSession.current = sessionId;
88
119
  setView(value);
89
120
  setError('');
121
+ ensureSubscription();
90
122
  } }, reason => { if (active)
91
123
  setError(String(reason)); }); };
124
+ refreshRef.current = refresh;
92
125
  refresh();
93
126
  const poll = setInterval(refresh, 3000);
94
- return () => { active = false; clearInterval(poll); clearTimeout(timer.current); };
127
+ return () => {
128
+ active = false;
129
+ clearInterval(poll);
130
+ clearTimeout(timer.current);
131
+ subscription.current?.stop();
132
+ subscription.current = undefined;
133
+ refreshRef.current = () => { };
134
+ };
95
135
  }, [rpc, sessionId]);
96
136
  const on = (event) => () => {
97
137
  clearTimeout(timer.current);
@@ -119,32 +159,77 @@ export function RoutingChip({ sessionId, rpc, initialView, initiallyOpen = false
119
159
  };
120
160
  const off = view?.disabled;
121
161
  const hint = off ? CATEGORY_HINTS[off.category] ?? off.category : undefined;
162
+ const locked = view?.autoSupport?.supported === false;
163
+ const support = view?.autoSupport;
164
+ const lockedModel = `${support?.provider ?? '未知 provider'} / ${support?.model ?? '未知模型'}`;
122
165
  // The panel sits inside the root (padding bridges the gap), so moving onto it keeps it open.
123
166
  const place = anchor.current ?? { left: (typeof window === 'undefined' ? 0 : window.innerWidth / 2), top: (typeof window === 'undefined' ? 0 : window.innerHeight) - 96 };
167
+ const panel = locked
168
+ ? [
169
+ h('p', { key: 'locked-title' }, h('strong', null, 'AUTO 不支持当前模型')),
170
+ h('p', { key: 'locked-model' }, `${lockedModel} 不在自动路由支持范围,已切换为 MANUAL。`),
171
+ h('p', { key: 'locked-note' }, `目前仅支持 ${FAMILIES} 系列模型;其他模型策略欢迎提交 issue 或 PR:`),
172
+ h('p', { key: 'locked-link' }, h('a', { href: REPO_URL, target: '_blank', rel: 'noopener noreferrer' }, REPO_URL)),
173
+ h('p', { key: 'locked-hint', className: 'amr-muted' }, `切回 ${FAMILIES} 系列模型后,可重新点击启用 AUTO。`),
174
+ error && h('p', { key: 'error', role: 'alert', className: 'amr-error' }, error),
175
+ ]
176
+ : [
177
+ h('p', { key: 'title' }, h('strong', null, view?.mode === 'AUTO' ? '自动调整推理强度' : '手动:不自动调整')),
178
+ h('p', { key: 'output' }, `本回合插件输出:${view?.effort ?? '未改写'} · ${view?.reason ?? '尚无决定'}`),
179
+ view?.decision && h('div', { key: 'latest', className: 'amr-latest' }, h('p', null, h('strong', null, '最近决策')), ...toastText(view.decision).map((line, index) => h('p', { key: index, className: index ? 'amr-muted' : undefined }, line))),
180
+ h('p', { key: 'depth', className: 'amr-muted' }, `AUTO · ${view?.depth ?? '尚无判断'};主模型不变,推理投入按请求应用;子任务自动选配。`),
181
+ off && h('p', { key: 'disabled', role: 'status', className: 'amr-error' }, `Jev 停用(${off.category}):${hint}。${new Date(off.at).toLocaleString()}。点击 AUTO 重试。`),
182
+ h('p', { key: 'data', className: 'amr-muted' }, 'AUTO 会把脱敏任务文本发送给设置中所选的决策服务判断。'),
183
+ error && h('p', { key: 'error', role: 'alert', className: 'amr-error' }, error),
184
+ ];
124
185
  return h('span', { ref: (el) => { rootRef.current = el; remember(); }, className: 'amr-root', onMouseEnter: on('enter'), onMouseLeave: on('leave'), onFocus: on('focus'), onBlur: on('blur') }, h('style', null, CSS), h('button', {
125
- type: 'button', className: 'amr-trigger', disabled: busy || !view, onClick: () => { void toggle(); },
126
- 'data-disabled': off ? 'true' : undefined, style: { border: 'none' },
127
- title: off ? `Jev 判档已停用:${hint}。点击 AUTO 重试` : '点击切换自动推理强度',
128
- 'aria-label': `推理强度模式:${view?.mode ?? '加载中'},点击切换`,
129
- }, h('span', null, off ? `${view?.mode} · Jev 停用` : view?.mode ?? '…'), h(SwapIcon)), toast && !open && (typeof document === 'undefined'
186
+ type: 'button', className: 'amr-trigger', disabled: busy || !view, onClick: () => { if (locked)
187
+ return; void toggle(); },
188
+ 'data-disabled': off ? 'true' : undefined, 'data-locked': locked ? 'true' : undefined,
189
+ 'aria-disabled': locked ? 'true' : undefined, style: { border: 'none' },
190
+ title: locked ? `AUTO 仅支持 ${FAMILIES} 系列模型;悬停查看说明` : off ? `Jev 判断已停用:${hint}。点击 AUTO 重试` : '点击切换自动推理强度',
191
+ 'aria-label': locked ? `推理强度模式:${view?.mode ?? 'MANUAL'}(AUTO 不支持当前模型,不可切换)` : `推理强度模式:${view?.mode ?? '加载中'},点击切换`,
192
+ }, h('span', null, locked ? (view?.mode ?? 'MANUAL') : off ? `${view?.mode} · Jev 停用` : view?.mode ?? '…'), locked ? h(IconInfoOutlineRegular, { size: 14, className: 'amr-icon amr-info' }) : h(SwapIcon)), toast && !open && (typeof document === 'undefined'
130
193
  ? h('div', { className: 'amr-toast', role: 'status', style: { pointerEvents: 'none' } }, ...toast.lines.map((line, index) => h('div', { key: index, className: index ? 'amr-muted' : undefined }, line)))
131
- : createPortal(h('div', { className: 'amr-toast', role: 'status', style: { pointerEvents: 'none', left: place.left, top: place.top } }, h('style', null, CSS), ...toast.lines.map((line, index) => h('div', { key: index, className: index ? 'amr-muted' : undefined }, line))), document.body)), open && h('section', { className: 'amr-panel', 'aria-label': '自动路由状态' }, h('div', { className: 'amr-card' }, h('p', null, h('strong', null, view?.mode === 'AUTO' ? '自动调整推理强度' : '手动:不自动调整')), h('p', null, `本回合插件输出:${view?.effort ?? '未改写'} · ${view?.reason ?? '尚无决定'}`), view?.decision && h('div', { className: 'amr-latest' }, h('p', null, h('strong', null, '最近决策')), ...toastText(view.decision).map((line, index) => h('p', { key: index, className: index ? 'amr-muted' : undefined }, line))), h('p', { className: 'amr-muted' }, view?.policy === 'policy-v2' ? `AUTO · ${view.depth ?? '尚无判断'};主模型不变,推理投入按请求应用;子任务自动选配。` : 'AUTO 的决定会同步到右侧模型选择;要手动指定 effort,请先切到 MANUAL。'), off && h('p', { role: 'status', className: 'amr-error' }, `Jev 停用(${off.category}):${hint}。${new Date(off.at).toLocaleString()}。点击 AUTO 重试。`), h('p', { className: 'amr-muted' }, 'AUTO 会把脱敏任务文本发送给设置中所选的决策服务判档。'), error && h('p', { role: 'alert', className: 'amr-error' }, error))));
194
+ : createPortal(h('div', { className: 'amr-toast', role: 'status', style: { pointerEvents: 'none', left: place.left, top: place.top } }, h('style', null, CSS), ...toast.lines.map((line, index) => h('div', { key: index, className: index ? 'amr-muted' : undefined }, line))), document.body)), open && h('section', { className: 'amr-panel', 'aria-label': '自动路由状态' }, h('div', { className: 'amr-card' }, ...panel)));
132
195
  }
133
196
  const labelUi = { Tooltip, Menu, Icon: IconGaugeOutlineRegular };
134
197
  export function apply(ctx) {
198
+ // Optional acceleration only: the official picker's per-session store triggers an immediate state read.
199
+ // When the service is absent (or appears late) the chip's 3s poll remains the source of truth.
200
+ const pickerSubscriber = { current: undefined };
201
+ const subscribeSelection = (id, onChange) => pickerSubscriber.current ? pickerSubscriber.current(id, onChange) : () => { };
202
+ try {
203
+ const inject = ctx.inject;
204
+ if (typeof inject === 'function') {
205
+ inject.call(ctx, ['modelDirectories'], (scope) => {
206
+ const service = scope.modelDirectories;
207
+ if (!service)
208
+ return;
209
+ pickerSubscriber.current = (id, onChange) => { try {
210
+ return service.directoryFor(id).store.subscribe(onChange);
211
+ }
212
+ catch {
213
+ return () => { };
214
+ } };
215
+ return () => { pickerSubscriber.current = undefined; };
216
+ });
217
+ }
218
+ }
219
+ catch { /* polling stays the fallback */ }
135
220
  ctx.slots.inject('settings.section', () => ctx.slots.register({
136
221
  name: 'settings.section', id: 'auto-model-routing', order: 300,
137
222
  label: () => '自动模型路由',
138
223
  inject: () => ({ rpc: ctx.connection.rpc, ui: settingsUi }),
139
224
  }, RoutingSettings));
140
225
  ctx.effect(() => typeof document === 'undefined' ? () => { } : installRoutingNavIcon(document), 'auto-model-routing: settings navigation icon');
141
- // Reply-bar tier button, after the official feedback (order 10); rendered only on judged turns.
226
+ // Reply-bar allocation feedback, after the official feedback (order 10); rendered only on bound replies.
142
227
  ctx.slots.inject('conversation.chat.assistant-actions', () => ctx.slots.register({
143
228
  name: 'conversation.chat.assistant-actions', id: 'auto-model-routing-label', order: 20,
144
229
  inject: (sessionId) => ({ sessionId, rpc: ctx.connection.rpc, ui: labelUi }),
145
230
  }, LabelButton));
146
231
  ctx.slots.inject('conversation.input.right', () => ctx.slots.register({
147
232
  name: 'conversation.input.right', id: 'auto-model-routing', order: chipOrder,
148
- inject: (sessionId) => ({ sessionId, rpc: ctx.connection.rpc }),
233
+ inject: (sessionId) => ({ sessionId, rpc: ctx.connection.rpc, subscribeSelection }),
149
234
  }, RoutingChip));
150
235
  }