本文档梳理 opencode 的 REST 与 SSE 接口,明确"它返回什么、请求里该传什么",为客户端封装提供依据。
2.1 接口概览
opencode serve 是一个标准 OpenAPI 3.1 服务。本项目只用到 6 类接口:
- 健康检查
- 会话管理(创建 / 查询 / 删除)
- 历史消息查询
- 发送消息(阻塞式返回)
- 撤回 / 恢复
- 实时事件流 /event(SSE) ← 流式打字机的关键
2.2 素材来源
- 在线文档:
http://127.0.0.1:4096/doc(Swagger UI) - 离线规范:
curl http://127.0.0.1:4096/doc -o doc.json下载
打开 doc.json 可以看到结构:
{
"openapi": "3.1.0",
"info": { "title": "opencode", "version": "1.0.0" },
"paths": { ... },
"components": { "schemas": { ... } }
}
提示:
doc.json有 4 万行,别用编辑器硬翻。用grep或 Python 脚本定位paths下的路径即可。也可以在 Swagger UI 里点。
2.3 会话管理
创建会话
POST /session
Content-Type: application/json
请求体(可选的字段很多,最小可用只要空对象或 title):
{
"title": "新对话",
"agent": "build"
}
parentID(以ses开头):继承父会话agent:指定 agent(build是默认的编程 agent)model:{ "id": "...", "providerID": "..." }permission:PermissionRuleset,可以给单个会话单独设权限workspaceID(以wrk开头):指定工作区
响应:一个 Session 对象,必填字段为 id, slug, projectID, directory, title, version, time。我们要的是 id(ses_... 开头的 ULID)。
列出 / 获取 / 删除
GET /session → 会话数组
GET /session/{id} → 单个 Session
DELETE /session/{id} → 200 / 204
PATCH /session/{id} → 改 title 等(可选)
2.4 消息
拉取历史消息
GET /session/{id}/message
响应是一个数组,每个元素:
[
{
"info": {
"id": "msg_fd77...", // ULID,以 msg 开头
"sessionID": "ses_...",
"role": "user", // user | assistant
"time": { "created": 1786027609749 },
"agent": "build",
"model": { "providerID": "opencode", "modelID": "deepseek-..." }
},
"parts": [
{ "id": "prt_...", "messageID": "msg_fd77...", "type": "text", "text": "你好" }
]
}
]
要点:
info是消息元数据,parts是内容块(文本 / 推理 / 工具调用 / 文件等)Message是UserMessage和AssistantMessage的联合类型,靠role区分- assistant 消息的
info里还有parentID(回复的是哪条)、finish(stop/error等)、tokens、cost - 文本内容在
parts[].text
发送消息(阻塞式)
POST /session/{id}/message
Content-Type: application/json
请求体:
{
"messageID": "msg_...", // 可选!强烈建议不要填(见 2.6 大坑)
"parts": [ { "type": "text", "text": "你好" } ],
"agent": "build", // 可选
"model": { "providerID": "...", "modelID": "..." }, // 可选
"system": "...", // 可选,附加系统提示
"tools": { "webSearch": true } // 可选,开关工具
}
响应:{ "info": <AssistantMessage>, "parts": [<Part>] },即完整的 assistant 回复。
⚠️ 这个请求会一直阻塞到 AI 回复完成。在终端里感觉不到,但如果在网页后端里直接
await它,用户就看不到打字机效果。解决办法是并发订阅/event,见 08 · SSE 流式对话。
2.5 撤回与恢复
POST /session/{id}/revert
POST /session/{id}/unrevert
请求体(revert 需要指定撤回哪条):
{ "messageID": "msg_..." }
revert:把某条消息之后的所有内容标记为已撤回,等价于 AI 界面里的"改改写写"unrevert:恢复所有已撤回的消息
⚠️ 文档注明两个接口都可能返回 409 SessionBusyError(会话忙时撤回会失败)。客户端要注意处理。
另外 opencode 还提供了更精细的 v2 分阶段撤回接口(
/api/session/{id}/revert/stage|clear|commit),本项目用不到,知道有这回事即可。
2.6 ⭐ 大坑:不要自己生成 messageID
POST /message 请求体里有一个可选的 messageID 字段,服务端规范允许客户端自定义(pattern: "^msg")。
本项目踩过这个坑,务必注意:
opencode 每条消息的 ID 是有序的 ULID(形如 msg_fd77e42d...)。在对话循环里,服务端判断"本轮是否结束"依赖消息 ID 的大小顺序(见 SessionPrompt.runLoop,条件类似 lastUser.id < lastAssistant.id 时退出循环)。
- 如果你传一个自己生成的
msg_<12位uuid十六进制>,它在字典序上小于服务端 ULID(msg_fd...) - 结果:第一轮正常,从第二轮开始,AI 会"假装收到"你的新消息,但实际上一眼认定"已处理过",直接返回上一条旧回复,什么都不生成
- 更糟的变体:如果你用
msg_zzzz...这类大于 ULID 的 ID,消息甚至可能不入库、接口挂起
结论:客户端永远不要传 messageID,让服务端自己生成单调递增的 ULID。
本项目 app/opencode_client.py 的 send_message 注释里专门记录了这个坑:
# NOTE: no client-generated ``messageID`` is sent. opencode's run loop
# exits when the newest user message id sorts below the last assistant
# message id ... a client id like ``msg_<uuid hex>`` (which sorts below
# server ULIDs ``msg_fd...``) makes every follow-up message silently
# produce no reply.
2.7 ⭐ 实时事件流 /event(SSE)
GET /event?directory=&workspace=
- 这是一个 Server-Sent Events(SSE) 长连接,服务端持续推送 JSON 事件
- 可选 query 参数:
directory、workspace(用来过滤事件,本项目不传,靠手动过滤 session) - 每个事件的标准形态:
{
"id": "evt_...",
"type": "message.part.delta",
"properties": { "sessionID": "ses_...", "messageID": "msg_...", "partID": "prt_...", "field": "text", "delta": "你" }
}
我们最关心的几种事件
| type | properties 要点 | 用途 | |
|---|---|---|---|
message.updated | sessionID, info: Message | assistant 消息完成/更新,info.finish 有值时表示回复结束 | |
message.part.updated | sessionID, part: Part, time | 某个内容块创建/落定 | |
message.part.delta | sessionID, messageID, partID, field, delta | 流式增量文本,打字机靠它 | |
session.status | sessionID, `status: {type: idle | busy}` | 会话忙闲状态 |
session.updated | sessionID, info: Session | 会话信息更新(含 title) | |
session.error | sessionID, error | 出错了 |
状态机小抄(写客户端必须懂)
SessionStatus 是这三种之一:
{ "type": "idle" } // 空闲
{ "type": "busy" } // 忙(AI 正在跑)
{ "type": "retry", ... } // 重试中
⚠️ 又一个坑:opencode 在保存用户消息之后、AI 开始生成之前,会先发一个
idle事件。所以不能把idle当作"回复结束"信号——否则 SSE 监听器会过早退出,POST 被取消,AI 永不回复。(08 会讲正确的完成判定。)
2.8 用 Python 快速看一眼 SSE 流
import httpx
with httpx.stream("GET", "http://127.0.0.1:4096/event", timeout=30) as r:
for line in r.iter_lines():
print(line)
打开另一个终端给这个会话发条消息,就能看到事件像瀑布一样打出来。这是 08 并发设计的依据。
2.9 小结
| 接口 | 方法 | 用途 | 阻塞? |
|---|---|---|---|
/global/health | GET | 健康检查 | 否 |
/session | POST | 创建会话 | 否 |
/session/{id}/message | GET | 拉历史 | 否 |
/session/{id}/message | POST | 发消息 | 是 |
/session/{id}/revert unrevert | POST | 撤回/恢复 | 是 |
/event | GET(SSE) | 实时事件流 | 长连接 |
三个必须记住的教训:
- 别自己生成
messageID - POST 发消息是阻塞的,流式要靠
/event idle不能当完成信号