dsh-auto-model-routing 0.3.0 → 0.4.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 (50) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +56 -25
  3. package/README.zh.md +56 -25
  4. package/docs/feedback-evaluation.md +18 -0
  5. package/docs/host-capability-report.md +91 -0
  6. package/docs/jev-policy-v2-validation.md +37 -0
  7. package/docs/magpie-live-acceptance.md +27 -0
  8. package/docs/policy-v2-development.md +68 -0
  9. package/docs/policy-v2-real-evaluation-plan.md +32 -0
  10. package/docs/routing-community-research-2026-10-24.md +98 -0
  11. package/docs/routing-intent-and-gaps.md +56 -0
  12. package/docs/routing-strategy-plan.md +101 -0
  13. package/lib/client/control.d.ts +9 -1
  14. package/lib/client/control.js +16 -1
  15. package/lib/client/index.d.ts +2 -0
  16. package/lib/client/index.js +2 -2
  17. package/lib/client/label.d.ts +4 -0
  18. package/lib/client/label.js +24 -8
  19. package/lib/client/settings-section.js +53 -3
  20. package/lib/client/settings-ui.d.ts +1 -1
  21. package/lib/client/strategy-details.d.ts +4 -0
  22. package/lib/client/strategy-details.js +8 -0
  23. package/lib/client.js +332 -47
  24. package/lib/decision-service.d.ts +39 -0
  25. package/lib/decision-service.js +28 -0
  26. package/lib/endpoints.d.ts +2 -1
  27. package/lib/endpoints.js +22 -1
  28. package/lib/feedback.d.ts +69 -0
  29. package/lib/feedback.js +65 -0
  30. package/lib/hints.js +1 -1
  31. package/lib/index.js +29 -7
  32. package/lib/jev.d.ts +27 -1
  33. package/lib/jev.js +51 -23
  34. package/lib/policy-host.d.ts +68 -0
  35. package/lib/policy-host.js +274 -0
  36. package/lib/policy-store.d.ts +96 -0
  37. package/lib/policy-store.js +60 -0
  38. package/lib/policy-v2-rubric.json +21 -0
  39. package/lib/policy-v2.d.ts +20 -0
  40. package/lib/policy-v2.js +53 -0
  41. package/lib/settings-contract.d.ts +4 -1
  42. package/lib/settings-contract.js +1 -1
  43. package/lib/settings-host.d.ts +2 -2
  44. package/lib/settings-host.js +26 -14
  45. package/lib/settings.js +14 -1
  46. package/lib/subagent-tool.js +32 -5
  47. package/package.json +3 -3
  48. package/skills/subagent-model-routing/SKILL.md +15 -0
  49. package/lib/selection-projection.d.ts +0 -8
  50. package/lib/selection-projection.js +0 -7
