图解 Pi AgentPi Agent, Illustratedv1.1.0
包Packages· pi-ai
演示Slides
pi-ai已完成Donepackages/ai · v1.1.0

pi-ai@earendil-works/pi-ai

统一的 LLM API:模型目录、认证、42 个 provider、事件流。Unified LLM API with automatic model discovery and provider configuration.

1.1.0
版本version
196
源文件src files
26,797
源码行src LOC
167
测试文件test files
1,678
测试用例test cases
9
导出路径export paths
依赖Depends ontelemetry被依赖Used byagent-corecoding-agentdurable

这个包是什么What this package ispi-ai 把 42 个 provider 统一成一个 Models 集合与一种事件流:provider 负责目录与认证,10 种对话 API 实现负责协议,二者都按需懒加载;任何失败都编码成流中的 error 事件,而不是异常。pi-ai unifies 42 providers behind one Models collection and one event stream: providers own catalogs and auth, 10 chat API implementations own the wire protocol, both load lazily, and every failure is an error event in the stream rather than an exception.

整个栈的最底层(仅依赖 pi-telemetry)。被 pi-agent-core、pi-coding-agent、pi-durable 使用。The bottom of the stack (depends only on pi-telemetry). Used by pi-agent-core, pi-coding-agent and pi-durable.

01pi-ai

入口与打包Entry points and bundling#

一句话结论Takeaway导入路径决定打包体积:根入口只给核心类型与工厂;/providers/<id> 只带一个 provider 的目录,SDK 首次请求时才加载;/providers/all 带全部目录但 SDK 依旧懒加载;/api/<id> 会立即加载 SDK。The import path decides bundle size: the root entry gives core types and factories; /providers/<id> brings one provider's catalog and loads its SDK on first request; /providers/all bundles every catalog but SDKs stay lazy; /api/<id> loads the SDK eagerly.

