# How Pi‑Web Model Discovery Fetches Upstream Provider Lists: A Deep Dive

> Discover how Pi-Web's model discovery fetches upstream provider lists using a server-side API route. Learn about authentication and response parsing for DiscoveredModel objects.

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

---

**Pi‑Web's model discovery system fetches upstream provider lists through a server-side API route (`/api/models-config/discover`) that constructs provider-specific URLs, authenticates via query parameters, and parses the response into standardized `DiscoveredModel` objects.**

The `agegr/pi-web` repository implements a clean separation between client-side model discovery initiation and server-side upstream fetching. This architecture keeps provider credentials secure while enabling dynamic model catalog population from external AI providers like Anthropic and OpenAI.

## How Model Discovery Works in Pi‑Web

The discovery flow spans three core files: [`lib/model-discovery.ts`](https://github.com/agegr/pi-web/blob/main/lib/model-discovery.ts), [`lib/model-discovery-auth.ts`](https://github.com/agegr/pi-web/blob/main/lib/model-discovery-auth.ts), and [`app/api/models-config/discover/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/models-config/discover/route.ts). Each handles a distinct phase of the pipeline.

### Authentication Resolution

Before any upstream request occurs, the system extracts provider credentials from the incoming request. The `resolveProviderAuth` helper in [`lib/model-discovery.ts`](https://github.com/agegr/pi-web/blob/main/lib/model-discovery.ts) (and its wrapper `resolveModelDiscoveryAuth` in the auth module) parses the query string for authentication data:

```typescript
// lib/model-discovery.ts
export function resolveProviderAuth(request: Request): ProviderAuth {
  const url = new URL(request.url);
  const provider = url.searchParams.get("provider") ?? "";
  const apiKey = url.searchParams.get("apiKey");
  const token = url.searchParams.get("token");
  if (apiKey) return { type: "apiKey", key: apiKey, provider };
  if (token) return { type: "oauth", token, provider };
  return { type: "none", provider };
}

```

The `ProviderAuth` union type supports three authentication modes: **API key**, **OAuth token**, and **no authentication**.

### URL Construction for Server-Side Discovery

The `buildModelsListUrl` function constructs a local endpoint URL that the Pi SDK intercepts and translates into provider-specific upstream requests:

```typescript
// lib/model-discovery.ts
export function buildModelsListUrl(provider: string, auth: ProviderAuth) {
  const base = `http://localhost:${process.env.PORT ?? 3000}/api/models-config/discover`;
  const params = new URLSearchParams();
  params.append("provider", provider);
  if (auth.type === "apiKey") {
    params.append("apiKey", auth.key);
  } else if (auth.type === "oauth") {
    params.append("token", auth.token);
  }
  return `${base}?${params}`;
}

```

Note the deliberate use of `localhost`—this URL is **not** meant for direct client consumption. The Pi SDK server intercepts this call and handles the actual HTTP request to the upstream provider, abstracting provider-specific APIs and keeping credentials server-bound.

### API Route Orchestration

The Next.js App Router handler in [`app/api/models-config/discover/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/models-config/discover/route.ts) ties the flow together:

```typescript
// app/api/models-config/discover/route.ts
export async function GET(request: Request) {
  const auth = resolveModelDiscoveryAuth(request);
  const url = buildModelsListUrl(auth.provider, auth);
  const response = await fetch(url, { method: "GET" });
  if (!response.ok) {
    return NextResponse.json({ error: "Failed to fetch models" }, { status: 502 });
  }
  const payload = await response.json();
  const models = parseDiscoveredModels(payload);
  return NextResponse.json(models);
}

```

The route returns **502 Bad Gateway** if the upstream fetch fails, clearly distinguishing provider unavailability from application errors.

### Response Parsing and Normalization

Raw provider responses vary in structure. The `parseDiscoveredModels` function normalizes these into a consistent `DiscoveredModel[]` array:

```typescript
// lib/model-discovery.ts
export function parseDiscoveredModels(value: unknown): DiscoveredModel[] {
  if (!Array.isArray(value)) {
    console.warn("Unexpected discovery payload", value);
    return [];
  }
  return value.flatMap((entry) => {
    if (typeof entry !== "object" || entry == null) return [];
    const { provider, models } = entry as { provider: string; models: ModelInfo[] };
    if (!Array.isArray(models)) return [];
    return models.map((m) => ({ ...m, provider } as DiscoveredModel));
  });
}

```

