# How Pi-Web Handles enabledModels Scoping with Minimatch Patterns

> Learn how pi-web scopes enabledModels using minimatch patterns. Discover how resolveModelScopeWithDiagnostics filters AI models for UI and AgentSession.

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

---

**Pi-Web delegates enabledModels pattern matching to `resolveModelScopeWithDiagnostics`, which uses minimatch globs, fuzzy string matching, and thinking-level suffix extraction to filter the available AI models before they reach the UI or AgentSession initialization.**

Pi-Web provides granular control over which AI models appear in the interface through the **enabledModels** configuration setting. This feature leverages the exact same pattern syntax as the π-agent CLI, supporting minimatch glob expressions, fuzzy matching, and thinking-level pinning. Understanding how pi-web handles enabledModels scoping with minimatch patterns reveals the architectural bridge between user preferences and the actual model runtime.

## enabledModels Pattern Syntax

The `enabledModels` setting in `~/.pi/agent/settings.json` accepts an array of pattern strings that `SettingsManager.getEnabledModels()` retrieves. These patterns follow three distinct syntax rules:

- **Minimatch globs** – Patterns containing wildcards like `anthropic/*` or `my-gateway/*` match against the full identifier `provider/modelId` using standard glob semantics.
- **Fuzzy (non-glob) patterns** – Plain strings such as `gpt-4` trigger fuzzy matching against model IDs without requiring exact provider prefixes.
- **Thinking-level suffix** – Any pattern may end with `:level` (e.g., `anthropic/*:high`) to pin a specific thinking level for every model the glob matches.

## The Resolution Pipeline