导入路径、得到的内容与加载成本Import paths, what you get, and what it costs to load
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
pi-ai entry points and bundling懒加载链Lazy-loading chain15 个 *.lazy.ts 包装15 *.lazy.ts wrappers@earendil-works/pi-ai无副作用side-effect freeTypeBox 立即加载eager TypeBox…/pi-ai/models最小smallest…/providers/<id>SDK 首次请求时加载SDK on first request…/providers/all目录全部打包all catalogs bundledSDK 仍懒加载SDKs still lazy…/api/<id>SDK 立即加载eager SDK…/oauth · /bun-oauth按需on demand…/utils/*按文件per file…/pi-ai/compat已弃用deprecatedproviders/anthropic.tscreateProvider({ api })anthropic-messages.lazy.tslazyApi(() => import(…))anthropic-messages.tsstream · streamSimple@anthropic-ai/sdk首次请求才加载loaded on first request导入路径IMPORT PATH得到什么WHAT YOU GET加载成本LOAD COST核心:类型、Type、createModels、createProvider、EventStream、工具函数core: types, Type, createModels, createProvider, EventStream, helpers仅 Models 运行时(不含 TypeBox 校验)Models runtime only (no TypeBox validation)一个 provider:模型目录 + 认证 + 懒加载的 APIone provider: catalog + auth + lazily-loaded APIbuiltinModels() · builtinProviders()(42 个)builtinModels() · builtinProviders() (42)直接调用某个 API 实现(stream / streamSimple)one API implementation directly (stream / streamSimple)OAuth 登录辅助(Node / Bun)OAuth login helpers (Node / Bun)event-stream · validation · transcript · overflow …旧的全局 API(stream / complete / getModel)超集old global API superset (stream / complete / getModel)

要点Key points

  • 每个 API 都有一个 *.lazy.ts:lazyApi(() => import("./x.ts")),provider 只引用这个包装。Every API has a *.lazy.ts: lazyApi(() => import("./x.ts")); providers only reference that wrapper.
  • /compat 保留旧的全局 API(stream、complete、getModel),已弃用。/compat keeps the old global API (stream, complete, getModel) and is deprecated.
  • bin: pi-ai 提供 CLI(OAuth 登录、模型列表)。bin: pi-ai ships a CLI (OAuth login, model listing).

工作原理How it works

  1. 应用导入 builtinModels() 或若干 xxxProvider()。The app imports builtinModels() or a few xxxProvider() factories.
  2. createProvider({ api: anthropicMessagesApi() }) 只保存懒包装。createProvider({ api: anthropicMessagesApi() }) stores only the lazy wrapper.
  3. 第一次 stream 时 import() 实现模块(及其 SDK),之后由模块缓存去重。The first stream call import()s the implementation (and its SDK); the module cache dedupes later loads.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/ai/package.jsonL13exports9 个导出路径nine export paths
packages/ai/src/api/lazy.tsL74lazyApi()懒加载 API 模块lazily loads an API module
packages/ai/src/api/anthropic-messages.lazy.tsL2anthropicMessagesApi懒包装示例an example lazy wrapper
packages/ai/src/providers/all.tsL136builtinProviders()全部内置 providerall built-in providers
packages/ai/src/providers/all.tsL184builtinModels()一行得到完整集合the whole collection in one call
02pi-ai

Models 与 ProviderModels and providers#

一句话结论TakeawayModels 是 provider 的集合,也是所有调用的门面:查模型同步完成,凭据存在 CredentialStore,动态目录通过 refresh() 先发布缓存、再联网更新。createProvider() 把一个或多个 API 实现、图像与分类器能力组装成 Provider。Models is a collection of providers and the facade for every call: model lookup is synchronous, credentials live in a CredentialStore, and dynamic catalogs refresh() by publishing the cache first and then updating from the network. createProvider() assembles one or more API implementations plus image and classifier capabilities into a Provider.

ModelsImpl、Provider 与目录刷新ModelsImpl, Provider, and catalog refresh
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
The Models collection and providersrefresh() · 每个 provider 的 refreshModels(ctx)refresh() · each provider’s refreshModels(ctx)builtinProviders()providers/all · 42 个providers/all · 42 providerscreateProvider({…})id · api | { [api]: … } · modelsfetchModels? · images? · classifiers? · auth?createModels({ providers })或 builtinModels()or builtinModels()«implements Models»ModelsImpl−providers: Map<id, Provider>−credentials: CredentialStore (InMemory)−store?: ModelsStore · authContext+getModel(provider, id) · getModels()+getAvailableModels() · checkAuth()+refresh({ allowNetwork, signal })+login(id, interaction) · logout(id)+stream · complete · streamSimple · completeSimple+streamDeferred · cancelDeferred+generateImages · classify (never reject)«interface»Provider<TApi>+id · name · baseUrl? · headers?+auth?: ApiKeyAuth | OAuthAuth …+getModels() · getAllModels()+refreshModels?(ctx)+stream · streamSimple+fetchDeferred? · cancelDeferred?+generateImages? · classify?dispatch(model, run)单一 api,或按 model.api 查表single api, or look up by model.api缺失 → error 流(不抛出)missing → error stream (no throw)读取 ModelsStoreread ModelsStorectx.stored.models先发布缓存publish cachedpublish({ update })allowNetwork?allowNetwork?否 / 已中止 → 结束no / aborted → stopfetchModels(ctx)网络获取目录fetch the catalog合并后发布merge & publish按 id 覆盖基线override baseline by id每个调用each callsProvider[]new*stream 时on streamgetModels() 永远同步:返回 基线目录 ∪ 动态目录;refresh 只改变动态部分getModels() stays synchronous: baseline ∪ dynamic models; refresh only swaps the dynamic part

要点Key points

  • 一个 provider 可以按 model.api 分派到多个 API(如 Fireworks 同时用 OpenAI 与 Anthropic 兼容 API)。One provider can dispatch to several APIs by model.api (e.g. Fireworks uses both OpenAI- and Anthropic-compatible APIs).
  • 缺少对应 API 时 dispatch() 返回一个以 error 结束的流,而不是抛出。With no matching API, dispatch() returns a stream that ends in error instead of throwing.
  • getModels() 只返回对话模型;图像与分类器模型通过 getAllModels() 查到。getModels() returns chat models only; image and classifier models are found via getAllModels().

工作原理How it works

  1. createModels({ providers }) 构造 ModelsImpl,以 id 为键注册 provider。createModels({ providers }) builds a ModelsImpl keyed by provider id.
  2. refresh() 对每个有 fetchModels 的 provider 调用 refreshModels(ctx)。refresh() calls refreshModels(ctx) on every provider with fetchModels.
  3. 动态模型按 (type, id) 覆盖基线目录,getModels() 始终同步返回合并结果。Dynamic models override baseline entries by (type, id); getModels() always returns the merge synchronously.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/ai/src/models.tsL250Models集合接口the collection interface
packages/ai/src/models.tsL390ModelsImpl实现the implementation
packages/ai/src/models.tsL150Providerprovider 接口the provider interface
packages/ai/src/models.tsL1041createProvider()组装 providerassembles a provider
packages/ai/src/models.tsL992createModels()构造集合builds the collection
03pi-ai

公共 APIPublic API#

一句话结论Takeaway9 个导出路径。根入口 src/index.ts 有 35 条 export 语句;其中最常用的是 Type。Nine export paths. The root entry src/index.ts has 35 export statements; the most used are Type.

导入路径Import path目标Target说明Notes
@earendil-works/pi-ai./dist/index.js核心:类型、Type、createModels、createProvider、事件流、工具函数core: types, Type, createModels, createProvider, event streams, helpers
@earendil-works/pi-ai/models./dist/models.js只有 Models 运行时Models runtime only
@earendil-works/pi-ai/compat./dist/compat.js旧全局 API(已弃用)old global API (deprecated)
@earendil-works/pi-ai/providers/*./dist/providers/*.js每个 provider 一个文件;all = 全部one file per provider; all = everything
@earendil-works/pi-ai/api/*./dist/api/*.js直接使用某个 API 实现(立即加载 SDK)one API implementation directly (eager SDK)
@earendil-works/pi-ai/utils/*./dist/utils/*.jsevent-stream、validation、overflow 等event-stream, validation, overflow …
@earendil-works/pi-ai/oauth./dist/oauth.jsOAuth 登录辅助OAuth login helpers
@earendil-works/pi-ai/bedrock-provider./dist/bedrock-provider.jsAmazon Bedrock(单独入口,依赖 AWS SDK)Amazon Bedrock (separate entry, AWS SDK)
@earendil-works/pi-ai/bun-oauth./dist/bun-oauth.jsBun 运行时的 OAuth 辅助OAuth helpers for Bun

package.json · exportsL13

04pi-ai

一次 streamSimple 调用One streamSimple call#

一句话结论Takeawaymodels.streamSimple() 同步返回一个外层流,真正的工作(找 provider、解析认证、加载 API 模块、发请求)在它背后异步进行;任何一步失败都会变成外层流的最后一个 error 事件。models.streamSimple() returns an outer stream synchronously while the real work (find the provider, resolve auth, load the API module, send the request) happens behind it; a failure at any step becomes the outer stream's final error event.

外层流同步返回,setup 在背后异步完成The outer stream returns now; setup completes behind it
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
models.streamSimple() end to endsetup()(异步,在外层流背后)setup() (async, behind the outer stream)models.streamSimple(model, context, options)normalizeContext(context)→ TranscriptContext(同步)→ TranscriptContext (sync)lazyStream(model, setup)立即返回外层流returns the outer stream now调用方开始迭代caller iteratesfor await … · result()requireChatProvider未知 provider → 抛出unknown provider → throwapplyAuth(model, opts)resolveProviderAuth()provider.streamSimpledispatch(model.api)lazyApi → import()首次加载 API 模块first load of the API moduleapi.streamSimple()SDK 请求 → 内层流SDK request → inner streamforwardStream(outer, inner)逐个 push 事件 → end(result)push each event → end(result)任一步骤抛出any step throws→ push({ type: "error", reason: "error" })done / error 消息done / error message写入 durationMs(1.1.0)gets durationMs (1.1.0)setup()resolveProviderAuth order:1 options.apiKey override2 stored credential (oauth refresh | api_key)3 ambient env (ANTHROPIC_API_KEY …)之后合并 headers → transformHeaders → env;then merge headers → transformHeaders → env;auth.baseUrl 存在时覆盖 model.baseUrlauth.baseUrl overrides model.baseUrlcatch约定:流式 API 从不 reject —— 所有失败都编码成流里的最终 error 事件,contract: streaming APIs never reject — every failure is encoded as the final error eventouter.result() 同样解析为带 stopReason 与 errorMessage 的 AssistantMessageouter.result() resolves to an AssistantMessage carrying stopReason + errorMessage1234567

要点Key points

  • 认证解析顺序:调用方的 apiKey 覆盖 → 已存凭据(OAuth 会先刷新)→ 环境变量。Auth resolution order: caller apiKey override → stored credential (OAuth refreshed first) → environment.
  • auth.baseUrl 会覆盖模型的 baseUrl(如企业网关);transformHeaders 最后执行。auth.baseUrl overrides the model's baseUrl (e.g. enterprise gateways); transformHeaders runs last.
  • forwardStream() 把内层事件逐个转发,最后用内层的 result() 结束外层流。forwardStream() forwards inner events one by one and ends the outer stream with the inner result().

工作原理How it works

  1. normalizeContext(context) 同步生成 TranscriptContext。normalizeContext(context) builds a TranscriptContext synchronously.
  2. lazyStream(model, setup) 立即返回外层 AssistantMessageEventStream。lazyStream(model, setup) immediately returns the outer AssistantMessageEventStream.
  3. setup():requireChatProvider → applyAuth → provider.streamSimple → dispatch → lazyApi 导入 → api.streamSimple。setup(): requireChatProvider → applyAuth → provider.streamSimple → dispatch → lazyApi import → api.streamSimple.
  4. 成功则转发;失败则 push 一个 { type: "error" } 并 end(message)。On success, forward; on failure, push { type: "error" } and end(message).

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/ai/src/models.tsL328ModelsImpl.streamSimple门面方法the facade method
packages/ai/src/models.tsL843applyAuth()认证、headers、baseUrlauth, headers, baseUrl
packages/ai/src/auth/resolve.tsL33resolveProviderAuth()凭据优先级credential precedence
packages/ai/src/api/lazy.tsL46lazyStream()外层流 + 异步 setupouter stream + async setup
packages/ai/src/api/lazy.tsL31forwardStream()转发并结束forward and end
packages/ai/src/utils/transcript.tsL30normalizeContext()规范化上下文normalize the context
05pi-ai

Provider 抽象Provider abstraction#

一句话结论Takeawaypi-ai 把 Provider(身份、认证、模型目录)与 API(线协议)分开:42 个 provider 复用少数几种 API 实现。pi-ai separates a Provider (identity, auth, model catalog) from an API (wire protocol): 42 providers reuse a handful of API implementations.

本节沿用第一版的图(原位于总览页),已按 v1.1.0 重新核对。This section keeps its first-edition diagram (formerly on the overview page), re-checked against v1.1.0.

Provider → API 实现 → 统一事件流Provider → API implementation → normalized event stream
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
Provider abstractionproviders/*.ts42 个内置 · createProvider()42 built-in · createProvider()api/*.lazy.ts10 种对话 API(KnownApi)10 chat APIs (KnownApi)AssistantMessageEventStream每个 API 都归一成这 12 种事件 · 失败也是事件,从不抛出every API normalizes to these 12 events · failures are events, never throwsrunLoop()streamFn(model, context)ModelRuntime实现 Models · registerProvider()implements Models · registerProvider()ant-lingbasetencerebrascloudflare-workers-aideepseekgroqhuggingfacemoonshotaimoonshotai-cnnvidiaqwen-token-planqwen-token-plan-cnqwen-token-plan-individualtogetherxiaomixiaomi-token-plan-amsxiaomi-token-plan-cnxiaomi-token-plan-sgpzaizai-coding-cnanthropickimi-codingminimaxminimax-cnvercel-ai-gatewaymetaopenaixaigoogleopenai-codexmistralgoogle-vertexamazon-bedrockradiustypesafecloudflare-ai-gatewayfireworksgithub-copilotopencodeopencode-goopenrouterazureModelsprovider / model 注册表provider · model registrygetModel(provider, id)checkAuth · getAuthprovider.streamSimple()×27openai-completions×11anthropic-messages×7openai-responses×2google-generative-ai×1azure-openai-responses×1openai-codex-responses×1mistral-conversations×1google-vertex×1bedrock-converse-stream×1pi-messagesstarttext_starttext_deltatext_endthinking_startthinking_deltathinking_endtoolcall_starttoolcall_deltatoolcall_enddoneerrorinterface Provider {id · name · baseUrl? · headers? · authgetModels() · stream() · streamSimple() }= provider 使用的对话 API(typesafe 只有分类器 API)= chat APIs a provider uses (typesafe has only a classifier API)streamSimple()lazyApi():首次流式调用时才 import 模块lazyApi(): module imported on its first stream callstream() · streamSimple()1调用call事件流回到 runLoopevent stream back to runLoop1顺序order234

要点Key points

  • 每个 provider 一个文件,用 createProvider() 创建,并在 providers/all.ts 中汇总。One file per provider, created with createProvider() and collected in providers/all.ts.
  • API 实现位于 api/,经 *.lazy.ts 中的 lazyApi() 包装:模块在第一次流式调用时才 import。API implementations live in api/, wrapped by lazyApi() in *.lazy.ts: a module is imported on its first stream call.
  • KnownApi 列出 10 种对话 API(另有 1 种图像 API、4 种分类器 API)。KnownApi lists 10 chat APIs (plus 1 image API and 4 classifier APIs).
  • 例:groq、deepseek 走 openai-completions;xai 走 openai-responses;openrouter 和 github-copilot 同时使用多种 API。e.g. groq and deepseek use openai-completions; xai uses openai-responses; openrouter and github-copilot use several APIs.

工作原理How it works

  1. 每个 API 导出 stream 与 streamSimple,返回 AssistantMessageEventStream。Every API exports stream and streamSimple, returning an AssistantMessageEventStream.
  2. 事件共 12 种:start、text_*、thinking_*、toolcall_*(各 start / delta / end)、done、error。There are 12 event types: start, text_*, thinking_*, toolcall_* (start / delta / end each), done, error.
  3. StreamFn 约定不抛异常:认证、模块加载或请求失败都编码为流中的 error 事件(见 lazyStream())。The StreamFn contract forbids throwing: auth, module-load and request failures become an error event in the stream (see lazyStream()).

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/ai/src/models.tsL1041createProvider()provider 工厂provider factory
packages/ai/src/providers/all.tsL4providers/all.ts42 个内置 providerthe 42 built-in providers
packages/ai/src/api/anthropic-messages.tsL928streamSimple一个 API 实现示例an example API implementation
packages/ai/src/types.tsL17KnownApi10 种对话 APIthe 10 chat APIs
packages/ai/src/utils/event-stream.tsL97AssistantMessageEventStream统一事件流the normalized stream
packages/agent/src/types.tsL20StreamFn不抛异常的约定the no-throw contract
06pi-ai

事件语法与 EventStreamEvent grammar and EventStream#

一句话结论Takeaway每个响应都是 start → (内容块的 start/delta*/end)* → done | error,每个事件都带当前 partial。EventStream 是一个极小的推拉队列:生产者 push,消费者 for await,result() 在终止事件时 resolve。Every response is start → (block start / delta* / end)* → done | error, and every event carries the current partial. EventStream is a tiny push/pull queue: producers push, consumers for await, and result() resolves on the terminal event.

