Model Picker

Focus Deep-Dive

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

Primary role

Load the model registry from all configured providers, resolve tagged entries (default, fallback, image), and expose set/scan commands for model selection.

Main inputs

User-supplied model key strings (e.g. gpt-4, claude-3-opus), the OpenClawConfig object, and provider auth credentials from AuthProfileStore.

Key decisions

Is the requested model key valid? Is the provider reachable and the model available? Should suppressed built-in models be hidden from the listing?

Outputs / side effects

Writes the selected model key into the persisted config via updateConfig(). Returns ModelRow[] arrays for display and versions.json-compatible availability maps.

Key insight: The model picker never calls provider APIs directly. It delegates to 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[];
};
FieldTypePurpose
keystringCanonical model identifier used as the lookup key in the registry (e.g. "openai/gpt-4")
ref.providerstringProvider slug such as "openai", "anthropic", or "ollama"
ref.modelstringProvider-specific model name (e.g. "gpt-4-turbo-preview")
tagsSet<string>Role tags: "default", "fallback#N", "image", "img-fallback#N", "configured"
aliasesstring[]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;
};
FieldTypePurpose
keystringCanonical model key for selection
namestringHuman-readable display name
inputstringSupported input modalities (e.g. "text", "text+image")
contextWindownumber | nullMaximum context window in tokens, or null if unknown
localboolean | nullWhether the model runs locally (Ollama, LM Studio) vs. cloud API
availableboolean | nullProvider reachability check result; null if not yet probed
tagsstring[]Flattened tag array from ConfiguredEntry.tags for display
missingbooleantrue if the model key exists in config but not in the registry

Decision Points

DecisionWhat controls itWhy it matters
Built-in model suppressionshouldSuppressBuiltInModel() filter in loadModelRegistryPrevents deprecated or internal-only models from appearing in user-facing listings
Availability probingProvider auth credentials in AuthProfileStoreModels from providers without valid credentials are marked available: null instead of false
Tag assignmentresolveConfiguredEntries() walks the config hierarchyTags like "default" and "fallback#N" control how the fallback chain is displayed and evaluated
Missing model detectiontoModelRow() checks registry lookup resultConfig can reference models that have been removed from the provider; the missing flag surfaces this
Provider availability fallbackallowProviderAvailabilityFallback parameter on toModelRowWhen 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 };
}
Performance note: 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 };
}
Ordering matters: If the same model key appears as both "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

ScenarioInputResultRationale
Unknown model key/model set nonexistent-v99Error: model not found in registryRegistry lookup fails; applyDefaultModelPrimaryUpdate is never called
Suppressed built-inModel with shouldSuppressBuiltInModel() = trueExcluded from loadModelRegistry resultsPrevents deprecated internal models from polluting listings
Duplicate config entrySame key as both default and configuredSingle ConfiguredEntry with merged tags {"default", "configured"}Avoids duplicate rows in listings while preserving all semantic roles
Provider auth missingAnthropic model when no Anthropic API key setavailable: null, not falseDistinguishes "can't probe" from "probed and unreachable"
Local model offlineOllama model when Ollama server not runningavailable: false, local: trueLocal models are always probed; no auth fallback applies
Scan with zero candidates/model scan --provider nonexistentEmpty results, no errorScan gracefully returns empty array rather than throwing
Image model set on text-only/model set-image gpt-3.5-turboConfig persists the choice (no validation)The picker does not validate modality; runtime handles mismatch

Configuration

SettingLocationDefaultDescription
Default modelagents.defaults.model in openclaw.configProvider-dependentThe primary model used for all agent interactions
Image modelagents.defaults.imageModel in openclaw.confignullModel used for image generation tasks; separate from text model
Fallback chainagents.defaults.fallbacks[] in openclaw.config[]Ordered list of fallback models tried when the primary is unavailable
Scan concurrency--concurrency flag on /model scan5Maximum parallel probe requests during model scanning
Scan timeout--timeout flag on /model scan10sPer-model probe timeout in seconds
Scan max candidates--maxCandidates flag on /model scan50Maximum number of models to probe during a scan
Scan min params--minParams flag on /model scanNoneMinimum parameter count filter for scan candidates
Tip: The 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,
)
Provider Auth

How authentication credentials are discovered and stored per-provider, feeding the registry loader's availability checks.

Fallback Chain

The runtime mechanism that tries fallback models when the primary is unavailable — configured via the entries resolved here.

Command Parsing

How raw /model set gpt-4 input is parsed into structured action and args before reaching the model picker.

Permission Gating

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