opencode 的 @opencode-ai/llm 深入解析:Schema 优先的多 Provider LLM 核心

发布时间:2026/9/7 5:06:32
opencode 的 @opencode-ai/llm 深入解析:Schema 优先的多 Provider LLM 核心 opencode 的 opencode-ai/llm 深入解析Schema 优先的多 Provider LLM 核心【免费下载链接】opencodeThe open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/openc/opencode本文基于 opencode 仓库中 packages/llm/README.md 的原始文档展开并结合同仓库源码缓存策略、路由架构、Provider 门面与配套示例完整讲解opencode-ai/llm这个包的设计目标与用法统一的请求/响应/事件/工具类型语言、开箱即用的 prompt 缓存策略、Route.make四轴路由组合模型以及它与 opencode 主包 session 层的集成方式。读完本文你将能够独立调用该包完成跨 provider 的生成、流式事件处理、结构化输出与工具调用并理解其在 opencode 内部的实际角色。一、包定位一种类型语言N 个 Provider 适配器opencode-ai/llm是 opencode 的 Schema-first LLM core仓库 packages/llm/package.json 中当前版本为1.18.29workspace 内部包。它的核心主张一句话概括一种带类型的请求、响应、事件和工具语言provider 的怪癖quirks住在适配器里而不是调用代码里。最小调用示例来自 packages/llm/README.md可直接复制运行import { Effect } from effect import { LLM, LLMClient } from opencode-ai/llm import { OpenAI } from opencode-ai/llm/providers const model OpenAI.configure({ apiKey: process.env.OPENAI_API_KEY }).responses(gpt-4o-mini) const request LLM.request({ model, system: You are concise., prompt: Say hello in one short sentence., generation: { maxTokens: 40 }, }) const program Effect.gen(function* () { const response yield* LLMClient.generate(request) console.log(response.text) })两个执行入口的语义边界非常明确LLMClient.generate(request)把整条事件流收集成一个LLMResponse对象适合一次性拿结果LLMClient.stream(request)返回逐条LLMEvent的 EffectStream适合 UI 渐进渲染。事件流是 provider 中立的——同一套LLMEvent形状同时覆盖 OpenAI Chat、OpenAI Responses、Anthropic Messages、Gemini、Bedrock Converse 以及任意 OpenAI 兼容部署。LLM.generate/LLM.stream还从LLMClient重新导出支持单一 import 的写法见 packages/llm/src/llm.ts 中export const generate LLMClient.generate。完整 Public APIREADME 列出的公共 API 如下逐一说明LLM.request({...})— 构建 provider 中立的LLMRequest。接受便捷输入system: string、prompt: string内部归一化为规范的 Schema 类。LLM.generate/LLM.stream— 从LLMClient重导出供一次性 import 使用。Message.user(...)/Message.assistant(...)/Message.tool(...)— 来自规范 schema 模型的消息构造器。Model.make(...)/ToolCallPart.make(...)/ToolResultPart.make(...)/ToolDefinition.make(...)— 模型与工具相关的构造器。LLMClient.prepare(request)— 把请求完整编译一遍协议体构建、校验、HTTP 准备但不发送用于检查和测试。LLMEvent.is.*— 类型守卫is.textDelta、is.toolCall、is.finish等用于过滤流。从 packages/llm/AGENTS.md 的约定看这类每类型的构造器挂在类型上的规则是刻意的Message.system(...)、ToolChoice.named(...)、SystemPart.make(...)等都直接挂在对应类型上顶层LLM命名空间只保留请求形态的调用 APILLM.request、LLM.generate、LLM.stream、LLM.updateRequest、LLM.generateObject。同一东西两种构造方式就是多一种是该包的 API 纪律。LLM.request的实际归一化逻辑很短packages/llm/src/llm.tssystem经SystemPart.content转为 part 数组prompt追加为一条Message.usertools、toolChoice、generation、http分别经对应的.make(...)转为规范 Schema 实例。也就是说便捷函数只是薄构造器不会产生第二套数据模型。二、Prompt 缓存默认开启数学说了算这是 README 中最有信息量的部分。Prompt 缓存默认开启每个LLMRequest在调用方不指定时都解析为cache: auto调用方可以用cache: none显式退出。各协议再把CacheHint翻译成各自的线格式Anthropic 的cache_control、Bedrock 的cachePointOpenAI 与 Gemini 是服务端隐式缓存不需要内联标记auto 在它们上面是 no-op。自动放置的三个断点auto会在三个位置放置缓存断点最后一个 tool definition 上最后一个 system part 上最新的 user message 上。其中最新 user message 边界是承重细节在工具调用循环中一个 user 回合会展开成多次 assistant/tool 往返它们共享同一段前缀。在这个边界上缓存能让回合内每一次 API 调用都命中前缀缓存。代价数学也直接写进了文档Anthropic 5 分钟缓存写入是基础价的 1.25 倍、读取是 0.1 倍5 分钟内只要复用一次就回本。而低于各模型可缓存 token 下限的一次性补全在线路上会静默 no-op所以最坏情况无害——这就是默认开启的底气。源码层面这些描述都能在 packages/llm/src/cache-policy.ts 里逐条对上const AUTO: CachePolicyObject { tools: true, system: true, messages: latest-user-message, } // 线格式忽略内联缓存标记的协议OpenAI 隐式前缀缓存、Gemini 隐式 带外 CachedContent const RESPECTS_INLINE_HINTS new Set([anthropic-messages, bedrock-converse]) const resolve (policy: CachePolicy | undefined): CachePolicyObject { if (policy undefined || policy auto) return AUTO if (policy none) return NONE return policy }几个值得注意的实现细节applyCachePolicy在编译期跑一次位于各协议 body builder 之前往被策略选中的 part 注入CacheHint之后走既有的内联 hint 降低路径手动放置的cache: CacheHint会被保留——markLastTool/markLastSystem/markMessageAt都先检查该位置已有 hint 则跳过自动策略只填空白不覆盖用户意图markMessageAt刻意用slice() 单点替换而非.map()注释说明长对话每次请求都会走到这里profiling 显示闭包分发和身份拷贝有明显开销——这是从源码结构看很典型的性能敏感路径优化。退出、细粒度策略与手动 hint显式退出LLM.request({ model, system, prompt: one-off question, cache: none, })细粒度策略object 形态完全按调用方要求执行cache: { tools?: boolean, system?: boolean, messages?: latest-user-message | latest-assistant | { tail: number }, ttlSeconds?: number, // ≥ 3600 → Anthropic/Bedrock 上 1h否则 5m }源码中messages的三种策略分别落到markMessageAt的三种索引最后一条 user 消息、最后一条 assistant 消息、或末尾tail条消息逐条打标packages/llm/src/cache-policy.ts。手动 hint在任何 text / system / tool / tool-result part 上内联CacheHint即可覆盖自动放置LLM.request({ model, system: [ { type: text, text: stable system prompt, cache: { type: ephemeral } }, ], ... })各协议行为表Protocolcache: autoAnthropic Messages至多输出 3 个cache_control标记强制 4 断点上限Bedrock Converse至多输出 3 个cachePoint块强制 4 断点上限OpenAI Chat / Responsesno-op超过 1024 token 时隐式缓存Geminino-op2.5 系列隐式缓存显式CachedContent是带外机制归一化的缓存用量会读回到response.usage.cacheReadInputTokens与cacheWriteInputTokens且对所有 provider 一致。三、Provider 门面先配置部署再选模型Provider 的调用形态统一为两阶段门面先用configure(...)配置 endpoint/认证/部署细节然后暴露只接受模型或部署 id 的选择器选出的 model 携带运行时使用的可执行 route 值。import { OpenAI, CloudflareAIGateway } from opencode-ai/llm/providers const openai OpenAI.configure({ apiKey: process.env.OPENAI_API_KEY }).responses(gpt-4o-mini) const gateway CloudflareAIGateway.configure({ accountId: process.env.CLOUDFLARE_ACCOUNT_ID, gatewayApiKey: process.env.CLOUDFLARE_API_TOKEN, }).model(workers-ai/cf/meta/llama-3.1-8b-instruct)仓库内置的 provider由 packages/llm/src/providers/index.ts 确认OpenAI、Anthropic、Google (Gemini)、Amazon Bedrock、Azure OpenAI、Cloudflare AI Gateway / Workers AI、GitHub Copilot、OpenRouter、xAI外加通用 OpenAI 兼容助手覆盖 DeepSeek、Cerebras、Groq、Fireworks、Together 等家族见 packages/llm/src/providers/openai-compatible-profile.ts。每个 provider 都有独立 subpath 导出如 packages/llm/package.json 中的./providers/openai、./providers/anthropic等也可以直接从./providers聚合引入。AGENTS.md 对门面的约定值得展开apiKey是 provider 专属糖auth是显式覆盖两者在选项类型中互斥ProviderAuthOptionAuthOptions.bearer(options, PROVIDER_API_KEY)负责把apiKey解析为Auth尊重显式auth覆盖并回退到Auth.config(envVar)——缺 key 时得到的是类型化的Authentication错误而不是运行时崩溃模型选择器里model表示默认 API 路径命名方法表示 provider 原生替代路径如 OpenAI 的responses、responsesWebSocket、chat需要每请求签名的 providerBedrock SigV4、未来的 Vertex IAM、Azure AAD把Auth实现为函数签名 body 并把签名头合并进结果。四、定制阶梯generation → providerOptions → httpREADME 定义了按稳定性排序的三个逃生舱generation— 可移植旋钮maxTokens、temperature、topP、topK、penalties、seed、stop**providerOptions: { provider: {...} }** — 在门面处类型化的 provider 专属旋钮OpenAIpromptCacheKey、Anthropicthinking、GeminithinkingConfig、OpenRouter 路由http: { body, headers, query }— 最后手段的可序列化 overlay合并进最终 HTTP 请求。只有在稳定的类型化路径尚不存在时才用它。Route/provider 上的默认值在每一轴上都会被请求级值覆盖。packages/llm/example/tutorial.ts 把这个阶梯演示得很完整const model OpenAI.configure({ apiKey, generation: { maxTokens: 160 }, providerOptions: { openai: { store: false } }, }).model(gpt-4o-mini) const request LLM.request({ model, system: You are concise and practical., prompt: Tell me a joke, generation: { maxTokens: 80, temperature: 0.7 }, providerOptions: { openai: { promptCacheKey: tutorial-joke } }, })httpoverlay 的形状教程第 55-63 行http: { body: { metadata: { example: tutorial } }, headers: { x-opencode-tutorial: 1 }, query: { debug: 1 }, }教程文件本身就是一个可运行的端到端 walkthrough在packages/llm目录下执行OPENAI_API_KEY... bun example/tutorial.ts即可它依次演示了 generate、stream、工具回合循环、generateObject与自定义协议组合。五、Route 架构四轴分解是协议复用的关键README 说新增一个模型或部署通常只需 5-15 行Route.make({ protocol, endpoint, auth, framing, ... })。这句话的展开在 AGENTS.md 的 Routes 一节一条 route 是四个正交部件的注册组合Protocolpackages/llm/src/route/protocol.ts— 语义 API 契约。拥有请求体构建body.from、body schemabody.schema、流式事件 schemastream.event、事件到LLMEvent的状态机stream.step。Route.make(...)会用body.schema校验并 JSON 编码 body用stream.event解码帧Endpointpackages/llm/src/route/endpoint.ts— URL 构建。host、path、query 都挂在 endpoint 上。常见是Endpoint.path(/chat/completions, { baseURL })对把模型 id 或 body 字段嵌进路径的场景传函数Authpackages/llm/src/route/auth.ts— 每请求传输认证。通常是Auth.bearer(apiKey)或Auth.header(name, apiKey)Framingpackages/llm/src/route/framing.ts— 字节到帧。SSEFraming.sse共享Bedrock 保留 AWS event-stream 帧作为类型化的Framingobject值。组合示例AGENTS.md 原文export const route Route.make({ id: openai-chat, provider: openai, protocol: OpenAIChat.protocol, endpoint: Endpoint.path(/chat/completions, { baseURL: https://api.openai.com/v1, }), auth: Auth.bearer(), framing: Framing.sse, })四轴分解的收益在文档中写得很直白DeepSeek、TogetherAI、Cerebras、Baseten、Fireworks、DeepInfra 全都原样复用OpenAIChat.protocol——每个部署只是一次 5-15 行的Route.make(...)而不是一次 300-400 行的 route 克隆。一个协议里的 bug 修复一个 commit 就传播给所有消费方。非 HTTP 传输如 OpenAI 的 WebSocket Responses 后端的接缝是TransportWebSocketTransport.jsonTransport.with(...)构造一个 IO 模板prepare在编译期拿到 route 的 endpoint/auth同一协议、同一 endpoint 来源、不同传输。LLMClient.prepare是这条流水线的检查口prepareBody(request)把请求编译到 route 原生 body 但不发送可选的Body类型参数把.body收窄到 route 原生形状如prepareOpenAIChatBody(...)运行时 body 完全相同泛型只是类型层断言。教程里用它检查一个自造的 fake provider 的最终 body是很好的测试示范。六、工具调用与结构化输出工具回路工具循环用共同的消息与事件表达const call ToolCallPart.make({ id: call_1, name: lookup, input: { query: weather } }) const result Message.tool({ id: call_1, name: lookup, result: { forecast: sunny } }) const followUp LLM.request({ model, messages: [Message.user(Weather?), Message.assistant([call]), result], })LLM.stream/LLM.generate各跑恰好一个 provider 回合。把工具 schema 用Tool.toDefinitions(tools)加进request.tools需要包的类型化单次执行时把每个本地tool-call事件交给ToolRuntime.dispatch(tools, call)。教程中的完整回路packages/llm/example/tutorial.ts收集事件 → 找到tool-call→ 跳过providerExecuted→dispatch→ 用LLM.updateRequest把 assistant 消息与 tool 结果追加进历史。Tool.make是 Effect Schema 类型化的parameters决定execute入参类型success校验返回值const get_weather Tool.make({ description: Get current weather for a city., parameters: Schema.Struct({ city: Schema.String }), success: Schema.Struct({ forecast: Schema.String }), execute: (input) Effect.succeed({ forecast: ${input.city}: sunny, 72F }), })AGENTS.md 对分发器划定了清晰边界它在tool-call上按名查工具、对parametersSchema 解码入参、分发到类型化execute、对successSchema 编码结果并返回规范tool-result事件它不流式 provider、不构造 Session 事件、不调度 fiber、不追加历史、不数步数、不继续模型回合——持久化与继续留给外层产品流。可恢复错误必须以ToolFailure表达三条路径会产生tool-error事件未知工具名、入参 Schema 解码失败、handler 返回ToolFailure非ToolFailure的异常视为缺陷、直接让流失败。Hosted 工具Anthropicweb_search/code_execution/web_fetchOpenAI Responses 的web_search_call、file_search_call、code_interpreter_call、mcp_call、local_shell_call、image_generation_call、computer_use_call原样穿过运行时调用以providerExecuted: true的tool-call事件浮出调用方据此跳过本地分发——这是把 provider 怪癖关在适配器后面的具体体现。generateObjectpackages/llm/src/llm.ts 的LLM.generateObject值得注意其实现策略它强制一个合成的generate_object工具调用toolChoice: ToolChoice.named(...)从而刻意回避各家 provider 原生的 JSON mode保证跨协议行为统一。支持两种输入schemaEffect Schema.object被解码并带T类型与jsonSchema运行时才知道形状的裸 JSON Schema.object为unknown调用方自行校验。模型未调用被强制的工具、或工具入参解码失败时都会得到InvalidProviderOutputReason类型的LLMError。七、与 opencode 主包的集成边界AGENTS.md 明确声明这个包必须保持独立于 session 关注点session 的认证、权限、插件、遥测头与运行时选择都归属packages/opencode/src/session/llm.ts及其本地适配器。文档列出的主要集成点从 AGENTS.md 描述看packages/opencode/src/session/llm.ts — session 持有的编排层决定某请求走 AI SDK 还是本包的原生 route 运行时packages/opencode/src/session/llm/native-request.ts— 把 opencode 的 session/AI SDK 形态数据降低lowering成本包LLMRequest模型的适配器;packages/opencode/src/session/llm/native-runtime.ts— 调用裸LLMClient.stream(request)、把 opencode 的一个工具调用回合桥接成本包类型化 dispatch 的执行适配器packages/opencode/src/session/llm/ai-sdk.ts— 把 AI SDK 流式 part 转成本包共享LLMEvent保持默认 AI SDK 路径兼容。这条边界很重要opencode 这种持久化 agent 需要自己拥有持久化、工具结算与继续权因此它消费的是单回合语义LLMClient.stream一个 turn而不是让 LLM 包替它做编排——这也解释了为什么本包公开 API 里generate/stream都只跑一个 provider turn。八、Effect 运行时与测试体系包建立在 Effect 之上公开方法返回Effect或Stream运行时提供LLMClient.layer做分发按需 import 用到的 provider/protocol 模块。packages/llm/example/tutorial.ts 展示了依赖装配const requestExecutorLayer RequestExecutor.fetchLayer const llmDeps Layer.mergeAll(requestExecutorLayer, WebSocketExecutor.layer) const llmClientLayer LLMClient.layer.pipe(Layer.provide(llmDeps))AGENTS.md 还给出包边界级的 Effect 风格要求优先HttpClient/HttpClientResponse而非 webfetch/Response流式一律用Stream.StreamJSON 编解码走 Effect Schema codecSchema.fromJsonString而非裸JSON.parse。测试体系是fixture 优先live provider 调用必须藏在RECORDtrue与必需 API key 检查之后录制测试cassette 每场景一个文件多步流程如工具循环、重试、轮询录进同一文件支持按RECORDED_PROVIDER、RECORDED_PREFIX、RECORDED_TAGS、RECORDED_TEST过滤重放/重录二进制响应如 AWS event-stream 帧以 base64 bodyEncoding保真存储。相关测试位于 packages/llm/test/test/provider/下为 fixture 优先的协议测试*.recorded.test.ts覆盖 live cassette。九、小结与延伸阅读opencode-ai/llm的设计可以浓缩为三句话请求/事件是 provider 中立的可序列化数据provider 差异被关进 Protocol/Endpoint/Auth/Framing 四轴路由缓存策略用明确的经济计算来定默认值。对 opencode 而言它是 session 编排层之下的执行内核对外部开发者而言它提供了一条从LLM.request到Route.make的渐进披露路径。原始文档packages/llm/README.md架构与贡献者指南路由四轴、门面约定、录制测试packages/llm/AGENTS.md后续opencode-ai/ai重构讨论稿generate 语义、Run/Turn 分层、hookspackages/llm/DESIGN.md可运行示例packages/llm/example/tutorial.ts缓存策略实现packages/llm/src/cache-policy.ts公共 API 入口packages/llm/src/index.ts、packages/llm/src/llm.ts协议与路由实现packages/llm/src/protocols/、packages/llm/src/route/Provider 门面packages/llm/src/providers/【免费下载链接】opencodeThe open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/openc/opencode创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考