16 万 star 的 opencode 项目最近连续合并了三个 LLM provider 层的 bug 修复 PR,都是核心维护者 kitlangton 提交的。这三个 bug 不是某次重构引入的回归问题,而是从协议层初始设计就存在的缺陷——只是在最近使用量增长后才暴露出来。
这篇文章分析这三个 bug 的根因、修复方式,以及对开发 AI 工具的启示。
背景:opencode 的 LLM 协议层
opencode 支持多个 LLM provider(OpenAI、Anthropic 等),每个 provider 有自己的协议实现文件,负责把统一的内部格式转换成各 provider 的 API 格式。比如 openai-responses.ts 处理 OpenAI Responses API,anthropic-messages.ts 处理 Anthropic Messages API。
这些协议层有两个核心工作:
- 下行(lowering):把 opencode 内部的消息格式转成 provider 的请求格式
- 上行(parsing):把 provider 返回的流式响应解析成 opencode 内部的消息格式
三个 bug 分别出在这两个方向上。
Bug 1:Tool Result 中的图片被字符串化
相关 PR:#28754(OpenAI,关闭 #28859)、#28755(Anthropic,关闭 #28861)
问题
当 LLM 调用一个工具(比如截图工具),工具返回的结果可能包含图片(base64 编码)。opencode 的协议层在处理这种 tool result 时,统一调用了 ProviderShared.toolResultText(part),这个函数把整个 tool result——包括图片——JSON.stringify 成一个字符串。
对于 OpenAI Responses API,这意味着:
| |
一个包含 base64 图片的 tool result 被序列化成字符串塞进了 function_call_output.output。对于 Anthropic Messages API 同样:图片被 JSON.stringify 后塞进了 tool_result.content。
修复
OpenAI 端:新增 lowerToolResultOutput 函数,判断 tool result 的类型:
- 文本/json/error → 保持原来的字符串行为(向后兼容)
- 图片 → 以
input_image结构化块发送
| |
Anthropic 端做了类似的处理,图片以 Anthropic 原生的 image 块发送。同时 function_call_output.output 的 schema 从 Schema.String 改成了 Schema.Union([Schema.String, Schema.Array(...)]),既支持旧的字符串格式,也支持新的结构化数组。
根因
这不是某次重构搞坏的,而是初始设计就没考虑到 tool result 会返回非文本内容。toolResultText() 作为一个通用函数,把所有内容都当文本处理——在只有文本 tool result 的世界里这是对的,但世界变了。
Bug 2:Stream Error 信息被吞掉
问题
LLM provider 的流式响应可能在中途出错(rate limit、context overflow、model overload 等)。opencode 的错误处理代码把这些错误全部压成了通用字符串:
| |
所有错误都变成了 "OpenAI Responses stream error" 这一句话。维护者在 PR 描述里提到,这使得某个 session 的诊断变得极其痛苦——底层原因(base64 图片过大)完全不可见。
OpenAI 的 response.failed 事件更惨:错误信息在 response.error 下面,但代码读的是 event.message 和 event.code(顶层字段),永远是 undefined。
修复
OpenAI 端:先读顶层 event.{code, message, param},再回退到嵌套的 event.response.error.{code, message, param}。当 code 和 message 同时存在时,用 code 做前缀:
| |
Anthropic 端:用 error.type 做前缀:
| |
根因
错误处理的代码写得太"乐观"了——假设错误信息总在预期的位置。但 OpenAI 的 API 在不同错误场景下把信息放在不同的嵌套层级,代码没有覆盖所有情况。
Bug 3:Anthropic Tool Result 类型检查不稳定
相关 PR:#28909
问题
这个 PR 在前两个修复合并后才出现。修复了图片 tool result 的结构化发送后,Anthropic 端的类型检查变得不稳定——某些边缘情况下 tool result 的类型推断会失败。
具体来说,tool_result.content 的类型从 string 扩展成了 string | ContentItem[],但下游代码没有完全适配这个联合类型。
修复
稳定了 Anthropic tool result 的类型检查逻辑,确保联合类型的所有分支都被正确处理。
根因
这是 Bug 1 修复的连锁反应。把 schema 从 Schema.String 改成 Schema.Union 后,类型系统变复杂了,之前不需要处理的分支现在必须处理。
三个 Bug 的关系
| |
Bug 1 和 Bug 2 是独立的初始设计缺陷。Bug 3 是 Bug 1 修复的副作用。
对 AI 工具开发的启示
1. 协议层的"够用就行"是最危险的
toolResultText() 在只有文本 tool result 的时候完全够用。但协议层的抽象一旦固定下来,后续扩展就很难——因为所有调用方都依赖当前行为。opencode 的修复保持了向后兼容(文本场景仍然用字符串),但代价是类型变复杂了(Bug 3)。
如果初始设计就把 tool result 分成文本/结构化两条路径,就不会有后面的问题。当然,这是事后诸葛亮——没有人能在第一天就预见到 tool result 会包含图片。
2. 错误处理要假设最坏情况
“错误信息总在预期位置"这个假设在 LLM API 上尤其不成立。各家 provider 的错误格式不一致,同一家 provider 的不同错误类型格式也不一致。写错误处理代码时,应该假设错误信息可能在任何嵌套层级,甚至完全缺失。
3. 修一个 bug 可能暴露下一个
opencode 的三个 PR 形成了一条修复链。修了图片序列化后,类型变复杂了,暴露了类型检查的漏洞。在提交修复时应该考虑到类型变更的下游影响。
4. 观察 bug 的方式决定了修复速度
kitlangton 在 #28757 的 PR 描述里提到,stream error 信息丢失使得某个 session 极其难诊断。如果错误信息是可见的,可能早就发现了。让错误可见是基础设施类代码的重要原则——宁可多输出一点,也不要吞掉信息。
总结
opencode 的这三个 LLM provider bug 都源自初始设计对复杂场景的简化处理。随着 AI 工具能力边界的扩展(从纯文本到多模态),早期"够用就行"的抽象开始出现裂缝。修复的方式是务实的:保持向后兼容的同时扩展新能力。对 AI 工具开发者来说,这是一个提醒——协议层的设计要为未知的变化留余地,错误处理要假设最坏的情况。
