# How Pi Web's enableModels Scope Resolution Works with Minimatch Patterns

> Discover how Pi Web's enableModels scope resolution uses minimatch patterns to match available models. Learn about supported formats and suffixes for efficient model selection.

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

---

**Pi Web resolves model scopes by converting user patterns into glob strings and matching them against available models using minimatch, supporting provider/modelId formats, bare model names, and optional thinking-level suffixes.**

In `agegr/pi-web`, the **enableModels** feature controls which AI models an Agent session can access. Users specify models through intuitive patterns like `"openai/gpt-4"` or `"claude:medium"`, which the system transforms into minimatch globs for flexible matching. This article breaks down the scope resolution algorithm implemented across three core source files.

## Core Components of Scope Resolution

The resolution pipeline splits into two main functions in [[`lib/model-scope.ts`](https://github.com/agegr/pi-web/blob/main/lib/model-scope.ts)](https://github.com/agegr/pi-web/blob/main/lib/model-scope.ts):

- `resolveModelScope()` – handles raw scope strings for runtime sessions
- `resolveModelScopePatterns()` – processes arrays of patterns from configuration

Both ultimately rely on **minimatch** glob matching against the model catalog format `provider/modelId`.

## How Patterns Become Minimatch Globs

The `resolveModelScopePatterns()` function (lines 43–67 in [[`model-scope.ts`](https://github.com/agegr/pi-web/blob/main/model-scope.ts)](https://github.com/agegr/pi-web/blob/main/lib/model-scope.ts#L43-L67)) performs three transformations on each pattern:

1. **Separate thinking level** – split on `:` to isolate an optional suffix like `medium` or `large`
2. **Build the glob base** – if the pattern contains `/`, use it directly as `provider/modelId`; otherwise wrap with wildcards as `*modelId*`
3. **Reattach level** – append the thinking level with colon if present

```typescript
const globs = patterns.map((p) => {
  const [base, level] = p.split(":");
  const glob = base.includes("/") ? base : `*${base}*`;
  return level ? `${glob}:${level}` : glob;
});

```

Each available model (always stored as `"provider/modelId"`) is then tested against these globs:

```typescript
for (const model of availableModels) {
  if (globs.some((g) => minimatch(model, g))) {
    matches.push(model);
  }
}

```

## Runtime Session Resolution

When starting an Agent session via [`startRpcSession()`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts#L12-L28) in [`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts), the scope string flows through `resolveModelScope()`:

```typescript
export async function startRpcSession(
  cwd: string,
  toolNames: string[],
  modelScope: string,
) {
  const availableModels = await getCachedModels();
  const scopedModels = resolveModelScope(modelScope, availableModels);
  
  const options: AgentSessionOptions = {
    cwd,
    allowTools: toolNames,
    models: scopedModels,  // Restricted model set
  };
  return new AgentSession(options);
}

```

This delegates to the Pi SDK's `resolveModelScopeWithDiagnostics()` with added logging for UI feedback, returning only matching model IDs.

## Configuration-Driven Pattern Matching

The UI stores user-enabled models in `settings.enabledModels` (a string array), retrieved via [`getEnabledModelsConfig()`](https://github.com/agegr/pi-web/blob/main/lib/models-config-store.ts#L10-L17) in [`lib/models-config-store.ts`](https://github.com/agegr/pi-web/blob/main/lib/models-config-store.ts):

```typescript
export function getEnabledModelsConfig() {
  const settings = readGlobalSettings();
  const patternList = settings.enabledModels || [];
  const availableModels = [] as string[]; // Populated from cache
  return resolveModelScopePatterns(patternList, availableModels);
}

```

This separation allows the UI to save flexible patterns while the resolver expands them to concrete model IDs.

## Pattern Examples and Matching Behavior

| User Pattern | Generated Glob | Matches | Does Not Match |
|-------------|----------------|---------|----------------|
| `openai/gpt-4` | `openai/gpt-4` | `openai/gpt-4`, `openai/gpt-4:medium` | `anthropic/gpt-4` |
| `gpt-4` | `*gpt-4*` | `openai/gpt-4`, `azure/gpt-4-turbo` | `gpt-4o` (unless `*gpt-4*` matches) |
| `claude:medium` | `*claude*:medium` | `anthropic/claude:medium` | `anthropic/claude:low` |
| `anthropic/claude:large` | `anthropic/claude:large` | `anthropic/claude:large` | `anthropic/claude` |

**Note:** The thinking level is treated as a literal suffix after glob matching, not as a separate filtering stage.

## Code Example: Complete Workflow

```typescript
// lib/example-usage.ts
import { startRpcSession } from "./rpc-manager";
import { getEnabledModelsConfig } from "./models-config-store";

// Scenario 1: Explicit scope for a specific task
const restrictedSession = await startRpcSession(
  "/project",
  ["read_file", "edit_file"],
  "anthropic/claude-opus:large"  // Single high-capability model
);

// Scenario 2: User-configured enabled models from settings
const userPatterns = ["openai/gpt-4", "gemini", "claude-sonnet"];
// Stored via UI to ~/.pi/agent/settings.json as { "enabledModels": [...] }

const resolvedModels = getEnabledModelsConfig(); 
// → ["openai/gpt-4", "google/gemini-pro", "anthropic/claude-sonnet"]

const flexibleSession = await startRpcSession(
  "/project",
  ["search", "shell"],
  resolvedModels.join(",")  // Passed as comma-separated scope string
);

```

## Summary

- **`resolveModelScope()`** in [`lib/model-scope.ts`](https://github.com/agegr/pi-web/blob/main/lib/model-scope.ts) handles single scope strings for runtime sessions, delegating to the SDK with diagnostic logging
- **`resolveModelScopePatterns()`** converts pattern arrays into minimatch globs, supporting three pattern styles: full `provider/modelId`, bare `modelId` with wildcards, and `:thinkingLevel` suffixes
- **Minimatch matching** occurs against the canonical `provider/modelId` format of available models
- **[`lib/rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/lib/rpc-manager.ts)** applies resolved scopes to restrict `AgentSession` initialization
- **[`lib/models-config-store.ts`](https://github.com/agegr/pi-web/blob/main/lib/models-config-store.ts)** bridges UI configuration to concrete model lists

## Frequently Asked Questions

### What pattern formats does enableModels support?

Pi Web accepts three formats: complete identifiers like `"openai/gpt-4"`, bare names like `"claude"` (automatically wrapped as `*claude*`), and thinking-level variants like `"gpt-4:medium"`. The colon separator is always parsed as a suffix delimiter, not part of the model identity.

### How does minimatch globbing handle overlapping patterns?

Patterns match independently with OR logic—any model matching at least one glob is included. More specific patterns like `"anthropic/claude-opus"` take precedence over broader ones like `"claude"` when both match, but both contribute the same model to the final list without duplication.

### Where does Pi Web store the user's enabledModels configuration?

The pattern list persists in the global settings file retrieved via `readGlobalSettings()` from [`lib/settings.ts`](https://github.com/agegr/pi-web/blob/main/lib/settings.ts). The UI reads and writes this location, while [`lib/models-config-store.ts`](https://github.com/agegr/pi-web/blob/main/lib/models-config-store.ts) transforms the stored patterns into resolved model IDs for session creation.

### What's the difference between resolveModelScope and resolveModelScopePatterns?

`resolveModelScope()` is designed for dynamic scope strings at session startup, integrating with the Pi SDK's diagnostic resolver. `resolveModelScopePatterns()` handles static configuration arrays with custom glob generation logic, used when loading saved UI preferences.