API 代理(packages/host/apiproxy)是每一个 client 都要与之对话的共享 /api surface 的 host 侧实现。尽管名字如此,它并不只是模型 API 转发器:它是应用的 API 载体——即契约层,web UI 以及任何未来的 client 都靠它到达会话、host 能力与事件。Typert gateway(见 API Gateway)在同一个路由上认领类型化的端点;一切未被认领的请求都会落到代理。
| 包 | 角色 |
|---|---|
@deepseek-ai/dsh-host-apiproxy | ApiProxyService(ctx.apiProxy)、createApiProxy、契约层、fetch 载体、会话导出 |
包布局
来自 packages/host/apiproxy/README.md,这个包分三层:
- 契约层(
src/api/)——零 Node 依赖的 TypeScript API 契约,浏览器可直接导入。正是它让线路保持诚实:同一套类型被编译进两个面。 - Fetch 载体(
src/fetch/)—— host 侧的toFetchHandler(把代理包装成 Connection HTTP 桥的 fetch handler),以及 client 侧的AbstractApiClient加平台子类。 - Host 实现(
src/api-proxy.ts)——createApiProxy加默认导出的ApiProxyServicegateway 插件,配置{ nativeOpen?, sessionExportCompressionLevel?, coldBlankProbeMaxBytes? },提供ctx.apiProxy。
这个包本身不注册任何路由;诸如 HTTP 之类的载体会包装 ctx.apiProxy。随附的 Web 装配在 packages/bundle/web-app/cordis.patch.yml 中接线它。
四象限线路
线路消息在 谁发起 × 请求/响应 上构成一个可判别联合,与物理通道解耦:
| 消息 | 方向 | 承载方式 |
|---|---|---|
ClientRequest | Client → Host | POST /api/<method> 的 body |
ServerResponse | Host → Client | 该 POST 的响应 body |
ServerRequest | Host → Client | SSE 帧 |
ClientResponse | Client → Host | POST /api/respond 的 body |
响应始终回显匹配请求的 rpcId,绝不新增一个。方法的参数/返回结构只存在于领域接口签名中(SessionsApi、HostApi、EventsApi);RpcMethodMap 注册这些方法,其余每个位置都通过 RequestPayload<K> / ResponseValue<K> 推导。
校验收两层:Zod schema 锚定 satisfies z.ZodType<Wire<T>>,先解析信封,再解析业务 payload,按方法派发。业务错误走 RpcResult 的错误分支(RpcErrorDetailsMap 封闭错误码集合);HTTP 状态只表达载体。
README 中有一个与安全相关的细节:每个 /api POST 必须声明 application/json 媒体类型——否则在派发之前就以 415 拒绝,因此跨站的"简单"请求(浏览器不会为它发 CORS 预检)永远无法盲目执行一个有副作用的盲方法。
服务端请求与 respond 通道
代理不只是请求/响应。Host 发起的事件——会话更新、agent 状态、问题提示——以 ServerRequest SSE 帧流式推送。client 通过向 /api/respond 的 ClientResponse POST 回答问题。问题响应在第一个答案认领它之前会对照其挂起请求进行校验;一个多选项条目可以在 selected 中携带请求的选项标签,同时在 custom 中携带非空文本,而单选项条目则必须二选一。重复标签、未知标签、不匹配的 id、不完整的批次以及空的 custom 文本都会被判定为 bad-response 拒绝。
分层与协议决策记录在 RFC 笔记 docs/../.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md 与 2026-07-19-gui-web-client-architecture.md 中(.agents/notes/ 下的仓库相对路径)。
模型请求中继与 agent-default-model
代理也是 client 模型流量落脚的地方。ApiProxyService 消费 ctx.agentDefaultModel(见 模型选择与默认值);它不拥有 provider/模型配置或 settings 区块。共享服务在 agent-default-model settings 区块下注册 { provider, model, reasoningEffort? }:基础 bundle 的装配条目是下层,settings.yaml 把用户的选择叠加在它之上。
会话在每次访问时从三层中解析其模型选择:
- 本次进程中做出的选择,
- 否则是会话最新记入日志的
request/header, - 否则是部署默认值。
一个跑过 turn 的会话从其日志推导选择,而一个空白的会话则观察在其创建之后保存的默认值。session.selectModel 把一次被接受的切换保存为部署默认值——没有单独的额外手势——并存储解析出的 ModelSelection,其中包括一个由适配器物化的默认 effort。
原生打开与会话导出
这个包还顺带承载两个仅 host 端的能力:
nativePathOpener(src/native-path-opener.ts)—— 用操作系统打开本地路径(原生目录选择器的对应侧;见 Host 平台)。sessionExport(src/session-export.ts)—— 产出可供下载的会话导出(ZIP)并服务给 client,带sessionExportCompressionLevel配置。
关键源文件
| 仓库相对路径 | 提供什么 |
|---|---|
packages/host/apiproxy/src/api-proxy.ts | createApiProxy、ApiProxyService、ctx.apiProxy |
packages/host/apiproxy/src/api/ | 浏览器安全契约层(零 Node 依赖) |
packages/host/apiproxy/src/fetch/ | toFetchHandler、AbstractApiClient 加平台子类 |
packages/host/apiproxy/src/session-export.ts | 会话导出(ZIP) |
packages/bundle/web-app/cordis.patch.yml | 随附的 Web 装配挂载它的位置 |
延伸阅读
- API Gateway ——
/api路由中类型化 RPC 的那一半 - Host 平台 —— webserver、frontend-static、plugin 清单
- 模型选择与默认值 ——
agent-default-modelseam - 前端:Client 运行时 —— 浏览器如何消费
ctx.apiProxy - 仓库文件:
packages/host/apiproxy/README.md、.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md