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 flow
  • refreshToken: Exchanges refresh tokens for new access tokens
  • getApiKey: 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 registerOAuthProvider system in packages/ai/src/oauth.ts treats OAuth tokens as standard API keys
  • Device-code flow: Interactive login uses OAuthSelectorComponent and LoginDialogComponent to guide users through browser-based authorization
  • Automatic refresh: getOAuthApiKey handles token expiration transparently, caching refreshed values
  • Subscription-aware: model-registry.ts detects 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →