How Pi Web Lists and Handles Capability-Driven Providers

Pi Web discovers and categorizes providers for its Models and Auth panels by inspecting each provider's declared capabilities—specifically whether it supports API-key login, OAuth login, or both—rather than relying on hard-coded provider lists.

The agegr/pi-web repository implements a two-stage pipeline that transforms raw provider metadata from the Pi SDK into UI-ready lists. This design automatically adapts when providers add or remove authentication methods, eliminating manual synchronization between the SDK and the web interface.

Understanding the Provider Listing Pipeline

Pi Web's provider discovery splits cleanly into data collection and list derivation. This separation keeps the core logic testable and the API routes thin.

Stage 1: Collect Raw Provider Data

The collectProviderListingInputs function in lib/provider-listing-runtime.ts interrogates the Pi SDK to build a uniform ProviderListingInput for every known provider.

It performs three key operations:

  • Pulls the complete model list via modelRuntime.getModels()
  • Gathers credential types (api_key or oauth) through modelRuntime.listCredentials()
  • Records per-provider metadata including auth status and model count
// lib/provider-listing-runtime.ts
export async function collectProviderListingInputs(
  modelRuntime: ModelRuntime,
): Promise<ProviderListingInput[]> {
  const models = modelRuntime.getModels();
  const credentialTypes = new Map<string, ProviderCredentialType>();
  for (const credential of await modelRuntime.listCredentials()) {
    if (credential.type === "api_key" || credential.type === "oauth") {
      credentialTypes.set(credential.providerId, credential.type);
    }
  }
  return modelRuntime.getProviders().map(provider => ({
    id: provider.id,
    name: provider.name,
    hasApiKeyLogin: Boolean(provider.auth.apiKey?.login),
    hasOAuth: Boolean(provider.auth.oauth),
    ...(provider.auth.oauth?.name ? { oauthName: provider.auth.oauth.name } : {}),
    status: modelRuntime.getProviderAuthStatus(provider.id),
    ...(credentialTypes.has(provider.id)
      ? { credentialType: credentialTypes.get(provider.id) }
      : {}),
    modelCount: models.filter(m => m.provider === provider.id).length,
  }));
}

Each provider object captures boolean capability flags (hasApiKeyLogin, hasOAuth) that drive all downstream filtering decisions.

Stage 2: Derive Concrete Lists

The lib/provider-listing.ts module contains two pure functions that transform ProviderListingInput arrays into panel-specific formats: buildApiKeyProviderList and buildOAuthProviderList.

Building the API-Key Provider List

The buildApiKeyProviderList function applies three filters:

  • Capability gate: Excludes providers without hasApiKeyLogin
  • Source exclusion: Skips custom providers from models.json (rendered separately)
  • Credential conflict prevention: Marks a provider as configured only when the stored credential is not an OAuth token

This last check prevents dual-auth providers like Anthropic from appearing in both panels simultaneously when the user has authenticated via OAuth.

// lib/provider-listing.ts – API-key list
export function buildApiKeyProviderList(
  providers: readonly ProviderListingInput[],
): ApiKeyProviderListing[] {
  const result: ApiKeyProviderListing[] = [];
  for (const provider of dedupeById(providers)) {
    if (!provider.hasApiKeyLogin) continue;
    if (provider.status.source && CUSTOM_PROVIDER_SOURCES.has(provider.status.source)) continue;
    const configured = provider.status.configured && provider.credentialType !== "oauth";
    result.push({
      id: provider.id,
      displayName: provider.name,
      configured,
      ...(configured && provider.status.source ? { source: provider.status.source } : {}),
      modelCount: provider.modelCount,
      supportsOAuth: provider.hasOAuth,
    });
  }
  return result;
}

Building the OAuth Provider List

The buildOAuthProviderList function follows a simpler pattern:

  • Includes only providers with hasOAuth set to true
  • Resolves display names through OAUTH_DISPLAY_NAMES mapping or the provider's oauthName
  • Sets loggedIn based on whether the stored credentialType equals "oauth"