When Pi-Web needs to determine which models are visible, it executes a six-step resolution process in [`lib/model-scope.ts`](https://github.com/agegr/pi-web/blob/main/lib/model-scope.ts):

1. **Collect raw patterns** from `SettingsManager.getEnabledModels()`.
2. **Clean the list** by trimming whitespace and filtering empty strings. If the list is empty, Pi-Web falls back to all available models.
3. **Call** `resolveModelScopeWithDiagnostics` to parse patterns, expand globs with minimatch, apply fuzzy matching, and extract `:thinkingLevel` suffixes.
4. **Derive the UI-visible list** from the `model` field of each returned `ScopedModel` object.
5. **Collect thinking-level pins** by mapping `provider/modelId → level` for any scoped model containing a thinking level specification.
6. **Return a `ModelScopeResult`** containing `visible`, `scopedModels`, `thinkingLevelPins`, and any `warnings`.

If the patterns resolve to zero models, Pi-Web again falls back to the full model catalogue and reports the situation via the `warnings` field, ensuring the UI never renders an empty dropdown.

## Core Implementation in lib/model-scope.ts

The `resolveVisibleModels` function in [`lib/model-scope.ts`](https://github.com/agegr/pi-web/blob/main/lib/model-scope.ts) orchestrates the scoping logic. It handles early exits for empty configurations and builds the final result object:

```typescript
// lib/model-scope.ts (excerpt)
export async function resolveVisibleModels(
  modelRuntime: ModelRuntime,
  patterns: string[] | undefined,
): Promise<ModelScopeResult> {
  const cleaned = (patterns ?? []).map(p => p.trim()).filter(Boolean);
  if (cleaned.length === 0) {
    return {
      visible: await modelRuntime.getAvailable(),
      scopedModels: [],
      thinkingLevelPins: {},
      warnings: [],
    };
  }

  const { scopedModels, diagnostics } = await resolveModelScopeWithDiagnostics(
    cleaned,
    modelRuntime,
  );
  const warnings = diagnostics.map(d => d.message);
  if (scopedModels.length === 0) {
    return {
      visible: await modelRuntime.getAvailable(),
      scopedModels: [],
      thinkingLevelPins: {},
      warnings,
    };
  }

  // Collect thinking-level pins (e.g. "anthropic/*:high")
  const thinkingLevelPins: Record<string, string> = {};
  for (const scoped of scopedModels) {
    if (scoped.thinkingLevel) {
      thinkingLevelPins[`${scoped.model.provider}/${scoped.model.id}`] = scoped.thinkingLevel;
    }
  }

  return {
    visible: scopedModels.map(s => s.model),
    scopedModels,
    thinkingLevelPins,
    warnings,
  };
}

```

According to the pi-web source code, lines 85-90 specifically handle the extraction of thinking-level pins by iterating through `scopedModels` and populating the record with composite keys in the format `provider/modelId`.

## Session Initialization and API Integration

Pi-Web resolves the enabled-model scope at two critical integration points: during RPC session startup and when serving the models API endpoint.

### Session Initialization

In [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts), the `startRpcSession` function resolves the scope once and passes the resulting data to the AgentSession constructor:

```typescript
// lib/rpc-manager.ts (excerpt)
const services = await createAgentSessionServices({ cwd: sessionCwd, agentDir });
const scope = await resolveVisibleModels(
  services.modelRuntime,
  services.settingsManager.getEnabledModels(),
);
const initial = selectInitialModelScope(scope, {
  ...(initialModel ? { requestedModel: initialModel } : {}),
  ...(defaultProvider && defaultModelId
    ? { defaultModel: { provider: defaultProvider, modelId: defaultModelId } }
    : {}),
  ...(thinkingLevel ? { thinkingLevel } : {}),
});

```

### API Endpoint

The `/api/models` route in [`app/api/models/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/models/route.ts) uses the same routine to populate the model selector in the sidebar:

```typescript
// app/api/models/route.ts (excerpt)
const settings = services.settingsManager;
const scope = await resolveVisibleModels(
  services.modelRuntime,
  settings.getEnabledModels(),
);
const { visible, thinkingLevelPins, warnings } = scope;
// `visible` is turned into the UI dropdown, `thinkingLevelPins` are sent to the client.

```

## Fallback Behavior and Edge Cases

Pi-Web implements defensive fallbacks to prevent configuration errors from breaking the user experience. If `enabledModels` contains malformed patterns or restrictive globs that exclude every available model, the system automatically returns the complete model list from `modelRuntime.getAvailable()` while preserving diagnostic warnings. This ensures that a misconfigured `~/.pi/agent/settings.json` never results in an unusable interface.

## Summary

- Pi-Web uses **minimatch glob patterns** in `enabledModels` to filter the `provider/modelId` namespace, supporting wildcards like `anthropic/*`.
- The **fuzzy matching** mechanism treats non-glob strings as partial matches against model IDs.
- **Thinking-level pinning** via `:level` suffixes maps specific reasoning intensities to matched models in [`lib/model-scope.ts`](https://github.com/agegr/pi-web/blob/main/lib/model-scope.ts).
- The resolution pipeline delegates pattern parsing to `resolveModelScopeWithDiagnostics` and always falls back to the full model catalogue if patterns resolve to empty sets.
- Both the RPC session manager ([`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts)) and the models API endpoint ([`app/api/models/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/models/route.ts)) consume `resolveVisibleModels` to maintain consistent scoping across the application.

## Frequently Asked Questions

### What pattern syntax does enabledModels support?

The `enabledModels` array accepts minimatch glob patterns (e.g., `openai/*`), fuzzy plain-text matches (e.g., `claude`), and composite strings with thinking-level suffixes (e.g., `anthropic/*:high`). These patterns match against the canonical `provider/modelId` identifier.

### How does Pi-Web handle thinking level pinning?

When a pattern ends with a colon-prefixed level like `:high` or `:low`, `resolveVisibleModels` extracts this suffix during the resolution loop (lines 85-90 in [`lib/model-scope.ts`](https://github.com/agegr/pi-web/blob/main/lib/model-scope.ts)) and builds a `thinkingLevelPins` map that associates specific `provider/modelId` combinations with their pinned levels.

### What happens if enabledModels matches no models?

If `resolveModelScopeWithDiagnostics` returns an empty array, Pi-Web immediately falls back to returning all available models from `modelRuntime.getAvailable()` while preserving any diagnostic warnings. This prevents the UI from rendering empty model selectors due to overly restrictive patterns.

### Where does Pi-Web store the enabledModels configuration?

The configuration persists in `~/.pi/agent/settings.json` and is accessed at runtime through `SettingsManager.getEnabledModels()`. Both the session initialization logic in [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts) and the API route in [`app/api/models/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/models/route.ts) retrieve the setting via this manager.