AssistantMessageEvent 语法与 EventStream 内部AssistantMessageEvent grammar and EventStream internals
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
AssistantMessageEvent grammar (12 types)内容块 × n(按 contentIndex)content blocks × n (by contentIndex)EventStream<T, R>utils/event-stream.tsstartpartialtext_starttext_delta× n · deltatext_end完整块full blockthinking_startthinking_delta× n · deltathinking_end完整块full blocktoolcall_starttoolcall_delta× n · deltatoolcall_end完整块full blockdonestop | length | toolUse | deferrederrorerror | abortedpendingstoplengthtoolUseerroraborteddeferred生产者producerpush(event) · end(result?)queue: FifoQueue<T>waiting: FifoQueue<resolve>消费者consumerfor await (const e of s)result(): Promise<R>done / error 时 resolveresolves on done / errorreason每个事件都带 partial(当前的 AssistantMessage);toolcall_delta 的参数由 parseStreamingJson 增量解析every event carries partial (the AssistantMessage so far); toolcall_delta args are parsed incrementally by parseStreamingJsonStopReasonpending 只出现在 partial 上;deferred = 异步任务,稍后 streamDeferred(handle)pending only on partials; deferred = async job, fetch later with streamDeferred(handle)无人等待no waiter直接交付hand off排队等待parkisComplete(e) 为真后 done = true,后续 push 被忽略;AssistantMessageEventStream 在 done/error/end 时用 performance.now() 写入 durationMsonce isComplete(e) is true, done = true and later pushes are dropped; AssistantMessageEventStream stamps durationMs (performance.now) on done / error / endFifoQueue 用两个数组实现摊还 O(1) 出队FifoQueue uses two arrays for amortised O(1) dequeue

