Chapter 10 · Auth Resolution and Credential Management
Chapter goals: Understand pi-ai's auth resolution order, master environment variables, CredentialStore persistence, and
transformHeadersrequest header transformation.
10.1 Each Provider Manages Its Own Auth
pi-ai has no centralized authentication service — auth belongs to the provider. Each provider decides: where the API Key comes from (stored credentials, environment variables, AWS profile, etc.), how to refresh OAuth, and how to error when it fails.
10.2 Resolution Priority
When calling models.stream(), auth is merged in the following order:
Explicitly passed options.apiKey ← Always highest priority
▼ If not provided
Provider internal resolution:
1. Stored credentials for this provider in CredentialStore
2. Environment variables (e.g. ANTHROPIC_API_KEY)
3. Environment sources (AWS profile / gcloud ADC, etc.)// Approach 1: Fully automatic — Provider resolves from environment variables (most common)
await models.complete(model, context);
// Approach 2: Explicit override — overrides all other sources (multi-tenant/proxy scenarios)
await models.complete(model, context, { apiKey: "sk-explicit" });Stored credentials "own" the provider
Once a provider has stored credentials in the credential store, environment variables are no longer consulted; an OAuth refresh failure will not silently fall back to the env key — this is intentional security design: it prevents silently degrading to a different identity after an old token expires.
10.3 Checking Auth Status (Without Making Requests)
getAuth() lets you check the configuration without making a request:
// Two overloads: provider-level or model-level
const auth = await models.getAuth(model);
if (auth) {
console.log(`Config source: ${auth.source}`); // e.g. "ANTHROPIC_API_KEY", "OAuth"
console.log(auth.auth.headers); // Headers that will be merged into the request
} else {
console.log("No credentials configured");
}Returns undefined when not configured; if credentials are corrupt (e.g. OAuth refresh failed), it throws ModelsError and retains the original credentials for re-login.
10.4 CredentialStore: Persist Your Keys
The default is an in-memory implementation — gone on process restart. Production applications should inject a persistent store:
import {
createModels,
type CredentialStore,
} from "@earendil-works/pi-ai";
import fs from "node:fs/promises";
// Implement the minimum contract: 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(() => "{}"));
// Expose only non-sensitive metadata: providerId and type
return Object.entries(all).map(([providerId, c]) => ({
providerId,
type: (c as any).type,
}));
},
// The only write path: serializing read-modify-write (OAuth refresh also goes through here to prevent concurrent double-refresh)
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));
},
};
// Inject into the collection; builtinModels() accepts the same options
const models = createModels({ credentials: fileStore });10.5 transformHeaders: Final Request Header Transformation
After auth headers, model headers, and explicit headers are all merged, there is one final transformation opportunity before the request is dispatched:
const response = await models.completeSimple(model, context, {
headers: { "X-Client": "my-app" }, // Explicit headers (override auth/model headers)
transformHeaders: async (headers) => ({
...headers,
// Inject a unique trace ID for every request
"X-Request-ID": crypto.randomUUID(),
}),
});Merge order:
provider auth headers → model.headers → options.headers → transformHeaders → dispatchUse it instead of manually calling getAuth
When you need dynamic signing (e.g. short-lived tokens), use transformHeaders rather than calling getAuth() first and then manually piecing headers together — the latter causes auth to be resolved twice.
10.6 Dynamic API Keys
You can also pass a getApiKey hook when constructing the Agent to handle expired tokens:
const agent = new Agent({
// Callback before each request; suitable for auto-refreshed OAuth tokens
getApiKey: async (provider) => refreshMyToken(provider),
initialState: { systemPrompt: "...", model },
streamFn: models.streamSimple.bind(models),
});10.7 Chapter Summary
- Auth belongs to providers; priority: explicit apiKey > stored credentials > environment variables > environment sources;
- Stored credentials own the provider: env is no longer a fallback; refresh failures do not silently degrade;
getAuth()can check config source without side effects;- Custom
CredentialStorerequires only four methods to plug in any persistence layer; modify is the only write path and prevents concurrent double-refresh; transformHeadersis the last header transformation before dispatch, taking priority over everything.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. If both stored credentials and the ANTHROPIC_API_KEY environment variable are set, which one is actually used?
2. What is the only allowed write path in CredentialStore?
3. When transformHeaders executes, which headers have already been merged in?
4. To confirm whether Anthropic's key is configured without making a request, which call should you use?
🛠️ Hands-on Practice
- Implement a localStorage-backed CredentialStore (browser-side approach) and inject it into createModels to verify credentials persist across restarts.
- In three cases — env only, stored credentials only, and both set — use getAuth to print source and compare to verify priority.
- Use transformHeaders to add
X-Trace-Idto all requests, and confirm it takes effect in the server (or onPayload callback).
With credentials sorted out, Chapter 11 unlocks the thinking mode of reasoning-capable models.