How Earendil π Handles OAuth Authentication for API Keys
Earendil π stores and refreshes OAuth credentials completely inside the client, letting users log in once and then use the resulting access token as an API key for any model that supports OAuth-based subscriptions.
Earendil π (from the earendil-works/pi repository) implements a client-side OAuth authentication system that seamlessly converts provider tokens into usable API keys. This architecture, as implemented in earendil-works/pi, allows the coding agent to support subscription-based AI providers without requiring users to manage static API keys.
The OAuth Architecture in Earendil π
The authentication system is built around three core concepts: secure credential storage, provider registration, and API key abstraction.
Credential Storage in auth-storage.ts
All OAuth credentials are persisted in ~/.pi/agent/auth.json. In packages/coding-agent/src/core/auth-storage.ts, the system defines the OAuthCredentials interface and provides helper functions to read from and write to this JSON file. This ensures tokens never leak into the repository or shell history.
Provider Registration via oauth.ts
Each supported provider (OpenAI, Anthropic, etc.) registers itself once using registerOAuthProvider in packages/ai/src/oauth.ts. The registration includes three critical callbacks:
login: Initiates the device-code flowrefreshToken: Exchanges refresh tokens for new access tokensgetApiKey: Extracts the bearer token from stored credentials
This plug-in architecture allows the rest of the codebase to treat OAuth tokens and static API keys identically.
The OAuth Login Flow
When a user initiates authentication, the system orchestrates a device-code flow through interactive UI components.
Device-Code Flow Implementation
The file packages/ai/src/utils/oauth/device-code.ts implements the OAuth Device-Code flow used by most providers. When you run /login in interactive mode, the OAuthSelectorComponent (located in packages/coding-agent/src/modes/interactive/components/oauth-selector.ts) displays available providers.
Selecting a provider instantiates a LoginDialogComponent from packages/coding-agent/src/modes/interactive/components/login-dialog.ts. This dialog displays the device-code URL and user code. The client polls the provider's token endpoint in the background, automatically refreshes when needed, and stores the fresh credentials in auth.json.
Token Management and API Key Abstraction
Once stored, OAuth tokens function as first-class API keys throughout the π ecosystem.
Resolving Tokens with getOAuthApiKey
The getOAuthApiKey function in auth-storage.ts resolves stored credentials to a valid bearer token, handling background refresh if the token expired. This abstracts away the complexity of token lifecycles from the rest of the application.
Model Registry Integration
In packages/coding-agent/src/core/model-registry.ts, the function isUsingOAuth(model) determines whether to display subscription-specific UI elements like cost estimates and usage limits. The registry also invokes provider-specific modifyModels hooks, allowing OAuth providers to adjust base URLs after successful login.
Security Architecture
Tokens are never persisted in plain text within the project repository. The ~/.pi/agent/auth.json file resides in the user's home directory with restricted permissions. The UI explicitly masks tokens in displays, and the codebase deliberately never logs raw token values to stdout or log files.
Adding Custom OAuth Providers
You can extend π to support custom OAuth providers by registering a new handler:
import { registerOAuthProvider } from "@earendil-works/pi-ai/oauth";
registerOAuthProvider({
name: "MyCustomAI",
login: async (cb) => {
// Implement device-code flow, return { accessToken, refreshToken, expiresAt }
return await myDeviceCodeFlow(cb);
},
refreshToken: async (creds) => {
// Refresh logic using creds.refreshToken
return await myRefresh(creds.refreshToken);
},
getApiKey: (creds) => creds.accessToken,
});
This registers your provider alongside built-in ones like OpenAI and Anthropic.
Practical Usage Examples
Logging In via the REPL
Trigger the interactive login and retrieve a usable API key programmatically:
// Trigger the login UI
await pi.runCommand("/login");
// After successful login, retrieve the API key
import { getOAuthApiKey } from "@earendil-works/pi-ai/oauth";
const token = await getOAuthApiKey("anthropic", {});
console.log(`Bearer ${token}`);
Using OAuth with Models
Access OAuth-backed models transparently:
import { Model } from "@earendil-works/pi-ai";
import { getOAuthProvider } from "@earendil-works/pi-ai/oauth";
const provider = getOAuthProvider("openai")!;
const model: Model = provider.models[0]; // e.g., gpt-4o
// The model registry automatically attaches the stored OAuth token
const response = await model.completions.create({
messages: [{ role: "user", content: "Hello!" }]
});
console.log(response.choices[0].message.content);
Summary
- Client-side storage: OAuth credentials live in
~/.pi/agent/auth.json, never in the repository - Unified interface: The
registerOAuthProvidersystem inpackages/ai/src/oauth.tstreats OAuth tokens as standard API keys - Device-code flow: Interactive login uses
OAuthSelectorComponentandLoginDialogComponentto guide users through browser-based authorization - Automatic refresh:
getOAuthApiKeyhandles token expiration transparently, caching refreshed values - Subscription-aware:
model-registry.tsdetects OAuth usage to display relevant pricing and usage UI
Frequently Asked Questions
How does Earendil π refresh expired OAuth tokens?
When a token nears expiration, the getOAuthApiKey function automatically invokes the provider's refreshToken callback registered in oauth.ts. This exchanges the stored refresh token for a new access token, updates ~/.pi/agent/auth.json, and returns the valid bearer token without user intervention.
Where are OAuth tokens stored in Earendil π?
Tokens are stored in a JSON file at ~/.pi/agent/auth.json, managed by packages/coding-agent/src/core/auth-storage.ts. This location keeps credentials outside version control and ensures they persist across sessions while remaining masked in the UI.
Can I use OAuth providers other than OpenAI and Anthropic?
Yes. The registerOAuthProvider function in packages/ai/src/oauth.ts allows you to register any provider implementing the device-code flow. You provide login, refreshToken, and getApiKey callbacks, and π treats your custom provider identically to built-in ones.
How does the system distinguish between OAuth and static API keys?
The isUsingOAuth(model) function in packages/coding-agent/src/core/model-registry.ts checks the model's configuration. If true, the UI displays subscription-specific information, and the credential resolver routes requests through getOAuthApiKey rather than reading from static environment variables.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →