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

> Learn how enabledModels scoping uses minimatch globs to filter models and set thinking levels. Discover model resolution and selection logic in Pi Web.

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

---

**Use glob patterns with optional `:level` suffixes in `enabledModels` to filter available models and pin thinking levels, with [`lib/model-scope.ts`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/lib/model-scope.ts) at lines 27-28, the resolver processes each pattern:

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

```typescript
// 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

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

```typescript
// 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.

```typescript
// 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.

```typescript
// 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`](https://github.com/agegr/pi-web/blob/main/lib/models-config-store.ts), which manages user preferences. This feeds directly into `resolveVisibleModels` whenever the model scope needs recalculation.