Refactor OAuth/API key handling: AuthStorage and ModelRegistry

- Add AuthStorage class for credential storage (auth.json)
- Add ModelRegistry class for model management with API key resolution
- Add discoverAuthStorage() and discoverModels() discovery functions
- Add migration from legacy oauth.json and settings.json apiKeys to auth.json
- Remove configureOAuthStorage, defaultGetApiKey, findModel, discoverAvailableModels
- Remove apiKeys from Settings type and SettingsManager methods
- Rename getOAuthPath to getAuthPath
- Update SDK, examples, docs, tests, and mom package

Fixes #296
This commit is contained in:
Mario Zechner 2025-12-25 03:48:36 +01:00
parent 9f97f0c8da
commit 54018b6cc0
29 changed files with 953 additions and 2017 deletions

View file

@ -13,8 +13,8 @@
* pi --hook examples/hooks/custom-compaction.ts
*/
import { complete } from "@mariozechner/pi-ai";
import { findModel, messageTransformer } from "@mariozechner/pi-coding-agent";
import { complete, getModel } from "@mariozechner/pi-ai";
import { messageTransformer } from "@mariozechner/pi-coding-agent";
import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
export default function (pi: HookAPI) {
@ -27,10 +27,9 @@ export default function (pi: HookAPI) {
event;
// Use Gemini Flash for summarization (cheaper/faster than most conversation models)
// findModel searches both built-in models and custom models from models.json
const { model, error } = findModel("google", "gemini-2.5-flash");
if (error || !model) {
ctx.ui.notify(`Could not find Gemini Flash model: ${error}, using default compaction`, "warning");
const model = getModel("google", "gemini-2.5-flash");
if (!model) {
ctx.ui.notify(`Could not find Gemini Flash model, using default compaction`, "warning");
return;
}

View file

@ -4,16 +4,27 @@
* Shows how to select a specific model and thinking level.
*/
import { createAgentSession, discoverAvailableModels, findModel } from "../../src/index.js";
import { getModel } from "@mariozechner/pi-ai";
import { createAgentSession, discoverAuthStorage, discoverModels } from "../../src/index.js";
// Option 1: Find a specific model by provider/id
const { model: sonnet } = findModel("anthropic", "claude-sonnet-4-20250514");
if (sonnet) {
console.log(`Found model: ${sonnet.provider}/${sonnet.id}`);
// Set up auth storage and model registry
const authStorage = discoverAuthStorage();
const modelRegistry = discoverModels(authStorage);
// Option 1: Find a specific built-in model by provider/id
const opus = getModel("anthropic", "claude-opus-4-5");
if (opus) {
console.log(`Found model: ${opus.provider}/${opus.id}`);
}
// Option 2: Pick from available models (have valid API keys)
const available = await discoverAvailableModels();
// Option 2: Find model via registry (includes custom models from models.json)
const customModel = modelRegistry.find("my-provider", "my-model");
if (customModel) {
console.log(`Found custom model: ${customModel.provider}/${customModel.id}`);
}
// Option 3: Pick from available models (have valid API keys)
const available = await modelRegistry.getAvailable();
console.log(
"Available models:",
available.map((m) => `${m.provider}/${m.id}`),
@ -23,6 +34,8 @@ if (available.length > 0) {
const { session } = await createAgentSession({
model: available[0],
thinkingLevel: "medium", // off, low, medium, high
authStorage,
modelRegistry,
});
session.subscribe((event) => {

View file

@ -1,40 +1,55 @@
/**
* API Keys and OAuth
*
* Configure API key resolution. Default checks: models.json, OAuth, env vars.
* Configure API key resolution via AuthStorage and ModelRegistry.
*/
import { getAgentDir } from "../../src/config.js";
import { configureOAuthStorage, createAgentSession, defaultGetApiKey, SessionManager } from "../../src/index.js";
import {
AuthStorage,
createAgentSession,
discoverAuthStorage,
discoverModels,
ModelRegistry,
SessionManager,
} from "../../src/index.js";
// Default: uses env vars (ANTHROPIC_API_KEY, etc.), OAuth, and models.json
await createAgentSession({
sessionManager: SessionManager.inMemory(),
});
console.log("Session with default API key resolution");
// Custom resolver
await createAgentSession({
getApiKey: async (model) => {
// Custom logic (secrets manager, database, etc.)
if (model.provider === "anthropic") {
return process.env.MY_ANTHROPIC_KEY;
}
// Fall back to default
return defaultGetApiKey()(model);
},
sessionManager: SessionManager.inMemory(),
});
console.log("Session with custom API key resolver");
// Use OAuth from ~/.pi/agent while customizing everything else
configureOAuthStorage(getAgentDir()); // Must call before createAgentSession
// Default: discoverAuthStorage() uses ~/.pi/agent/auth.json
// discoverModels() loads built-in + custom models from ~/.pi/agent/models.json
const authStorage = discoverAuthStorage();
const modelRegistry = discoverModels(authStorage);
await createAgentSession({
agentDir: "/tmp/custom-config", // Custom config location
// But OAuth tokens still come from ~/.pi/agent/oauth.json
systemPrompt: "You are helpful.",
skills: [],
sessionManager: SessionManager.inMemory(),
authStorage,
modelRegistry,
});
console.log("Session with OAuth from default location, custom config elsewhere");
console.log("Session with default auth storage and model registry");
// Custom auth storage location
const customAuthStorage = new AuthStorage("/tmp/my-app/auth.json");
const customModelRegistry = new ModelRegistry(customAuthStorage, "/tmp/my-app/models.json");
await createAgentSession({
sessionManager: SessionManager.inMemory(),
authStorage: customAuthStorage,
modelRegistry: customModelRegistry,
});
console.log("Session with custom auth storage location");
// Runtime API key override (not persisted to disk)
authStorage.setRuntimeApiKey("anthropic", "sk-my-temp-key");
await createAgentSession({
sessionManager: SessionManager.inMemory(),
authStorage,
modelRegistry,
});
console.log("Session with runtime API key override");
// No models.json - only built-in models
const simpleRegistry = new ModelRegistry(authStorage); // null = no models.json
await createAgentSession({
sessionManager: SessionManager.inMemory(),
authStorage,
modelRegistry: simpleRegistry,
});
console.log("Session with only built-in models");

View file

@ -2,38 +2,36 @@
* Full Control
*
* Replace everything - no discovery, explicit configuration.
* Still uses OAuth from ~/.pi/agent for convenience.
*
* IMPORTANT: When providing `tools` with a custom `cwd`, use the tool factory
* functions (createReadTool, createBashTool, etc.) to ensure tools resolve
* paths relative to your cwd.
*/
import { getModel } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox";
import { getAgentDir } from "../../src/config.js";
import {
AuthStorage,
type CustomAgentTool,
configureOAuthStorage,
createAgentSession,
createBashTool,
createReadTool,
defaultGetApiKey,
findModel,
type HookFactory,
ModelRegistry,
SessionManager,
SettingsManager,
} from "../../src/index.js";
// Use OAuth from default location
configureOAuthStorage(getAgentDir());
// Custom auth storage location
const authStorage = new AuthStorage("/tmp/my-agent/auth.json");
// Custom API key with fallback
const getApiKey = async (model: { provider: string }) => {
if (model.provider === "anthropic" && process.env.MY_ANTHROPIC_KEY) {
return process.env.MY_ANTHROPIC_KEY;
}
return defaultGetApiKey()(model as any);
};
// Runtime API key override (not persisted)
if (process.env.MY_ANTHROPIC_KEY) {
authStorage.setRuntimeApiKey("anthropic", process.env.MY_ANTHROPIC_KEY);
}
// Model registry with no custom models.json
const modelRegistry = new ModelRegistry(authStorage);
// Inline hook
const auditHook: HookFactory = (api) => {
@ -55,7 +53,7 @@ const statusTool: CustomAgentTool = {
}),
};
const { model } = findModel("anthropic", "claude-sonnet-4-20250514");
const model = getModel("anthropic", "claude-opus-4-5");
if (!model) throw new Error("Model not found");
// In-memory settings with overrides
@ -73,7 +71,8 @@ const { session } = await createAgentSession({
model,
thinkingLevel: "off",
getApiKey,
authStorage,
modelRegistry,
systemPrompt: `You are a minimal assistant.
Available: read, bash, status. Be concise.`,

View file

@ -29,50 +29,63 @@ npx tsx examples/sdk/01-minimal.ts
## Quick Reference
```typescript
import { getModel } from "@mariozechner/pi-ai";
import {
AuthStorage,
createAgentSession,
configureOAuthStorage,
discoverAuthStorage,
discoverModels,
discoverSkills,
discoverHooks,
discoverCustomTools,
discoverContextFiles,
discoverSlashCommands,
discoverAvailableModels,
findModel,
defaultGetApiKey,
loadSettings,
buildSystemPrompt,
ModelRegistry,
SessionManager,
codingTools,
readOnlyTools,
readTool, bashTool, editTool, writeTool,
} from "@mariozechner/pi-coding-agent";
// Auth and models setup
const authStorage = discoverAuthStorage();
const modelRegistry = discoverModels(authStorage);
// Minimal
const { session } = await createAgentSession();
const { session } = await createAgentSession({ authStorage, modelRegistry });
// Custom model
const { model } = findModel("anthropic", "claude-sonnet-4-20250514");
const { session } = await createAgentSession({ model, thinkingLevel: "high" });
const model = getModel("anthropic", "claude-opus-4-5");
const { session } = await createAgentSession({ model, thinkingLevel: "high", authStorage, modelRegistry });
// Modify prompt
const { session } = await createAgentSession({
systemPrompt: (defaultPrompt) => defaultPrompt + "\n\nBe concise.",
authStorage,
modelRegistry,
});
// Read-only
const { session } = await createAgentSession({ tools: readOnlyTools });
const { session } = await createAgentSession({ tools: readOnlyTools, authStorage, modelRegistry });
// In-memory
const { session } = await createAgentSession({
sessionManager: SessionManager.inMemory(),
authStorage,
modelRegistry,
});
// Full control
configureOAuthStorage(); // Use OAuth from ~/.pi/agent
const customAuth = new AuthStorage("/my/app/auth.json");
customAuth.setRuntimeApiKey("anthropic", process.env.MY_KEY!);
const customRegistry = new ModelRegistry(customAuth);
const { session } = await createAgentSession({
model,
getApiKey: async (m) => process.env.MY_KEY,
authStorage: customAuth,
modelRegistry: customRegistry,
systemPrompt: "You are helpful.",
tools: [readTool, bashTool],
customTools: [{ tool: myTool }],
@ -81,7 +94,6 @@ const { session } = await createAgentSession({
contextFiles: [],
slashCommands: [],
sessionManager: SessionManager.inMemory(),
settings: { compaction: { enabled: false } },
});
// Run prompts
@ -97,11 +109,12 @@ await session.prompt("Hello");
| Option | Default | Description |
|--------|---------|-------------|
| `authStorage` | `discoverAuthStorage()` | Credential storage |
| `modelRegistry` | `discoverModels(authStorage)` | Model registry |
| `cwd` | `process.cwd()` | Working directory |
| `agentDir` | `~/.pi/agent` | Config directory |
| `model` | From settings/first available | Model to use |
| `thinkingLevel` | From settings/"off" | off, low, medium, high |
| `getApiKey` | Built-in resolver | API key function |
| `systemPrompt` | Discovered | String or `(default) => modified` |
| `tools` | `codingTools` | Built-in tools |
| `customTools` | Discovered | Replaces discovery |
@ -112,7 +125,7 @@ await session.prompt("Hello");
| `contextFiles` | Discovered | AGENTS.md files |
| `slashCommands` | Discovered | File commands |
| `sessionManager` | `SessionManager.create(cwd)` | Persistence |
| `settings` | From agentDir | Overrides |
| `settingsManager` | From agentDir | Settings overrides |
## Events