How Enabled Models Scoping Works with Minimatch Globs and Thinking Level in Pi Web

Use glob patterns with optional :level suffixes in enabledModels to filter available models and pin thinking levels, with lib/model-scope.ts handling resolution, ambiguity detection, and initial model selection.

The enabledModels configuration drives which models appear in the Pi Web UI, which the interface can cycle through, and which thinking levels apply when a new AgentSession begins. According to the agegr/pi-web source code, this scoping system lives in lib/model-scope.ts and uses the Pi SDK's own resolver to ensure consistent behavior with the Pi CLI's --models flag.

Glob Matching with Minimatch Syntax

Patterns in enabledModels support standard glob characters (*, ?, [...]) and resolve against provider/modelId strings or bare modelId values. The matching delegates to the Pi SDK's resolveModelScopeWithDiagnostics rather than custom logic.

In lib/model-scope.ts at lines 27-28, the resolver processes each pattern:

// From lib/model-scope.ts
const resolved = await resolveModelScopeWithDiagnostics(runtime, pattern, {
  allowFuzzyMatch: !hasGlob(pattern),
});

This delegation ensures Pi Web stays synchronized with Pi's evolving resolver rules.

Thinking Level Suffixes

Any pattern may append a colon-prefixed thinking level using the ThinkingLevel enum values: off, minimal, low, medium, high, or max. For example, anthropic/claude-3-opus:high pins "high" thinking for that specific model, while anthropic/*:medium applies "medium" to all matching Anthropic models.

The suffix detection occurs in assertNoAmbiguousExactPatterns (lines 82-87), and resolveVisibleModels (lines 38-47) builds the thinkingLevelPins map:

// Conceptual pattern from lib/model-scope.ts lines 38-47
for (const pattern of enabledModels) {
  const [basePattern, level] = splitThinkingLevel(pattern);
  const models = await resolvePattern(basePattern);
  if (level && isValidThinkingLevel(level)) {
    for (const model of models) {
      thinkingLevelPins.set(model.id, level);
    }
  }
}

These pins apply automatically when selectInitialModelScope chooses a model unless the caller provides an explicit thinkingLevel override.

Fuzzy Matching for Non-Glob Patterns

When a pattern contains no glob characters, exactReferenceMatches (lines 64-71) attempts resolution in two stages:

  1. Exact match: Try provider/modelId first
  2. Fallback match: Case-insensitive match against modelId alone
// Example configuration demonstrating both forms
{
  "enabledModels": [
    "openai/gpt-4o",      // exact provider/modelId
    "claude-3-opus"       // bare modelId, matches any provider
  ]
}

The bare modelId form enables portable configurations across different API keys or provider setups.

Ambiguity Protection

Non-glob patterns matching multiple models trigger an error from assertNoAmbiguousExactPatterns (lines 73-99). This prevents silent confusion when two providers offer identically-named models:

// lib/model-scope.ts lines 89-96 - throws if ambiguous
if (matches.length > 1) {
  throw new Error(
    `Pattern "${pattern}" is ambiguous: matches ${matches
      .map((m) => `${m.provider}/${m.modelId}`)
      .join(", ")}. Use fully-qualified "provider/modelId".`
  );
}

The error message lists all matches, guiding users toward explicit provider/modelId specifications.

Fallback Behavior

If enabledModels is empty or all patterns resolve to zero models, resolveVisibleModels returns all available models (lines 13-20). This safety mechanism prevents configuration typos from rendering the model selector unusable.

// Early return from lib/model-scope.ts lines 13-20
if (!enabledModels?.length) {
  return {
    visibleModels: allModels,
    scopedModels: allModels,
    thinkingLevelPins: new Map(),
  };
}

Initial Model Selection

selectInitialModelScope (lines 63-98) implements a four-tier fallback for choosing the starting model:

  1. Explicit request: Use caller-provided model if it exists in scope
  2. Default model: Use configured default if it passes scope validation
  3. First scoped model: Use first model from enabledModels resolution
  4. First visible model: Final fallback to any available model

The function simultaneously resolves the thinking level: explicit override takes precedence, then the matching pin from thinkingLevelPins, then no preference.

// Example: Starting a session with scoped model selection
import { resolveVisibleModels, selectInitialModelScope } from "./model-scope";
import { modelRuntime } from "./model-runtime";

async function startSession() {
  const scope = await resolveVisibleModels(modelRuntime, [
    "anthropic/*:high",
    "openai/gpt-4o",
  ]);

  const { model, thinkingLevel } = selectInitialModelScope(scope, {
    requestedModel: { provider: "openai", modelId: "gpt-4o" },
  });

  console.log(model?.id, thinkingLevel); // "gpt-4o", undefined
}

Summary

  • Glob syntax: *, ?, [...] supported via Pi SDK's resolveModelScopeWithDiagnostics
  • Thinking levels: Append :off|minimal|low|medium|high|max to any pattern
  • Bare model IDs: Case-insensitive fallback matching when no provider specified
  • Ambiguity guard: Errors force fully-qualified IDs when patterns match multiple models
  • Empty scope fallback: Returns all models rather than empty list
  • Selection priority: Requested → default → first scoped → first visible

Frequently Asked Questions

What happens if two providers have the same model ID?

Pi Web throws an ambiguity error from assertNoAmbiguousExactPatterns and requires you to use the fully-qualified form provider/modelId. The error message lists all matching providers to help you correct the configuration.

Can I mix glob patterns and exact model IDs in the same enabledModels array?

Yes. resolveVisibleModels processes each pattern independently, so you can combine anthropic/*:high with openai/gpt-4o and gemini-pro. Each pattern resolves according to its own syntax rules.

Does the thinking level suffix work with glob patterns?

Yes. The suffix applies to every model matched by the glob. anthropic/*:high pins "high" thinking for all Anthropic models in scope, while */*:minimal would pin "minimal" across all providers.

Where does Pi Web store the enabledModels configuration?

The enabledModels array and default model settings persist through lib/models-config-store.ts, which manages user preferences. This feeds directly into resolveVisibleModels whenever the model scope needs recalculation.

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 →