要点Key points

  • 7 种 StopReason:pending 只出现在 partial 上;deferred 表示异步任务,之后用 streamDeferred(handle) 取结果。Seven StopReasons: pending appears only on partials; deferred means an async job, fetched later with streamDeferred(handle).
  • 1.1.0:AssistantMessageEventStream 自动为最终消息写入 durationMs(单调时钟,从流创建开始计时)。1.1.0: AssistantMessageEventStream stamps durationMs on the final message (monotonic clock from stream creation).
  • 终止后 push 被忽略——迟到的事件不会污染已完成的消息。After termination, push is ignored — late events cannot corrupt a finished message.

工作原理How it works

  1. API 实现创建 AssistantMessageEventStream,边解析 SSE 边 push。API implementations create an AssistantMessageEventStream and push while parsing SSE.
  2. 有等待中的消费者就直接交付,否则进入 FIFO 队列。If a consumer is waiting, hand off directly; otherwise enqueue FIFO.
  3. done / error 时 isComplete 为真,result() 得到最终 AssistantMessage。On done / error, isComplete is true and result() yields the final AssistantMessage.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/ai/src/types.tsL788AssistantMessageEvent12 种事件12 event types
packages/ai/src/types.tsL456StopReason7 种停止原因seven stop reasons
packages/ai/src/utils/event-stream.tsL26EventStream推拉队列the push/pull queue
packages/ai/src/utils/event-stream.tsL97AssistantMessageEventStreamdurationMs 计时durationMs timing
07pi-ai

