# How Pi Web Lists and Handles Capability-Driven Providers

> Discover how Pi Web lists and handles capability-driven providers by inspecting their declared capabilities for API-key and OAuth login, not hard-coded lists.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: deep-dive
- Published: 2026-08-10

---

**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`](https://github.com/agegr/pi-web/blob/main/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

```ts
// 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`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/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.

```ts
// 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"`

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

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

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

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

```ts
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`](https://github.com/agegr/pi-web/blob/main/lib/provider-listing.ts) | Pure logic for transforming raw inputs into UI-ready lists; defines `buildApiKeyProviderList` and `buildOAuthProviderList` |
| [`lib/provider-listing-runtime.ts`](https://github.com/agegr/pi-web/blob/main/lib/provider-listing-runtime.ts) | Runtime data collection from Pi SDK; defines `collectProviderListingInputs` |
| [`app/api/auth/all-providers/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/auth/all-providers/route.ts) | API endpoint for API-key provider discovery |
| [`app/api/auth/providers/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/auth/providers/route.ts) | API endpoint for OAuth provider discovery |
| [`lib/provider-credential-store.ts`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/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.