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

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, lib/model-discovery-auth.ts, and 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 (and its wrapper resolveModelDiscoveryAuth in the auth module) parses the query string for authentication data:

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

// 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 ties the flow together:

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

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

// 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 URL building, response parsing, auth resolution utilities
lib/model-discovery-auth.ts Typed auth extraction from request (wraps resolveProviderAuth)
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.

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 →