前三章的桌面 Agent 始终使用一个固定模型。现在产品提出一个很自然的需求:
用户可以在 OpenAI、Anthropic、Gemini 等模型之间切换,并且切换后继续当前对话。
表面看,这只是增加一个下拉框。真正实现时,我们至少要回答六个问题:
- 下拉框里的模型从哪里来?
- “目录里存在”和“现在可以调用”是不是一回事?
- API Key、OAuth 和环境凭据由谁管理?
- 模型的图片、Thinking、上下文窗口等能力怎样展示?
- 用户切换模型后,已有对话是否还能继续?
- 不同 Provider 的消息格式为什么不会互相冲突?
这一章会先做出一个最小模型选择器,再沿着它进入 Pi 的模型系统。
1. 先认识四个层次
在写界面前,先不要把 Provider、API 和 Model 混为一谈。
| 层次 | 例子 | 在 Pi 中负责什么 |
|---|---|---|
| Provider | OpenAI、Anthropic、Google | 认证、模型目录、请求分发 |
| API | openai-responses、anthropic-messages |
一套具体的请求与响应协议 |
| Model | gpt-*、claude-*、gemini-* |
能力、价格、上下文窗口等元数据 |
| Models | 一个运行时集合 | 注册 Provider、解析认证、查找模型、发起请求 |
它们的关系不是“一个 Provider 必然只有一个 API”。createProvider() 既可以接收一套统一的流实现,也可以接收按 model.api 分派的实现表。因此同一个 Provider 可以根据不同模型选择不同 API 适配器。
flowchart LR
UI["桌面模型选择器"] --> MS["Models 集合"]
MS --> P1["Provider: OpenAI"]
MS --> P2["Provider: Anthropic"]
MS --> P3["Provider: Google"]
P1 --> A1["API: openai-responses"]
P2 --> A2["API: anthropic-messages"]
P3 --> A3["API: google-generative-ai"]
A1 --> M1["Model 元数据"]
A2 --> M2["Model 元数据"]
A3 --> M3["Model 元数据"]
这里最重要的认识是:
桌面应用选择的是
Model,真正接管认证和请求发送的是拥有它的Provider。
2. 最小模型选择器
课程示例先注册全部内置 Provider:
import {
type Api,
type Model,
} from "@earendil-works/pi-ai";
import {
builtinModels,
} from "@earendil-works/pi-ai/providers/all";
const models = builtinModels();
async function loadModelOptions() {
const available = await models.getAvailable();
return available.map((model) => ({
key: `${model.provider}/${model.id}`,
provider: model.provider,
id: model.id,
name: model.name,
reasoning: model.reasoning,
vision: model.input.includes("image"),
contextWindow: model.contextWindow,
}));
}
用户选择后,不要只保存一个可能重名的 model.id。用 Provider 和 Model ID 一起查找:
let selectedModel: Model<Api> | undefined;
function selectModel(providerId: string, modelId: string) {
const next = models.getModel(providerId, modelId);
if (!next) {
throw new Error(`模型不存在:${providerId}/${modelId}`);
}
selectedModel = next;
}
这里为了聚焦选择流程,示例把泛型写得较宽。实际使用 Model<Api> 即可;如果直接使用某个具体 Provider Factory,TypeScript 还能保留更精确的 API 类型。
下一次发送消息时,把新选择的模型传给同一个 Models 集合:
if (!selectedModel) {
throw new Error("请先选择模型");
}
const stream = models.streamSimple(selectedModel, context);
Context 没有因为模型切换而被清空。变化的是这次请求使用的 Model。
3. getModels() 和 getAvailable() 不是一回事
模型选择器最容易出现的错误,是直接把 getModels() 的结果全部显示为“可调用”。
Pi 明确区分:
const knownModels = models.getModels();
const availableModels = await models.getAvailable();
getModels()是同步读取,返回各 Provider 当前已知的模型目录;getAvailable()会检查 Provider 是否具备完整认证,只返回已配置 Provider 的模型;- Provider 还可以实现
filterModels(),根据当前凭据进一步过滤模型。
因此“目录中有 Claude”不等于“当前用户可以调用 Claude”。
桌面界面通常需要两种状态:
type ModelOptionState =
| { kind: "ready"; model: Model<Api> }
| { kind: "needs-auth"; model: Model<Api> };
一种实现方式是先读取完整目录,再用可用模型的复合键做集合:
async function loadAllModelStates() {
const all = models.getModels();
const available = await models.getAvailable();
const ready = new Set(
available.map((model) => `${model.provider}/${model.id}`),
);
return all.map((model) => ({
kind: ready.has(`${model.provider}/${model.id}`)
? "ready"
: "needs-auth",
model,
}));
}
这样界面既能展示尚未登录的 Provider,也不会让用户误以为它们已经可用。
4. Model 不是一个名字,而是一份能力契约
Model 的核心字段包括。下面是为了教学省略部分精确类型后的声明:
interface Model<TApi extends Api> {
id: string;
name: string;
api: TApi;
provider: ProviderId;
baseUrl: string;
reasoning: boolean;
thinkingLevelMap?: ThinkingLevelMap;
input: ("text" | "image")[];
cost: ModelCost;
contextWindow: number;
maxTokens: number;
headers?: Record<string, string>;
compat?: /* 源码按 TApi 条件选择具体兼容类型 */ unknown;
}
源码位置:packages/ai/src/types.ts 中的 Model
这些字段可以直接驱动桌面界面:
name:显示名称;provider:分组和唯一定位;reasoning:是否展示 Thinking 入口;input.includes("image"):是否允许添加图片;contextWindow:显示上下文容量;maxTokens:单次最大输出;cost:展示估算价格。
不过,能力字段是请求边界,不是营销标签。例如:
reasoning: true不保证支持所有 Thinking Level;input不包含"image"时,不能把图片直接交给这个模型;contextWindow是模型上下文容量,不等于应用可以无条件填满的“剩余 Token”;maxTokens是输出限制,不是上下文窗口。
第三章已经讲过,应使用 getSupportedThinkingLevels(model) 获取具体可选级别。
5. Provider Factory 把什么组装在一起
看看三个内置 Provider Factory 的形状:
export function openaiProvider() {
return createProvider({
id: "openai",
name: "OpenAI",
baseUrl: "https://api.openai.com/v1",
auth: {
apiKey: envApiKeyAuth("OpenAI API key", ["OPENAI_API_KEY"]),
},
models: Object.values(OPENAI_MODELS),
api: openAIResponsesApi(),
});
}
Anthropic 和 Google 的 Factory 结构类似,但认证方式、模型目录和 API 实现不同。Anthropic 同时注册 API Key 与 OAuth;Google 使用 GEMINI_API_KEY。
源码位置:
packages/ai/src/providers/openai.tspackages/ai/src/providers/anthropic.tspackages/ai/src/providers/google.ts
createProvider() 把这些部件组合成一个 Provider:
flowchart TD
F["Provider Factory"] --> C["createProvider()"]
C --> I["id / name / baseUrl"]
C --> AU["auth"]
C --> MC["静态模型目录"]
C --> DM["可选动态目录"]
C --> API["一套或多套 API Stream"]
C --> P["Provider"]
生成后的 Provider 负责:
- 返回当前模型目录;
- 必要时刷新动态目录;
- 按凭据过滤模型;
- 根据
model.api找到正确的流实现; - 调用
stream()或streamSimple()。
如果一个模型声明的 api 没有对应实现,createProvider() 不会偷偷选择另一个协议,而是返回一条 Stream error。
6. Models 是 Provider 的运行时容器
createModels() 返回一个 MutableModels。它内部用 Provider ID 建立 Map:
import { createModels } from "@earendil-works/pi-ai";
import { anthropicProvider } from "@earendil-works/pi-ai/providers/anthropic";
import { googleProvider } from "@earendil-works/pi-ai/providers/google";
import { openaiProvider } from "@earendil-works/pi-ai/providers/openai";
const models = createModels();
models.setProvider(openaiProvider());
models.setProvider(anthropicProvider());
models.setProvider(googleProvider());
同一个 ID 再次注册会替换旧 Provider。还可以删除或清空 Provider:
models.deleteProvider("google");
models.clearProviders();
builtinModels() 只是一个便利入口:它创建 Models,然后把 builtinProviders() 返回的全部内置 Provider 逐一注册进去。
如果桌面产品只支持少数 Provider,可以只注册需要的 Factory,避免把“支持所有 Provider”与“必须加载全部 Provider”混为一谈:
const models = createModels();
models.setProvider(openaiProvider());
models.setProvider(anthropicProvider());
源码位置:
packages/ai/src/models.ts中的ModelsImpl、createModelspackages/ai/src/providers/all.ts中的builtinProviders、builtinModels
7. Model Registry 为什么又出现了
README 的源码阅读入口还列出了 Model Registry。它位于 coding-agent,而不是 pi-ai 的最小模型层:
const registry = new ModelRegistry(modelRuntime);
const all = registry.getAll();
const available = registry.getAvailable();
const selected = registry.find("openai", "some-model-id");
源码注释把它定义为“暴露给 Extensions 的同步兼容外观”,coding-agent 内部直接使用 ModelRuntime。
三层架构分别承担:
pi-ai Models:底层 Provider 容器,以及认证和模型请求入口;- coding-agent
ModelRuntime:实现Models,再叠加models.json配置、内置与 Extension Provider 组合、目录持久化和可用模型快照; ModelRegistry:把ModelRuntime包装成面向 Extension 的同步兼容 API。
它们的关系是:
ModelRegistry 方法 |
实际委托 |
|---|---|
getAll() |
runtime.getModels() |
getAvailable() |
runtime.getAvailableSnapshot() |
find(provider, id) |
runtime.getModel(provider, id) |
registerProvider(provider) |
runtime.registerNativeProvider(provider) |
registerProvider(name, config) |
runtime.registerProvider(name, config) |
unregisterProvider(name) |
runtime.unregisterProvider(name) |
getAvailable() 特别值得注意:它读取 ModelRuntime 已维护的同步快照,不会在调用现场发起异步认证检查。若需要刷新,应先:
await registry.refresh();
const available = registry.getAvailable();
因此,前文直接使用 Models.getAvailable() 的桌面示例与 Model Registry 并不冲突:
Models是pi-ai的模型运行时接口;ModelRuntime是 coding-agent 的组合、配置和快照层;ModelRegistry是给 Extension 兼容使用的同步门面。
ModelRuntime.create() 在创建阶段默认不允许联网刷新模型目录;只有显式设置 allowModelNetwork: true,才会在创建阶段进行带超时的网络刷新。无论是否联网,它都会执行 refresh(),从配置与缓存重建当前运行时。
源码位置:
packages/coding-agent/src/core/model-registry.tspackages/coding-agent/src/core/model-runtime.ts
8. 静态目录、动态目录和 Model Store
Provider 的模型列表不一定永远写死在代码里。
createProvider() 支持两部分:
models:静态基线目录;fetchModels:可选的动态目录获取函数。
动态刷新时,Pi 会先尝试恢复 Provider 自己的缓存,再在允许联网时请求新目录。新目录中的同 ID 模型会覆盖静态基线,否则追加到列表中。
sequenceDiagram
participant App as 桌面主进程
participant Models
participant Provider
participant Store as ModelsStore
participant Remote as 远程目录
App->>Models: refresh()
Models->>Provider: refreshModels(context)
Provider->>Store: read(providerId)
Store-->>Provider: 已缓存目录
alt 允许联网
Provider->>Remote: fetchModels()
Remote-->>Provider: 最新目录
Provider->>Store: write(...)
end
Provider-->>Models: 保留最新可用目录
ModelsStore 的接口很小:
interface ModelsStore {
read(providerId: string): Promise<ModelsStoreEntry | undefined>;
write(
providerId: string,
entry: ModelsStoreEntry,
): Promise<void>;
delete(providerId: string): Promise<void>;
}
默认 InMemoryModelsStore 只活在当前进程中。桌面应用如果希望重启后仍能离线展示上次模型目录,应注入持久化实现。
在 coding-agent 中,FileModelsStore 提供了 JSON 文件实现;ModelRuntime.create() 默认使用 Agent 目录下的 models-store.json。这是 coding-agent 的上层策略,不是 pi-ai 强制所有应用都写这个文件。
源码位置:
packages/ai/src/models-store.tspackages/ai/src/models.ts中的createProvider和refreshpackages/coding-agent/src/core/models-store.tspackages/coding-agent/src/core/model-runtime.ts
9. 认证为什么属于 Provider
API Key 和 OAuth 都是 Provider 级问题。同一个 Provider 的多个模型通常共享认证,因此 Model 本身不保存用户秘密。
Models 可以:
await models.checkAuth("anthropic");
await models.getAuth("anthropic");
await models.login("anthropic", "oauth", interaction);
await models.logout("anthropic");
checkAuth() 用于判断配置是否完整,不负责刷新 OAuth。真正准备请求时,getAuth() 会解析认证;如果传入的是 Model,还会合并模型自己的静态 Headers。
桌面架构中,推荐把 Models 和 Credential Store 留在主进程:
flowchart LR
R["渲染进程\n只持有 provider/id/状态"] -->|IPC: 选择模型| M["主进程"]
M --> MS["Models"]
MS --> CS["CredentialStore"]
MS --> P["Provider"]
P --> N["模型服务"]
“主进程持有秘密”是本书的桌面安全建议,不是 Pi 自动建立的进程隔离。Pi 提供的是可注入的 CredentialStore 抽象。
默认 createModels() 使用 InMemoryCredentialStore。要持久化登录状态,应用需要注入自己的安全存储实现,而不是把 Key 放进前端状态或普通配置文件。
10. Auth Resolution 的准确顺序
resolveProviderAuth() 的顺序值得单独记住:
- 请求显式传入
overrides.apiKey,并且 Provider 支持 API Key; - Credential Store 中已有该 Provider 的凭据;
- 没有已存凭据时,才查询环境变量、配置文件等 ambient 来源。
flowchart TD
O{"请求显式 API Key?"}
S{"Credential Store 有记录?"}
T{"凭据类型有对应处理器?"}
A["解析 ambient 来源"]
R["返回 AuthResult"]
U["未配置"]
O -->|是| R
O -->|否| S
S -->|是| T
T -->|是| R
T -->|否| U
S -->|否| A
A -->|找到| R
A -->|没找到| U
一个容易忽略的细节是:
已存凭据一旦存在,就“拥有”这个 Provider;如果它类型不匹配或 OAuth 刷新失败,Pi 不会静默退回环境变量。
这样可以避免用户以为正在使用已登录账号,实际却悄悄换成另一份环境凭据。
OAuth 过期时,源码使用带二次检查的锁定更新:锁外先判断过期,锁内再次检查,只让一个并发请求刷新并写回新凭据。
源码位置:
packages/ai/src/auth/resolve.tspackages/ai/src/auth/types.tspackages/ai/src/auth/credential-store.tspackages/ai/src/auth/context.ts
11. 请求发送时认证如何进入 Provider
Models.streamSimple() 并不是直接调用 Provider:
sequenceDiagram
participant App
participant Models
participant Auth as Auth Resolution
participant Provider
participant API as API Adapter
App->>Models: streamSimple(model, context, options)
Models->>Models: requireProvider(model)
Models->>Auth: getAuth(model)
Auth-->>Models: apiKey / headers / baseUrl / env
Models->>Models: 合并请求级覆盖
Models->>Provider: streamSimple(requestModel, context, options)
Provider->>API: 按 model.api 分发
合并规则也很明确:
- 请求参数中的 API Key 优先于解析结果;
AuthResultHeaders 先与 Model 静态 Headers 合并,显式请求 Headers 再覆盖;transformHeaders最后运行;- Auth 可以覆盖这一次请求所用的
baseUrl; - 环境覆盖值会与解析结果合并。
Provider.headers 不在 Models.applyAuth() 这条路径中直接合并。具体 API 适配器还可能合并 model.headers、请求 Headers、SDK 或协议默认 Headers;那是下一层行为,不能与这里的认证合并混写。
这些准备过程包在 lazyStream() 中,所以认证失败会进入统一的 Stream error 路径。
12. 切换模型时究竟改变了什么
对于最小聊天应用,切换模型可以简单理解为:
selectedModel = nextModel;
然后下一次请求使用:
models.streamSimple(selectedModel, context);
Pi 的 coding-agent 还展示了一个完整产品怎样处理切换。AgentSession.setModel() 会:
- 检查新 Provider 是否已配置认证;
- 保存旧模型用于事件通知;
- 修改
agent.state.model; - 在 Session 中追加一次 Model Change;
- 更新默认 Provider 和 Model;
- 按新模型能力重新限制 Thinking Level;
- 发出
model_select扩展事件。
源码位置:packages/coding-agent/src/core/agent-session.ts 中的 setModel
cycleModel() 则会在限定模型集合或全部可用模型中前后循环。它使用 Provider + Model 的相等判断,而不是只比较 Model ID。
coding-agent 的模型解析也体现了同一规则:完整引用使用 provider/modelId;只给裸 Model ID 时,若多个 Provider 都有精确匹配,findExactModelReferenceMatch() 会拒绝这个歧义结果,而不是随便挑一个。
源码位置:packages/coding-agent/src/core/model-resolver.ts 中的 findExactModelReferenceMatch
这些是 coding-agent 对 pi-ai 模型系统的产品化封装。你的桌面 Agent 可以借鉴,但不必复制全部设置和 Session 逻辑。
13. 为什么旧消息可以交给新 Provider
上一章已经看到,Pi 的 Context 使用统一消息类型:
interface Context {
systemPrompt?: string;
messages: Message[];
tools?: Tool[];
}
Provider API 适配器在真正组装网络请求前调用 transformMessages()。这层转换专门处理跨模型兼容问题。
它首先判断一条历史 Assistant Message 是否来自同一模型:
const isSameModel =
assistantMessage.provider === model.provider &&
assistantMessage.api === model.api &&
assistantMessage.model === model.id;
当历史消息来自别的模型时,转换规则包括:
- 普通文本保留为普通文本;
- 非 redacted 的 Thinking 文本转成普通文本;
- redacted Thinking 被丢弃,因为它是不透明且只对原模型有效的内容;
- 带签名的空 Thinking 只在同模型回放时保留;
- Tool Call 上的 Provider 特有
thoughtSignature被移除; - 必要时规范化 Tool Call ID,并同步修改对应 Tool Result;
- 不支持图片的目标模型会把图片替换为文字占位;
- 缺失 Tool Result 的孤立 Tool Call 会得到一个合成错误结果;
error或aborted的不完整 Assistant Message 不参与回放。
flowchart TD
H["统一历史消息"] --> S{"仍是同一模型?"}
S -->|是| K["保留可回放签名"]
S -->|否| T["移除模型专有数据"]
T --> TH["Thinking 转普通文本或丢弃"]
T --> ID["规范化 Tool Call ID"]
T --> IMG["按目标能力降级图片"]
K --> R["目标 API 请求消息"]
TH --> R
ID --> R
IMG --> R
源码位置:packages/ai/src/api/transform-messages.ts
这里的结论不是“所有 Provider 消息天然相同”,恰好相反:
Pi 保存统一历史,在每次发送前由目标 API 适配器做兼容转换。
14. coding-agent 还有一层消息转换
coding-agent 自己有比 pi-ai 更多的消息类型,例如:
- Bash 执行记录;
- Extension 注入的自定义消息;
- 分支摘要;
- 压缩摘要。
convertToLlm() 先把这些 AgentMessage 转成 pi-ai 的标准 Message:
- Bash 执行转成 User Message 文本;
- Custom Message 转成 User Message;
- Branch/Compaction Summary 加包装后转成 User Message;
- 已经是 User、Assistant、ToolResult 的消息直接保留。
之后目标 API 适配器再执行上一节的 transformMessages()。
flowchart LR
A["coding-agent AgentMessage"] --> C["convertToLlm()"]
C --> U["pi-ai Message"]
U --> T["transformMessages()"]
T --> P["目标 Provider API 格式"]
源码位置:packages/coding-agent/src/core/messages.ts
这两层不能混为一谈:
convertToLlm()解决应用自定义消息怎样进入 LLM Context;transformMessages()解决统一 LLM 消息怎样适配当前模型和 API。
15. 一个更完整的桌面端模型状态
把本章内容组合起来,桌面主进程可以维护:
type DesktopModelState = {
selected?: {
provider: string;
modelId: string;
};
options: Array<{
provider: string;
modelId: string;
name: string;
available: boolean;
reasoning: boolean;
vision: boolean;
contextWindow: number;
maxTokens: number;
}>;
loading: boolean;
error?: string;
};
刷新流程:
async function refreshModelState(): Promise<DesktopModelState["options"]> {
await models.refresh({ allowNetwork: true });
const all = models.getModels();
const available = await models.getAvailable();
const availableKeys = new Set(
available.map((model) => `${model.provider}/${model.id}`),
);
return all.map((model) => ({
provider: model.provider,
modelId: model.id,
name: model.name,
available: availableKeys.has(
`${model.provider}/${model.id}`,
),
reasoning: model.reasoning,
vision: model.input.includes("image"),
contextWindow: model.contextWindow,
maxTokens: model.maxTokens,
}));
}
这是课程建议的 View Model,不是 Pi 内置类型。它有意不包含 API Key、OAuth Token、Provider Headers 等秘密。
16. 生产环境中的五个边界
16.1 不要把目录当作实时授权清单
getModels() 是已知目录;getAvailable() 才检查认证。远端账号是否真的有权限调用某个模型,仍可能要到请求时才能由 Provider 服务确认。
16.2 动态目录刷新可以失败
Models.refresh() 并发刷新动态 Provider,把每个 Provider 的错误放进返回结果,不因单个 Provider 失败而整体 reject。Provider 刷新失败时会尽力恢复缓存,并保留先前目录。
界面可以显示“目录可能不是最新”,而不是清空所有模型。
16.3 默认 Store 都是内存实现
createModels() 默认使用内存 Credential Store 和内存 Models Store。桌面产品需要自己决定怎样安全持久化。
16.4 切换到小上下文模型可能溢出
统一消息转换解决的是协议兼容,不会让历史自动变短。若新模型的 contextWindow 更小,长对话仍可能溢出。第 13 章会专门实现压缩。
16.5 降级不是无损转换
图片替换成占位文字、丢弃 redacted Thinking、移除 Provider 签名,都会损失信息。跨 Provider 继续对话是“尽可能兼容”,不是位级等价回放。
17. 源码调用链复盘
从用户点击模型到下一次回答,完整链路如下:
sequenceDiagram
participant UI as 桌面界面
participant Main as 主进程
participant Models
participant Auth
participant Provider
participant Transform
participant API
UI->>Main: 选择 provider/modelId
Main->>Models: getModel(provider, modelId)
Models-->>Main: Model
UI->>Main: 发送下一条消息
Main->>Models: streamSimple(model, context)
Models->>Auth: resolveProviderAuth()
Auth-->>Models: AuthResult
Models->>Provider: streamSimple(...)
Provider->>API: 按 model.api 分发
API->>Transform: transformMessages(context.messages)
Transform-->>API: 目标模型兼容消息
API-->>UI: AssistantMessageEventStream
对应源码入口:
packages/ai/src/providers/all.ts:构造内置 Provider;packages/ai/src/models.ts:注册、查找、认证和请求分发;packages/ai/src/auth/resolve.ts:解析凭据;packages/ai/src/models-store.ts:持久化动态目录的接口;packages/ai/src/api/transform-messages.ts:跨模型消息兼容;packages/coding-agent/src/core/model-runtime.ts:上层模型运行时;packages/coding-agent/src/core/agent-session.ts:产品级切换行为。
18. 本章小结
- Provider 负责认证、模型目录和流实现;Model 描述一次可选择的模型能力。
model.api决定 Provider 把请求交给哪一种 API 适配器。Models是 Provider 的运行时集合,也是桌面应用调用模型的统一入口。getModels()返回已知目录,getAvailable()返回认证已配置的模型。- Model ID 可能跨 Provider 重名,应使用
provider + id定位。 Model中的 Thinking、图片、上下文窗口和输出上限可以驱动界面能力。createProvider()合并静态目录与可选动态目录,并按 API 分发。ModelRegistry是 coding-agent 暴露给 Extension 的同步兼容门面,内部委托ModelRuntime。ModelsStore只保存模型目录;CredentialStore保存 Provider 凭据,两者职责不同。- 已存凭据存在时,认证失败不会静默退回 ambient 环境凭据。
- 切换模型改变后续请求使用的
Model,不必清空统一Context。 transformMessages()会移除或降级不能跨模型安全回放的数据。- coding-agent 的
ModelRuntime与AgentSession.setModel()是在pi-ai之上的产品化封装。
下一章会让模型第一次调用桌面工具:查询下载目录中的 PDF。届时 Model 不再只输出文字,而会生成结构化 ToolCall。
19. 自测
- 为什么不能把
getModels()的结果全部标记为“可用”? - Provider、API 和 Model 分别解决什么问题?
- 为什么选择项的唯一键不能只使用
model.id? ModelsStore和CredentialStore各自保存什么?- 已有存储凭据解析失败时,Pi 为什么不自动退回环境变量?
- 从 Anthropic 切换到 OpenAI 后,哪些历史消息数据可能被移除或降级?
- 为什么跨 Provider 消息转换不能解决上下文窗口变小的问题?
AgentSession.setModel()比最小聊天应用多处理了哪些产品状态?- 为什么
ModelRegistry.getAvailable()不需要写成await registry.getAvailable()?
本章源码依据
packages/ai/src/types.tspackages/ai/src/models.tspackages/ai/src/models-store.tspackages/ai/src/providers/all.tspackages/ai/src/providers/openai.tspackages/ai/src/providers/anthropic.tspackages/ai/src/providers/google.tspackages/ai/src/auth/context.tspackages/ai/src/auth/credential-store.tspackages/ai/src/auth/helpers.tspackages/ai/src/auth/resolve.tspackages/ai/src/auth/types.tspackages/ai/src/api/transform-messages.tspackages/coding-agent/src/core/messages.tspackages/coding-agent/src/core/models-store.tspackages/coding-agent/src/core/model-registry.tspackages/coding-agent/src/core/model-runtime.tspackages/coding-agent/src/core/model-resolver.tspackages/coding-agent/src/core/agent-session.ts