# How Earendil π Handles OAuth Authentication for API Keys

> Learn how Earendil π handles OAuth authentication for API keys. Store and refresh OAuth credentials client-side for seamless API access with models supporting OAuth subscriptions.

- Repository: [Earendil Works/pi](https://github.com/earendil-works/pi)
- Tags: how-to-guide
- Published: 2026-05-25

---

**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`](https://github.com/earendil-works/pi/blob/main/auth-storage.ts)

All **OAuth credentials** are persisted in `~/.pi/agent/auth.json`. In [`packages/coding-agent/src/core/auth-storage.ts`](https://github.com/earendil-works/pi/blob/main/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`](https://github.com/earendil-works/pi/blob/main/oauth.ts)

Each supported provider (OpenAI, Anthropic, etc.) registers itself once using `registerOAuthProvider` in [`packages/ai/src/oauth.ts`](https://github.com/earendil-works/pi/blob/main/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`](https://github.com/earendil-works/pi/blob/main/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`](https://github.com/earendil-works/pi/blob/main/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`](https://github.com/earendil-works/pi/blob/main/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`](https://github.com/earendil-works/pi/blob/main/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`](https://github.com/earendil-works/pi/blob/main/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`](https://github.com/earendil-works/pi/blob/main/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:

```typescript
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:

```typescript
// 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:

```typescript
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`](https://github.com/earendil-works/pi/blob/main/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`](https://github.com/earendil-works/pi/blob/main/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`](https://github.com/earendil-works/pi/blob/main/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`](https://github.com/earendil-works/pi/blob/main/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`](https://github.com/earendil-works/pi/blob/main/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`](https://github.com/earendil-works/pi/blob/main/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.