Load the model registry from all configured providers, resolve tagged entries (default, fallback, image), and expose set/scan commands for model selection.
Model Picker
Discovering, filtering, and selecting AI models across providers
When a user runs /model set gpt-4 or /model scan, the model picker subsystem is responsible for loading the full model registry, resolving configured entries (defaults, fallbacks, image models), presenting filterable rows, and persisting the user's selection back into the configuration. It bridges the gap between raw provider catalogs and the single "active model" that the agent runtime consumes.
📚 What is this & why does it matter?
The model picker is the user-facing layer that turns a sprawling multi-provider model catalog into a manageable selection experience. Every /model subcommand — set, set-image, list, scan — relies on the registry loader and configured-entries resolver to know which models exist, which are available, and which the user has already chosen. Understanding this module is essential for debugging "model not found" errors, adding new provider integrations, or tracing how a fallback chain is evaluated at runtime.
💡 Real-world analogy
Think of a streaming service's "Choose your plan" screen. Behind the scenes the service queries every available plan from multiple billing providers, marks which ones are available in your region, highlights your current selection, and lets you switch with a single click. The model picker does the same thing — except the "plans" are AI models from OpenAI, Anthropic, local Ollama instances, and more. It aggregates, filters, tags, and presents them so the user can pick one without knowing the underlying provider plumbing.
Contents
Overview
User-supplied model key strings (e.g. gpt-4, claude-3-opus), the OpenClawConfig object, and provider auth credentials from AuthProfileStore.
Is the requested model key valid? Is the provider reachable and the model available? Should suppressed built-in models be hidden from the listing?
Writes the selected model key into the persisted config via updateConfig(). Returns ModelRow[] arrays for display and versions.json-compatible availability maps.
discoverModels() which abstracts every provider behind a uniform Model<Api> interface. This keeps the picker purely about selection, not about protocol differences between providers.
Core Types
The model picker revolves around two data structures: ConfiguredEntry which represents a model reference extracted from the user's config, and ModelRow which is the display-ready row shown to the user in listings and scan results.
ConfiguredEntry
Each entry in the user's configuration — default model, fallback chain, image model — is normalized into a ConfiguredEntry. The tags set indicates why this entry exists (e.g. "default", "fallback#1", "image").
// src/commands/models/list.types.ts
export type ConfiguredEntry = {
key: string;
ref: { provider: string; model: string };
tags: Set<string>;
aliases: string[];
};
| Field | Type | Purpose |
|---|---|---|
key | string | Canonical model identifier used as the lookup key in the registry (e.g. "openai/gpt-4") |
ref.provider | string | Provider slug such as "openai", "anthropic", or "ollama" |
ref.model | string | Provider-specific model name (e.g. "gpt-4-turbo-preview") |
tags | Set<string> | Role tags: "default", "fallback#N", "image", "img-fallback#N", "configured" |
aliases | string[] | Alternative names that resolve to this entry (e.g. ["gpt4", "gpt-4-turbo"]) |
ModelRow
The presentation-layer type used by the /model list and /model scan commands to render tabular output. Produced by the toModelRow() transform function.
// src/commands/models/list.types.ts
export type ModelRow = {
key: string;
name: string;
input: string;
contextWindow: number | null;
local: boolean | null;
available: boolean | null;
tags: string[];
missing: boolean;
};
| Field | Type | Purpose |
|---|---|---|
key | string | Canonical model key for selection |
name | string | Human-readable display name |
input | string | Supported input modalities (e.g. "text", "text+image") |
contextWindow | number | null | Maximum context window in tokens, or null if unknown |
local | boolean | null | Whether the model runs locally (Ollama, LM Studio) vs. cloud API |
available | boolean | null | Provider reachability check result; null if not yet probed |
tags | string[] | Flattened tag array from ConfiguredEntry.tags for display |
missing | boolean | true if the model key exists in config but not in the registry |
Decision Points
| Decision | What controls it | Why it matters |
|---|---|---|
| Built-in model suppression | shouldSuppressBuiltInModel() filter in loadModelRegistry | Prevents deprecated or internal-only models from appearing in user-facing listings |
| Availability probing | Provider auth credentials in AuthProfileStore | Models from providers without valid credentials are marked available: null instead of false |
| Tag assignment | resolveConfiguredEntries() walks the config hierarchy | Tags like "default" and "fallback#N" control how the fallback chain is displayed and evaluated |
| Missing model detection | toModelRow() checks registry lookup result | Config can reference models that have been removed from the provider; the missing flag surfaces this |
| Provider availability fallback | allowProviderAvailabilityFallback parameter on toModelRow | When an individual model probe fails, falls back to provider-level availability if allowed |
How It Works
The model picker pipeline has three distinct phases: registry loading, configured entry resolution, and model selection/scanning. Each phase builds on the previous one's output.
Phase 1 — Registry Loading
The loadModelRegistry() function bootstraps the full model catalog from all configured providers. It discovers the agent directory, reads auth credentials, calls each provider's model enumeration API, and filters out suppressed built-in models.
// src/commands/models/list.registry.ts
export async function loadModelRegistry(
cfg: OpenClawConfig,
opts?: { sourceConfig?: OpenClawConfig },
) {
const agentDir = resolveOpenClawAgentDir();
const authStorage = discoverAuthStorage(agentDir);
const registry = discoverModels(authStorage, agentDir);
const models = registry
.getAll()
.filter((model) =>
!shouldSuppressBuiltInModel({
provider: model.provider,
id: model.id,
}),
);
// ... availability checking against auth credentials
return { registry, models, availableKeys, availabilityErrorMessage };
}
discoverModels() caches provider responses for the lifetime of the process. Subsequent calls to loadModelRegistry() within the same session are nearly free because the discovery layer returns cached catalogs.
Phase 2 — Configured Entry Resolution
Once the registry is loaded, resolveConfiguredEntries() walks the user's OpenClawConfig to extract every model reference and assign semantic tags. The function processes entries in a fixed order: default model first, then fallback chain, then image model, then image fallback chain, and finally any explicitly configured model entries.
// src/commands/models/list.configured.ts
export function resolveConfiguredEntries(cfg: OpenClawConfig) {
// resolves default, fallbacks, image model, configured models
// Tags: "default", "fallback#N", "image", "img-fallback#N", "configured"
return { entries };
}
"default" and "configured", the tags are merged into a single ConfiguredEntry rather than creating a duplicate. This ensures each model key appears at most once in the resolved list, with all applicable tags combined.
Phase 3 — Model Selection Commands
The modelsSetCommand and modelsSetImageCommand functions handle the final step: persisting the user's choice into the config file. Both follow the same pattern — call updateConfig() with a mutation function, log the result, and confirm to the user.
// src/commands/models/set.ts
export async function modelsSetCommand(
modelRaw: string,
runtime: RuntimeEnv,
) {
const updated = await updateConfig((cfg) => {
return applyDefaultModelPrimaryUpdate({
cfg,
modelRaw,
field: "model",
});
});
logConfigUpdated(runtime);
runtime.log(
`Default model: ${
resolveAgentModelPrimaryValue(updated.agents?.defaults?.model)
?? modelRaw
}`,
);
}
// src/commands/models/set-image.ts
export async function modelsSetImageCommand(
modelRaw: string,
runtime: RuntimeEnv,
) {
const updated = await updateConfig((cfg) => {
return applyDefaultModelPrimaryUpdate({
cfg,
modelRaw,
field: "imageModel",
});
});
logConfigUpdated(runtime);
runtime.log(
`Image model: ${
resolveAgentModelPrimaryValue(updated.agents?.defaults?.imageModel)
?? modelRaw
}`,
);
}
Model Selection Pipeline
The following flowchart shows the complete decision path from a user's /model set command to the final config write. Each diamond is a decision point; each rectangle is a processing step.
flowchart TD
CMD["User runs /model set <key>"]
LOAD["loadModelRegistry(cfg)"]
DISCOVER["discoverModels(authStorage, agentDir)"]
FILTER{"shouldSuppressBuiltInModel?"}
AVAIL["Check availability against auth"]
RESOLVE["resolveConfiguredEntries(cfg)"]
LOOKUP{"Model key in registry?"}
APPLY["applyDefaultModelPrimaryUpdate()"]
WRITE["updateConfig() — persist to disk"]
LOG["logConfigUpdated(runtime)"]
CONFIRM["runtime.log — confirm to user"]
ERR["Error: model not found"]
CMD --> LOAD
LOAD --> DISCOVER
DISCOVER --> FILTER
FILTER -->|Suppressed| DISCOVER
FILTER -->|Kept| AVAIL
AVAIL --> RESOLVE
RESOLVE --> LOOKUP
LOOKUP -->|Yes| APPLY
LOOKUP -->|No| ERR
APPLY --> WRITE
WRITE --> LOG
LOG --> CONFIRM
The pipeline is intentionally linear for the set commands — there is no branching or retry logic. If the model key is not found in the registry, the command fails immediately with an error message. The scan command, by contrast, adds a probing loop before the selection step.
modelsSetCommand Flow
This sequence diagram traces the exact call chain when a user runs /model set gpt-4. Each participant corresponds to a source file, and every arrow is a real function call in the codebase.
sequenceDiagram
participant User
participant CLI as commands/models/set.ts
participant Config as updateConfig()
participant Apply as applyDefaultModelPrimaryUpdate()
participant Resolve as resolveAgentModelPrimaryValue()
participant Runtime as RuntimeEnv
User->>CLI: /model set gpt-4
CLI->>Config: updateConfig(mutationFn)
Config->>Apply: applyDefaultModelPrimaryUpdate({cfg, modelRaw, field})
Apply-->>Config: updated OpenClawConfig
Config-->>CLI: persisted config object
CLI->>Resolve: resolveAgentModelPrimaryValue(updated.agents.defaults.model)
Resolve-->>CLI: "gpt-4" or null
CLI->>Runtime: logConfigUpdated(runtime)
CLI->>Runtime: runtime.log("Default model: gpt-4")
Runtime-->>User: "Default model: gpt-4"
Notice that resolveAgentModelPrimaryValue is called after the config write. This is intentional — it reads the updated config to resolve any alias or provider-qualified name back to a human-readable form. If resolution fails (returns null), the raw user input is echoed instead.
Model Scan Pipeline
The modelsScanCommand extends the basic selection flow with concurrent availability probing. It enumerates candidate models, probes each for reachability (respecting concurrency and timeout limits), and optionally auto-selects the best candidate.
flowchart TD
SCAN["User runs /model scan"]
OPTS["Parse scan options"]
LOAD2["loadModelRegistry(cfg)"]
CANDIDATES["Filter candidates by provider, minParams, maxAge"]
PROBE{"Probe each candidate"}
REACHABLE["Collect reachable models"]
RANK["Rank by context window and capabilities"]
AUTO{"--setDefault or --setImage?"}
SET["modelsSetCommand / modelsSetImageCommand"]
DISPLAY["Display scan results as ModelRow table"]
JSON{"--json flag?"}
JSONOUT["Output JSON array"]
SCAN --> OPTS
OPTS --> LOAD2
LOAD2 --> CANDIDATES
CANDIDATES --> PROBE
PROBE --> REACHABLE
REACHABLE --> RANK
RANK --> AUTO
AUTO -->|Yes| SET
AUTO -->|No| DISPLAY
RANK --> JSON
JSON -->|Yes| JSONOUT
JSON -->|No| DISPLAY
Code Walkthrough: toModelRow
The toModelRow function is the bridge between the internal Model<Api> objects and the user-facing ModelRow display type. Here is how each piece of data is resolved, step by step.
Accept parameters and destructure
The function receives a parameter bag that includes the registry model (which may be undefined if the config references a model that no longer exists), the canonical key, tags, aliases, the set of available keys from probing, and optional config/auth references for provider-level fallback.
// src/commands/models/list.registry.ts
export function toModelRow(params: {
model?: Model<Api>;
key: string;
tags: string[];
aliases?: string[];
availableKeys?: Set<string>;
cfg?: OpenClawConfig;
authStore?: AuthProfileStore;
allowProviderAvailabilityFallback?: boolean;
}): ModelRow
Handle missing models
If params.model is undefined, the model exists in config but not in any provider's catalog. The function returns a ModelRow with missing: true and all capability fields set to null. This allows the UI to render a warning row without crashing.
Resolve availability
For existing models, availability is determined by checking params.availableKeys. If the key is present, available: true. If the set exists but the key is absent, available: false. If no availability set was provided (probing was skipped), available: null. When allowProviderAvailabilityFallback is set and the model-level check is null, the function falls back to checking whether any model from the same provider is available.
Build the display row
The final ModelRow is assembled from the model's metadata (name, input modalities, context window, local flag) combined with the resolved availability and the flattened tags array. This is returned to the caller for rendering.
Edge Cases
| Scenario | Input | Result | Rationale |
|---|---|---|---|
| Unknown model key | /model set nonexistent-v99 | Error: model not found in registry | Registry lookup fails; applyDefaultModelPrimaryUpdate is never called |
| Suppressed built-in | Model with shouldSuppressBuiltInModel() = true | Excluded from loadModelRegistry results | Prevents deprecated internal models from polluting listings |
| Duplicate config entry | Same key as both default and configured | Single ConfiguredEntry with merged tags {"default", "configured"} | Avoids duplicate rows in listings while preserving all semantic roles |
| Provider auth missing | Anthropic model when no Anthropic API key set | available: null, not false | Distinguishes "can't probe" from "probed and unreachable" |
| Local model offline | Ollama model when Ollama server not running | available: false, local: true | Local models are always probed; no auth fallback applies |
| Scan with zero candidates | /model scan --provider nonexistent | Empty results, no error | Scan gracefully returns empty array rather than throwing |
| Image model set on text-only | /model set-image gpt-3.5-turbo | Config persists the choice (no validation) | The picker does not validate modality; runtime handles mismatch |
Configuration
| Setting | Location | Default | Description |
|---|---|---|---|
| Default model | agents.defaults.model in openclaw.config | Provider-dependent | The primary model used for all agent interactions |
| Image model | agents.defaults.imageModel in openclaw.config | null | Model used for image generation tasks; separate from text model |
| Fallback chain | agents.defaults.fallbacks[] in openclaw.config | [] | Ordered list of fallback models tried when the primary is unavailable |
| Scan concurrency | --concurrency flag on /model scan | 5 | Maximum parallel probe requests during model scanning |
| Scan timeout | --timeout flag on /model scan | 10s | Per-model probe timeout in seconds |
| Scan max candidates | --maxCandidates flag on /model scan | 50 | Maximum number of models to probe during a scan |
| Scan min params | --minParams flag on /model scan | None | Minimum parameter count filter for scan candidates |
modelsScanCommand accepts all of these as optional CLI flags. When omitted, the defaults shown above are used. The --json flag outputs raw ModelRow[] as JSON for scripting and automation.
// src/commands/models/scan.ts — full signature
export async function modelsScanCommand(
opts: {
minParams?: string;
maxAgeDays?: string;
provider?: string;
maxCandidates?: string;
timeout?: string;
concurrency?: string;
yes?: boolean;
input?: boolean;
setDefault?: boolean;
setImage?: boolean;
json?: boolean;
probe?: boolean;
},
runtime: RuntimeEnv,
)
Related Topics
How authentication credentials are discovered and stored per-provider, feeding the registry loader's availability checks.
The runtime mechanism that tries fallback models when the primary is unavailable — configured via the entries resolved here.
How raw /model set gpt-4 input is parsed into structured action and args before reaching the model picker.
Authorization checks that may restrict which models a user is allowed to select or scan.
Source Files
Key files involved in model picking
src/
└── commands/
└── models/
├── list.types.ts ← ConfiguredEntry & ModelRow type definitions
├── list.registry.ts ← loadModelRegistry(), toModelRow() transform
├── list.configured.ts ← resolveConfiguredEntries() tag resolution
├── set.ts ← modelsSetCommand() — set default model
├── set-image.ts ← modelsSetImageCommand() — set image model
└── scan.ts ← modelsScanCommand() — probe & auto-select