跨模型消息转换Cross-model message transform#

一句话结论Takeaway每次请求前,transformMessages() 把任意 provider 产生的历史改写成目标模型能接受的形式:降级图像、处理 thinking 签名、规范化工具调用 id、跳过失败消息、补齐孤立的工具结果。这就是会话中途换模型仍然可行的原因。Before every request, transformMessages() rewrites history produced by any provider into something the target model accepts: downgrade images, handle thinking signatures, normalize tool-call ids, skip failed messages, fill in orphaned tool results. This is why switching models mid-session works.

transformMessages 的六条规则The six rules of transformMessages
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
transformMessages: cross-model handoffMessage[]来自任意 provider 的历史history from any providertransformMessages(messages, model, normalizeToolCallId?)api/transform-messages.ts目标 API 的请求target API requestconvertMessages(…)用户消息user messagethinking 块thinking blocks工具调用 idtool-call ids失败的 assistantfailed assistant孤立工具调用orphaned tool callssystem 消息system messages模型不支持图像 → 图像换成文字占位model lacks image input → images become a text placeholder同一模型:保留签名;换模型:转成文本,空的丢弃;redacted 跨模型直接丢弃same model: keep signature; other model: become text, empty ones dropped; redacted dropped cross-modelnormalizeToolCallId:如 OpenAI 450+ 字符 id → Anthropic ^[a-zA-Z0-9_-]+$ ≤ 64;toolResult 同步改写normalizeToolCallId: e.g. 450+ char OpenAI ids → Anthropic ^[a-zA-Z0-9_-]+$ ≤ 64; toolResults remapped toostopReason 为 error / aborted 的消息整条跳过(不可回放)messages with stopReason error / aborted are skipped (not replayable)没有结果的调用 → 合成 isError 的 toolResult("No result provided")calls without a result → synthesize an isError toolResult ("No result provided")在工具调用与其结果之间出现时延后,保证结果紧跟调用held back while tool results are pending, so results stay adjacent to their calls按顺序应用于整个applied in order to the历史;每次请求都会执行whole history, every request结果:在 Claude 中途切到 GPT(或反之)时,同一份会话仍能被新 provider 接受result: switch from Claude to GPT (or back) mid-session and the same history is still accepted by the new provider123456

