How Pi-Web Handles enabledModels Scoping with Minimatch Patterns

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:

  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 orchestrates the scoping logic. It handles early exits for empty configurations and builds the final result object:

// 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, the startRpcSession function resolves the scope once and passes the resulting data to the AgentSession constructor:

// 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 uses the same routine to populate the model selector in the sidebar:

// 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.
  • 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) and the models API endpoint (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) 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 and the API route in app/api/models/route.ts retrieve the setting via this manager.

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 →