ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

opencode packages/llm 调用位点设计:Route 执行、Provider Facade 组织、Model 携带路由值

opencode packages/llm 调用位点设计:Route 执行、Provider Facade 组织、Model 携带路由值 opencode packages/llm 调用位点设计Route 执行、Provider Facade 组织、Model 携带路由值【免费下载链接】opencodeThe open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/openc/opencodeopencode 的 LLM 核心包opencode-ai/llm正在把各 provider 的私有行为从 AI SDK 的 transform 路径迁移到自有代码中。设计文档LLM Call Site Sketches定义了这次迁移的调用位点骨架路由Route负责执行Provider Facade 负责组织已配置的路由集合Model 直接携带可执行的路由值。读懂这篇文章后你能掌握该包的六层职责划分Route / Facade / Model Selector / Model / LLM Request / Compile、Route.with(...)的补丁语义、各 Provider 的调用位点范式以及 opencode 会话层如何在应用边界把持久化模型标识解析成可执行模型。设计起点先写调用位点再谈抽象文档开宗明义Scratchpad for examples first, abstractions second。Kit 和 Aidan 的目标是把 provider 特定的 LLM 行为尽量挪进packages/llm但要明确拒绝的方向是不做大的通用 transform 层而是做小型、可组合的 route 定义 录制好的 golden 测试支撑不引入一等的Deployment抽象除非它获得真实语义Provider Facade 只是人因工程上舒服的已配置路由组不是执行注册表路由构建不发布到全局注册表Model 直接携带 route 值。这套设计要持续接受测试验证的维度来自文档Things to keep testing against缓存位点cache: auto、手动缓存断点、provider 缓存用量图片对声称支持图片的 provider/协议做 golden 图片测试推理Reasoning规范化的 reasoning parts/events 对比 provider 原生开关认证bearer、自定义 header、多凭证、query 认证、SigV4、OAuth、无认证OpenAI 兼容 providerDeepSeek、Together、Groq、Alibaba/DashScope、自定义路由Provider 切换过期签名、加密推理、provider 元数据、不兼容的 parts错误质量类型化错误而非泛化的 SDK/server 失败。持久身份与运行时能力的分离这是整套设计里最重要的约束之一。文档要求把持久身份durable identity与运行时能力runtime capability严格分开持久身份{ providerID, modelID }这样的小型可序列化数据用于配置、会话、日志、目录catalog。它不能自行执行运行时能力Model值携带 route 值、protocol、transport、auth、defaults允许包含函数和 schema如果持久身份需要变成可执行的必须先经过应用边界app boundary解析而不是让LLMRequest从全局路由边表里找回行为。另一个原则是未配置的值保持为值而不是工厂像HttpTransport.sseJson这样的 transport 应该是可复用的不可变常量只有当调用方传入选项或构建需要新鲜状态时才使用函数。以及先用常量消灭重复再发明抽象provider id 每个 facade 只 brand 一次并在各路由间复用普通的导出对象就够用了除非某个 helper 确实通过消除重复的路由投影挣得自己的一席之地。六层职责模型文档把一次 LLM 调用拆成六个职责层这个划分是全文骨架Route——执行机制的完整载体route id、provider id、protocolbody schema、body builderstream event schema、解析器/状态机transportmethod / IO shape、framing、request preparation未配置时用常量配置了才用函数endpointbase URL、静态 path、由 body/model 派生的 path、query 参数authbearer、自定义 header、多凭证、SigV4、nonedefaultsheaders、generation 默认值、provider options、limitsProvider Facade——默认已配置的 provider 实例 provider 特定的.configure(...) 覆盖一个或多个路由的普通对象/函数 facade只有代表一个连贯的配置面时才导出顶层入口除非获得运行时行为不做被动的Provider.make(...)包装。Model Selector——归属 route/provider 的选择器只接受 model id返回可执行模型不接受 endpoint/auth/deployment 覆盖。Model——model id、route 值、provider id、选择时刻的已配置 route 值。LLM Request——model、messages/tools、generation/cache/reasoning/response-format 选项外加 per-request 的 HTTP overlayheaders/query/body 追加但不用于 provider endpoint/auth 的重配置。Compile——从 model 读取 route、合并 route 默认值与请求覆盖、从 route endpoint 构建最终 URL、应用已配置 route 的 auth、用 protocol 构建 body、用 transport 执行并用 protocol 解析。这个模型在源码中可以直接对上号。Route 接口定义packages/llm/src/route/client.ts中Route类型恰好包含文档列出的字段id、provider、protocol、endpoint、auth、transport、defaults、body含 schema 与 builder以及with补丁方法和model选择器。而 Compile 层的实现见同文件的compile函数packages/llm/src/route/client.ts#L344-L359它从resolved.model.route读取路由经route.body.from构建 provider 原生 body、schema 校验、route.prepareTransport生成传输私有数据——客户端运行时只执行 model 上携带的路由这一设计由此落地。model选择器的实现packages/llm/src/route/client.ts#L93-L103还强制了两个不变量route 必须带 provider且 endpoint 必须已有baseURL否则直接抛错——即 endpoint/auth 必须在选模型之前配置好选择器本身没有逃生口。Route.with(...) 的补丁语义文档对Route.with(...)的要求是无聊且显式boring and explicit省略的字段从原路由继承endpoint补丁与既有 endpoint 合并覆盖baseURL会保留既有pathendpoint.query默认合并后值胜出auth是替换语义headers默认合并undefined 值被省略补丁中id可选route id 是诊断/provider API 标签不是全局运行时注册键。源码实现packages/llm/src/route/client.ts#L257-L268与之一一对应with内部对endpoint走Endpoint.mergeheaders走mergeHeaders过滤 undefined 后按序合并auth/transport用??实现替换语义defaults 走mergeRouteDefaults逐轴合并。Provider Facade 的形状文档给出的 facade 契约是一个刻意简单的类型type ProviderFacadeAPIs, Config { readonly id: ProviderID readonly model: (id: string) Model readonly configure: (input?: Config) ProviderFacadeAPIs, Config } APIs并且明确手工构造是可以的在重复痛到需要一个 helper 之前都应该是默认export const OpenAI { id: openAIProvider, model: openAIResponses.model, responses: openAIResponses.model, chat: openAIChat.model, configure: configureOpenAI, } satisfies ProviderFacade { responses: (id: string) Model chat: (id: string) Model }, OpenAIConfig 只有当多个 provider 重复了从 route 值到 model 方法的同一投影时才引入极小的 helperconst configureOpenAI (input: OpenAIConfig {}) Provider.define({ id: openAIProvider, routes: { responses: openAIResponses.with(openAIConfig(input)), chat: openAIChat.with(openAIConfig(input)), }, default: responses, configure: configureOpenAI, }) export const OpenAI configureOpenAI()此时Provider.define(...)只做两件事投影路由方法、保留类型。它不注册路由、不动态选择路由、不参与执行——执行永远读取 model 上携带的 route 值。Facade 的一致性规则一个连贯的 provider/product 配置面 → 一个顶层 facade共享该配置的 API/model kind → facade 上的方法配置要求不同的不同产品 → 各自独立的顶层 facade而不是一个塞满无关子项的共享命名空间只有当具体默认值或惰性 env/credential 默认值让 facade 有效时才暴露默认 facade。由此得到文档总结的调用位点范式OpenAI.responses(gpt-4o) OpenAI.chat(gpt-4o) OpenAI.responsesWebSocket(gpt-4o) Azure.configure({ resourceName, apiKey }).responses(my-deployment) AmazonBedrock.configure({ region, credentials }).model(anthropic.claude-3-5-sonnet-20241022-v2:0) CloudflareAIGateway.configure({ accountId, gatewayId, gatewayApiKey, apiKey }).model(openai/gpt-4o) CloudflareWorkersAI.configure({ accountId, apiKey }).model(cf/meta/llama-3.1-8b-instruct) OpenAICompatible.configure({ provider: custom, baseURL: https://custom.example/v1, auth: Auth.bearer(apiKey), }).model(custom-model)对比仓库中的实际实现可以确认这不只是纸面设计。OpenAI provider 实现packages/llm/src/providers/openai.ts中顶层id常量只 brand 一次ProviderID.make(openai)configure(input)对OpenAIResponses.route、webSocketRoute、OpenAIChat.route三条基础路由分别做route.with({ auth, endpoint })得到已配置路由responses/responsesWebSocket/chat三个选择器各自只接受 model id默认 facadeprovider configure()有效的前提是 auth 支持 env 回退AuthOptions.bearer(options, OPENAI_API_KEY)——正是文档默认 facade 只在惰性默认值成立时才有效的落地。包的 README也给出了同形的公开用法OpenAI.configure({ apiKey: process.env.OPENAI_API_KEY }).responses(gpt-4o-mini)。理想调用位点从原生 provider 到兼容家族定义具体路由再经 facade 投影OpenAI 的三条路由Responses / Chat / Responses-WebSocket展示了同协议不同 transport 就是不同路由const openAIProvider ProviderID.make(openai) const openAIResponses Route.make({ id: openai-responses, provider: openAIProvider, protocol: OpenAIResponses.protocol, transport: HttpTransport.sseJson, endpoint: { baseURL: https://api.openai.com/v1, path: /responses, }, auth: Auth.envBearer(OPENAI_API_KEY), }) const openAIChat Route.make({ id: openai-chat, provider: openAIProvider, protocol: OpenAIChat.protocol, transport: HttpTransport.sseJson, endpoint: { baseURL: https://api.openai.com/v1, path: /chat/completions, }, auth: Auth.envBearer(OPENAI_API_KEY), }) const openAIResponsesWebSocket openAIResponses.with({ id: openai-responses-websocket, transport: WebSocketTransport.json, })OpenAI 的configure把 endpoint/auth/organization/project 头统一注入三条路由选择器保持纯粹const openAIConfig (input: OpenAIConfig) ({ endpoint: input.endpoint, auth: input.auth ?? (input.apiKey ? Auth.bearer(input.apiKey) : undefined), headers: { OpenAI-Organization: input.organization, OpenAI-Project: input.project, }, }) const configureOpenAI (input: OpenAIConfig {}) { const responses openAIResponses.with(openAIConfig(input)) const responsesWebSocket openAIResponsesWebSocket.with(openAIConfig(input)) const chat openAIChat.with(openAIConfig(input)) return { id: openAIProvider, responses: responses.model, responsesWebSocket: responsesWebSocket.model, chat: chat.model, model: responses.model, configure: configureOpenAI, } } export const OpenAI configureOpenAI()OpenAI 兼容 provider 是路由特化不是新协议DeepSeek 只是对openAIChat的一次 endpoint/auth 特化const deepSeekProvider ProviderID.make(deepseek) const deepseekChat openAIChat.with({ id: deepseek-chat, provider: deepSeekProvider, endpoint: { baseURL: https://api.deepseek.com/v1, }, auth: Auth.envBearer(DEEPSEEK_API_KEY), }) const configureDeepSeek (input: OpenAICompatibleConfig {}) { const route deepseekChat.with({ endpoint: input.endpoint, auth: input.auth ?? (input.apiKey ? Auth.bearer(input.apiKey) : undefined), }) return { id: deepSeekProvider, model: route.model, configure: configureDeepSeek, } } export const DeepSeek { id: deepSeekProvider, model: deepseekChat.model, configure: configureDeepSeek, }provider 特定配置发生在模型选择之前const deepseek DeepSeek.configure({ endpoint: { baseURL: https://proxy.example.com/v1 }, auth: Auth.bearer(apiKey), }) const model deepseek.model(deepseek-chat)最终的请求调用位点因此保持无聊const response yield* LLM.generate( LLM.request({ model: DeepSeek.model(deepseek-chat), prompt: Hello., }), )HTTP vs WebSocket 是命名路由选择器OpenAI.responses(gpt-4o) OpenAI.responsesWebSocket(gpt-4o)客户端不因选中的路由用了 WebSocket 而要求不同的公开层只用一个LLMClient.layerHTTP 与 WebSocket 运行时能力都可用不需要 WebSocket 的路由永远不触碰它在无 WebSocket 支持的环境中选中了 WebSocket 路由则以类型化的 transport 配置错误失败。client.ts 的 layer 正是这么实现的webSocket运行时通过Effect.serviceOption可选注入http运行时必需——单一 layer 统一暴露可用能力。Azure带输入映射的路由特化Azure 是auth/path/defaults 变更 输入映射的路由特化公开展面上先配置 Azure 资源一次再用纯 model 选择器选部署 idconst azureProvider ProviderID.make(azure) const azureResponses openAIResponses.with({ id: azure-openai-responses, provider: azureProvider, auth: Auth.envHeader(api-key, AZURE_OPENAI_API_KEY), }) const configureAzure (input: AzureConfig {}) { const route azureResponses.with({ endpoint: { baseURL: input.baseURL ?? Endpoint.envBaseURL( AZURE_RESOURCE_NAME, (resourceName) https://${resourceName}.openai.azure.com/openai/v1, ), query: { api-version: input.apiVersion ?? v1 }, }, auth: input.apiKey ? Auth.header(api-key, input.apiKey) : Auth.envHeader(api-key, AZURE_OPENAI_API_KEY), }) return { id: azureProvider, model: route.model, responses: route.model, configure: configureAzure, } } export const Azure configureAzure() const azure Azure.configure({ resourceName: my-resource, apiVersion: v1, }) const model azure.responses(my-deployment)默认 provider facade 只在必需配置有惰性默认来源时才有效Azure.responses(my-deployment)成立是因为 endpoint 解析会惰性读取AZURE_RESOURCE_NAME缺失时给出类型化配置错误。反之没有合理惰性默认值的 provider 不暴露默认 model 选择器只暴露已配置的入口。Cloudflare不同产品 不同 facadeAI Gateway 与 Workers AI 的配置面不同因此不做根级Cloudflare.configure(...)而是两个独立顶层 facadeconst cloudflareProvider ProviderID.make(cloudflare-ai-gateway) const cloudflareOpenAIChat openAIChat.with({ id: cloudflare-ai-gateway-openai-chat, provider: cloudflareProvider, auth: Auth.bearerHeader(cf-aig-authorization).andThen(Auth.bearer()), }) const configureCloudflareAIGateway (input: CloudflareAIGatewayConfig) { const route cloudflareOpenAIChat.with({ endpoint: { baseURL: https://gateway.ai.cloudflare.com/v1/${input.accountId}/${input.gatewayId}/openai, }, auth: Auth.bearerHeader(cf-aig-authorization, input.gatewayApiKey).andThen(Auth.bearer(input.apiKey)), }) return { id: cloudflareProvider, model: (modelID: string) route.model({ id: modelID }), configure: configureCloudflareAIGateway, } } export const CloudflareAIGateway { id: cloudflareProvider, configure: configureCloudflareAIGateway, } const gateway CloudflareAIGateway.configure({ accountId: account, gatewayId: gateway, gatewayApiKey, apiKey, }) const model gateway.model(openai/gpt-4o)注意这里 auth 的组合cf-aig-authorization头与Authorization: Bearer头通过andThen串联——Gateway 同时校验两个凭证。如果某个 Cloudflare 产品将来获得了完整的惰性 env 默认值可以再补一个直选选择器在那之前CloudflareAIGateway.model(...)被有意省略让缺少 account/gateway 配置在 API 层面无法表达。应用边界动态 provider 在哪里分支opencode 的动态运行时不在公开的 provider API 里暴露一个巨大的无结构模型构造函数或通用动态解析器而是在应用边界构造可执行模型const model providerID azure ? Azure.configure(resolvedAzureConfig).responses(apiModelID) : endpoint.websocket ? OpenAI.responsesWebSocket(apiModelID) : OpenAI.responses(apiModelID)这个边界可以基于持久化配置/目录元数据分支直接调用类型化的 provider APItransport 选择也归这里把endpoint.websocket这类元数据映射到OpenAI.responsesWebSocket(apiModelID)否则走普通OpenAI.responses(apiModelID)。客户端运行时只执行 model 携带的路由。仓库中这个边界的真实位置是 native-request.tspackages/opencode/src/session/llm/native-request.ts。其model()函数按目录里的model.api.npm元数据分支ai-sdk/openai→OpenAI.configure(options).responses(model.api.id)、ai-sdk/azure→Azure.configure({...}).responses(...)baseURL 缺失时显式抛错、ai-sdk/anthropic/ai-sdk/google/ai-sdk/amazon-bedrock/openrouter/ai-sdk-provider各自对应configure(...).model(...)OpenAI 兼容家族走OpenAICompatible.configure({ ..., provider, baseURL }).model(...)。文件内注释明确写道This is the only native adapter boundary that should construct canonical opencode-ai/llm request objects from opencodes session/AI SDK-shaped dataL183-L184——唯一的适配器边界这一表述与文档动态分支属于应用边界的原则严格一致。这组设计剔除了什么文档用一个否定清单锁定了反模式这是理解其取舍的关键不以Provider.make(...)作为核心抽象也不用它仅仅把 id 绑到 model 函数上——用 branded provider id 常量 普通导出的 facade 即可不做Deployment.define(...)除非未来例子逼出来常规执行路径上没有全局路由注册表模型可执行前不需要任何 import 副作用不重复携带provider.id对象——选中的 model 已经带有 provider id没有model(id, overrides)逃生口endpoint/auth/deployment 定制通过先配置 route 完成model/request 上没有 transport 覆盖HTTP SSE 与 WebSocket 是responses与responsesWebSocket这样的命名路由选择器没有独立的公开LLMClient.layerWithWebSocket运行时暴露一个携带可用 transport 能力的 client layer没有可执行的ModelRef可执行句柄是Model持久的 model 身份独立存在且不能独自执行。实现进展与开放问题文档末尾的实现清单截至文档撰写时显示主体迁移已完成已完成用Model替换可执行的ModelRefModel.route携带 route 值而非RouteID字符串route.model(id)返回附带 route 值、可执行的 model删除独立的Route.model(route, defaults, mapInput)helper从路由模型选择中移除 endpoint/auth 逃生口从Model中移除请求整形 defaults默认值归 route 或 requestLLMClient.prepare/stream/generate直接读request.model.route而不再查registeredRoute(...)Route.make(...)不再做全局注册route id 仅作诊断标签endpoint 建模为{ baseURL, path, query }Route.with(...)获得显式补丁语义未配置 transport 成为HttpTransport.sseJson这类可复用常量WebSocket 运行时收敛进单一LLMClient.layerOpenAI / Azure / Cloudflare拆为CloudflareAIGateway、CloudflareWorkersAI/ xAI / GitHub Copilot / OpenRouter / OpenAI 兼容家族 / Anthropic / Google / Amazon Bedrock 全部转为Provider.configure(options).model(id)的已配置 facade 形态packages/opencode/src/session/llm/native-request.ts改为在会话边界用显式 provider facade 调用构造可执行模型该条已完成与上文源码验证一致。未完成独立的持久 model 身份类型{ providerID, modelID }尚未定型测试尚未全面断言route 值由可执行 model 携带、边界基于选择路由兼容导出与过期文档的清理留到内部调用位点全部迁移之后Provider.define(...)helper 是否值得引入等两三个 provider 转换后再定。文档同样保留了开放问题值得跟踪默认 facade 是否只在全部必需配置都有惰性 env/credential 默认值时才暴露Endpoint.envBaseURL(...)与 env 凭证的类型化错误该在 compile/prepare 期还是 transport 执行期抛出Route.with(...)合并语义下如何显式清除继承值Provider.define(...)立即加还是继续用普通对象Auth 是否拆成放置策略/来源两层baseURL是否应改名为origin/urlPrefix以澄清 routepath会被追加。竞争形态与选型依据文档把选型放在相邻生态的坐标里注意这是设计文档的自我定位不是性能或质量对比结论AI SDK已配置 provider 实例暴露 provider 特定的 model 方法——本文 facade 的.responses(id)/.chat(id)形态与之同向Effect AI可执行模型携带 provider 需求、可由应用边界解析——本文Model 携带 route 值 边界解析的来源LiteLLM / opencode 配置动态providerID/modelID分支属于应用边界不属于类型化公开 API 或全局运行时解析器LangChain / LlamaIndex构造器式配置 model id 方便但这里刻意避免选模型的同时配置 endpoint/auth。最终的四行分工Route execution mechanics Provider facade configured route group Model selected executable model carrying route value App boundary explicit durable-config - typed-provider call延伸阅读设计文档原文packages/llm/example/call-sites.md可运行的端到端示例tutorial.tsRoute 客户端与 Compile 实现packages/llm/src/route/client.ts公开导出面packages/llm/src/route/index.tsProvider 抽象辅助类型packages/llm/src/provider.tsProvider.make目前定位为高级结构化 provider 定义 helper内置 provider 优先显式configure(options).model(id)facadeOpenAI facade 实例packages/llm/src/providers/openai.tsprovider 索引packages/llm/src/providers/index.ts请求入口LLM.request/generate/generateObjectpackages/llm/src/llm.tsopencode 会话层的应用边界packages/opencode/src/session/llm/native-request.ts包级公开 API、缓存策略与 provider 清单packages/llm/README.md架构细节见 packages/llm/AGENTS.md【免费下载链接】opencodeThe open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/openc/opencode创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表