要点Key points

  • thinking 签名只在同一模型之间有效;跨模型时 thinking 转成普通文本,redacted thinking 直接丢弃。Thinking signatures are only valid on the same model; across models thinking becomes plain text and redacted thinking is dropped.
  • 工具调用 id 的映射同时应用到对应的 toolResult,保证配对关系不被破坏。The tool-call id map is applied to matching toolResults too, so pairs stay intact.
  • 没有结果的工具调用会得到合成的 isError 结果——多数 API 要求每个调用都有结果。Tool calls without a result get a synthesized isError result — most APIs require one per call.

工作原理How it works

  1. 各 API 的 convertMessages() 先调用 transformMessages(messages, model, normalizeToolCallId)。Each API's convertMessages() first calls transformMessages(messages, model, normalizeToolCallId).
  2. 然后才把统一消息映射成该 API 的请求格式。Only then are unified messages mapped to that API's request format.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/ai/src/api/transform-messages.tsL64transformMessages()跨模型转换cross-model transform
packages/ai/src/utils/transcript.tsL150getToolStateChanges()工具声明的增删tool declaration diffs
packages/ai/src/utils/validation.tsL317validateToolArguments()TypeBox 参数校验TypeBox argument validation
08pi-ai

一个 API 实现:anthropic-messagesOne API implementation: anthropic-messages#

一句话结论Takeaway一个 API 实现的全部工作就是:把 SSE 事件翻译成统一事件,维护内容块的 partial,最后映射停止原因并计算成本。streamSimple 额外负责把统一的 reasoning 等级换算成该 API 的 thinking 参数。An API implementation's whole job: translate SSE events into unified events, maintain content-block partials, then map the stop reason and compute cost. streamSimple additionally turns the unified reasoning level into this API's thinking parameters.

Anthropic SSE → 统一事件、停止原因与 thinking 配置Anthropic SSE → unified events, stop reasons and thinking
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
anthropic-messages: SSE → eventsmapStopReason()streamSimple() 的 thinking 配置thinking in streamSimple()message_startstartcontent_block_start · texttext_startcontent_block_start · thinkingthinking_start… · redacted_thinkingthinking_startcontent_block_start · tool_usetoolcall_startcontent_block_deltatext_deltathinking_deltatoolcall_deltacontent_block_stop*_endmessage_deltamessage_stopdonestoplengthtoolUseerrorAnthropic SSEANTHROPIC SSE处理HANDLING发出的事件EMITTED记录 id · usagerecord id · usage新建 text 块new text block新建 thinking 块new thinking block"[Reasoning redacted]" + 签名"[Reasoning redacted]" + signature新建 toolCall,partialJson = ""new toolCall, partialJson = ""text / thinking / input_json / signature删除 partialJson,完成块delete partialJson, finish blockstop_reason → mapStopReason · usagecalculateCost(model, usage)end_turn · pause_turn · stop_sequence→max_tokens→tool_use→refusal · sensitive→!reasoningthinkingEnabled: false自适应模型adaptive modelsthinking: adaptive + effort(由 reasoning 映射)thinking: adaptive + effort (mapped from reasoning)其余模型other modelsadjustMaxTokensForThinking → clampMaxTokensToContextbudgetmin(budget, maxTokens − 1024)

要点Key points

  • 工具参数用 parseStreamingJson 增量解析,UI 可以在参数流式到达时就显示。Tool arguments are parsed incrementally with parseStreamingJson, so a UI can show them while they stream.
  • refusal 与 sensitive 被映射为 error,pause_turn 视为正常 stop。refusal and sensitive map to error; pause_turn is treated as a normal stop.
  • 请求经过 retryProviderRequest,并提供 onPayload / onResponse 钩子用于调试与改写。Requests go through retryProviderRequest and expose onPayload / onResponse hooks for debugging and rewriting.

