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:
- Creates a
ModelRuntimeinstance and verifies the provider supports OAuth - Generates a temporary token in the format
<provider>-<ts>-<rand>to track the request/response pair - Invokes
modelRuntime.login(provider, "oauth", ...)with apromptcallback 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:
GETto check authentication statusPOSTto store a new API keyDELETEto 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?.logincapabilities - Providers listed in the hard-coded
OAUTH_PROVIDER_IDSset - Providers with
auth.status.sourceequal to"models_json_key"(custom providers configured viamodels.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
ModelRuntimeclass from@earendil-works/pi-coding-agentas 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.tsendpoint filters out unauthenticated and custom providers (those configured viamodels.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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →