How Pi Web Handles Authentication for Different Model Providers

Pi Web delegates all provider authentication to the ModelRuntime class from @earendil-works/pi-coding-agent, supporting interactive OAuth flows via Server-Sent Events for providers like Anthropic and OpenAI, while using simple HTTP POST requests for static API-key credentials.

The agegr/pi-web repository implements a flexible, provider-agnostic authentication layer that abstracts away the differences between AI model providers. By centralizing authentication logic in the ModelRuntime class, the application handles everything from interactive OAuth logins to static API-key storage through a uniform interface used by Next.js route handlers.

The ModelRuntime Authentication Hub

At the core of Pi Web's authentication system is the ModelRuntime class provided by the external package @earendil-works/pi-coding-agent. This runtime maintains a registry of each provider's capabilities, distinguishing between oauth and api_key authentication methods.

When a route handler needs to authenticate a user, it instantiates a ModelRuntime and calls the appropriate methods based on the provider type. The runtime exposes getProviders() to enumerate available providers and login() to initiate the authentication flow, using a callback-based prompt system to communicate with the client UI.

OAuth Authentication via Server-Sent Events

For OAuth-based providers such as Anthropic, GitHub Copilot, and OpenAI Codex, Pi Web implements an interactive authentication flow using Server-Sent Events (SSE). This approach drives a dynamic login UI that can request user input at multiple steps.

SSE Implementation and Temporary Tokens

The OAuth flow is handled in app/api/auth/login/[provider]/route.ts. When a client opens an SSE connection to GET /api/auth/login/[provider], the server:

  1. Creates a ModelRuntime instance and verifies the provider supports OAuth
  2. Generates a temporary token in the format <provider>-<ts>-<rand> to track the request/response pair
  3. Invokes modelRuntime.login(provider, "oauth", ...) with a prompt callback that streams UI instructions to the client

The SSE stream sends event types including select_request, prompt_request, auth (containing URLs to open), device_code, success, and error. When the client receives a prompt, it submits the response via POST /api/auth/login/[provider] with the temporary token, which resolves the promise waiting in the runtime.

// Client-side OAuth initiation
const evtSource = new EventSource(`/api/auth/login/${provider}`);

evtSource.addEventListener('message', async e => {
  const data = JSON.parse(e.data);
  switch (data.type) {
    case 'select_request':
    case 'prompt_request': {
      const answer = await showPromptToUser(data);
      await fetch(`/api/auth/login/${provider}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ token: data.token, code: answer })
      });
      break;
    }
    case 'auth':
      window.open(data.url, '_blank');
      break;
    case 'success':
      console.log('Login succeeded');
      evtSource.close();
      break;
    case 'error':
      console.error('Login error:', data.message);
      evtSource.close();
      break;
  }
});

After successful OAuth authentication, the route calls invalidateModelsCache() to refresh the available model list.

API-Key Authentication

For providers that accept static credentials, Pi Web uses a simpler HTTP-based flow handled in app/api/auth/api-key/[provider]/route.ts. The supported operations include:

  • GET to check authentication status
  • POST to store a new API key
  • DELETE to remove stored credentials

When storing a key via POST, the route creates a ModelRuntime and calls modelRuntime.login(provider, "api_key", ...). The prompt callback first receives a select prompt to choose the authentication method (e.g., "api-key" versus "bearer-token"), followed by a secret prompt where the raw key is supplied.

// Store an API key
await fetch(`/api/auth/api-key/${provider}`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ apiKey: 'sk-my-secret-key' })
});

After successful storage, the models cache is invalidated to reflect the new authentication state.

Filtering Providers by Authentication Requirements

Not all providers require UI-based authentication. The route app/api/auth/all-providers/route.ts (lines 5-30) filters the complete provider list to return only those requiring stored credentials.

This handler enumerates providers from ModelRuntime.getProviders() and excludes:

  • Providers missing auth.apiKey?.login capabilities
  • Providers listed in the hard-coded OAUTH_PROVIDER_IDS set
  • Providers with auth.status.source equal to "models_json_key" (custom providers configured via models.json)

The filtered response includes fields for id, displayName, configured, source, and modelCount, driving the Settings UI with only relevant authentication options.

// Fetch providers requiring authentication
const resp = await fetch('/api/auth/all-providers');
const { providers } = await resp.json();
/* Example output:
[
  { id: 'openai', displayName: 'OpenAI', configured: true, source: 'env', modelCount: 12 },
  { id: 'anthropic', displayName: 'Anthropic', configured: false, source: undefined, modelCount: 5 }
]
*/

Clearing Credentials and Logout

OAuth credentials are cleared via app/api/auth/logout/[provider]/route.ts (lines 10-17), which creates a ModelRuntime and calls the logout method for the specified provider. API-key credentials are removed via DELETE requests to the API-key endpoint.

// Logout from OAuth provider
await fetch(`/api/auth/logout/${provider}`, { method: 'POST' });

// Remove API key
await fetch(`/api/auth/api-key/${provider}`, { method: 'DELETE' });

Summary

  • Pi Web uses the ModelRuntime class from @earendil-works/pi-coding-agent as a centralized authentication hub, abstracting provider-specific implementations behind a uniform interface.
  • OAuth providers use interactive Server-Sent Events with temporary tokens to drive multi-step login UIs, handled in app/api/auth/login/[provider]/route.ts.
  • API-key providers use simple HTTP POST/DELETE requests managed by app/api/auth/api-key/[provider]/route.ts, supporting selection prompts for different credential types.
  • The app/api/auth/all-providers/route.ts endpoint filters out unauthenticated and custom providers (those configured via models.json), exposing only providers requiring credential storage.
  • All authentication changes trigger invalidateModelsCache() to ensure the model list stays synchronized with the current authentication state.

Frequently Asked Questions

How does Pi Web initiate an OAuth login flow?

Pi Web initiates OAuth by opening an SSE connection to GET /api/auth/login/[provider], which creates a ModelRuntime instance and streams interactive prompts to the client. The client submits responses via POST requests to the same endpoint using temporary tokens to correlate the request/response pairs.

What distinguishes API-key authentication from OAuth in Pi Web's implementation?

API-key authentication uses simple HTTP POST requests to app/api/auth/api-key/[provider]/route.ts with the key in the request body, while OAuth uses streaming Server-Sent Events via app/api/auth/login/[provider]/route.ts to handle interactive, multi-step login flows that may require opening external URLs or entering device codes.

Why do some providers not appear in the Pi Web authentication UI?

Providers configured via models.json (source models_json_key) or those that do not support stored credentials are filtered out by app/api/auth/all-providers/route.ts. This endpoint intentionally skips OAuth-only providers and custom configurations to show only providers requiring explicit credential storage through the UI.

How does Pi Web handle cache invalidation after authentication?

After any successful login or logout operation, the route handlers call invalidateModelsCache() from lib/models-cache.ts to clear the cached model list, ensuring subsequent requests reflect the updated authentication state and newly available or restricted models.

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 →