跳转至

20. packages/ai:把多家模型服务压成同一种对话接口

这个模块干什么

packages/ai 把 provider、模型目录、认证和流式输出放在同一层,让上层不必直接面对各厂商不同的请求与事件格式。(packages/ai/src/models.ts:122-126) 它的核心输入是 Context,核心输出是带 usagestopReason 和内容块的 AssistantMessage。(packages/ai/src/types.ts:390-403;packages/ai/src/types.ts:477-481) 真正的运行时单位是 Provider:它拥有认证、模型列表和 stream/streamSimple 行为;Models 负责按模型所属 provider 路由。(packages/ai/src/models.ts:66-120;packages/ai/src/models.ts:122-126) 根入口只导出无副作用的核心类型和工具;内置 provider、具体 API 实现及旧全局 API 分别走独立子路径,避免默认把全部目录和 SDK 打进包。(packages/ai/src/index.ts:4-8)

核心文件速查

文件 一句话职责 必读优先级
src/types.ts 定义统一的 Model、消息、工具、用量和流事件协议。packages/ai/src/types.ts:16-32 ⭐⭐⭐
src/utils/event-stream.ts 实现可异步迭代且可取最终结果的 AssistantMessageEventStreampackages/ai/src/utils/event-stream.ts:4-83 ⭐⭐⭐
src/models.ts 把 provider 集合、认证、动态模型和请求派发组合成 Modelspackages/ai/src/models.ts:122-187 ⭐⭐⭐
src/api/ 放各协议的请求转换和流解析;每个文本 API 模块实现统一双流函数。packages/ai/src/types.ts:221-232 ⭐⭐⭐
src/api/lazy.ts 把异步模块加载和认证准备包成同步返回的错误可见流。packages/ai/src/api/lazy.ts:41-74 ⭐⭐
src/api/transform-messages.ts 跨模型回放时降级图片、思考块和工具调用 ID,并补齐孤立工具结果。packages/ai/src/api/transform-messages.ts:64-222 ⭐⭐
src/compat.ts 保留旧式全局 registry、环境变量注入和 complete() 接口。packages/ai/src/compat.ts:1-10 ⭐⭐
src/legacy-api-aliases.ts 用过时别名把旧的按厂商 streamXxx 函数转到懒加载 API。packages/ai/src/legacy-api-aliases.ts:19-108
src/index.ts 根入口的最小、无副作用导出面。packages/ai/src/index.ts:4-47 ⭐⭐⭐

精读

1. types.ts:先固定共同语言

先读 KnownApi,再读 Api:前者列出内置协议标识,后者额外允许任意字符串,所以自定义 provider 不会被类型系统关在门外。(packages/ai/src/types.ts:16-28) KnownImagesApiImagesApi 是图像生成的平行命名空间,文本聊天与图像生成不是同一套调用接口。(packages/ai/src/types.ts:30-32;packages/ai/src/types.ts:234-246) KnownProvider 收集内置 provider ID,而 ProviderId 同样允许任意字符串;因此运行时可以注册没有生成目录的新 provider。(packages/ai/src/types.ts:34-77) ThinkingLevel 是上层只需认识的六档推理强度,ModelThinkingLevel 额外提供 off。(packages/ai/src/types.ts:79-81) CacheRetention 只有 noneshortlong 三种偏好,具体怎样变成厂商参数由 API adapter 决定。(packages/ai/src/types.ts:100-103) Transport 暴露 ssewebsocketwebsocket-cachedauto,不支持多传输的 provider 可以忽略它。(packages/ai/src/types.ts:103-104;packages/ai/src/types.ts:120-124) ProviderEnv 是单次请求的 provider 配置覆盖值,ProviderHeaders 允许用 null 删除底层默认头。(packages/ai/src/types.ts:105-108;packages/ai/src/types.ts:147-154)

StreamOptions 是所有协议共享的请求壳:温度、输出上限、中止信号和显式 API key 都在这里。(packages/ai/src/types.ts:115-120) onPayload 可以观察或替换真正送出的厂商 payload,返回 undefined 则保留原 payload。(packages/ai/src/types.ts:136-140) onResponse 在 HTTP 响应到达、正文流还没消费前运行,因此适合采集状态码和响应头。(packages/ai/src/types.ts:141-145) headers 的调用方值覆盖 provider 默认头;Bedrock 例外会在签名之前以中间件注入,且保留安全敏感头。(packages/ai/src/types.ts:147-153) timeoutMsmaxRetriesmaxRetryDelayMs 让上层保持同一配置名,adapter 再转给各 SDK 或重试包装器。(packages/ai/src/types.ts:156-178) metadataenv 都是可选的扩展槽:前者交给 provider 选择性消费,后者优先于环境进程变量。(packages/ai/src/types.ts:180-190)

ApiOptionsMap 把 API ID 映射到各自完整 options 类型;已知 API 保留精确类型,未知字符串退回通用 StreamOptions。(packages/ai/src/types.ts:195-219) 这就是 Models.stream() 能同时接受统一调用形状与具体厂商选项的类型基础。(packages/ai/src/models.ts:63-64;packages/ai/src/models.ts:173-186) SimpleStreamOptions 把跨厂商可比较的 reasoning 和可选预算放到统一入口。(packages/ai/src/types.ts:296-301)

简化签名是 StreamFunction(model, context, options?) -> AssistantMessageEventStream。(packages/ai/src/types.ts:303-315) 它的失败约定很关键:调用后发生的请求、模型或运行时失败应该写进返回流,而不是从函数抛到调用点。(packages/ai/src/types.ts:305-310) 对应地,失败终止要给出 stopReason: "error""aborted" 以及 errorMessage。(packages/ai/src/types.ts:307-310)

内容块只有三类:TextContentThinkingContentToolCall。(packages/ai/src/types.ts:329-357) ThinkingContent.thinkingSignature 是不透明的可回放上下文,而 redacted 表示文本被过滤、签名仍可回传。(packages/ai/src/types.ts:335-343) ToolCall 带稳定的 id、名称、参数对象,并预留 Google 的 thoughtSignature。(packages/ai/src/types.ts:351-357) Usage 将输入、输出、读写缓存、推理 token 和分项成本统一到同一张账。(packages/ai/src/types.ts:359-380) StopReason 只允许正常结束、长度截断、工具调用、错误和中止五种语义。(packages/ai/src/types.ts:382-382)

UserMessage 可以是纯字符串,也可以是文字与 base64 图片块数组。(packages/ai/src/types.ts:384-388) AssistantMessage 记录请求的 API/provider/model、可选的实际响应模型和响应 ID、统一用量及终止原因。(packages/ai/src/types.ts:390-403) ToolResultMessage 支持文字和图片结果;addedToolNames 给支持延迟工具加载的 adapter 一个加载点。(packages/ai/src/types.ts:405-420) Message 因而是用户、助手、工具结果三种消息的联合类型。(packages/ai/src/types.ts:423-423) Context 只含系统提示、消息历史和工具定义,故可被任何 adapter 重写成对方协议。(packages/ai/src/types.ts:470-481)

2. types.ts + utils/event-stream.ts:流是上层真正消费的协议

AssistantMessageEvent 规定一条流先发 start,随后按块发文本、思考或工具调用的 start/delta/end,最后只以 doneerror 收束。(packages/ai/src/types.ts:483-503) contentIndex 不是装饰:同一上游 chunk 可以交错输出不同块,消费者用它把增量归回 partial.content 的正确位置。(packages/ai/src/types.ts:491-503) done 只能携带 stoplengthtoolUseerror 只能携带 abortederror。(packages/ai/src/types.ts:502-503)

