opencode 客户端 SDK 深度解析:@opencode-ai/client 的契约驱动代码生成与双入口架构
本篇技术指南以 packages/client/README.md 为主体,深入拆解 OpenCode 仓库中的 @opencode-ai/client 包——一个从权威 Effect HttpApi 契约直接生成、面向零依赖 Promise 场景与 Effect 运行时场景双轨交付的 HTTP 客户端。读完本文,你将掌握该客户端的两个公开入口(Promise 根与 /effect 子路径)的适用边界、契约到代码的完整生成链路(bun run generate / bun run check:generated)、生成物的错误模型,以及仓库如何用打包边界测试锁定两套入口的依赖图谱。
1. 包定位:一个私有的“生成目标”
@opencode-ai/client 在 package.json 中被标记为 "private": true,其自我定位(见 README 首行)是 Private generation target for clients derived directly from OpenCode's authoritative Effect HttpApi——即“直接从 OpenCode 权威 Effect HttpApi 派生客户端的私有生成目标”。这意味着该包的核心价值不在手写逻辑,而在由契约单向生成的确定性与可验证性:
- 生成产物全部落在 src/generated/ 与 src/generated-effect/ 两个目录,文件头明确标注
// Generated by @opencode-ai/httpapi-codegen. Do not edit.; - 生成链路以仓库内的 httpapi-codegen 包 为编译器,以 src/contract.ts 中声明的
ClientApi为输入; - 契约变更后,README 给出了标准工作流:运行
bun run generate重新生成,再运行bun run check:generated检测已提交产物的漂移。对照 package.json 的 scripts 可见,check:generated的实际实现就是bun run generate && git diff --exit-code -- src/generated src/generated-effect——重新生成后对两个产物目录做零差异断言。
依赖关系也印证了“生成物只依赖契约侧”的设计:运行时依赖仅有 @opencode-ai/schema 与 @opencode-ai/protocol 两个 workspace 包;effect 被声明为可选 peerDependency(peerDependenciesMeta 中标记 optional: true,版本固定为 4.0.0-beta.83),这保证纯 Promise 使用者不必安装 Effect。而 @opencode-ai/server、@opencode-ai/core、@opencode-ai/httpapi-codegen 均只出现在 devDependencies 中——README 所述“构建编译器读取 Server 的具体 API”正是通过开发期依赖实现的,不进入任何生产导入图。
2. 双入口架构
README 的 “Entrypoints” 一节定义了包的公开表面,与 package.json 的 exports 字段一一对应:
| 入口 | exports 映射 | 运行时依赖 | 定位 |
|---|---|---|---|
@opencode-ai/client |
./src/index.ts |
无 Effect、无 Core,仅 fetch |
零 Effect 的 Promise 客户端 |
@opencode-ai/client/effect |
./src/effect.ts |
Effect、Schema、Protocol | 基于环境提供 HttpClient 的 Effect 网络客户端 |
2.1 Promise 根入口:结构化的 fetch 客户端
根入口 src/index.ts 只做两件事:转发 generated/index 的全部导出,并把生成的 EventsSubscribeOutput 类型以 OpenCodeEvent 别名再导出一次。生成后的 generated/client.ts 展示了这套客户端的完整骨架,可直接作为阅读参考:
export interface ClientOptions {
readonly baseUrl: string
readonly fetch?: typeof globalThis.fetch // 可注入自定义 fetch(测试/代理场景)
readonly headers?: HeadersInit // 全局默认请求头
}
export interface RequestOptions {
readonly signal?: AbortSignal // 逐请求中止
readonly headers?: HeadersInit
}
make(options) 工厂内部以 RequestDescriptor(method、path、query、headers、body、successStatus、declaredStatuses)描述每个端点,请求处理链遵循清晰的错误分类:
- 传输层异常(fetch 本身抛错)→
ClientError("Transport", { cause }); - 响应状态命中契约声明的错误状态(
declaredStatuses)→ 直接解析 JSON 错误体抛出; - 其他非预期状态 →
ClientError("UnexpectedStatus", { cause: { status } }); - SSE 端点额外校验
Content-Type必须为text/event-stream,否则抛ClientError("UnsupportedContentType")。
这种“按契约声明的状态码分流”的语义,是纯手写 fetch 封装很容易缺失、而由代码生成保证一致性的典型收益。
2.2 /effect 入口:规范化解码值 + 环境注入的 HttpClient
README 强调 Effect 入口使用 canonical decoded values,如 Session.ID、Location.Ref、Prompt。这些数据类型来自轻量级 @opencode-ai/schema 包,并由 src/effect.ts 再导出——Agent、Location、Model、Session、SessionMessage、Prompt、AbsolutePath/RelativePath 等二十余个命名导出都列在该文件中,其目标是让调用方只需依赖客户端公开表面,内部模型重组不会迫使调用方迁移。文件头部的注释还固化了一条架构约束:/effect 永远不得导入 Core 或 Server。
生成的 Effect 客户端 generated-effect/client.ts 基于 Effect 的 HttpApiClient.ForApi<typeof ClientApi>,把 HttpClientError、SchemaError、Sse.Retry 统一归一为包的 ClientError,而 OpenCode.make({ baseUrl }) 即 README 示例中构造客户端的工厂。
README 中的官方示例(可直接复制运行,前提是本地或远端已有 OpenCode Server)完整展示了 Effect 侧调用方式:
import { AbsolutePath, Location, OpenCode, Prompt } from "@opencode-ai/client/effect"
const client = yield * OpenCode.make({ baseUrl: "https://opencode.example" })
yield *
client.sessions.create({
location: Location.Ref.make({ directory: AbsolutePath.make("/workspace") }),
})
yield * client.sessions.prompt({ sessionID, prompt: Prompt.make({ text: "Hello" }) })
注意示例中三个要点:baseUrl 指向一个已运行的 OpenCode Server 实例;sessions.create 的 location 参数用 Location.Ref.make 包装绝对路径,而非裸字符串;prompt 载荷同样通过 Prompt.make 构造——调用方全程接触的是规范化解码类型而非原始 JSON 结构。
3. 生成表面:覆盖了哪些 HTTP 组,又刻意排除了什么
“生成表面包含 Server 具体 API 的每一个标准 HTTP 组”并非空话。src/contract.ts 用 makeDefaultApi 从 Protocol 构建客户端本地的 ClientApi 投影,并维护了三份清单:
组名映射 groupNames:把 18 个服务端组键映射为客户端命名空间,例如 server.health → health、server.session → sessions、server.message → messages、server.pty → ptys、server.projectCopy → projectCopies。这就是为什么 README 示例里能直接写 client.sessions.*。
端点别名 endpointNames:对少量组内端点重命名,如 session.messages → list、integration.connect.key → connectKey、permission.saved.remove → removeSaved,使生成物读起来符合 REST 客户端惯例。
显式排除 omitEndpoints:new Set(["fs.read", "pty.connect", "pty.connectToken"])。这与 README 中“PTY WebSocket 连接等自定义传输保持在通用 HTTP 客户端之外”的说明互为印证——文件读取与 PTY 建连走的是各自专用传输(WebSocket 等),不经过通用 HTTP 客户端表面。
中间件方面,contract 声明了 LocationMiddleware 与带错误声明(InvalidRequestError、SessionNotFoundError)的 SessionLocationMiddleware。README 的职责划分表述是:“Protocol 拥有端点构建与中间件放置;Server 提供构建期 API 所用的具体中间件键”——即抽象契约归 Protocol,具体键归 Server,客户端包自身只持有一份客户端本地投影。
4. 代码生成链路:从 ClientApi 到两套产物
生成入口是 package.json 中 "generate": "bun run script/build.ts",实现见 script/build.ts,核心流程为:
compile(ClientApi, { groupNames, endpointNames, omitEndpoints })把契约编译为中间表示;emitPromise(contract, { outputTypes: ... })产出 Promise 客户端,其中特化了events.subscribe的输出类型:以OpenCodeEventEncoded命名并直接import type自@opencode-ai/protocol/groups/event,避免在生成物中复制事件联合类型;emitEffectImported(contract, { module: "../contract", api: "ClientApi" })产出 Effect 客户端,且让它import { ClientApi } from "../contract"——这正是 README 所说“生成的 Effect 运行时导入一份由 Protocol 构建的客户端本地投影”;write并发(concurrency: 2)落盘到src/generated与src/generated-effect。
“生成等价测试防止传输漂移”(generation-equivalence test)对应 test/contract-identity.test.ts;配合 check:generated 的 git 零差异断言,仓库对“契约、生成脚本、已提交产物”三者一致性做了双重锁定。
5. 打包边界测试:两套入口的依赖图谱
README 最后一句——“Promise 根保持结构化、无 Core 或 Effect 运行时依赖;/effect 仅依赖 Effect、Schema、Protocol,且对浏览器打包安全。打包边界测试强制约束这两套导入图”——有明确的测试佐证:test/import-boundaries.test.ts。
该测试用 Bun bundler 以 browser 目标把每个入口打包成临时产物,读取 metafile 的 inputs 集合,然后断言:
- 根入口
@opencode-ai/client的 inputs 中effect、schema、protocol、core、server五个目录的命中数全部为 0; /effect入口的 inputs 中effect、schema、protocol命中数必须大于 0,而core、server命中数必须为 0。
换言之,这不是依赖 lint 层面的静态规则,而是真实浏览器打包结果的机器验证,保证客户端可安全嵌入浏览器 bundle 而不把服务端实现卷进来。包内测试还包括 promise.test.ts 与 effect.test.ts,分别覆盖两条入口的行为正确性。
6. 实践要点小结
- 选择入口:浏览器或任何不想引入 Effect 的场景用
@opencode-ai/client(纯 Promise + fetch,可注入自定义fetch/headers/AbortSignal);Effect 运行时中需要结构化错误与环境注入HttpClient的场景用@opencode-ai/client/effect。 - 类型来源:Effect 侧所有构造器(
Session、Location、Prompt、AbsolutePath等)从客户端入口本身导入即可,不必直接依赖@opencode-ai/schema。 - 契约变更流程:修改 Protocol/Server 契约后先
bun run generate再bun run check:generated;CI 中两者组合即可拦截产物漂移。 - 不要手写生成物:
src/generated与src/generated-effect均由 httpapi-codegen 产出,所有定制(组名、端点别名、排除项、中间件)都应回写到 src/contract.ts 后再重新生成。
综合来看,@opencode-ai/client 展示了 OpenCode 仓库中“契约单一来源 + 生成客户端 + 测试锁定边界”的完整工程范式:README 给出入口与示例,contract 定义表面与排除项,codegen 脚本产出双轨客户端,而 identity 测试与打包边界测试则把上述承诺固化为可执行的持续验证。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00