模型路由
一句话版:模型层是能力 seam:
ctx.llm定义 provider 无关的抽象与流式调用 API,llm-deepseek/llm-pi-ai等 adapter 提供实现;路由决定哪个 provider/model 服务哪个 agent,错误有理可循、可配重试。
这是"模型是怎么被调用的、配多个模型时怎么选"的一篇。读完你能配明白 provider、看懂一次请求的路由和失败分类。
一、LLM 层结构
| 包 | 角色 |
|---|---|
llm/llm | 抽象服务 ctx.llm:adapter 注册、流式调用、模型解析 |
llm/llm-deepseek | chat-completions 适配器(direct fetch + SSE) |
llm/llm-pi-ai | 多 provider 网关(OpenAI 兼容) |
llm/llm-retry | exact-provider 重试策略 |
web/web-search-deepseek | Web 搜索,走独立 Anthropic 端点 |
LlmRuntime = 一个 adapter 注册表 + 单一流式调用 API,可用 llm/stream waterfall 拦截。
二、抽象服务 ctx.llm
核心 API(源码 llm/README):
| API | 作用 |
|---|---|
registerAdapter(providers, adapter) | 为 provider 路由注册一个 adapter 实例(全或无) |
listProviders() | 列出已注册 provider 路由 |
stream(options) | 流式调用一次模型,返回原始 chunk(block-start/text-delta/tool-call-delta/…/finish) |
resolveModelInfo(provider, model, signal?) | 解析确切的模型身份 + 能力元数据(context/output-default/reasoning) |
resolveCallConfig(config, signal?) | 校验、物化 adapter 配置的调用默认值 |
prepareCall(config, signal?) | 一次精确模型查找,返回可取消的一次性调用,带 adapter 注册与不可变重试策略 |
失败归一:最终 adapter 选择 / 同步 dispatch / 迭代 / 构造失败,统一收敛成流协议的单一终态 finish { kind:'error'|'aborted', failure }。llm/stream middleware、嵌套调用、adapter 清理、下游消费者的错误则是抛错(插件/消费者失败,不是模型请求结果)。
消息与内容块
Message是共享不可变值:必带MessageId、角色、内容、类型化 source- 内容块类型:
text/reasoning/tool-call/tool-result;经ContentBlockMap声明合并可加新块类型 - 核心块集只含每个 shipping 路径都 honored 的类型:多模态(图/音)没有核心块类型,要用就自己加块 + 配套 adapter/UI/压缩支持
- 流是原始 chunk 协议(
block-start,text-delta,block-end,usage,finish);BlockAssembler是唯一共享实现,把 chunk 组装成块/消息
三、Provider 配置:两种形状
llm-deepseek 是扁平顶层字段,不嵌套 providers:
llm-deepseek:
apiKeyEnv: DEEPSEEK_API_KEY
# baseURL 可选:省略时先回退 $DEEPSEEK_BASE_URL,再内置默认 api.deepseek.com
thinking: enabled
reasoningEffort: high # off | high | max
llm-pi-ai 用 providers 字典承载多个 OpenAI 兼容端点:
llm-pi-ai:
providers:
<provider-id>:
apiKeyEnv: <env>
api: openai-completions
baseURL: <端点>
models:
- id: <模型名>
要点:
- key 经
ctx.credentials逐请求解析,回退到环境 llm-deepseek只是 chat-completions 适配器;Anthropic 端点属于web-search-deepseek,不含在llm-deepseek里- 每个 provider 路由可带自己的
retryPolicy(见下)
四、多 provider 路由(不是"自动切模型")
真实的"多模型"来自多 provider + 路由配置:
| 配置 | 作用 |
|---|---|
agent-default-model | 部署默认 provider/model(web / headless / API 入口共用) |
| Models 设置页 | 按需选模型 |
纠个误区:
@deepseek-ai/dsh-plan-mode(/plan命令 +exit_plan_mode)只维护规划协作状态 + 政策提示段,不切换模型、不做 plan/execute 双路由。早期文档里"plan→规划模型 / 批准后→执行模型"是误解。
路由解析的可证部分:压缩摘要类辅助请求复用会话最后的路由请求头(对齐 prefix-cache);能力读取按需 fallback 到 agent 选项。agent-default-model 具体怎么配见 配置。
五、一次请求:从 agent/request 到 stream
一次正常模型调用的链路(和 上下文、Agent 主循环 呼应):
prepareCall() 的关键:它保留精确的 adapter 注册,跨异步解析 / header 记录 / 终端 dispatch 都绑定同一个注册:HMR 不会把一个 adapter 的能力结果混到另一个请求(同 agent-loop 的 adapter-default 标记)。
LlmCallConfig 是 per-conversation 状态(provider/model/reasoningEffort/temperature/maxTokens/stop),记录在 request/header,不是静默可调的单次旋钮。
六、错误分类(provider-neutral codes)
LlmError 带稳定的 code 字符串,message 与之分离:
| code | 含义 | 与重试的关系 |
|---|---|---|
NO_ADAPTER / DUPLICATE_ADAPTER | 无 adapter / 重复 | : |
AUTH / RATE_LIMIT | 认证 / 限流 | 限流可重试 |
CONTEXT_WINDOW_EXCEEDED_CODE | 超出模型上下文窗口 | : |
QUOTA | 配额/余额/预算耗尽(非瞬时) | 不重试 |
EMPTY_RESPONSE_CODE | 终态 stop 但没有任何内容块 | 默认重试(安全) |
INVALID_CREDENTIAL_CODE | 凭据给了但非法(修值) | 排除出默认可重试集 |
MISSING_CREDENTIAL | 凭据缺(去提供) | : |
errorChain(value) 渲染完整 cause 链(TypeError: fetch failed → 底层的 ECONNREFUSED/DNS/TLS),用于诊断但路由请按 code,不要解析文本。
七、重试策略:llm-retry
dsh-llm-retry 不包 ctx.llm.stream():每次 adapter 调用仍是单次 provider 尝试;每次重试打开一个全新的编号轮次。它在 agent-loop 的 agent/request-error waterfall 上工作。
- 每个 provider 有自己的
retryPolicy,在路由注册时捕获、随调用携带;途中路由被卸载/替换,在途失败的调用仍保留 serving policy - normal 模式(默认):
EMPTY_RESPONSE/RATE_LIMIT/SERVER/TIMEOUT/TRANSPORT重试 2 次,有界指数退避(500ms→10s,+10% 抖动) - always 模式:先问下游恢复,再无限重试每个模型请求失败(无轮次上限);成功/取消/插件卸载停止
# llm-deepseek 的扁平配置里带 retryPolicy
- name: '@deepseek-ai/dsh-llm-deepseek'
config:
apiKeyEnv: DEEPSEEK_API_KEY
retryPolicy:
mode: always
backoff: { initialDelayMs: 1000, maxDelayMs: 30000, jitterRatio: 0.2 }
- name: '@deepseek-ai/dsh-llm-retry' # 执行器,无 config
- 等待前追加非 surface 的
llm/retry事件(带retryId/provider/mode/failure/delay);等待完再llm/retry-started,前一刻才返回{ kind:'retry' } - 重试轮在相同的持久化历史上重建相同的显式 provider/model 请求;失败 chunk 绝不进入派生消息
- 多 provider 的
llm-pi-ai把retryPolicy放在每个 provider profile 里
八、凭据与归属
normalizeApiKey:去首尾空白,接受非空可打印 ASCII(排除空格),否则报ApiKeyRejection- 每个产品 adapter 会在 provider HTTP 请求上发
User-Agent(attributionHeaders);白标部署可替换但不可抑制 agent-default-model的 provider/model 落在 配置
九、验证
# 列出已注册 provider
dsh web --dump-config | grep -B2 -A6 "llm-"
# 看一次请求的实际路由(request/header,默认 zstd 压缩、两级目录)
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd | grep "request/header" | tail -1
# 看重试情况(llm/retry 事件)
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd | grep -E "llm/retry" | head