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 sessionsresolveModelScopePatterns()– 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:
- Separate thinking level – split on
:to isolate an optional suffix likemediumorlarge - Build the glob base – if the pattern contains
/, use it directly asprovider/modelId; otherwise wrap with wildcards as*modelId* - 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()inlib/model-scope.tshandles single scope strings for runtime sessions, delegating to the SDK with diagnostic loggingresolveModelScopePatterns()converts pattern arrays into minimatch globs, supporting three pattern styles: fullprovider/modelId, baremodelIdwith wildcards, and:thinkingLevelsuffixes- Minimatch matching occurs against the canonical
provider/modelIdformat of available models lib/rpc-manager.tsapplies resolved scopes to restrictAgentSessioninitializationlib/models-config-store.tsbridges 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →