# dsh-quota-router:需求与技术设计(v0.1) ## 1. 问题与目标 DSH 用户可能同时拥有免费、订阅、按额度、不限量、低价按量与手动付费灾备等多种模型来源。现有关键词 Router 通常只能将一条规则映射到单一模型,或只有一跳 fallback;当多个任务共享同一主模型却需要不同备用模型时,无法可靠追溯原始规则。 `dsh-quota-router` 是一个 **只负责策略层** 的 DSH 插件:它复用 DSH 已注册的 provider、凭据、模型目录和重试机制,在任务 profile 中选择有序候选源,优先消耗用户声明的既有资源,并在可解释的边界内切换候选。 ### v0.1 目标 1. 按用户消息关键词确定性地选择 task profile(规则按顺序,首次命中优先)。 2. 每个 profile 通过 `modelBySource` 声明候选链,解析后自然表达免费→订阅→不限量等候选链。 3. 用每 Agent/turn 保存的 **决策身份**(profile id、候选下标、route)驱动 fallback,避免「同一个主模型对应不同 fallback」的碰撞。 4. 稳定错误(配额、余额、鉴权)立即推进到下一候选,并向 DSH 请求同一轮 retry。 5. 瞬态错误(限流、超时、服务端、传输)在达到阈值后为 route 建立 cooldown,再推进到下一健康候选。 6. 子代理的命中路由应在第一个请求生效。 7. 在内存 ledger 记录每次选择、fallback、cooldown、usage;输出结构化日志,后续可接 UI。 8. 所有 route 都必须先在 DSH 原生模型目录中精确校验;校验失败不得写 session header。 ### 非目标 - 不注册或复制任何 provider/adapter,不保存 API key。 - 不调用 LLM Judge,不根据模型名称猜测能力或价格。 - 不主动抓取第三方余额;订阅/不限量/免费等是用户声明的 source tier。未来再做可选 usage adapter。 - 不在 v0.1 自动使用 `manual` 或 `emergency` 候选。 - 不伪造 assistant 消息或步骤事件。 - 不触发或改写 DSH compaction;上下文压缩由对应 DSH 插件独立配置和执行。 - 不做媒体、图像、视频、音频路由。 ## 2. 用户的首版资源策略 | Profile | 优先候选 | 自动 fallback | 目的 | |---|---|---|---| | `simple` | `opencode/mimo-v2.5-free` | `token-share/gpt-5.4-mini` | 免费小任务 | | `coding` | `opencode-go/mimo-v2.5` | `token-share/gpt-5.6-luna` | 常规开发 | | `planning` | `opencode-go/deepseek-v4-flash` (high) | `token-share/gpt-5.6-luna` | 常规规划分析 | | `writing` | `opencode-go/deepseek-v4-flash` | `token-share/gpt-5.6-luna` | 技术文档与普通写作 | | `deep-thinking` | `token-share/gpt-5.6-terra` | 无 | 研究、架构、关键审查 | | `hard-coding` | `opencode-go/mimo-v2.5` | `token-share/gpt-5.6-terra` | 高难编码 | `starchasing/*` 与 `openai-codex/*` 在 v0.1 可以注册为 `manual` / `emergency` 来源供观测与手动路由,但不进入自动链路。 ## 3. 配置模型(v0.2:来源中心) 采用**两个正交维度**(贴合「属源拼多多」心智): - **维度一 `sources`:全局来源优先级链**(省钱主轴,一次配置)。每个 source 声明 DSH provider、cost tier、priority。 - **维度二 `profiles`:任务 → 各源上的模型**(质量主轴)。每个任务只需说「到这个源时用哪个模型」。 ```yaml quota-router: enabled: true matchCase: false transientFailureThreshold: 2 cooldownMs: 60000 sources: - id: opencode provider: opencode tier: free priority: 1 - id: opencode-go provider: opencode-go tier: subscription priority: 2 - id: token-share provider: token-share tier: unlimited priority: 3 - id: starchasing provider: starchasing tier: paid priority: 4 autoEligible: false # 付费来源默认不自动烧钱;需要全局 allowPaidFallback: true 才会自动进入 fallback profiles: - id: coding enabled: true keywords: [写代码, 实现, 修复, 测试, bug, code, implement, fix, test] modelBySource: opencode-go: { model: mimo-v2.5 } token-share: { model: gpt-5.6-luna } starchasing: { model: gpt-5.6-terra } - id: hard-coding enabled: true keywords: [性能优化, 并发, 死锁, 编译器, 跨模块重构, performance, concurrency, deadlock] modelBySource: opencode-go: { model: mimo-v2.5 } token-share: { model: gpt-5.6-terra } starchasing: { model: gpt-5.6-terra } ``` 每个 profile 的 `modelBySource` 在解析时按 `sources.priority` 展开成有序候选列表: `source(provider) + model` 一起组成候选。路由时按该列表从最低 priority 起尝试,失败则推进到下一源。 ### Source tiers `free | subscription | unlimited | quota | payg | paid | manual | emergency` tier 只用于日志/面板/策略说明;实际优先级由 `sources.priority` 决定,插件不擅自按 tier 重排。`manual`/`emergency` 候选永远自动跳过;`paid` 候选还需要 `allowPaidFallback: true` 的显式总开关才会自动选中。 ## 4. 路由与状态机 ### 4.1 初始选择 1. 收到用户文本消息; 2. 依 profile 顺序 substring 匹配,第一条命中者胜出; 3. 选择该 profile 中第一个 `autoEligible !== false`、已校验、未 cooldown 的候选; 4. 保存 Agent 决策 `{ profileId, candidateIndex, selection, turn? }`; 5. 写 DSH `request/header`,并为子代理安装官方 `installModelSelection`。 未命中不修改模型,保留 DSH 默认/人工选择。 ### 4.2 稳定失败 这些错误立即跳过当前候选: `QUOTA`、`INSUFFICIENT_BALANCE`、`AUTH`、`UNAUTHORIZED`、`FORBIDDEN`、`INVALID_CREDENTIAL`、`MISSING_CREDENTIAL`、HTTP 401/403。 行为:当前 profile 的 candidate index +1,选择下一个健康且可自动使用的候选;写 header;记录 ledger;返回 DSH retry 指令。若耗尽候选,保持 DSH 原错误路径。 ### 4.3 瞬态失败 `RATE_LIMIT`、`TIMEOUT`、`SERVER`、`TRANSPORT`、`EMPTY_RESPONSE`、HTTP 429、5xx 属于瞬态失败。 在同一路由累计达到 `transientFailureThreshold`(默认 2)后,route 进入 `cooldownMs`(默认 60 秒)冷却;再切下一个候选并请求 retry。阈值前交给 DSH 原有 retry,不主动切换。 ### 4.4 防循环 - 一个 turn 内不能对同一 candidate 反复 retry。 - 候选只向前推进,不回跳。 - `manual` / `emergency` / `autoEligible:false` 候选自动跳过。 - 当没有健康候选时不吞掉原始错误。 ## 5. 可观测性 ### 5.1 每个路由决策 记录:时间、agent、profile、候选序号、provider/model、sourceTier、触发原因(initial/fallback/cooldown/manual)、失败代码。 日志格式必须能直接排查,例如: ```text [dsh-quota-router] profile=hard-coding candidate=2/2 route=token-share/gpt-5.6-terra tier=unlimited reason=fallback failure=QUOTA ``` ### 5.2 Ledger 内存 ledger 保存有限条目(默认 200): - route attempts; - fallback 次数; - cooldown 打开/结束; - assistant usage(input/output/cache/reasoning tokens,如果事件提供); - source tier 聚合。 v0.1 先注册只读 `quota_router_status` 工具;Web UI 和可重放 projection 是后续版本。 ## 6. DSH 集成边界 已对照 DSH rc.8 源码(`dsh-agent-loop`、`dsh-agent`、`dsh-llm`、`dsh-session` 公共运行时类型)核实: - `agent/pre-step`:waterfall,payload 带有本轮精确的 `{ turn, messages, agent }`;插件在此匹配 user message、等待原生目录校验并绑定 turn,避免队列/丢弃消息造成 route 与 turn 错配。 - `agent/request-error`:waterfall,payload `{ turn, step, provider, failure, retryPolicy, signal, agent }`;`failure` 是 `LlmError.failure = { message, code, status? }`。返回 `{ kind: 'retry' }` 会让 driver `continue`,走 `agent/request` 重新构建请求——同一轮换路由此生效。 - `agent/request`:waterfall,payload `{ turn, step, signal, agent }`;`next()` 返回 `seedConfig`(`{ provider, model, reasoningEffort?, maxTokens? }`),插件覆盖 provider/model/reasoningEffort 即为最终路由。 - `assistant/message` 是 **session 事件流**:agent-loop 通过 `session.append("assistant/message", { turn, step, message, usage })` 写入,经 `session/event` 广播(监听签名为 `(session, event)`,见 `dsh-session-projection/lib/index.js` 的 `ctx.on("session/event", …)`)。**不是** `ctx.on('assistant/message')`、也不是 `agent/...` app 事件——usage 记账必须订阅 `session/event`,并按 `session.id` 关联到当前 source tier。 - `installModelSelection(agentCtx, selection)`:官方子代理模型选择装置,耦合 `system-prompt/assemble` + `agent/request`;子代理 session(`origin === 'subagent'`)需要它才能在第一个请求应用路由。 - `request/header`:持久写入通道,`reason: 'change'`。 ## 7. 验收标准 1. 单元测试证明 first-match 规则顺序、候选自动跳过和 profile 选择正确。 2. 同一 primary(Mimo)在 `coding` 与 `hard-coding` profile 中失败后分别前进到 Luna 与 Terra。 3. 稳定失败立即产生 retry 选择;瞬态失败达到阈值前不选择 fallback,达到后 cooldown 并选择 fallback。 4. `manual` / `emergency` 候选不能被自动选择。 5. 失去全部候选时不请求 retry、不覆盖原始错误。 6. `npm test`、`npm run check`、`npm run build` 通过。 7. 通过 mock DSH Context 的集成测试验证 header 写入、request-error retry 和子代理 selection 安装的接线。 ## 8. 自审清单 - [x] 明确候选顺序是唯一实际优先级,tier 不暗中排序。 - [x] 明确 v0.1 不假装知道第三方余额。 - [x] 明确 paid/manual/emergency 默认不能自动烧钱。 - [x] 明确 profile identity 解决同 primary、多 fallback 碰撞。 - [x] 明确瞬态错误先尊重 DSH retry。 - [x] 明确无候选时保留原始失败。 - [x] 明确不与 provider、凭据和适配器职责重叠。 ## 9. 后续技术方案 已拆分 subtask 的模型能力路由、`taskClass/complexity/precision`、`allowedModels`、subtask 内模型锁定、有限 retry/fallback 和 route telemetry 的演进方案见 [`docs/task-aware-routing-plan.md`](./docs/task-aware-routing-plan.md)。 该方案明确收缩 quota-router 的职责:Planner 负责任务规划与拆解,上层负责上下文回归、语义验收和 replan;quota-router 只负责模型策略选择和基础设施失败恢复。现有 source/provider 候选链继续按 v0.1 规则工作,本方案不重新设计多源选择,也不把 context compaction、跨 Harness 迁移或 dsh-agent-suite Receipt/Ledger 纳入本插件。 ## 10. 自审后调整 初稿曾把“header 改写”描述成能保证 in-flight retry 换路;这不可靠,因为 DSH retry 的具体 waterfall 顺序与模型快照时机必须由集成测试确认。因此 v0.1 的 `agent/request-error` 实现将显式返回 retry,并在 `agent/request` 阶段按本 turn 的内存决策覆盖为下一候选;header 同时写入用于会话可见性与后续 turn。这样既支持同轮切换,也不依赖 header 对 in-flight retry 的时序假设。