这个包是什么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.
入口与打包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.
要点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),已弃用。/compatkeeps the old global API (stream,complete,getModel) and is deprecated.bin: pi-ai提供 CLI(OAuth 登录、模型列表)。bin: pi-aiships a CLI (OAuth login, model listing).
工作原理How it works
- 应用导入
builtinModels()或若干xxxProvider()。The app importsbuiltinModels()or a fewxxxProvider()factories. createProvider({ api: anthropicMessagesApi() })只保存懒包装。createProvider({ api: anthropicMessagesApi() })stores only the lazy wrapper.- 第一次
stream时import()实现模块(及其 SDK),之后由模块缓存去重。The firststreamcallimport()s the implementation (and its SDK); the module cache dedupes later loads.
关键文件Key files · v1.1.0
| 文件File | 符号Symbol | 作用Role |
|---|---|---|
| packages/ai/package.jsonL13 | exports | 9 个导出路径nine export paths |
| packages/ai/src/api/lazy.tsL74 | lazyApi() | 懒加载 API 模块lazily loads an API module |
| packages/ai/src/api/anthropic-messages.lazy.tsL2 | anthropicMessagesApi | 懒包装示例an example lazy wrapper |
| packages/ai/src/providers/all.tsL136 | builtinProviders() | 全部内置 providerall built-in providers |
| packages/ai/src/providers/all.tsL184 | builtinModels() | 一行得到完整集合the whole collection in one call |
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.
要点Key points
- 一个 provider 可以按
model.api分派到多个 API(如 Fireworks 同时用 OpenAI 与 Anthropic 兼容 API)。One provider can dispatch to several APIs bymodel.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 viagetAllModels().
工作原理How it works
createModels({ providers })构造ModelsImpl,以 id 为键注册 provider。createModels({ providers })builds aModelsImplkeyed by provider id.refresh()对每个有fetchModels的 provider 调用refreshModels(ctx)。refresh()callsrefreshModels(ctx)on every provider withfetchModels.- 动态模型按 (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.tsL250 | Models | 集合接口the collection interface |
| packages/ai/src/models.tsL390 | ModelsImpl | 实现the implementation |
| packages/ai/src/models.tsL150 | Provider | provider 接口the provider interface |
| packages/ai/src/models.tsL1041 | createProvider() | 组装 providerassembles a provider |
| packages/ai/src/models.tsL992 | createModels() | 构造集合builds the collection |
公共 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/*.js | event-stream、validation、overflow 等event-stream, validation, overflow … |
@earendil-works/pi-ai/oauth | ./dist/oauth.js | OAuth 登录辅助OAuth login helpers |
@earendil-works/pi-ai/bedrock-provider | ./dist/bedrock-provider.js | Amazon Bedrock(单独入口,依赖 AWS SDK)Amazon Bedrock (separate entry, AWS SDK) |
@earendil-works/pi-ai/bun-oauth | ./dist/bun-oauth.js | Bun 运行时的 OAuth 辅助OAuth helpers for Bun |
一次 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.
要点Key points
- 认证解析顺序:调用方的
apiKey覆盖 → 已存凭据(OAuth 会先刷新)→ 环境变量。Auth resolution order: callerapiKeyoverride → stored credential (OAuth refreshed first) → environment. auth.baseUrl会覆盖模型的baseUrl(如企业网关);transformHeaders最后执行。auth.baseUrloverrides the model'sbaseUrl(e.g. enterprise gateways);transformHeadersruns last.forwardStream()把内层事件逐个转发,最后用内层的result()结束外层流。forwardStream()forwards inner events one by one and ends the outer stream with the innerresult().
工作原理How it works
normalizeContext(context)同步生成TranscriptContext。normalizeContext(context)builds aTranscriptContextsynchronously.lazyStream(model, setup)立即返回外层AssistantMessageEventStream。lazyStream(model, setup)immediately returns the outerAssistantMessageEventStream.setup():requireChatProvider→applyAuth→provider.streamSimple→dispatch→lazyApi导入 →api.streamSimple。setup():requireChatProvider→applyAuth→provider.streamSimple→dispatch→lazyApiimport →api.streamSimple.- 成功则转发;失败则 push 一个
{ type: "error" }并end(message)。On success, forward; on failure, push{ type: "error" }andend(message).
关键文件Key files · v1.1.0
| 文件File | 符号Symbol | 作用Role |
|---|---|---|
| packages/ai/src/models.tsL328 | ModelsImpl.streamSimple | 门面方法the facade method |
| packages/ai/src/models.tsL843 | applyAuth() | 认证、headers、baseUrlauth, headers, baseUrl |
| packages/ai/src/auth/resolve.tsL33 | resolveProviderAuth() | 凭据优先级credential precedence |
| packages/ai/src/api/lazy.tsL46 | lazyStream() | 外层流 + 异步 setupouter stream + async setup |
| packages/ai/src/api/lazy.tsL31 | forwardStream() | 转发并结束forward and end |
| packages/ai/src/utils/transcript.tsL30 | normalizeContext() | 规范化上下文normalize the context |
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.
要点Key points
- 每个 provider 一个文件,用
createProvider()创建,并在providers/all.ts中汇总。One file per provider, created withcreateProvider()and collected inproviders/all.ts. - API 实现位于
api/,经*.lazy.ts中的lazyApi()包装:模块在第一次流式调用时才 import。API implementations live inapi/, wrapped bylazyApi()in*.lazy.ts: a module is imported on its first stream call. KnownApi列出 10 种对话 API(另有 1 种图像 API、4 种分类器 API)。KnownApilists 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 useopenai-completions; xai usesopenai-responses; openrouter and github-copilot use several APIs.
工作原理How it works
- 每个 API 导出
stream与streamSimple,返回AssistantMessageEventStream。Every API exportsstreamandstreamSimple, returning anAssistantMessageEventStream. - 事件共 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. StreamFn约定不抛异常:认证、模块加载或请求失败都编码为流中的error事件(见lazyStream())。TheStreamFncontract forbids throwing: auth, module-load and request failures become anerrorevent in the stream (seelazyStream()).
关键文件Key files · v1.1.0
| 文件File | 符号Symbol | 作用Role |
|---|---|---|
| packages/ai/src/models.tsL1041 | createProvider() | provider 工厂provider factory |
| packages/ai/src/providers/all.tsL4 | providers/all.ts | 42 个内置 providerthe 42 built-in providers |
| packages/ai/src/api/anthropic-messages.tsL928 | streamSimple | 一个 API 实现示例an example API implementation |
| packages/ai/src/types.tsL17 | KnownApi | 10 种对话 APIthe 10 chat APIs |
| packages/ai/src/utils/event-stream.tsL97 | AssistantMessageEventStream | 统一事件流the normalized stream |
| packages/agent/src/types.tsL20 | StreamFn | 不抛异常的约定the no-throw contract |
事件语法与 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.
要点Key points
- 7 种
StopReason:pending只出现在 partial 上;deferred表示异步任务,之后用streamDeferred(handle)取结果。SevenStopReasons:pendingappears only on partials;deferredmeans an async job, fetched later withstreamDeferred(handle). - 1.1.0:
AssistantMessageEventStream自动为最终消息写入durationMs(单调时钟,从流创建开始计时)。1.1.0:AssistantMessageEventStreamstampsdurationMson the final message (monotonic clock from stream creation). - 终止后
push被忽略——迟到的事件不会污染已完成的消息。After termination,pushis ignored — late events cannot corrupt a finished message.
工作原理How it works
- API 实现创建
AssistantMessageEventStream,边解析 SSE 边push。API implementations create anAssistantMessageEventStreamandpushwhile parsing SSE. - 有等待中的消费者就直接交付,否则进入 FIFO 队列。If a consumer is waiting, hand off directly; otherwise enqueue FIFO.
done/error时isComplete为真,result()得到最终AssistantMessage。Ondone/error,isCompleteis true andresult()yields the finalAssistantMessage.
关键文件Key files · v1.1.0
| 文件File | 符号Symbol | 作用Role |
|---|---|---|
| packages/ai/src/types.tsL788 | AssistantMessageEvent | 12 种事件12 event types |
| packages/ai/src/types.tsL456 | StopReason | 7 种停止原因seven stop reasons |
| packages/ai/src/utils/event-stream.tsL26 | EventStream | 推拉队列the push/pull queue |
| packages/ai/src/utils/event-stream.tsL97 | AssistantMessageEventStream | durationMs 计时durationMs timing |
跨模型消息转换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.
要点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 synthesizedisErrorresult — most APIs require one per call.
工作原理How it works
- 各 API 的
convertMessages()先调用transformMessages(messages, model, normalizeToolCallId)。Each API'sconvertMessages()first callstransformMessages(messages, model, normalizeToolCallId). - 然后才把统一消息映射成该 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.tsL64 | transformMessages() | 跨模型转换cross-model transform |
| packages/ai/src/utils/transcript.tsL150 | getToolStateChanges() | 工具声明的增删tool declaration diffs |
| packages/ai/src/utils/validation.tsL317 | validateToolArguments() | TypeBox 参数校验TypeBox argument validation |
一个 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.
要点Key points
- 工具参数用
parseStreamingJson增量解析,UI 可以在参数流式到达时就显示。Tool arguments are parsed incrementally withparseStreamingJson, so a UI can show them while they stream. refusal与sensitive被映射为error,pause_turn视为正常stop。refusalandsensitivemap toerror;pause_turnis treated as a normalstop.- 请求经过
retryProviderRequest,并提供onPayload/onResponse钩子用于调试与改写。Requests go throughretryProviderRequestand exposeonPayload/onResponsehooks for debugging and rewriting.
工作原理How it works
message_start→start;每个content_block_start→*_start;delta →*_delta;stop →*_end。message_start→start; eachcontent_block_start→*_start; deltas →*_delta; stop →*_end.message_delta带stop_reason与 usage;结束时calculateCost()并 pushdone。message_deltacarriesstop_reasonand usage; at the endcalculateCost()and pushdone.
关键文件Key files · v1.1.0
| 文件File | 符号Symbol | 作用Role |
|---|---|---|
| packages/ai/src/api/anthropic-messages.tsL571 | stream | SSE → 事件SSE → events |
| packages/ai/src/api/anthropic-messages.tsL928 | streamSimple | thinking 配置thinking configuration |
| packages/ai/src/api/anthropic-messages.tsL1613 | mapStopReason() | 停止原因映射stop-reason mapping |
| packages/ai/src/utils/provider-retry.tsL107 | retryProviderRequest() | 重试retries |
思考等级、预算与成本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).
要点Key points
xhigh与max只有在模型的thinkingLevelMap明确声明时才可用;null表示该等级被禁用。xhighandmaxexist only if the model'sthinkingLevelMapdeclares them;nulldisables 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
clampThinkingLevel(model, level)找到最近的受支持等级。clampThinkingLevel(model, level)finds the nearest supported level.adjustMaxTokensForThinking+clampMaxTokensToContext计算最终maxTokens与思考预算。adjustMaxTokensForThinking+clampMaxTokensToContextcompute the finalmaxTokensand thinking budget.- 响应结束时
calculateCost(model, usage)填充usage.cost。At the end of a responsecalculateCost(model, usage)fillsusage.cost.
关键文件Key files · v1.1.0
| 文件File | 符号Symbol | 作用Role |
|---|---|---|
| packages/ai/src/models.tsL1200 | calculateCost() | 成本计算cost calculation |
| packages/ai/src/models.tsL1224 | getSupportedThinkingLevels() | 可用思考等级available thinking levels |
| packages/ai/src/api/simple-options.tsL70 | DEFAULT_THINKING_BUDGETS | 默认预算表default budget table |
| packages/ai/src/api/simple-options.tsL18 | clampMaxTokensToContext() | 不超过上下文窗口stay within the context window |
| packages/ai/src/types.tsL433 | Usage | token 与成本tokens and cost |
| packages/ai/src/utils/overflow.tsL137 | isContextOverflow() | 上下文溢出识别context-overflow detection |
图像与分类器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.
要点Key points
- 分类器 API 有 4 种;1.1.0 新增
openai-decisions,以及ClassifierContext.images。There are four classifier APIs; 1.1.0 addsopenai-decisionsandClassifierContext.images. - 问题有三种:
choice(带概率分布)、score、bool,每个答案都带置信度或概率。Questions come in three kinds:choice(with a probability distribution),scoreandbool; each answer carries confidence or probability.
工作原理How it works
createProvider({ images, classifiers })按 API 名注册实现。createProvider({ images, classifiers })registers implementations by API name.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.tsL669 | ClassifierContext | state · images · questionsstate · images · questions |
| packages/ai/src/types.tsL700 | ClassifierResult | answers · usageanswers · usage |
| packages/ai/src/types.tsL637 | AssistantImages | 图像结果image results |
| packages/ai/src/api/openai-decisions.tsL161 | openai-decisions | 1.1.0 新增new in 1.1.0 |
扩展点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.测试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).