// lib/provider-listing.ts – OAuth list
export function buildOAuthProviderList(
  providers: readonly ProviderListingInput[],
): OAuthProviderListing[] {
  const result: OAuthProviderListing[] = [];
  for (const provider of dedupeById(providers)) {
    if (!provider.hasOAuth) continue;
    result.push({
      id: provider.id,
      name: OAUTH_DISPLAY_NAMES[provider.id] ?? provider.oauthName ?? provider.name,
      usesCallbackServer: false,
      loggedIn: provider.credentialType === "oauth",
      supportsApiKey: provider.hasApiKeyLogin,
    });
  }
  return result;
}

Both functions use dedupeById to handle any duplicate provider entries gracefully.

How API Routes Expose Provider Lists

The capability-driven lists surface through two distinct API endpoints, each delegating to the pipeline described above.

API-Key Providers Endpoint

The GET /api/auth/all-providers route returns providers capable of API-key authentication:

// app/api/auth/all-providers/route.ts
const modelRuntime = await ModelRuntime.create();
const providers = buildApiKeyProviderList(
  await collectProviderListingInputs(modelRuntime),
);
return Response.json({ providers });

OAuth Providers Endpoint

The GET /api/auth/providers route returns OAuth-capable providers:

// app/api/auth/providers/route.ts
const modelRuntime = await ModelRuntime.create();
const providers = buildOAuthProviderList(
  await collectProviderListingInputs(modelRuntime),
);
return Response.json({ providers });

Consuming Provider Lists from the Client

Frontend code fetches these lists through standard fetch calls against the API routes.

Fetch API-key capable providers:

import { agentClient } from "@/lib/agent-client";

async function fetchApiKeyProviders() {
  const resp = await fetch("/api/auth/all-providers");
  const { providers } = await resp.json();
  return providers; // [{ id, displayName, configured, supportsOAuth, … }]
}

Fetch OAuth capable providers:

async function fetchOAuthProviders() {
  const resp = await fetch("/api/auth/providers");
  const { providers } = await resp.json();
  return providers; // [{ id, name, loggedIn, supportsApiKey, … }]
}

Key Files in the Provider Listing System

File Purpose
lib/provider-listing.ts Pure logic for transforming raw inputs into UI-ready lists; defines buildApiKeyProviderList and buildOAuthProviderList
lib/provider-listing-runtime.ts Runtime data collection from Pi SDK; defines collectProviderListingInputs
app/api/auth/all-providers/route.ts API endpoint for API-key provider discovery
app/api/auth/providers/route.ts API endpoint for OAuth provider discovery
lib/provider-credential-store.ts Persistence layer for provider credentials

Summary

  • Pi Web uses capability flags (hasApiKeyLogin, hasOAuth) to dynamically categorize providers instead of hard-coded IDs
  • Two-stage pipeline: collectProviderListingInputs gathers raw data from the Pi SDK, then buildApiKeyProviderList and buildOAuthProviderList derive filtered, formatted lists
  • Dual-auth providers (those supporting both API-key and OAuth) are handled carefully: OAuth credentials prevent API-key panel membership to avoid duplicate entries
  • Automatic synchronization: Provider lists stay current with SDK updates because they're rebuilt from capability declarations on every request
  • Clean separation of concerns between runtime data collection, pure transformation logic, and thin API route handlers

Frequently Asked Questions

What makes Pi Web's provider listing "capability-driven"?

The system inspects each provider's declared authentication capabilities—specifically whether provider.auth.apiKey?.login or provider.auth.oauth exists—rather than maintaining a static registry. When the Pi SDK updates a provider to add OAuth support, Pi Web automatically includes it in the OAuth panel without code changes.

How does Pi Web prevent a provider from appearing in both panels?

The buildApiKeyProviderList function checks provider.credentialType !== "oauth" when determining the configured flag. If a user has stored an OAuth credential for a dual-auth provider like Anthropic, that provider's configured status becomes false in the API-key list, keeping it exclusive to the OAuth panel.

Where does the provider metadata originate?

All metadata comes from the Pi SDK's ModelRuntime class. The collectProviderListingInputs function calls modelRuntime.getProviders(), modelRuntime.getModels(), modelRuntime.listCredentials(), and modelRuntime.getProviderAuthStatus() to assemble a complete picture of available providers and their authentication state.

Can custom providers appear in these lists?

Custom providers defined in models.json are explicitly excluded from the API-key list via the CUSTOM_PROVIDER_SOURCES check. These providers render through a separate UI path, keeping the standard provider lists clean and predictable.

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 →