EventStream<T, R> 同时是 AsyncIterable<T>,并内部保存队列、等待者和最终结果 Promise。(packages/ai/src/utils/event-stream.ts:3-19) push() 碰到终止事件会先解析最终结果,再把事件交给等待者或队列。(packages/ai/src/utils/event-stream.ts:21-36) end() 唤醒所有等待中的异步迭代器;result() 返回同一份最终结果 Promise。(packages/ai/src/utils/event-stream.ts:38-66) AssistantMessageEventStreamdoneerror 定义成完成事件,分别提取 messageerror。(packages/ai/src/utils/event-stream.ts:69-83) 所以 for awaitawait stream.result() 可以并用:前者画增量 UI,后者拿可写回历史的完整 AssistantMessage。(packages/ai/src/utils/event-stream.ts:50-66;packages/ai/src/utils/event-stream.ts:69-83)

3. models.ts:把 provider 做成可组合的运行时集合

Provider 是模型调用的最小运行时边界:ID、展示名、可选 endpoint/头、认证、模型读、刷新和两种流调用都归它。(packages/ai/src/models.ts:66-120) 每个 provider 都必须声明认证语义,哪怕它依赖 AWS profile、ADC 或无 key 的本地服务。(packages/ai/src/models.ts:82-89) 静态 provider 的 getModels() 返回目录;动态 provider 则在 refreshModels() 后给出最近一次列表。(packages/ai/src/models.ts:91-104) Models 聚合 provider,公开同步模型查询、认证检查、登录登出、两种流与 complete 快捷调用。(packages/ai/src/models.ts:127-187) ModelsImpl 默认使用内存 credential store、内存 models store 与默认 auth context,因此调用方可按需注入持久实现。(packages/ai/src/models.ts:218-228) getModels() 对单个或全部 provider 都是 best-effort;失控的 getModels() 会被吞掉并视作无模型。(packages/ai/src/models.ts:250-270) refresh() 并发刷新可刷新的 provider,错误收集在结果对象中,并在失败后尝试离线恢复缓存目录。(packages/ai/src/models.ts:276-328) getAvailable() 先检查认证,再应用 provider 可选的 credential 专属模型过滤。(packages/ai/src/models.ts:394-409)

请求前 applyAuth() 先解析 provider/model 认证,再按「显式 key、认证头、显式 headers、transformHeaders」的顺序组装请求。(packages/ai/src/models.ts:463-486) 认证还可以返回覆盖 baseUrl,所以最终送给 provider 的 requestModel 可能是复制后换 endpoint 的模型。(packages/ai/src/models.ts:475-486) transformHeaders 在集合层消费并从 provider options 中剥离,具体 adapter 不必认识它。(packages/ai/src/models.ts:477-484) stream()streamSimple() 都用 lazyStream() 包装异步认证准备,随后才调拥有该模型的 provider。(packages/ai/src/models.ts:489-526) complete()completeSimple() 只是对应流的 .result(),并没有另一套非流式协议。(packages/ai/src/models.ts:504-525)

createProvider() 接受静态 models 和单一或按 API 分派的 ProviderStreams。(packages/ai/src/models.ts:533-548) 动态目录会覆盖同 ID 的静态模型;刷新先恢复 provider 专属存储,允许联网时再取新列表并写回。(packages/ai/src/models.ts:556-617) 混合 API provider 用 model.api 选 streams;没有对应实现时,返回的是一个错误流而非同步抛错。(packages/ai/src/models.ts:570-587) hasApi() 是从动态查得的 Model<Api> 收窄到 Model<具体 API> 的运行时类型守卫。(packages/ai/src/models.ts:625-637) calculateCost() 支持输入阈值价阶,也单独处理 Anthropic 一小时缓存写入的双倍基础输入费率。(packages/ai/src/models.ts:639-658)

Model<TApi> 是纯数据:ID、显示名、协议、provider、endpoint、推理能力、输入模态、成本、上下文窗口和输出上限都在对象上。(packages/ai/src/types.ts:748-765) 因此模型可安全被目录、会话或配置层保存,真实网络行为始终由 provider 的 streams 提供。(packages/ai/src/types.ts:748-776;packages/ai/src/models.ts:113-120) headers 是模型级默认请求头,compat 则按 API 类型收窄为 OpenAI Completions、Responses、Anthropic 或 Bedrock 的兼容设置。(packages/ai/src/types.ts:765-775) 模型的 thinkingLevelMap 可将 pi 的统一档位映射为厂商值,null 明确声明该档不支持。(packages/ai/src/types.ts:755-760) getSupportedThinkingLevels() 只在 reasoning 为真时开放标准档位,xhigh/max 必须有显式非空映射。(packages/ai/src/models.ts:661-672) clampThinkingLevel() 在请求档不可用时,先向更高档、再向更低档搜索最近的可用档位。(packages/ai/src/models.ts:674-693) 这解释了为什么上层保存的是统一 ThinkingLevel,而不是把某一家 API 的 effort 字符串写进 agent 状态。(packages/ai/src/types.ts:79-81;packages/ai/src/types.ts:755-760)

4. api/:把共同消息翻译到 wire protocol

ProviderStreams 要求每个文本 API 模块都提供 stream()streamSimple();懒加载包装器和 provider factory 因而能把模块当值传递。(packages/ai/src/types.ts:221-232) 目录里的协议实现包括 Anthropic Messages、OpenAI Completions/Responses/Codex、Google Generative AI/Vertex、Mistral、Bedrock、Azure、pi-messages 等。(packages/ai/src/compat.ts:178-189) 各 .lazy.ts 文件把对应实现交给 lazyApi(),这样 provider 工厂不在构造时导入厂商 SDK。(packages/ai/src/compat.ts:13-22;packages/ai/src/api/lazy.ts:63-74) lazyStream() 立即返回外层流,异步执行认证或模块加载;准备失败会转成统一 error 事件和最终错误消息。(packages/ai/src/api/lazy.ts:41-61) forwardStream() 逐个转发内层事件,并在源流具有 result() 时把最终结果同步给外层。(packages/ai/src/api/lazy.ts:25-39)

图像生成与文本对话共用模型/认证理念,但协议接口是 ProviderImages.generateImages(),其结果是一次性 Promise<AssistantImages>。(packages/ai/src/types.ts:234-246;packages/ai/src/types.ts:434-444) ImagesOptions 也提供 key、signal、headers、payload/response hook、超时、重试、metadata 与 provider-scoped env。(packages/ai/src/types.ts:248-292) 这意味着不要把图像调用硬塞进 AssistantMessageEventStream;它没有文本聊天的块级事件序列。(packages/ai/src/types.ts:234-246;packages/ai/src/types.ts:491-503) 图像 API 的失败语义由 AssistantImages.stopReasonerrorMessage 承担,仍沿用统一的 stop 概念。(packages/ai/src/types.ts:432-443) ImagesModel 复用聊天模型的大部分元数据,但以 output 能力替换聊天侧的 reasoning、上下文和最大输出 token 字段。(packages/ai/src/types.ts:778-783)

transformMessages() 是跨 provider 回放的共同前处理。(packages/ai/src/api/transform-messages.ts:64-75) 对不支持视觉的模型,它把用户图片与工具结果图片替成单个文字占位,且避免重复连续占位。(packages/ai/src/api/transform-messages.ts:12-33;packages/ai/src/api/transform-messages.ts:35-57) 同 provider、同 API、同 model 的助手思考块会原样保留;跨模型的普通思考会降为文本,红acted 思考则被丢弃。(packages/ai/src/api/transform-messages.ts:92-117) 跨模型重放时会删除工具调用的 thought signature,并让目标 adapter 有机会规范工具 ID。(packages/ai/src/api/transform-messages.ts:127-145) 它跳过已错误或中止的助手消息,避免把不完整回合重新发送给厂商。(packages/ai/src/api/transform-messages.ts:189-197) 如果助手调用工具后没有对应结果,它会在下一条助手或用户消息前插入 No result provided 的错误工具结果。(packages/ai/src/api/transform-messages.ts:158-180;packages/ai/src/api/transform-messages.ts:182-220)