工作原理How it works

  1. message_start → start;每个 content_block_start → *_start;delta → *_delta;stop → *_end。message_start → start; each content_block_start → *_start; deltas → *_delta; stop → *_end.
  2. message_delta 带 stop_reason 与 usage;结束时 calculateCost() 并 push done。message_delta carries stop_reason and usage; at the end calculateCost() and push done.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/ai/src/api/anthropic-messages.tsL571streamSSE → 事件SSE → events
packages/ai/src/api/anthropic-messages.tsL928streamSimplethinking 配置thinking configuration
packages/ai/src/api/anthropic-messages.tsL1613mapStopReason()停止原因映射stop-reason mapping
packages/ai/src/utils/provider-retry.tsL107retryProviderRequest()重试retries
09pi-ai

思考等级、预算与成本Thinking levels, budgets and cost#

一句话结论Takeaway统一的思考等级 off … max 按模型裁剪;token 型 API 用默认预算表换算,并保证 maxTokens 留出至少 1024 个回答 token、不超过上下文窗口。每次响应的 Usage 都按模型目录价格(含分层与 1h 缓存写入)计算美元成本。Unified thinking levels off … max are clamped per model; token-budget APIs convert them with a default budget table while keeping maxTokens at least 1024 answer tokens and within the context window. Every response's Usage is priced in USD from the catalog (tiers and 1h cache writes included).

思考等级、maxTokens 推导与成本公式Thinking levels, maxTokens derivation and the cost formula
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
Thinking levels, token budgets and costmaxTokens 的推导(token 型思考 API)deriving maxTokens (token-budget thinking APIs)«interface»Usage+input · output · cacheRead · cacheWrite+cacheWrite1h? (Anthropic only)+reasoning? (subset of output)+totalTokens+cost: { input · output · cacheRead cacheWrite · total } (USD)ModelThinkingLevel(getSupportedThinkingLevels / clampThinkingLevel)ModelThinkingLevel (getSupportedThinkingLevels / clampThinkingLevel)off不思考no thinkingminimal1,024 toklow2,048 tokmedium8,192 tokhigh16,384 tokxhigh需 thinkingLevelMap 声明only if thinkingLevelMap has itmax需 thinkingLevelMap 声明only if thinkingLevelMap has it预算 = DEFAULT_THINKING_BUDGETS(可被 options.thinkingBudgets 覆盖);不支持的等级被 clamp 到最近的可用等级budget = DEFAULT_THINKING_BUDGETS (override with options.thinkingBudgets); unsupported levels clamp to the nearest available one输入(估算)input (estimated)安全余量safety思考预算thinking budget回答 ≥ 1024answer ≥ 1024空闲freecontextWindowclampMaxTokensToContext:maxTokens ≤ contextWindow − 估算输入 − 4096;思考预算 ≤ maxTokens − MIN_ANSWER_TOKENSclampMaxTokensToContext: maxTokens ≤ contextWindow − estimated input − 4096; thinking budget ≤ maxTokens − MIN_ANSWER_TOKENSmodels.tscalculateCost(model, usage):inputTokens = input + cacheRead + cacheWriterates = highest tier with inputTokens > tier.inputTokensAbovecost.input = rates.input / 1e6 × inputcost.cacheWrite = (rates.cacheWrite × short + rates.input × 2 × cacheWrite1h) / 1e6cost.total = input + output + cacheRead + cacheWriteisContextOverflow(message, contextWindow)(utils/overflow.ts)识别各家“上下文过长”错误,供压缩重试isContextOverflow(message, contextWindow) (utils/overflow.ts) recognises each provider’s context-overflow error for compact-and-retryprovider-retry.ts:retryProviderRequest 对 429 / 5xx 重试,受 maxRetryDelayMs 约束provider-retry.ts: retryProviderRequest retries 429 / 5xx, bounded by maxRetryDelayMs

要点Key points

  • xhigh 与 max 只有在模型的 thinkingLevelMap 明确声明时才可用;null 表示该等级被禁用。xhigh and max exist only if the model's thinkingLevelMap declares them; null disables a level.
  • 分层定价:输入 token(含缓存)超过某层阈值时整次请求按该层计价。Tiered pricing: when input tokens (cache included) exceed a tier threshold, the whole request uses that tier.
  • isContextOverflow() 识别各家的“上下文过长”错误,coding-agent 据此压缩后重试。isContextOverflow() recognises each provider's context-overflow error; coding-agent compacts and retries on it.

工作原理How it works

  1. clampThinkingLevel(model, level) 找到最近的受支持等级。clampThinkingLevel(model, level) finds the nearest supported level.
  2. adjustMaxTokensForThinking + clampMaxTokensToContext 计算最终 maxTokens 与思考预算。adjustMaxTokensForThinking + clampMaxTokensToContext compute the final maxTokens and thinking budget.
  3. 响应结束时 calculateCost(model, usage) 填充 usage.cost。At the end of a response calculateCost(model, usage) fills usage.cost.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/ai/src/models.tsL1200calculateCost()成本计算cost calculation
