How pi-web Supports Dual-Auth Providers with OAuth and API Key Methods: A Complete Technical Guide

pi-web treats dual-auth providers as single logical entities that can authenticate via either OAuth or API key, using capability-based discovery and type-aware credential storage to keep both methods available without hard-coding provider IDs.

The agegr/pi-web repository implements a flexible authentication system for AI model providers that support both OAuth and API key methods. Rather than duplicating providers or forcing a single authentication path, the codebase dynamically discovers capabilities from the Pi SDK and surfaces both options in the UI. This design ensures providers like Anthropic, GitHub Copilot, and Kimi-Coding automatically appear with the correct authentication badges without manual configuration.

Capability Discovery from the Pi SDK

The foundation of dual-auth support lives in lib/provider-listing-runtime.ts. This module queries ModelRuntime to extract each provider's authentication capabilities and any stored credentials.

const credentialTypes = new Map<string, ProviderCredentialType>();
for (const cred of await modelRuntime.listCredentials()) {
  if (cred.type === "api_key" || cred.type === "oauth") {
    credentialTypes.set(cred.providerId, cred.type);
  }
}
// build ProviderListingInput for each provider …

This runtime detection means new providers added to the Pi SDK immediately surface their auth methods without code changes to pi-web. The credentialTypes map tracks which method is currently active for each provider, preventing conflicting credential states.

Pure Functions for Provider List Building

The lib/provider-listing.ts module contains two core functions that transform raw SDK data into UI-ready lists:

buildApiKeyProviderList (lines 81-99) filters providers where hasApiKeyLogin is true and excludes those already authenticated via OAuth. It adds supportsOAuth: true for dual-auth providers so the UI can display "also supports OAuth" badges.

buildOAuthProviderList (lines 102-117) does the inverse: providers with hasOAuth get listed, with supportsApiKey: true added for reciprocal UI cues.

This separation allows the frontend to present distinct entry points while maintaining awareness of alternative authentication methods.

API Routes for Separate Auth Flows

API-Key Providers Endpoint

GET /api/auth/all-providers returns the complete list of API-key capable providers, including dual-auth providers that haven't been OAuth-authenticated yet.

// app/api/auth/all-providers/route.ts
export async function GET() {
  const providers = await buildApiKeyProviderList(runtimeInput);
  return Response.json({ providers });
}

OAuth Login Endpoint

GET /api/auth/login/[provider] implements a Server-Sent Events (SSE) flow for OAuth authentication (lines 44-70). The same route handles POST requests for user responses during multi-step OAuth flows.

Credential Storage with Type Safety

The lib/provider-credential-store.ts module persists credentials to ~/.pi/agent/auth.json with strict type preservation. Two critical functions manage this:

  • storeProviderCredential writes credentials tagged with their type (api_key or oauth)
  • removeStoredCredentialIfType validates type matching before deletion

This prevents accidental credential corruption. If an API-key credential exists, OAuth deletion attempts receive a type_mismatch error, and vice versa.

Complete Authentication Flows

API-Key Authentication

The POST /api/auth/api-key/[provider] endpoint validates provider capability before storing:

// Example: Store an API key for Anthropic
await fetch('/api/auth/api-key/anthropic', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ apiKey: 'sk-ant-…' }),
});

The route handler in app/api/auth/api-key/[provider]/route.ts (lines 20-55) verifies hasApiKeyLogin before calling storeProviderCredential.

OAuth Authentication

OAuth flows use SSE for real-time status updates:

const es = new EventSource(`/api/auth/login/github-copilot`);

es.addEventListener('message', ev => {
  const data = JSON.parse(ev.data);
  
  switch (data.type) {
    case 'auth':
      window.open(data.url, '_blank');
      break;
    case 'device_code':
      console.log(`Enter ${data.userCode} at ${data.verificationUri}`);
      break;
    case 'prompt_request':
    case 'select_request':
      // Forward to UI, then POST response
      break;
    case 'success':
      es.close();
      break;
  }
});

The server sends select_request, prompt_request, auth, and device_code events, receiving answers via companion POST requests to the same route.

UI Integration with Automatic Badging

Components like SkillsConfig.tsx and ModelsConfig.tsx consume both provider lists and render authentication method badges dynamically. Because capabilities derive from SDK flags rather than hard-coded lists, new dual-auth providers automatically display "Supports OAuth" or "Supports API key" indicators without frontend changes.

Summary

  • Dynamic discovery via provider-listing-runtime.ts eliminates hard-coded provider IDs
  • Capability-based filtering in provider-listing.ts creates clean separation between auth methods while preserving dual-auth awareness
  • Dual endpoints (/api/auth/all-providers and /api/auth/login/[provider]) serve distinct UI entry points
  • Type-safe storage in provider-credential-store.ts prevents credential conflicts
  • Automatic UI updates ensure new providers appear with correct badges immediately

Frequently Asked Questions

How does pi-web prevent mixing OAuth and API key credentials for the same provider?

The removeStoredCredentialIfType function in lib/provider-credential-store.ts validates the stored credential type before deletion. When deleting an API-key credential, it checks that the stored type matches; if OAuth is stored instead, it returns a type_mismatch error. This enforcement prevents accidental credential overwrites.

What happens when a new dual-auth provider is added to the Pi SDK?

No code changes are required in pi-web. The provider-listing-runtime.ts module queries ModelRuntime.getProviders() and reads each provider's auth capabilities dynamically. The provider automatically appears in both filtered lists with appropriate supportsOAuth or supportsApiKey flags set based on its capability metadata.

Can users switch from API key to OAuth authentication for the same provider?

Yes, but not directly. Users must first delete their existing API-key credential via DELETE /api/auth/api-key/[provider], then initiate OAuth flow through GET /api/auth/login/[provider]. The type-checking in the credential store prevents mixed states during this transition.

Where are provider credentials physically stored?

Credentials persist in ~/.pi/agent/auth.json as managed by lib/provider-credential-store.ts. The file maintains a record of provider IDs mapped to their credential type and value, enabling the runtime to reconstruct authentication state on application startup.

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 →