simple-options.tsstreamSimple() 的共享实现零件。(packages/ai/src/api/simple-options.ts:1-10) 它会用估算上下文 token 与 4096 的安全余量夹住最大输出 token,至少保留 1。(packages/ai/src/api/simple-options.ts:12-19) buildBaseOptions() 只复制各协议共同字段,使每个 streamSimple() 只处理本厂商的 reasoning 映射。(packages/ai/src/api/simple-options.ts:21-45) 它把 xhighmax 先压为 high,并为预算型 thinking 计算输出与思考 token 的分配。(packages/ai/src/api/simple-options.ts:47-76)

5. compat.ts:旧 API 仍可跑,但边界被明确隔离

文件顶部直接说明:这是临时入口,为旧的全局 stream()/complete()、API registry、环境 key 注入和生成目录读取保留行为。(packages/ai/src/compat.ts:1-10) 新代码应使用 createModels() 与 provider factory;compat 计划随 coding-agent 的迁移移除。(packages/ai/src/compat.ts:7-10) 它重导出 API 懒包装、环境 key、图像接口、根入口、旧别名和内置图像注册。(packages/ai/src/compat.ts:13-29) getModelgetModelsgetProviders 是生成目录读取函数的 deprecated 重命名。(packages/ai/src/compat.ts:62-69)

全局 apiProviderRegistry 以 API 字符串为键保存包装后的 stream/streamSimple。(packages/ai/src/compat.ts:83-100) 包装函数先校验 model.api 与注册 API 一致,防止把 OpenAI 模型交到 Anthropic adapter。(packages/ai/src/compat.ts:102-124) registerApiProvider() 可附带 sourceIdunregisterApiProviders() 只删除匹配来源的注册项。(packages/ai/src/compat.ts:126-154) registerFauxProvider() 为测试生成唯一来源 ID,并把注销能力交还给调用者。(packages/ai/src/compat.ts:160-176) 内置 API 只在该 ID 未被已有注册覆盖时注册,因此测试或扩展可先装替代实现。(packages/ai/src/compat.ts:178-213)

compat 的 withEnvApiKey() 仅当调用方没有显式 key 时查环境;<authenticated> 这类环境认证标记不会当真 key 填入。(packages/ai/src/compat.ts:215-230) stream() 优先把内置模型送给其 builtin provider;否则才按全局 API registry 分派。(packages/ai/src/compat.ts:236-264) Cloudflare 在缺少已解析认证时会走 compatModels.stream(),让 provider 层补齐其特殊认证和 endpoint 处理。(packages/ai/src/compat.ts:255-263) complete()streamSimple()completeSimple() 分别只是该分派链的薄包装。(packages/ai/src/compat.ts:266-298)

6. legacy-api-aliases.ts:兼容的是旧函数名,不是旧实现

文件先创建每种懒 API 的 streams 值,再将其字段投射成旧函数名。(packages/ai/src/legacy-api-aliases.ts:19-26) streamAnthropicstreamSimpleAnthropic 分别指向 anthropicMessagesApi() 的两个流函数,并保留准确 options 类型。(packages/ai/src/legacy-api-aliases.ts:28-37) Azure、Google、Vertex、Mistral、OpenAI Codex、OpenAI Completions 和 OpenAI Responses 都以同样方式提供 deprecated 别名。(packages/ai/src/legacy-api-aliases.ts:39-108) 这些别名仍是懒包装,所以迁移旧调用点不会立即把厂商 SDK 装进根 bundle。(packages/ai/src/legacy-api-aliases.ts:1-26;packages/ai/src/api/lazy.ts:63-74)

