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

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):

  • 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/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
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:

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() in lib/rpc-manager.ts, the scope string flows through resolveModelScope():

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() in lib/models-config-store.ts:

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

// 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 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 applies resolved scopes to restrict AgentSession initialization
  • 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. The UI reads and writes this location, while 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.

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 →