packages/ai/src/models.tsL1224getSupportedThinkingLevels()可用思考等级available thinking levels
packages/ai/src/api/simple-options.tsL70DEFAULT_THINKING_BUDGETS默认预算表default budget table
packages/ai/src/api/simple-options.tsL18clampMaxTokensToContext()不超过上下文窗口stay within the context window
packages/ai/src/types.tsL433Usagetoken 与成本tokens and cost
packages/ai/src/utils/overflow.tsL137isContextOverflow()上下文溢出识别context-overflow detection
10pi-ai

图像与分类器Images and classifiers#

一句话结论Takeaway除对话流之外,Models 还有两种一次性 API:generateImages() 与 classify()。它们返回 Promise 但从不 reject——失败同样编码为结果里的 stopReason: "error"。Beyond chat streams, Models has two one-shot APIs: generateImages() and classify(). They return Promises but never reject — failures are again encoded as stopReason: "error" in the result.

generateImages() 与 classify()generateImages() and classify()
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
One-shot APIs: images and classifiersgenerateImages(model, context, opts)→ Promise<AssistantImages>classify(model, context, opts)→ Promise<ClassifierResult>Models不 reject:失败 = 结果里的never reject: failure =stopReason "error"stopReason "error" in resultopenrouterprovideropenrouter-imagesImageApiAssistantImagesoutput[] · usage? · stopReasontypesafe-system-onecloudflare-workers-ai-system-onellama-cpp-classifyopenai-decisionsImageModel(model.type = "image")不出现在 getModels(),用 getAllModels() 查找ImageModels (type "image") are not in getModels(); find them via getAllModels()无 generateImages 的 provider → 返回 error 结果providers without generateImages → error resulttypesafe · opencode · openrouter · vercel-ai-gatewaycloudflare-workers-ai无内置 provider · 通过 createProvider 注册no built-in provider · register via createProvideropenaiClassifierContext{ state: JSON, images?(1.1.0), questions: { [k]: choice | score | bool } }{ state: JSON, images? (1.1.0), questions: { [k]: choice | score | bool } }ClassifierResult{ answers: { choice·probabilities | score | probability }, usage?, stopReason }

要点Key points

  • 分类器 API 有 4 种;1.1.0 新增 openai-decisions,以及 ClassifierContext.images。There are four classifier APIs; 1.1.0 adds openai-decisions and ClassifierContext.images.
  • 问题有三种:choice(带概率分布)、score、bool,每个答案都带置信度或概率。Questions come in three kinds: choice (with a probability distribution), score and bool; each answer carries confidence or probability.

工作原理How it works

  1. createProvider({ images, classifiers }) 按 API 名注册实现。createProvider({ images, classifiers }) registers implementations by API name.
  2. models.classify(model, context) 走与流相同的认证路径,然后调用 provider。models.classify(model, context) follows the same auth path as streams, then calls the provider.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/ai/src/types.tsL669ClassifierContextstate · images · questionsstate · images · questions
packages/ai/src/types.tsL700ClassifierResultanswers · usageanswers · usage
packages/ai/src/types.tsL637AssistantImages图像结果image results
packages/ai/src/api/openai-decisions.tsL161openai-decisions1.1.0 新增new in 1.1.0
11pi-ai

扩展点Extension points#

createProvider()注册自定义 provider:任意 baseUrl、自己的目录与认证。Register a custom provider: any baseUrl, your own catalog and auth.
ProviderStreams实现 stream / streamSimple 即可接入一种新的线协议。Implement stream / streamSimple to add a new wire protocol.
Model.compat为 OpenAI / Anthropic 兼容服务声明差异(字段名、能力开关)。Declare quirks of OpenAI / Anthropic-compatible servers (field names, capability flags).
ApiKeyAuth · OAuth auth自定义登录与凭据解析;CredentialStore 可替换为持久化存储。Custom login and credential resolution; swap CredentialStore for persistent storage.
ModelsStore持久化动态目录,启动时先发布缓存。Persist dynamic catalogs and publish the cache at start-up.
onPayload · onResponse · transformHeaders在发送前检查或改写请求与响应。Inspect or rewrite requests and responses on the way.
12pi-ai

测试Tests#

一句话结论Takeaway测试以 provider 为单位组织:每个 API 实现都有流式、工具调用、停止原因与错误路径的测试,另有跨 provider 交接与成本计算测试。Tests are organised per provider: every API implementation has tests for streaming, tool calls, stop reasons and error paths, plus cross-provider handoff and cost tests.

另有 147 个测试文件未列出(共 167 个,1678 个用例)。147 more test files not listed (167 files, 1678 cases in total).

用例数 = 源码中 it( / test( 的出现次数(静态统计,不运行测试)。Cases = occurrences of it( / test( in the source (static count; tests were not run).

13pi-ai

交叉链接Cross-links#