7. index.ts:让上层只拿到需要的公共积木

根入口导出 TypeBox 的 TypeStaticTSchema,以便上层用同一 schema 描述工具参数。(packages/ai/src/index.ts:1-2) 它只 type-export 各 API 的具体 options,避免 type 引用变成运行时 SDK 依赖。(packages/ai/src/index.ts:9-20) 根入口同时导出 API 懒加载工具、认证 contracts、Models、models store、faux provider、会话资源、基础类型和关键 utils。(packages/ai/src/index.ts:15-47) 它不从根路径导出内置 provider factory、生成目录、API registry、OAuth 实现或 compat;这些都必须显式选子路径。(packages/ai/src/index.ts:4-8) 生成文件 models.generated.tsimage-models.generated.ts 不应作为阅读主线;它们由 provider 目录和图像目录的入口按需读取。(packages/ai/src/providers/all.ts:1-4;packages/ai/src/providers/openrouter-images.ts:1-20)

紧接着读 packages/agent 时,agent loop 直接消费根入口的 AssistantMessageContextEventStreamToolResultMessagevalidateToolArguments。(packages/ai/src/index.ts:33-47;packages/agent/src/agent-loop.ts:6-12) 这些类型配合 Models.streamSimple 时,后者已经完成认证、headers 合并和 provider 派发,并返回相同的流协议。(packages/ai/src/models.ts:463-486;packages/ai/src/models.ts:512-526)

数据流

一条推荐的新调用链如下,重点是把「选择模型」和「解析厂商协议」分离:

createModels() + providerFactory()
  -> models.setProvider(provider)
  -> models.getModel(providerId, modelId)
  -> models.streamSimple(model, Context, options)
  -> lazyStream() 等待认证与 provider dispatch
  -> Provider.streamSimple()
  -> lazyApi() 首次加载具体 api/*.ts
  -> 厂商 SDK / SSE / 事件流
  -> AssistantMessageEventStream 统一事件
  -> agent loop / UI 迭代事件,再 await result()

createModels() 只实例化集合;provider 的注册由 setProvider()provider.id 覆盖或加入。(packages/ai/src/models.ts:218-240;packages/ai/src/models.ts:529-531) provider factory 最终调用 createProvider(),将目录、认证和一套或多套 API streams 装成 Provider。(packages/ai/src/models.ts:533-556) Models.streamSimple() 先确认 provider 存在,再调用 applyAuth(),随后把处理后的模型与 options 交给 provider。(packages/ai/src/models.ts:455-486;packages/ai/src/models.ts:512-517) lazyStream() 保证即使认证或动态 import 失败,调用方也先拿到一个可消费流,并从流中看到错误。(packages/ai/src/api/lazy.ts:41-61) provider 的 streams 若来自 .lazy.tslazyApi() 到第一次请求才加载真正模块。(packages/ai/src/api/lazy.ts:63-74) 具体 adapter 把 Context 重写为厂商 payload,并把上游增量翻译为 AssistantMessageEvent。(packages/ai/src/types.ts:477-503;packages/ai/src/types.ts:221-232) 消费者用 contentIndex 维护部分消息,收到 doneerror 后从 .result() 获得最终 AssistantMessage。(packages/ai/src/types.ts:491-503;packages/ai/src/utils/event-stream.ts:64-83)

自测

  1. 为什么 ApiProviderId 都不是封闭联合类型?
  2. stream()complete() 的关系是什么,失败最终在哪里出现?
  3. contentIndex 为什么不能省略,UI 为什么不能假定同一块事件连续?
  4. Models.applyAuth() 在 provider adapter 之前合并了哪些请求信息?
  5. 为什么新代码不应从根入口导入 compat 的旧全局 API?