@@ -0,0 +1,98 @@
1
+ # AUTO routing 外部方案调研(2026-10-24)
2
+
3
+ 状态:只读调研,未采纳实现;主会话复核 + gpt-6.1-sol/high 子智能体调研。核心诉求见 routing-intent-and-gaps.md。
4
+
5
+ ## 先分清三类自动路由
6
+
7
+ 1. 服务网关选模型/供应商:对请求做能力与价格路由,可能改变主模型,不天然符合本项目主模型固定的不变量。
8
+ 2. 固定模型调推理预算:最接近本项目主会话,但需处理缓存前缀、模型支持与同回合稳定性。
9
+ 3. 编排层委派:决定是否派发、任务边界、worker能力与预算、验收升级;网关分类器无法代替主Agent完成这些决策。
10
+
11
+ ## 已复核的一手证据
12
+
13
+ ### Anthropic:强主轻子的生产案例,但不保证省token
14
+
15
+ 来源:https://www.anthropic.com/research/multi-agent-research-system
16
+
17
+ 官方研究系统使用 Opus 4 lead + Sonnet 4 workers;适合多条独立方向的并行研究与上下文压缩,强调任务目标/输出格式/边界/工具指导,以及按最终成果评估。不能将该内部研究评估的收益直接推广到所有编码任务;多agent会增加token消耗,只有收益足够时才值得。
18
+
19
+ Anthropic platform effort/caching文档本次直接抓取存在地区跳转限制。仓库 eval/README.md 的2026-10-21/22来源摘要是历史依据,不能冒充本次全文复核。
20
+
21
+ ### OpenAI:effort不是无缓存代价的开关
22
+
23
+ 来源:https://developers.openai.com/api/docs/guides/prompt-caching.md
24
+
25
+ 官方说明 reasoning.effort 可改变隐藏模型指令;支持GPT-6+模型可以追加 configuration_update、保持顶层原值,以保存既有前缀。是否适用于DSH订阅适配器需单独检查实际协议,不能从API文档直接推断。
26
+
27
+ ### LiteLLM:分类 + 运行约束 + 影子评估
28
+
29
+ 来源:https://docs.litellm.ai/docs/auto_router
30
+ 来源:https://docs.litellm.ai/docs/auto_router/evaluate
31
+
32
+ 当前已经有 Auto Router add-on,不能再将LiteLLM仅归为负载均衡。最值得借鉴的是 shadow evaluation:同输入复制请求、盲评实际输出、对比多个策略;官方明确指出 tier标签匹配不等于选中模型答得好。
33
+
34
+ 费用核算包括classifier与下游费用,记录fallback和p95;继续回合的对照费用应采用暖缓存,切换后新增cache write应扣除。其按最昂贵配置估算的savings仍是反事实估计,不能等同本项目实测节约。
35
+
36
+ ### Magpie:回合稳定、缓存迟滞、可选effort更新
37
+
38
+ 项目确认为 https://github.com/yetone/magpie ,不是同名数据集或仅DSH连接器。
39
+
40
+ 固定代码:https://github.com/yetone/magpie/tree/f3eb92924e56605860f6a85a3449247c26c0bd90
41
+
42
+ - internal/gateway/rules.go:用户回合开始决策,工具结果继续沿用;上下文超过模型能力等例外另处理。缓存仍暖时保留更高effort,不因新一轮简单就立刻降档;冷却或上下文显著缩短时才允许降低。
43
+ - Pick/Wanted区分实际保留档位与新判断期望,方便展示“推荐值≠生效值”。
44
+ - internal/gateway/effort_update.go:MAGPIE_EFFORT_UPDATES=on的preview才启用,默认关闭;限GPT-6、Responses、OpenAI/ChatGPT账户,并排除某些服务端历史管理/自动截断路径。
45
+ - 超过初始base的升档重设顶层effort;注释声称configuration_update升到高档时实际推理不足。这是上游实测自述,非官方保证,也未由本项目复现。
46
+ - 保存每个thread/account/model的更新位置并重放;历史不匹配、compact或重启可能导致重新缓存。不是只改一个effort字段就能实现。
47
+
48
+ 可借鉴:主会话调effort要有缓存意识;判档值与生效值可分离;缓存更新路径需显式支持与能力验证。不可照搬:默认启用preview、假定跨provider都支持、将所有回合只升不降当作永久最优。
49
+
50
+ ## Sol 补充核验:官方建议
51
+
52
+ - https://claude.com/resources/articles/claude-model-and-effort-level-in-claude-code :model是能力边界,effort也影响读文件/工具/核验/坚持程度;从模型默认起,把effort当总体偏好。认真尝试仍错可能需强model,漏读/不测/早退可能需更多effort。
53
+ - https://code.claude.com/docs/en/model-config.md :当前Opus/Sonnet/Haiku5.5默认medium;各模型同名effort不等价。
54
+ - https://code.claude.com/docs/en/prompt-caching.md :多数模型不同effort有独立cache;当前Claude Code在部分模型与直API/订阅组合可保cache,但其他后端、禁beta等不适用。这是client×backend×model组合能力,不自动适用于DSH。
55
+ - https://developers.openai.com/api/docs/guides/model-selection.md :用同输入比较模型和effort,选择达到质量要求的最轻配置。
56
+ - https://developers.openai.com/api/docs/guides/reasoning.md :Sol默认medium,high适合复杂调试,xhigh需eval收益;模型在各effort档内已会根据任务自适应。
57
+ - https://learn.chatgpt.com/docs/agent-configuration/subagents.md :主线程管需求/决策/最终交付,子线程做噪音探索并摘要;Sol适合歧义多步,Luna适合窄且可重复任务,显式Luna起high/Astra起low;子agent不是必定更弱。
58
+
59
+ ## 社区策略与维护快照
60
+
61
+ 2026-10-24约02:09–02:15 Asia/Shanghai,Sol读取GitHub REST repo与commits端点。stars仅关注度;last push/commit不等于质量。下表均为当时值。
62
+
63
+ | 项目 | 关注/维护 | 路由与适用边界 |
64
+ |---|---|---|
65
+ | [Magpie](https://github.com/yetone/magpie) | 20,159 stars / 1,547 forks,HEAD f3eb929,10/23 18:05Z提交;9/23建仓,活跃但年轻,maintainers-only PR | 最接近本项目的回合判档与effort缓存问题,但网关不负责拆解/验收 |
66
+ | [LiteLLM](https://github.com/BerriAI/litellm) | 61,909 / 12,608,HEAD 1e6dc81,10/23 18:11Z提交 | 大平台;Auto Router仍Beta/add-on。四档→模型/pool,可用heuristic/LLM/Jev/local Laya;上下文/模态约束、stall escalation、session pin。平台成熟不证明每个路由策略成熟 |
67
+ | [RouteLLM](https://github.com/lm-sys/RouteLLM) | 5,633 / 446,HEAD 0b64fda,最后push 2024-08-10 | 强弱两模型win-rate/阈值路由;MF/Elo/BERT等,强调本域校准。影响力研究基线而非持续维护的新模型产品;旧静态benchmark非长程coding证据 |
68
+ | [DSH Laya](https://github.com/HapyRain/dsh-router-laya) | 25 / 6,HEAD 29624e7,10/17提交 | 本地约846MB分类器+规则,默认同Flash调low/high/max,同回合锁定;故障回low。标签99%与未公开实验不能当端到端收益 |
69
+ | [Neptune810 router](https://github.com/Neptune810/dsh-model-router) | 2 / 3,HEAD 1d1968a,10/04提交 | full/effort/model作用范围,错误/重复升级与降档迟滞;child preferCheap未经质量证明 |
70
+ | [thedeveloper256 router](https://github.com/thedeveloper256/dsh-model-router) | 1 / 0,9/20最后push | root planner/child executor角色配置;当前两者默认均Flash,README与旧description不一致;角色分离不等于实际强主轻子 |
71
+ | [incrhst router](https://github.com/incrhst/dsh-model-router) | 0 / 0,8/31提交 | size/tool/code/关键词分数阈值切Pro/Flash,图片vision;未见验收闭环 |
72
+ | [superboy911 router](https://github.com/superboy911/dsh-model-router) | 4 / 1,9/11最后push | 有序关键词→model+可选effort,无匹配不改;确定规则而非学习型AUTO |
73
+
74
+ 社区DSH路由尚无充分证据表明已形成大规模验证的收敛方案;不能把同名repo混为一个热门项目。
75
+
76
+ ## OpenRouter Auto 现行策略
77
+
78
+ 来源:https://openrouter.ai/docs/guides/routing/routers/auto-router.md (Sol本次读取)
79
+
80
+ 当前是轻classifier分约30任务类→最近7日该类真实aggregate spend share排序→cost_tier区间→allow/exclude/tool/modality/ZDR约束下选模型与fallback。不是旧NotDiamond描述;市场支出份额不等于能力评分,cost_tier不等于reasoning effort,也不是纯价格上限。session stickiness有条件:原模型仍须在新任务top候选中。可借鉴约束和实际模型透明度,不能原样替换固定主模型策略。
81
+
82
+ ## Magpie进一步代码观察
83
+
84
+ - classify.go/decide.go:普通小模型可intent+effort两个调用并行;Jev可单调用多questions;effort score 0..3映射low/medium/high/xhigh,不同于本项目choice tier再加模型offset。
85
+ - 意图normalized confidence门槛0.4;level低confidence保前级;结果10分钟缓存与inflight合并,失败rest30秒,可保持前intent。
86
+ - 当前上下文含最新请求前3000+后1000runes、前轮user/reply各1000,不含工具结果/推理。
87
+ - https://usemagpie.ai/docs/intent 标记v0.1.99,仍写只看最新消息/不支持group classifier,与固定提交当前代码不同;不能只读旧文档。
88
+ - smart/order/rotate/usage/pace大量处理账户可用性与额度,不应统称质量最优路由。
89
+
90
+ ## 对本项目的建议顺序
91
+
92
+ 1. **P0 缓存与结果基线**:固定主模型+官方默认effort,与主effort-only、child-model-only、child-effort-only、联合AUTO对照。按窄实现/探索/debug/跨系统设计分层,以独立测试与最终结果为准,计父子/分类/验收/重试/cache总费用、端到端与p95。不要只评tier。
93
+ 2. **P1 分离能力与预算**:保留强主轻子;主模型负责规划验收,子模型按任务能力和可验证性选,不因是child就便宜。小模型先用推荐effort起点测,不能未经验证再次降档。
94
+ 3. **P1 缓存感知调节**:核验当前DSH实际wire能力,再选择逐轮更新/较稳定默认/自然任务边界/冷却或compact后的降档迟滞;不照搬Magpie五分钟。
95
+ 4. **P2 验收与升级**:能力失败与429/timeout分开;根据失败原因补上下文/加effort/换model,设上限与停止条件。现有Jev失败退出AUTO/暂停委派契约不能静默换成low fallback。
96
+ 5. **P2 可观测与影子对照**:展示判档与生效配置、缓存/费用/升级原因;真实shadow output有对照价值但额外花钱,推荐值或理论估费不等实际效果。
97
+
98
+ 未安装外部项目、未执行付费基准、未更改运行配置。以上外部收益数字均未独立复现;实施仍需单独确认规范与验收条件。
@@ -0,0 +1,56 @@
1
+ # AUTO routing:核心诉求与待补差距
2
+
3
+ 记录时间:2026-10-24(Asia/Shanghai)。状态:需求回顾与调研,不是已批准实施方案;不修改现行运行规范、0.3.0 默认行为或生产配置。
4
+
5
+ ## 用户核心诉求(后续方案的验收方向)
6
+
7
+ 1. 决策配置遵循 Anthropic 与 OpenAI 对具体模型用途、effort、委派和成本优化的官方建议;区分官方支持能力、官方推荐和本项目经验策略,不将自定义四档表称为官方标准。
8
+ 2. 主会话保留用户选择的 provider/model,只调整 effort。保留主模型的理由包括行为/上下文连续性与减少缓存扰动;不承诺改变 effort 一定保留缓存,须按实际适配器核验。
9
+ 3. 强主模型承担理解、拆解、协调与验收;适宜的执行任务交给较轻量子模型。不因缺乏跨模型评估而简单退回“所有子会话默认使用强主模型”,也不把所有小事都委派。
10
+ 4. AUTO 的目标是质量、速度、总开销的平衡,避免一律最高或最低配置。总开销包括判档、上下文移交、worker 执行、主模型复核与失败重试。
11
+ 5. 用户记得曾要求“主模型让简单子模型干活”:当前环境级指令确有委派场景指导,但不是所有简单任务强制委派;后续应补齐与独立插件一致的执行边界。
12
+
13
+ ## 当前证据
14
+
15
+ - 当前规范:../../../openspec/specs/auto-model-routing/spec.md,固定主模型、共享四档 rubric、childRoutes 与派发规则。
16
+ - 默认主模型起点与偏移:../src/effort.ts;Claude 子表 Haiku/Sonnet/Opus/Opus;GPT 子表 Luna/Sol/Sol/Sol。
17
+ - 子工具:../src/subagent-tool.ts,单独判断子任务;指定模型时只调 effort;校验目录与允许列表。
18
+ - 包内指导:../skills/subagent-model-routing/SKILL.md,主要约束选模、调用与复用;是否派发的场景主要位于 ../../../instructions/dsh-home.md。历史提交 14d68627(2026-10-22)已明确加入较大编码、独立复核、大量探索、可并行工作的派发规则,以及小修复/讨论不派发;这确认用户记忆中的委派要求确实曾落地,但不是所有简单任务强制派发。
19
+ - 来源与评估范围:../eval/README.md,现有回归是判档一致性,不是最终任务质量/速度/费用基准。
20
+
21
+ ## Gap 与建议(待调研校准,不是实施授权)
22
+
23
+ | Gap | 影响 | 建议与验证 |
24
+ |---|---|---|
25
+ | 同一难度标签同时决定模型能力档与effort | simple 可能同时降到小模型与低effort,不能由“强模型轻松做”推导“小模型低预算可靠做” | 保留轻量worker;分开能力适配与推理预算,以所选模型官方起点为基线,再验证偏移;不急于增加第二次网络判档 |
26
+ | 固定model被误当作缓存保持保证 | Claude顶层effort可能影响前缀,省下输出费用但新增缓存写入 | 检查当前Claude/GPT适配器与官方文档;记录cache read/write和总延迟;比较稳定effort与逐轮effort策略 |
27
+ | 缺少是否值得委派的成本边界 | 一行任务派发可能比主模型直接做更慢更贵 | 主Agent依据任务独立性、工作量、移交成本与可验证性决定派发;不要“simple必委派” |
28
+ | 独立插件与个人环境指导不完全相同 | 安装工具/skill不保证强主轻子的工作方式真正执行 | 明确包内最小派发/复核指导及尽力而为边界,真实Host验证,不宣称强制拦截 |
29
+ | 缺少结果验收与质量失败升级闭环 | 弱配置可能持续重试,最终成本反增 | 区分工具/网络故障与推理失败;定义验收、有限重试与能力升级策略,尊重用户固定模型约束 |
30
+ | 官方推荐与经验配置边界不清 | 默认表随模型更新失效,用户误以为有官方保障 | 维护版本化来源、适用任务、支持effort、推荐起点;将自行推导明确标注为待评估预设 |
31
+ | 没有端到端收益证据 | 分类准确不等于任务成功或省钱 | 同任务比较固定高配置、固定推荐配置、当前AUTO及候选AUTO;记录独立通过率、全链路耗时/费用与重试次数 |
32
+
33
+ ## 修订后的优先级与职责边界
34
+
35
+ 以用户后续确认的 [策略与推进计划](routing-strategy-plan.md) 为准:
36
+
37
+ 1. 先明确该怎么 route,再把决策说清楚交给 Jev;不能让现有四档输出反向定义策略。
38
+ 2. Agent 保留拆解、派发、协作、验收权;AUTO 补齐资源选择与实际生效;Jev 负责有限范围的能力/深度判断。
39
+ 3. 分开子模型能力与 effort,保留强主轻子;不因轻量模型而自动再次降低其 effort。
40
+ 4. 补齐创建路径 gap:当前 subagent 暴露模型与 effort 参数,fork/teammate 未暴露。返回推荐不等于成功路由,须检查正式接入点并验证实际请求;不能把主 Agent 自选当作所有路径都可用的替代方案。
41
+ 5. 分别验证策略质量、Jev 判断忠实度、宿主接入正确性。
42
+
43
+ 本轮按用户范围约定排除 dsh-plugin-subscriptions 与 MANUAL 改造,缓存/适配器实验不再是前置事项。上表与下节缓存内容保留为历史证据和评估限制,不构成本轮实施要求,也不宣称固定模型必保缓存。此范围约定不表示订阅适配器在技术上只运行于 MANUAL。
44
+
45
+ ## 已独立核验的缓存证据
46
+
47
+ - [OpenAI Prompt caching](https://developers.openai.com/api/docs/guides/prompt-caching.md)(2026-10-24读取):request-level reasoning.effort 可以改变模型侧指令;支持的 GPT-6+ 模型通过追加 configuration_update 并保持顶层原值来保留早先前缀。这是 API 能力,不证明当前 DSH/订阅适配器已支持。
48
+ - [Magpie rules.go](https://github.com/yetone/magpie/blob/main/internal/gateway/rules.go)(2026-10-24读取 main,可变引用):同回合保持决策;keepsEffort 在缓存仍暖时阻止降 effort,但允许升档,缓存过冷或上下文缩小至一半以下时允许降档;区分 Pick 与 Wanted,注释说明支持 effort updates 的路径可使用 Wanted。项目注释中的缓存命中观测是上游自述,不是本项目复现结果。它是可借鉴的缓存策略,不能直接抄作所有 provider 的普遍规律。Sol 后续对固定提交 f3eb92924e56605860f6a85a3449247c26c0bd90 的检查指出,effort_update.go 的更新通路为 MAGPIE_EFFORT_UPDATES=on 显式启用的 preview,默认关闭,限 GPT-6/OpenAI或ChatGPT账户/Responses;不能将源码出现 takesEffortUpdates 解读为所有部署默认具备缓存保持能力。
49
+
50
+ ## 官方强主轻子的适用边界
51
+
52
+ [Anthropic 多智能体研究系统](https://www.anthropic.com/research/multi-agent-research-system)(2026-10-24全文读取)描述 Opus 4 lead + Sonnet 4 workers,在其内部研究评估优于单 Opus 4;这是特定研究系统结果,不是本项目收益证明,也不是所有 coding 都适合多agent的结论。文章强调并行独立方向、上下文压缩、清晰任务边界与最终结果评估;同时指出多agent显著增加token,编码任务往往比研究更难并行。应采用其编排与验收原则,不直接复制旧代模型组合或收益百分比。
53
+
54
+ ## 外部调研
55
+
56
+ 已由 gpt-6.1-sol/high 完成官方指引、维护/热度较高社区路由与 yetone/magpie 策略调研,主会话复核了OpenAI缓存、Anthropic研究编排、LiteLLM评估与Magpie更新路径。详见 [调研记录](routing-community-research-2026-10-24.md)。结果区分官方文档、代码事实、项目自述与未经复现的效果声明;热度不等于效果,不把调研建议直接写为现行spec。
@@ -0,0 +1,101 @@
1
+ # AUTO 路由策略与推进计划(修订版)
2
+
3
+ 日期:2026-10-24。状态:已确认职责方向的设计草案,具体规则和宿主接入待验证;不是实施或部署记录,不改变现行规范与运行行为。
4
+
5
+ ## 1. 核心定位
6
+
7
+ 尊重 Agent 的任务组织权;AUTO 补齐资源选择与实际生效能力;Jev 按明确的策略快速判断。先讨论什么决定是正确的,再设计决策问题,最后优化判断性能。不能因 Jev 支持四档就反推策略必须是四档。
8
+
9
+ | 责任方 | 负责 | 不负责 |
10
+ |---|---|---|
11
+ | 主 Agent / Team Lead | 理解目标、决定是否派发、拆解任务、选择协作形式、定义验收、整合与失败处置 | 不要求它通过没有模型参数的接口完成资源选配 |
12
+ | AUTO | 主会话 effort、子任务 model 与 effort 的独立选择,确定性约束,以及在支持的宿主路径实际生效 | 不重新实现团队任务调度、消息与任务板,不替代主模型验收 |
13
+ | Jev | 按已明确的策略判断当前工作的能力需求和推理投入 | 不制定目标权衡、不拆整个项目、不判断未经执行的任务已成功 |
14
+ | 宿主接入与确定性代码 | 模型目录/允许列表、显式选择优先级、映射、决策绑定、实际请求核验 | 不把推荐结果当作已生效结果 |
15
+
16
+ ## 2. 范围与不变量
17
+
18
+ - 主会话保留用户选择的 provider/model,只调整 effort。
19
+ - 适宜任务交给较轻量 worker,但不要求所有子任务都降模;复杂调查和独立审查可使用强模型。
20
+ - 不改变 MANUAL 的继承/选择行为。本轮按用户范围约定排除 dsh-plugin-subscriptions,不调查或修改它,不以其缓存实验作为前置条件。此处是范围约定,不是宣称该适配器在技术上仅运行于 MANUAL。
21
+ - 不替换 Jev,不引入多分类器,不改底层缓存协议,不自动发布或部署。
22
+ - 保留同回合稳定与显式用户约束;Jev 失败沿用现有可见退出/暂停契约,不静默降档。
23
+ - 缓存影响保留为评估限制,不承诺固定模型或调 effort 必然保缓存。
24
+ - 本轮讨论 Teams 支持不等于授权启动团队。
25
+
26
+ ## 3. 策略草案
27
+
28
+ ### 3.1 工作组织交给 Agent
29
+
30
+ 任务能独立描述、执行和验收,且并行/上下文隔离收益超过移交与复核成本时考虑派发。小修复、紧密依赖当前上下文的讨论留在主线程。不另设一个 Jev“是否组队”分类器。
31
+
32
+ ### 3.2 主会话 effort
33
+
34
+ 以官方推荐起点为默认参照,按当前承担的认知工作调整,而非整个项目大小。明确低推理需求可降低;复杂取舍、原因分析和结果冲突可提高;信息不足时不强行偏移。同回合稳定。具体档位、映射和未知输入处理在后续决策契约中定义;暂不增加只升不降、固定冷却等状态机。
35
+
36
+ ### 3.3 子模型能力与 effort 分离
37
+
38
+ 先选择有合理把握胜任任务的模型能力,再确定推理投入。轻量模型先以其官方推荐 effort 起点验证,不自动再次降低 effort。可验证性越强,越可积极试用轻量模型;风险通过权限、验收和确认控制,不直接当作推理难度。
39
+
40
+ - 明确、成熟、可直接验证的执行任务:优先轻量模型。
41
+ - 范围可控但需要探索推断的任务:选择足够能力的模型。
42
+ - 歧义大、多步推断、困难调试、独立审查:允许强模型。
43
+
44
+ 这不是新的三档 rubric,也不意味着必须两次调用 Jev;输出维度与请求次数分别设计。
45
+
46
+ ### 3.4 验收与升级
47
+
48
+ 主模型依据执行证据验收。描述不清先补描述,缺信息先补上下文,漏核验/浅尝辄止考虑加 effort,充分尝试仍失败考虑更强子模型。网络与限流不视为能力不足。升级有预算和停止条件,具体上限在规则确认阶段确定,不通过初始分类自动断言成功。
49
+
50
+ ## 4. 创建路径的真实能力差异
51
+
52
+ 以下仅依据当前会话暴露的工具 schema,不代表所有宿主版本,也不证明已经找到扩展点:
53
+
54
+ | 路径 | 当前创建接口 | 现有“推荐后传参”是否够用 | 后续必须验证 |
55
+ |---|---|---|---|
56
+ | subagent | 暴露 provider/model/reasoning_effort | 接口上可表达,仍需核验实际生效 | 传参、显式指定优先级、实际请求配置 |
57
+ | subagent_fork | 未暴露上述参数 | 不够 | 官方创建/首次请求扩展点,继承与前缀限制 |
58
+ | spawn_teammate | 未暴露上述参数 | 不够 | 官方扩展点、任务到成员的绑定、首个请求配置 |
59
+
60
+ AUTO 的关键增量是补出默认继承路径上的异构执行能力,而非仅让主 Agent 重复一次它已能表达的选择。主 Agent 能决定派发,不等于创建接口允许它选择资源。
61
+
62
+ 不得以 route_subagent 返回了推荐就声称 Teams/fork 已覆盖。没有可靠接入点的路径明确标为不支持;不静默绕过用户限制。长驻 teammate 的创建时选配与后续换任务重选分开设计,首版不默认每条消息重路由。
63
+
64
+ ## 5. 策略演练案例
65
+
66
+ | 场景 | 组织方式 | 资源策略与理由 |
67
+ |---|---|---|
68
+ | 上下文齐全的一行修改 | 主模型直接做 | 派发成本高于收益;低推理需求可降低 effort |
69
+ | 规则明确且有测试的批量迁移 | Agent 划分独立任务 | 轻量 worker + 推荐 effort 起点;量大不等于能力要求高 |
70
+ | 明确字段提取并附来源 | 独立子任务 | 轻量优先,主模型核验来源与格式 |
71
+ | 冲突资料的因果判断 | 独立调查或主模型 | 足够强的模型,按深度加 effort |
72
+ | 未知原因的跨模块 bug | 主模型组织调查 | 不默认降模;并行假设由 Agent 决定 |
73
+ | 复杂实现后的独立 review | 独立 reviewer | 以审查能力为先,不因子任务身份降模 |
74
+ | 多个结果的整合 | 主模型 | 按冲突和整合难度,不继承项目总难度 |
75
+ | 高后果的简单操作 | 主模型控制权限和验收 | 风险不直接映射高 effort |
76
+
77
+ ## 6. 修订后的推进顺序
78
+
79
+ 1. **策略定稿**:用以上案例与反例确认派发边界、模型能力、effort、未知情况与升级规则。此阶段不先改 rubric。
80
+ 2. **Jev 决策契约**:分别定义主会话与子任务的输入、问题、输出、信息不足处理及确定性映射。主回合只能依赖本轮推理前已有信号;子任务可使用主模型已经写清的目标、范围、输出和验收。
81
+ 3. **宿主能力核验**:只读检查三条创建路径的正式扩展点,明确能覆盖的路径及实现边界。此项与策略细化可并行,但不以宿主接口反推策略。
82
+ 4. **创建独立 OpenSpec change**:将确认策略、接入范围和验收条件写为提案,与既有发布 change 分开;再进入实现。
83
+ 5. **实现与分层验证**:先离线契约与路由集成测试,再经明确预算的小规模真实任务对照;验证实际模型与 effort,而非只验证推荐。
84
+ 6. **根据证据决定上线**:保留回滚与原继承行为,未通过验收不替换默认策略。
85
+
86
+ ## 7. 分开验证三件事
87
+
88
+ - **策略质量**:假设每次都正确执行策略,任务最终质量、总消耗和耗时是否改善?保留独立任务集,包含验收与重试成本。
89
+ - **Jev 忠实度**:是否按已确认策略作出期望判断?单独测错误类型、稳定性、分类成本与延迟。快速判断不等于正确策略。
90
+ - **接入正确性**:每种支持的创建路径是否真正使用选定 model/effort?MANUAL、显式指定、同回合稳定、失败与不支持路径是否符合契约?
91
+
92
+ Jev 的价值不仅是体验和性能;误判会损害质量。但策略质量不能用 Jev 标签一致率代替,接入正确性也不能用推荐输出代替。
93
+
94
+ ## 8. 参考与当前规范
95
+
96
+ - [核心诉求与差距](routing-intent-and-gaps.md)
97
+ - [外部调研](routing-community-research-2026-10-24.md):历史调研建议,实施优先级以本修订为准。
98
+ - [现行规范](../../../openspec/specs/auto-model-routing/spec.md):仍描述当前行为,本稿不静默替换它。
99
+ - [Claude Code model configuration](https://code.claude.com/docs/en/model-config.md):opusplan 是规划阶段 Opus、执行阶段 Sonnet,不等于固定强主监督轻子。
100
+ - [Claude Code subagents](https://code.claude.com/docs/en/sub-agents.md):主模型根据职责描述委派。
101
+ - [Claude Code Agent Teams](https://code.claude.com/docs/en/agent-teams.md):Lead 组织共享任务与协作,不天然保证异构模型或低成本。
@@ -22,13 +22,21 @@ export interface Decision {
22
22
  confidence?: number;
23
23
  probabilities?: Record<string, number>;
24
24
  ms: number;
25
+ depth?: string;
26
+ capability?: string;
27
+ capabilityConfidence?: number;
28
+ capabilityProbabilities?: Record<string, number>;
29
+ decisionProvider?: string;
30
+ decisionModel?: string;
31
+ provider?: string;
32
+ status?: string;
25
33
  }
26
34
  export interface Toast {
27
35
  id: number;
28
36
  lines: string[];
29
37
  }
30
38
  export declare const TOAST_MS = 3000;
31
- /** Line 1: who judged, tier → (model) effort, duration. Line 2: the distribution in tier order, 0% tiers omitted. */
39
+ /** Shared by the persistent hover panel and transient decision hint. */
32
40
  export declare function toastText(d: Decision): string[];
33
41
  /** A hint is shown only for a decision id not seen before (it is hidden TOAST_MS later by the chip). */
34
42
  export declare function nextToast(seen: number | undefined, d: Decision | undefined): Toast | undefined;
@@ -9,8 +9,23 @@ export const panelOpenAfter = (event) => event === 'enter' || event === 'focus';
9
9
  const TIER_ORDER = ['simple', 'standard', 'complex', 'escalated'];
10
10
  const pct = (v) => `${Math.round(v * 100)}%`;
11
11
  export const TOAST_MS = 3000;
12
- /** Line 1: who judged, tier → (model) effort, duration. Line 2: the distribution in tier order, 0% tiers omitted. */
12
+ const spreadText = (order, values) => {
13
+ const entries = order.filter(key => values?.[key] !== undefined && Number.isFinite(values[key]) && values[key] >= 0 && values[key] <= 1);
14
+ return entries.length ? entries.map(key => `${key} ${pct(values[key])}`).join(' / ') : '未提供';
15
+ };
16
+ /** Shared by the persistent hover panel and transient decision hint. */
13
17
  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
+ ];
14
29
  const head = [
15
30
  d.source === 'main' ? '主会话判断' : '子会话判断',
16
31
  `${d.tier} → ${d.model ? `${d.model} · ` : ''}${d.effort}`,
@@ -3,6 +3,8 @@ import { type Decision, type Rpc, type Toast } from './control.js';
3
3
  import { type JevCategory } from '../hints.js';
4
4
  interface View {
5
5
  mode: 'AUTO' | 'MANUAL';
6
+ policy?: string;
7
+ depth?: string;
6
8
  effort?: string;
7
9
  reason?: string;
8
10
  disabled?: {
@@ -24,7 +24,7 @@ const CSS = `
24
24
  .amr-latest{margin:6px 0;padding:6px 0;border-top:1px solid var(--dsw-alias-border-l1);border-bottom:1px solid var(--dsw-alias-border-l1)}
25
25
  .amr-muted{color:var(--dsw-alias-label-tertiary)}
26
26
  .amr-error{color:var(--dsw-alias-state-error-primary)}
27
- .amr-toast{position:fixed;z-index:1100;transform:translate(-50%,-100%);padding:4px 8px;border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-bg,#fff);color:var(--dsw-alias-label-secondary);box-shadow:var(--dsw-elevation-prominent);font-size:12px;line-height:18px;white-space:nowrap}
27
+ .amr-toast{position:fixed;z-index:1100;transform:translate(-50%,-100%);padding:4px 8px;border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-bg,#fff);color:var(--dsw-alias-label-secondary);box-shadow:var(--dsw-elevation-prominent);font-size:12px;line-height:18px;white-space:normal;max-width:min(480px,calc(100vw - 24px));overflow-wrap:anywhere}
28
28
  `;
29
29
  // Left-right swap arrows at the picker chevron's size; colour follows currentColor.
30
30
  function SwapIcon() {
@@ -128,7 +128,7 @@ export function RoutingChip({ sessionId, rpc, initialView, initiallyOpen = false
128
128
  'aria-label': `推理强度模式:${view?.mode ?? '加载中'},点击切换`,
129
129
  }, h('span', null, off ? `${view?.mode} · Jev 停用` : view?.mode ?? '…'), h(SwapIcon)), toast && !open && (typeof document === 'undefined'
130
130
  ? 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' }, '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 会把任务文本发送给 OpenRouter/TypeSafe 判档。'), error && h('p', { role: 'alert', className: 'amr-error' }, error))));
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))));
132
132
  }
133
133
  const labelUi = { Tooltip, Menu, Icon: IconGaugeOutlineRegular };
134
134
  export function apply(ctx) {
@@ -5,6 +5,10 @@ interface Labelled {
5
5
  tier: Tier;
6
6
  effort?: string;
7
7
  label?: Tier;
8
+ policy?: string;
9
+ depth?: string;
10
+ model?: string;
11
+ feedback?: string | null;
8
12
  }
9
13
  /** Official primitives, injected so the component carries no styles of its own (tests pass doubles). */
10
14
  export interface LabelUi {
@@ -1,27 +1,31 @@
1
1
  import { createElement as h, useEffect, useRef, useState } from 'react';
2
+ const FEEDBACK = { appropriate: '分配合适', 'too-high': '投入偏高', 'too-low': '投入不足', unknown: '无法判断' };
2
3
  const TIERS = ['simple', 'standard', 'complex', 'escalated'];
3
4
  const NAMES = { simple: '简单', standard: '标准', complex: '复杂', escalated: '升级' };
4
5
  const CSS = `.amr-label-action{width:calc(28px + var(--dsh-content-font-delta,0px));height:calc(28px + var(--dsh-content-font-delta,0px));border-radius:var(--dsw-radius-sm);color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;justify-content:center;align-items:center;padding:6px;display:inline-flex}.amr-label-action svg{width:calc(17px + var(--dsh-content-font-delta,0px));height:calc(17px + var(--dsh-content-font-delta,0px))}.amr-label-action:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}`;
5
- async function callLabel(rpc, sessionId, messageId, label) {
6
- const result = await rpc.call('/api', 'auto-model-routing.label', { sessionId, messageId, ...(label ? { label } : {}) });
6
+ async function callLabel(rpc, sessionId, messageId, label, feedback) {
7
+ const result = await rpc.call('/api', 'auto-model-routing.label', { sessionId, messageId, ...(label ? { label } : {}), ...(feedback !== undefined ? { feedback } : {}) });
7
8
  if (!result.ok)
8
9
  throw new Error(result.error?.message ?? '标注请求失败');
9
10
  return result.value;
10
11
  }
11
12
  export function tooltipText(view) {
13
+ if (view.policy === 'policy-v2')
14
+ return `AUTO 分配:${view.depth} → ${view.effort} · ${view.model}${view.feedback ? ` · 你反馈:${FEEDBACK[view.feedback]}` : ' · 评价资源分配是否合适'}`;
12
15
  return `AUTO 判档:${NAMES[view.tier]}${view.effort ? ` → ${view.effort}` : ''}${view.label ? ` · 你标注:${NAMES[view.label]}` : ''}`;
13
16
  }
14
17
  /** Reply-bar tier button (design D13): shown only for replies of a judged turn; selecting a tier records a label. */
15
18
  export function LabelButton({ sessionId, messageId, rpc, ui }) {
16
19
  const [view, setView] = useState(undefined);
17
20
  const [open, setOpen] = useState(false);
21
+ const [error, setError] = useState('');
18
22
  const alive = useRef(true);
19
23
  useEffect(() => () => { alive.current = false; }, []);
20
24
  useEffect(() => {
21
25
  let active = true;
22
26
  setView(undefined);
23
27
  // One query per message; `{}` (or an error) renders nothing.
24
- callLabel(rpc, sessionId, messageId).then(value => { if (active && 'tier' in value)
28
+ callLabel(rpc, sessionId, messageId).then(value => { if (active && ('tier' in value || value.policy === 'policy-v2'))
25
29
  setView(value); }, () => { });
26
30
  return () => { active = false; };
27
31
  }, [rpc, sessionId, messageId]);
@@ -30,17 +34,29 @@ export function LabelButton({ sessionId, messageId, rpc, ui }) {
30
34
  const choose = (label) => {
31
35
  setOpen(false);
32
36
  // A failure leaves the shown label unchanged.
33
- callLabel(rpc, sessionId, messageId, label).then(value => { if (alive.current && 'tier' in value)
37
+ callLabel(rpc, sessionId, messageId, label).then(value => { if (alive.current && ('tier' in value || value.policy === 'policy-v2'))
34
38
  setView(value); }, () => { });
35
39
  };
40
+ const allocation = view.policy === 'policy-v2';
41
+ const chooseFeedback = (id) => {
42
+ setOpen(false);
43
+ setError('');
44
+ callLabel(rpc, sessionId, messageId, undefined, id === 'clear' ? null : id).then(value => { if (alive.current && value.policy === 'policy-v2')
45
+ setView(value); }, () => { if (alive.current)
46
+ setError('反馈未保存,请重试'); });
47
+ };
36
48
  const button = h('button', { type: 'button', className: 'amr-label-action', 'aria-label': tooltipText(view), onClick: () => setOpen(value => !value) }, h(ui.Icon));
37
49
  return h('span', null, h('style', null, CSS), h(ui.Menu, {
38
50
  open, side: 'top', portal: true, onClose: () => setOpen(false),
39
51
  // The official menu draws the check on the selected (labelled) row.
40
- ...(view.label ? { selectedId: view.label } : {}),
41
- items: [{ type: 'label', id: 'title', text: '这轮应该用哪档?' }, ...TIERS.map(tier => ({ id: tier, label: `${NAMES[tier]}${tier === view.tier ? ' · Jev' : ''}` }))],
42
- onSelect: id => { if (TIERS.includes(id))
52
+ ...((allocation ? view.feedback : view.label) ? { selectedId: (allocation ? view.feedback : view.label) } : {}),
53
+ items: allocation ? [{ type: 'label', id: 'title', text: '这次资源分配合适吗?' }, ...Object.entries(FEEDBACK).map(([id, label]) => ({ id, label })), ...(view.feedback ? [{ id: 'clear', label: '撤销反馈' }] : [])] : [{ type: 'label', id: 'title', text: '这轮应该用哪档?' }, ...TIERS.map(tier => ({ id: tier, label: `${NAMES[tier]}${tier === view.tier ? ' · Jev' : ''}` }))],
54
+ onSelect: id => { if (allocation) {
55
+ if (Object.hasOwn(FEEDBACK, id) || id === 'clear')
56
+ chooseFeedback(id);
57
+ }
58
+ else if (TIERS.includes(id))
43
59
  choose(id); },
44
60
  anchor: h(ui.Tooltip, { label: tooltipText(view), side: 'bottom', children: button }),
45
- }));
61
+ }), error && h('span', { role: 'alert' }, error));
46
62
  }
@@ -3,9 +3,14 @@ import { AUTO_EFFORTS, PROVIDERS } from '../settings-contract.js';
3
3
  import { selectableModels, supportedFamilyEfforts, validateChildRoute, validateFamilyMainEffort } from '../settings-validation.js';
4
4
  import { CHILD_MODELS, mapEffort, tiers } from '../effort.js';
5
5
  import { RoutePicker } from './route-picker.js';
6
+ import { DECISION_SERVICES, resolveDecisionService } from '../decision-service.js';
7
+ import { CATEGORY_HINTS } from '../hints.js';
8
+ import { StrategyDetails } from './strategy-details.js';
6
9
  const CSS = `
7
10
  .amr-settings{font-size:13px;line-height:1.5;color:var(--dsw-alias-label-primary);min-width:0;container-type:inline-size}.amr-settings-section{padding:16px 0;border-bottom:.5px solid var(--dsw-alias-border-l2)}.amr-settings h3{font-size:13px;font-weight:600;margin:0 0 12px}.amr-settings p{margin:6px 0;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}.amr-settings-row{display:flex;align-items:center;justify-content:space-between;gap:16px;margin:14px 0}.amr-settings-row>div:first-child{min-width:0}.amr-settings-number{width:76px;text-align:right;font:inherit;font-variant-numeric:tabular-nums;border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);color:inherit;background:var(--dsw-alias-bg-layer-1);padding:6px}.amr-settings-number:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary)}
8
11
  .amr-settings-table{border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-lg);margin:12px 0;min-width:0}.amr-settings-tier,.amr-settings-head{display:grid;grid-template-columns:minmax(85px,.65fr) minmax(0,1fr) minmax(0,1.5fr);gap:10px;padding:12px;align-items:start}.amr-settings-tier,.amr-settings-head{border-bottom:.5px solid var(--dsw-alias-border-l2)}.amr-settings-tier:last-child{border-bottom:0}.amr-settings-head{color:var(--dsw-alias-label-secondary)}.amr-settings-tier-name{display:flex;align-items:center;gap:4px;min-height:30px}.amr-settings-info,.amr-settings-reset{border:0;background:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;font:inherit;padding:2px}.amr-settings-info{display:inline-flex}.amr-settings-reset{font-size:12px;margin-top:4px}.amr-settings-reset:hover{color:var(--dsw-alias-label-primary)}.amr-settings-info:focus-visible,.amr-settings-reset:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary)}.amr-settings .amr-settings-error{color:var(--dsw-alias-state-error-primary)}.amr-settings-cell{min-width:0}.amr-settings-cell-label{display:none}.amr-settings .amr-settings-segments{display:inline-grid;flex:none;width:max-content;max-width:100%;height:28px;box-sizing:border-box;gap:2px;padding:2px;border-radius:var(--dsw-radius-md)}.amr-settings .amr-settings-segments>button{height:24px;min-width:44px;padding:0 8px;border-radius:var(--dsw-radius-sm);font-size:12px;line-height:20px}.amr-settings .amr-settings-segments>[aria-hidden=true]{top:2px;bottom:2px;left:2px;height:calc(100% - 4px);width:calc((100% - 4px - 2px * (var(--dsh-segment-count) - 1)) / var(--dsh-segment-count))}.amr-settings .amr-settings-tabs{--dsh-segment-count:2}.amr-settings .amr-settings-tabs>[aria-hidden=true]{width:calc((100% - 6px) / 2)!important;transform:translateX(calc(var(--amr-tab-index,0) * (100% + 2px)))!important}.amr-settings .amr-settings-tabs:has([aria-selected=true]:last-child){--amr-tab-index:1}
12
+ .amr-test-row{display:flex;align-items:center;flex-wrap:wrap;gap:10px;margin:12px 0 6px}.amr-test-row>span{font-size:12px;color:var(--dsw-alias-label-secondary)}.amr-test-button{height:28px;padding:0 10px;border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;cursor:pointer}.amr-test-button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.amr-test-button:disabled{opacity:.5;cursor:not-allowed}.amr-test-button:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
13
+ .amr-service-trigger,.amr-service-input{box-sizing:border-box;min-width:0;width:240px;max-width:100%;height:32px;padding:0 10px;border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit}.amr-service-trigger{display:flex;align-items:center;justify-content:space-between;gap:12px;cursor:pointer;text-align:left}.amr-service-trigger:hover{background:var(--dsw-alias-interactive-bg-hover)}.amr-service-trigger svg{color:var(--dsw-alias-label-tertiary);flex:none}.amr-service-trigger:focus-visible,.amr-service-input:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
9
14
  @container(max-width:480px){.amr-settings-head{display:none}.amr-settings-tier{grid-template-columns:1fr;gap:8px}.amr-settings-cell{display:grid;grid-template-columns:minmax(110px,.7fr) minmax(0,1fr);align-items:center;gap:8px}.amr-settings-cell-label{display:block;color:var(--dsw-alias-label-secondary)}.amr-settings-cell p,.amr-settings-reset{grid-column:2}.amr-settings-row{flex-wrap:wrap}}
10
15
  `;
11
16
  async function request(rpc, action, payload = {}) {
@@ -24,9 +29,13 @@ export function RoutingSettings({ rpc, ui }) {
24
29
  const [patch, setPatch] = useState({});
25
30
  const [childDrafts, setChildDrafts] = useState({});
26
31
  const [provider, setProvider] = useState('claude');
32
+ const [serviceMenu, setServiceMenu] = useState(false);
33
+ const [modelMenu, setModelMenu] = useState(false);
27
34
  const [key, setKey] = useState('');
28
35
  const [removeKey, setRemoveKey] = useState(false);
29
36
  const [saving, setSaving] = useState(false);
37
+ const [testing, setTesting] = useState(false);
38
+ const [testResult, setTestResult] = useState();
30
39
  const [message, setMessage] = useState('');
31
40
  const [failed, setFailed] = useState(false);
32
41
  const [saveIssues, setSaveIssues] = useState([]);
@@ -41,6 +50,19 @@ export function RoutingSettings({ rpc, ui }) {
41
50
  return h('div', { className: 'amr-settings', role: 'status' }, message || '正在读取设置…');
42
51
  const settings = { ...view.settings, ...patch };
43
52
  const cap = settings.effortCap;
53
+ const serviceConfig = settings.decisionService ?? { provider: 'openrouter' };
54
+ const serviceProvider = serviceConfig.provider;
55
+ const decisionModel = serviceConfig.model ?? DECISION_SERVICES[serviceProvider].model;
56
+ // Preset choices are documented snapshots, not a live availability claim.
57
+ const decisionModels = [...new Set([...(serviceProvider === 'openrouter' ? ['typesafe/jev-1.13'] : ['jev-1.13.0', 'jev-1.14.0']), decisionModel])];
58
+ const changingService = serviceProvider !== (view.settings.decisionService?.provider ?? 'openrouter');
59
+ let invalidService = false;
60
+ try {
61
+ resolveDecisionService(serviceConfig);
62
+ }
63
+ catch {
64
+ invalidService = true;
65
+ }
44
66
  const invalidFamily = (p) => {
45
67
  const family = view.settings.mainEfforts?.[p];
46
68
  return family !== undefined && (!family || typeof family !== 'object' || Array.isArray(family) || Object.keys(family).some(key => !tiers.includes(key)));
@@ -105,7 +127,7 @@ export function RoutingSettings({ rpc, ui }) {
105
127
  let nextView = view;
106
128
  if (key || removeKey) {
107
129
  try {
108
- const response = await request(rpc, 'setKey', { value: removeKey ? null : key });
130
+ const response = await request(rpc, 'setKey', { value: removeKey ? null : key, ...(serviceProvider !== 'openrouter' ? { provider: serviceProvider } : {}) });
109
131
  if (response?.key)
110
132
  nextView = { ...nextView, key: response.key };
111
133
  setKey('');
@@ -140,8 +162,36 @@ export function RoutingSettings({ rpc, ui }) {
140
162
  const keyHint = !view.key.available ? 'DSH 凭据服务不可用。' : !view.key.writable ? '先自行保留现有 key,从启动环境(如 .env.local)删除 OPENROUTER_API_KEY 并重启,再在这里保存。' : view.key.configured ? '保存在 DSH 凭据存储,不写入设置文件;留空表示保持当前 key。' : '未配置时 AUTO 判档会停用;保存 key 后不会自动重启已停用的会话。';
141
163
  return h('div', { className: 'amr-settings' }, h('style', null, CSS), h(ui.SettingsForm, {
142
164
  labels: { unavailable: '无法读取设置', readOnly: '设置只读', saveFailed: '部分更改未保存,请检查提示', save: '保存', saving: '保存中…' },
143
- state: { available: true, writable: true, dirty, invalid: conflicts.length > 0 || invalidTimeout || invalidKey, saving, failed }, onSave: () => { void save(); }, onDiscard: () => { },
144
- children: h('fieldset', { disabled: saving, style: { border: 0, padding: 0, margin: 0, minWidth: 0 } }, view.configStatus === 'invalid' && h('p', { role: 'status' }, '设置文件无法解析或含无效字段,已按可解析值与默认值显示;保存会覆盖损坏部分'), message && h('p', { role: 'status', className: failed ? 'amr-settings-error' : undefined }, message), h('section', { className: 'amr-settings-section' }, h('h3', null, '判档服务'), h('p', null, 'AUTO 时每个用户回合请 Jev(经 OpenRouter)判断任务难度。'), h(ui.SettingsSecretField, { id: 'amr-key', label: 'OpenRouter API Key', hint: keyHint, text: key, disabled: saving || !view.key.available || !view.key.writable, onEdit: text => { setKey(text); setRemoveKey(false); }, configured: view.key.configured, stateLabel: keyState }), view.key.available && view.key.writable && view.key.configured && h('button', { type: 'button', className: 'amr-settings-reset', onClick: () => { setRemoveKey(!removeKey); setKey(''); } }, removeKey ? '取消删除 key' : '删除 key'), invalidKey && h('p', { className: 'amr-settings-error' }, 'API key 不能含空白,且不能超过 512 字符'), h('div', { className: 'amr-settings-row' }, h('div', null, h('label', { htmlFor: 'amr-timeout' }, '判档等待上限'), h('p', null, '1000–4500 毫秒;超时后该会话停用 AUTO。')), h('span', null, h('input', { id: 'amr-timeout', type: 'number', className: 'amr-settings-number', min: 1000, max: 4500, step: 1, value: Number.isNaN(settings.jevTimeoutMs) ? '' : settings.jevTimeoutMs, 'aria-invalid': invalidTimeout, onChange: e => setPatch(old => ({ ...old, jevTimeoutMs: e.currentTarget.value === '' ? NaN : Number(e.currentTarget.value) })) }), ' 毫秒'))), h('section', { className: 'amr-settings-section' }, h('h3', null, '会话'), h('div', { className: 'amr-settings-row' }, h('div', null, '新会话默认模式', h('p', null, settings.defaultMode === 'AUTO' ? '仅影响新会话;每个用户回合都会把消息发送给 OpenRouter/TypeSafe 请求 Jev。' : '仅影响新会话,不改变已有会话。')), h(ui.SegmentedControl, { id: 'amr-default-mode', className: 'amr-settings-segments', label: '新会话默认模式', value: settings.defaultMode, options: [{ value: 'MANUAL', label: 'MANUAL' }, { value: 'AUTO', label: 'AUTO' }], onChange: value => setPatch(old => ({ ...old, defaultMode: value })) })), h('div', { className: 'amr-settings-row' }, h('span', null, '判档提示'), h(ui.Switch, { checked: settings.decisionToast, label: '判档提示', onChange: value => setPatch(old => ({ ...old, decisionToast: value })) })), h('div', { className: 'amr-settings-row' }, h('span', null, '推理强度上限'), h(ui.SegmentedControl, { id: 'amr-cap', className: 'amr-settings-segments', label: '推理强度上限', value: cap, options: AUTO_EFFORTS.map(value => ({ value, label: value })), onChange: value => setPatch(old => ({ ...old, effortCap: value })) }))), h('section', { className: 'amr-settings-section' }, h('h3', null, '路由映射'), h(ui.SegmentedTabs, { className: 'amr-settings-segments amr-settings-tabs', label: '模型体系', value: provider, items: [{ value: 'claude', label: 'Claude', id: 'amr-tab-claude', panelId: 'amr-panel-claude' }, { value: 'codex', label: 'GPT', id: 'amr-tab-codex', panelId: 'amr-panel-codex' }], onChange: value => setProvider(value) }), h('div', { role: 'tabpanel', id: `amr-panel-${provider}`, 'aria-labelledby': `amr-tab-${provider}` }, h('p', null, '主会话推理等级适用于本体系所有模型;默认随实际模型计算。可选覆盖值为当前体系模型共同支持且不超过上限的等级。'), !supportedFamilyEfforts(view, provider, cap).length && h('p', { role: 'status' }, '当前体系没有共同支持且不超过上限的推理等级;主会话覆盖暂不可选,可恢复默认。'), familyUnresolved(provider) && h('p', { className: 'amr-settings-error', role: 'status' }, '主会话映射格式无效(含旧版按模型配置),不会按默认值执行。请重新配置或', h('button', { type: 'button', className: 'amr-settings-reset', onClick: () => setPatch(old => ({ ...old, mainEfforts: { ...old.mainEfforts, [provider]: null } })) }, '恢复本体系默认值')), h('p', null, '主会话保留当前模型,只调 effort;子会话在 route_subagent 判档后返回推荐 model + effort,不改既有子会话、不决定是否派发。'), view.childAllowlist.status !== 'enabled' || !selectableModels(view, provider, 'child').length ? h('p', { role: 'status' }, view.childAllowlist.status === 'unavailable' ? '无法读取子智能体允许列表;请检查官方子智能体设置。' : '请在官方子智能体设置启用并配置允许列表。') : null, h('div', { className: 'amr-settings-table' }, h('div', { className: 'amr-settings-head', 'aria-hidden': true }, h('span', null, '档位'), h('span', null, '主会话推理等级'), h('span', null, '子会话推荐')), ...tiers.map(tier => {
165
+ state: { available: true, writable: true, dirty, invalid: conflicts.length > 0 || invalidTimeout || invalidKey || invalidService, saving, failed }, onSave: () => { void save(); }, onDiscard: () => { },
166
+ children: h('fieldset', { disabled: saving || testing, style: { border: 0, padding: 0, margin: 0, minWidth: 0 } }, view.configStatus === 'invalid' && h('p', { role: 'status' }, '设置文件无法解析或含无效字段,已按可解析值与默认值显示;保存会覆盖损坏部分'), message && h('p', { role: 'status', className: failed ? 'amr-settings-error' : undefined }, message), h('section', { className: 'amr-settings-section' }, h('h3', null, '判档服务'), h('p', null, 'AUTO 把脱敏任务发送给所选决策服务;不跨服务自动回退。'), h('div', { className: 'amr-settings-row' }, h('span', null, '决策服务'), h(ui.Menu, { open: serviceMenu, portal: true, side: 'bottom', selectedId: serviceProvider, onClose: () => setServiceMenu(false),
167
+ items: Object.entries(DECISION_SERVICES).map(([id, service]) => ({ id, label: service.label })),
168
+ onSelect: id => { if (!Object.hasOwn(DECISION_SERVICES, id))
169
+ return; setKey(''); setRemoveKey(false); setPatch(old => ({ ...old, decisionService: { provider: id } })); setServiceMenu(false); },
170
+ anchor: h('button', { type: 'button', className: 'amr-service-trigger', 'aria-label': '决策服务', 'aria-haspopup': 'menu', 'aria-expanded': serviceMenu, onClick: () => setServiceMenu(!serviceMenu) }, h('span', null, DECISION_SERVICES[serviceProvider].label), h(ui.IconChevronDownOutlineRegular, { size: 14 })), })), h('p', null, changingService ? '切换服务后先保存,再管理该服务的独立 API key;不会沿用原服务的 key。' : `凭据引用:${DECISION_SERVICES[serviceProvider].credential}`), h('div', { className: 'amr-settings-row' }, h('span', null, '决策模型'), serviceProvider === 'custom'
171
+ ? h('input', { className: 'amr-service-input', 'aria-label': '决策模型', value: decisionModel, onChange: e => setPatch(old => ({ ...old, decisionService: { ...serviceConfig, model: e.currentTarget.value } })) })
172
+ : h(ui.Menu, { open: modelMenu, portal: true, side: 'bottom', selectedId: decisionModel, onClose: () => setModelMenu(false),
173
+ items: decisionModels.map(id => ({ id, label: id })),
174
+ onSelect: model => { if (!decisionModels.includes(model))
175
+ return; setPatch(old => ({ ...old, decisionService: { ...serviceConfig, model } })); setModelMenu(false); },
176
+ anchor: h('button', { type: 'button', className: 'amr-service-trigger', 'aria-label': '决策模型', 'aria-haspopup': 'menu', 'aria-expanded': modelMenu, onClick: () => setModelMenu(!modelMenu) }, h('span', null, decisionModel), h(ui.IconChevronDownOutlineRegular, { size: 14 })),
177
+ })), serviceProvider === 'custom' && h('label', { className: 'amr-settings-row' }, '兼容端点 ', h('input', { className: 'amr-service-input', 'aria-label': '兼容端点', value: serviceConfig.endpoint ?? '', placeholder: 'https://example.com/v1/systemone', onChange: e => setPatch(old => ({ ...old, decisionService: { ...serviceConfig, endpoint: e.currentTarget.value } })) })), h('p', null, serviceProvider === 'custom' ? '仅兼容 System One state/questions → answers;HTTPS 或回环 HTTP;禁止 URL 凭据、查询参数与重定向。' : `接口:${DECISION_SERVICES[serviceProvider].endpoint}`), h('div', { className: 'amr-test-row' }, h('button', { type: 'button', className: 'amr-test-button', disabled: dirty || saving || testing, 'aria-busy': testing, onClick: async () => {
178
+ if (testing)
179
+ return;
180
+ setTesting(true);
181
+ setTestResult(undefined);
182
+ try {
183
+ const r = await rpc.call('/api', 'auto-model-routing.testDecision', {});
184
+ const v = r.value;
185
+ const ok = r.ok && v?.kind === 'ok';
186
+ setTestResult({ failed: !ok, text: ok ? `连接成功 · ${v.ms} ms` : `连接失败:${v?.category ? CATEGORY_HINTS[v.category] ?? '服务不可用' : '无法连接决策服务,请重试'}` });
187
+ }
188
+ catch {
189
+ setTestResult({ failed: true, text: '连接测试失败,请检查网络后重试' });
190
+ }
191
+ finally {
192
+ setTesting(false);
193
+ }
194
+ } }, testing ? '测试中…' : '测试连接'), h('span', { role: 'status', 'aria-live': 'polite', className: testResult?.failed ? 'amr-settings-error' : undefined }, testing ? '正在请求所选决策服务…' : testResult?.text ?? (dirty ? '请先保存设置,再测试连接' : ''))), h('p', null, '测试会产生一次决策请求,不包含会话内容;请先保存设置。'), invalidService && h('p', { role: 'alert', className: 'amr-settings-error' }, '请填写有效模型和兼容端点'), h(ui.SettingsSecretField, { id: 'amr-key', label: `${DECISION_SERVICES[serviceProvider].label} API Key`, hint: changingService ? '请先保存服务选择' : keyHint, text: key, disabled: changingService || saving || !view.key.available || !view.key.writable, onEdit: text => { setKey(text); setRemoveKey(false); }, configured: !changingService && view.key.configured, stateLabel: changingService ? '保存后读取' : keyState }), !changingService && view.key.available && view.key.writable && view.key.configured && h('button', { type: 'button', className: 'amr-settings-reset', onClick: () => { setRemoveKey(!removeKey); setKey(''); } }, removeKey ? '取消删除 key' : '删除 key'), invalidKey && h('p', { className: 'amr-settings-error' }, 'API key 不能含空白,且不能超过 512 字符'), h('div', { className: 'amr-settings-row' }, h('div', null, h('label', { htmlFor: 'amr-timeout' }, '判档等待上限'), h('p', null, '1000–4500 毫秒;超时后该会话停用 AUTO。')), h('span', null, h('input', { id: 'amr-timeout', type: 'number', className: 'amr-settings-number', min: 1000, max: 4500, step: 1, value: Number.isNaN(settings.jevTimeoutMs) ? '' : settings.jevTimeoutMs, 'aria-invalid': invalidTimeout, onChange: e => setPatch(old => ({ ...old, jevTimeoutMs: e.currentTarget.value === '' ? NaN : Number(e.currentTarget.value) })) }), ' 毫秒'))), h('section', { className: 'amr-settings-section' }, h('h3', null, '会话'), h('p', { role: 'status' }, settings.policy === 'legacy' ? '当前使用旧策略回滚配置。' : 'AUTO 已使用新策略,无需额外启用。主模型不变;子任务独立选择模型能力与推理投入,并在创建时应用。MANUAL 不进行自动判档。'), h('div', { className: 'amr-settings-row' }, h('div', null, '新会话默认模式', h('p', null, settings.defaultMode === 'AUTO' ? '仅影响新会话;每个用户回合都会把脱敏任务发送给所选决策服务请求 Jev。' : '仅影响新会话,不改变已有会话。')), h(ui.SegmentedControl, { id: 'amr-default-mode', className: 'amr-settings-segments', label: '新会话默认模式', value: settings.defaultMode, options: [{ value: 'MANUAL', label: 'MANUAL' }, { value: 'AUTO', label: 'AUTO' }], onChange: value => setPatch(old => ({ ...old, defaultMode: value })) })), h('div', { className: 'amr-settings-row' }, h('span', null, '判档提示'), h(ui.Switch, { checked: settings.decisionToast, label: '判档提示', onChange: value => setPatch(old => ({ ...old, decisionToast: value })) })), h('div', { className: 'amr-settings-row' }, h('span', null, '推理强度上限'), h(ui.SegmentedControl, { id: 'amr-cap', className: 'amr-settings-segments', label: '推理强度上限', value: cap, options: AUTO_EFFORTS.map(value => ({ value, label: value })), onChange: value => setPatch(old => ({ ...old, effortCap: value })) }))), settings.policy !== 'legacy' && h('section', { className: 'amr-settings-section' }, h('h3', null, '自动选配'), h('p', null, '主会话仅调整推理投入;子任务按目标与范围选择合适的能力档,再确定投入。保留显式指定的模型和推理等级,不自动重试或升级。'), h('p', null, 'Magpie 使用完整模型 ID 与官方允许列表;目录不可用或目标不清楚时停止并提示。fresh 子任务受支持,fork 暂不支持。旧四档配置保留供回滚,不参与当前策略。'), h(StrategyDetails)), settings.policy === 'legacy' && h('section', { className: 'amr-settings-section' }, h('h3', null, '路由映射'), h(ui.SegmentedTabs, { className: 'amr-settings-segments amr-settings-tabs', label: '模型体系', value: provider, items: [{ value: 'claude', label: 'Claude', id: 'amr-tab-claude', panelId: 'amr-panel-claude' }, { value: 'codex', label: 'GPT', id: 'amr-tab-codex', panelId: 'amr-panel-codex' }], onChange: value => setProvider(value) }), h('div', { role: 'tabpanel', id: `amr-panel-${provider}`, 'aria-labelledby': `amr-tab-${provider}` }, h('p', null, '主会话推理等级适用于本体系所有模型;默认随实际模型计算。可选覆盖值为当前体系模型共同支持且不超过上限的等级。'), !supportedFamilyEfforts(view, provider, cap).length && h('p', { role: 'status' }, '当前体系没有共同支持且不超过上限的推理等级;主会话覆盖暂不可选,可恢复默认。'), familyUnresolved(provider) && h('p', { className: 'amr-settings-error', role: 'status' }, '主会话映射格式无效(含旧版按模型配置),不会按默认值执行。请重新配置或', h('button', { type: 'button', className: 'amr-settings-reset', onClick: () => setPatch(old => ({ ...old, mainEfforts: { ...old.mainEfforts, [provider]: null } })) }, '恢复本体系默认值')), h('p', null, '主会话保留当前模型,只调 effort;子会话在 route_subagent 判档后返回推荐 model + effort,不改既有子会话、不决定是否派发。'), view.childAllowlist.status !== 'enabled' || !selectableModels(view, provider, 'child').length ? h('p', { role: 'status' }, view.childAllowlist.status === 'unavailable' ? '无法读取子智能体允许列表;请检查官方子智能体设置。' : '请在官方子智能体设置启用并配置允许列表。') : null, h('div', { className: 'amr-settings-table' }, h('div', { className: 'amr-settings-head', 'aria-hidden': true }, h('span', null, '档位'), h('span', null, '主会话推理等级'), h('span', null, '子会话推荐')), ...tiers.map(tier => {
145
195
  const effort = mainValue(provider, tier);
146
196
  return h('div', { key: tier, className: 'amr-settings-tier', 'data-tier': tier }, h('div', { className: 'amr-settings-tier-name' }, tier, h(ui.Tooltip, { label: view.criteria[tier], side: 'top', portal: true, children: h('button', { type: 'button', className: 'amr-settings-info', 'aria-label': `${tier} 判档标准`, onFocus: () => { } }, h(ui.IconInfoOutlineRegular, { size: 14 })) })), cell('主会话推理等级', h(RoutePicker, { ui, catalog: view, provider, cap, mode: 'effort', label: `${tier} 主会话推理等级`, value: { model: '', reasoningEffort: effort === undefined ? undefined : typeof effort === 'string' ? effort : '无效配置' }, invalid: familyUnresolved(provider), disabled: saving, onChange: value => setMain(tier, value.reasoningEffort) }), mainExplicit(provider, tier), () => setMain(tier, null), issueText(`mainEfforts.${provider}.${tier}`)), cell('子会话推荐', h(RoutePicker, { ui, catalog: view, provider, cap, label: `${tier} 子会话推荐`, value: childValue(provider, tier), disabled: saving || !selectableModels(view, provider, 'child').length, onChange: value => setChild(tier, value) }), childExplicit(provider, tier), () => setChild(tier, null), issueText(`childRoutes.${provider}.${tier}`)));
147
197
  })), h('p', null, '官方允许列表是会话创建时的快照;已有会话仍可能拒绝新配置的推荐。'))), ...conflicts.filter(issue => !issue.path.startsWith(`childRoutes.${provider}.`) && !issue.path.startsWith(`mainEfforts.${provider}.`)).map(issue => h('p', { key: issue.path, role: 'status', className: 'amr-settings-error' }, `${issue.path}:${issue.message}`))),
@@ -1,3 +1,3 @@
1
1
  import type * as Primitives from '@deepseek-ai/dsh-client-ui-primitives';
2
2
  /** Shell-owned primitives are injected so DOM tests exercise draft behavior independently. */
3
- export type SettingsUi = Pick<typeof Primitives, 'SettingsForm' | 'SettingsSecretField' | 'SegmentedTabs' | 'SegmentedControl' | 'Switch' | 'Tag' | 'Tooltip' | 'IconInfoOutlineRegular' | 'IconChevronDownOutlineRegular' | 'IconChevronLeftOutlineRegular' | 'IconChevronRightOutlineRegular' | 'IconCheckOutlineRegular' | 'MenuSurface' | 'MenuGroup' | 'useAnchoredPosition' | 'focusWithoutRing'>;
3
+ export type SettingsUi = Pick<typeof Primitives, 'Menu' | 'SettingsForm' | 'SettingsSecretField' | 'SegmentedTabs' | 'SegmentedControl' | 'Switch' | 'Tag' | 'Tooltip' | 'IconInfoOutlineRegular' | 'IconChevronDownOutlineRegular' | 'IconChevronLeftOutlineRegular' | 'IconChevronRightOutlineRegular' | 'IconCheckOutlineRegular' | 'MenuSurface' | 'MenuGroup' | 'useAnchoredPosition' | 'focusWithoutRing'>;
@@ -0,0 +1,4 @@
1
+ /** Read-only: criteria and slots are imported from the executable policy, never copied. */
2
+ export declare function StrategyDetails(): import("react").DetailedReactHTMLElement<{
3
+ className: string;
4
+ }, HTMLElement>;