第 10 章 · 认证解析与凭据管理
本章目标:理解 pi-ai 的认证解析顺序,掌握环境变量、CredentialStore 持久化与
transformHeaders请求头变换。
10.1 每个 Provider 自管认证
pi-ai 没有中心化的认证服务——认证属于 Provider。每个 Provider 决定:API Key 从哪来(存储的凭据、环境变量、AWS profile 等环境源)、如何刷新 OAuth、失败时怎么报错。
10.2 解析优先级
调用 models.stream() 时认证按以下顺序合并:
显式传入 options.apiKey ← 永远最高
▼ 未提供则
Provider 内部解析:
1. CredentialStore 中该 provider 的存储凭据
2. 环境变量(如 ANTHROPIC_API_KEY)
3. 环境源(AWS profile / gcloud ADC 等)// 方式一:全自动 —— Provider 从环境变量解析(最常用)
await models.complete(model, context);
// 方式二:显式指定 —— 覆盖一切其他来源(多租户/代理场景)
await models.complete(model, context, { apiKey: "sk-explicit" });存储凭据「独占」Provider
一旦某 Provider 在凭据库中存了凭据,环境变量不再被咨询;OAuth 刷新失败也不会静默回退到 env key——这是刻意的安全设计:避免旧 token 失效后悄悄降级到另一套身份。
10.3 检查认证状态(不发请求)
getAuth() 可以在不发请求的情况下检查配置情况:
// provider 级或 model 级两种重载
const auth = await models.getAuth(model);
if (auth) {
console.log(`配置来源: ${auth.source}`); // 如 "ANTHROPIC_API_KEY"、"OAuth"
console.log(auth.auth.headers); // 将被合并的请求头
} else {
console.log("未配置任何凭据");
}返回 undefined 表示未配置;若凭据损坏(如 OAuth 刷新失败)会抛出 ModelsError 并保留原凭据供重新登录。
10.4 CredentialStore:持久化你的密钥
默认是内存实现——进程重启即失。生产应用应注入持久化存储:
import {
createModels,
type CredentialStore,
} from "@earendil-works/pi-ai";
import fs from "node:fs/promises";
// 实现最小契约:read / list / modify / delete
const fileStore: CredentialStore = {
async read(providerId) {
const all = JSON.parse(await fs.readFile("creds.json", "utf-8").catch(() => "{}"));
return all[providerId];
},
async list() {
const all = JSON.parse(await fs.readFile("creds.json", "utf-8").catch(() => "{}"));
// 只暴露非敏感元数据:providerId 与 type
return Object.entries(all).map(([providerId, c]) => ({
providerId,
type: (c as any).type,
}));
},
// 唯一的写入路径:串行化读改写(OAuth 刷新也走这里,防止并发双刷)
async modify(providerId, fn) {
const all = JSON.parse(await fs.readFile("creds.json", "utf-8").catch(() => "{}"));
all[providerId] = await fn(all[providerId]);
await fs.writeFile("creds.json", JSON.stringify(all, null, 2));
},
async delete(providerId) {
const all = JSON.parse(await fs.readFile("creds.json", "utf-8").catch(() => "{}"));
delete all[providerId];
await fs.writeFile("creds.json", JSON.stringify(all, null, 2));
},
};
// 注入集合;builtinModels() 接受相同选项
const models = createModels({ credentials: fileStore });10.5 transformHeaders:最终请求头变换
在认证头、模型头、显式头全部合并之后、真正发出之前,还有一道变换机会:
const response = await models.completeSimple(model, context, {
headers: { "X-Client": "my-app" }, // 显式头(覆盖认证/模型头)
transformHeaders: async (headers) => ({
...headers,
// 注入每次请求唯一的追踪 ID
"X-Request-ID": crypto.randomUUID(),
}),
});合并顺序:
provider 认证头 → model.headers → options.headers → transformHeaders → 发出用它替代手动 getAuth
需要动态加签(如短期 token)时,用 transformHeaders 而不是先调 getAuth() 再手动拼——后者会导致认证被解析两次。
10.6 动态 API Key
构造 Agent 时还能传 getApiKey 钩子处理过期令牌:
const agent = new Agent({
// 每次请求前回调,适合自动续期的 OAuth token
getApiKey: async (provider) => refreshMyToken(provider),
initialState: { systemPrompt: "...", model },
streamFn: models.streamSimple.bind(models),
});10.7 本章小结
- 认证归 Provider 所有;优先级:显式 apiKey > 存储凭据 > 环境变量 > 环境源;
- 存储凭据独占 Provider:env 不再兜底、刷新失败不静默降级;
getAuth()可无副作用地检查配置来源;- 自定义
CredentialStore四方法即可接入任意持久层,modify 是唯一写路径且防并发双刷; transformHeaders是发出前的最后一道头变换,优先于一切。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. 同时设置了存储凭据和 ANTHROPIC_API_KEY 环境变量,实际会用哪个?
2. CredentialStore 中唯一允许的写入路径是?
3. transformHeaders 执行时,哪些头已经合并进来了?
4. 想在不发请求的前提下确认 Anthropic 的 key 是否已配置,应该调用?
🛠️ 动手实践
- 实现 localStorage 版本的 CredentialStore(浏览器端思路),并注入 createModels 验证重启后凭据仍在。
- 分别只设置 env、只存凭据、两者都设三种情况,用 getAuth 打印 source 对比验证优先级。
- 用 transformHeaders 给所有请求加
X-Trace-Id,并在服务端(或 onPayload 回调)确认其生效。
凭据无忧后,第 11 章解锁推理型模型的思考模式。