跳到主要内容
路径文档

模型路由

一句话版:模型层是能力 seam:ctx.llm 定义 provider 无关的抽象与流式调用 API,llm-deepseek / llm-pi-ai 等 adapter 提供实现;路由决定哪个 provider/model 服务哪个 agent,错误有理可循、可配重试。

这是"模型是怎么被调用的、配多个模型时怎么选"的一篇。读完你能配明白 provider、看懂一次请求的路由和失败分类。

一、LLM 层结构

角色
llm/llm抽象服务 ctx.llm:adapter 注册、流式调用、模型解析
llm/llm-deepseekchat-completions 适配器(direct fetch + SSE)
llm/llm-pi-ai多 provider 网关(OpenAI 兼容)
llm/llm-retryexact-provider 重试策略
web/web-search-deepseekWeb 搜索,走独立 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-aiproviders 字典承载多个 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-airetryPolicy 放在每个 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

下一步