The parser is **defensive**: it silently drops malformed entries rather than failing the entire request, ensuring partial availability does not break discovery.

## Complete Model Discovery Flow

Here is the end-to-end flow a client application uses:

```typescript
// Client initiates discovery
const params = new URLSearchParams({
  provider: "anthropic",
  apiKey: process.env.ANTHROPIC_API_KEY!
});

const response = await fetch(`/api/models-config/discover?${params}`);
const models: DiscoveredModel[] = await response.json();

// models[] now contains: { id, name, description, provider: "anthropic", ... }

```

On the server, this triggers:

1. **Auth extraction** via `resolveModelDiscoveryAuth`
2. **URL construction** via `buildModelsListUrl`
3. **Server-side fetch** to the Pi SDK discovery endpoint
4. **Upstream provider request** (handled by Pi SDK internals)
5. **Response parsing** via `parseDiscoveredModels`
6. **JSON serialization** back to the client

## Key Design Decisions

| Decision | Rationale |
|----------|-----------|
| **Server-side credential handling** | Prevents API keys and OAuth tokens from exposing to browser environments |
| **Local URL interception** | Allows Pi SDK to inject provider-specific logic without client code changes |
| **Defensive parsing** | Graceful degradation when providers return unexpected payload shapes |
| **Flat model structure** | Simplifies UI consumption by attaching `provider` to every model object |

## File Reference Map

| File Path | Primary Responsibility |
|-----------|------------------------|
| [`lib/model-discovery.ts`](https://github.com/agegr/pi-web/blob/main/lib/model-discovery.ts) | URL building, response parsing, auth resolution utilities |
| [`lib/model-discovery-auth.ts`](https://github.com/agegr/pi-web/blob/main/lib/model-discovery-auth.ts) | Typed auth extraction from request (wraps `resolveProviderAuth`) |
| [`app/api/models-config/discover/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/models-config/discover/route.ts) | HTTP handler orchestrating the discovery pipeline |
| `@/lib/pi-types` | `ModelInfo` base type definition |
| `@/lib/model-catalog` | `Model` type integration |

## Summary

- **Model discovery** in `agegr/pi-web` uses a **server-side API route** (`/api/models-config/discover`) to proxy upstream provider requests securely.
- **Authentication** flows through query parameters parsed by `resolveProviderAuth` and normalized to the `ProviderAuth` type.
- **URL construction** via `buildModelsListUrl` creates local endpoints that the Pi SDK intercepts for actual upstream communication.
- **Response parsing** via `parseDiscoveredModels` flattens and validates provider-specific payloads into uniform `DiscoveredModel` objects.
- The architecture keeps **credentials server-bound** while exposing a simple JSON API for client consumption.

## Frequently Asked Questions

### What authentication methods does Pi‑Web model discovery support?

Pi‑Web supports three authentication modes as defined in the `ProviderAuth` type: **API key** (`type: "apiKey"`), **OAuth token** (`type: "oauth"`), and **no authentication** (`type: "none"`). These are extracted from query parameters in `resolveProviderAuth` and passed through to the Pi SDK for upstream requests.

### Why does `buildModelsListUrl` use localhost instead of the actual provider URL?

The localhost URL serves as a **server-side interception point** for the Pi SDK. The Pi SDK captures this request internally and translates it into the appropriate provider-specific HTTP call. This design abstracts provider API differences and ensures credentials never travel to the client.

### How does `parseDiscoveredModels` handle malformed upstream responses?

The parser implements **defensive validation**: it checks that the payload is an array, filters out non-object entries, verifies the `models` property exists as an array, and uses `flatMap` to silently drop invalid entries. This prevents partial provider failures from breaking the entire discovery flow.

### Can model discovery work without authentication?

Yes. When neither `apiKey` nor `token` query parameters are present, `resolveProviderAuth` returns `{ type: "none", provider }`. Whether this succeeds depends on the upstream provider's public API availability—the Pi SDK will attempt the request regardless.