sipeed/picoclaw · error

Cannot set virtual model %q as default

Error message

Cannot set virtual model %q as default

What it means

Returned by POST /api/models/default when the requested model exists but ModelConfig.IsVirtual() is true. Virtual models are synthetic per-key entries generated by multi-key expansion (one logical model with several API keys becomes N virtual children); they have no independent identity to be a chat default, so the handler rejects them with 400 even though they appear in model_list.

Source

Thrown at web/backend/api/models.go:576

		return
	}

	// Verify the model_name exists in model_list and is not a virtual model
	found := false
	isVirtual := false
	for _, m := range cfg.ModelList {
		if m.ModelName == req.ModelName {
			found = true
			isVirtual = m.IsVirtual()
			break
		}
	}
	if !found {
		http.Error(w, fmt.Sprintf("Model %q not found in model_list", req.ModelName), http.StatusNotFound)
		return
	}
	if isVirtual {
		http.Error(w, fmt.Sprintf("Cannot set virtual model %q as default", req.ModelName), http.StatusBadRequest)
		return
	}
	for _, m := range cfg.ModelList {
		if m.ModelName == req.ModelName {
			if !defaultModelAllowedForModelConfig(m) {
				http.Error(
					w,
					fmt.Sprintf("Model %q cannot be used as the default chat model", req.ModelName),
					http.StatusBadRequest,
				)
				return
			}
			break
		}
	}

	cfg.Agents.Defaults.ModelName = req.ModelName

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Set the default to the parent (non-virtual) model entry instead of the per-key expansion
  2. Filter the picker: exclude entries where the list response marks the model as virtual
  3. If multi-key expansion is unwanted, collapse the model back to a single API key so no virtual children exist

Example fix

// before
const target = models.find(m => m.model_name.includes('gpt-4o'));

// after
const target = models.find(m => m.model_name.includes('gpt-4o') && !m.is_virtual);
Defensive patterns

Strategy: validation

Validate before calling

const models = await (await fetch('/api/models')).json();
const eligible = models.filter((m: any) => !m.is_virtual); // exclude multi-key expansion entries
if (eligible.length === 0) throw new Error('no non-virtual models available to default');
await postDefault(eligible[0].model_name);

Type guard

function isDefaultEligible(m: { model_name: string; is_virtual?: boolean }): boolean {
  return m.is_virtual !== true;
}

Try / catch

try {
  const res = await fetch('/api/models/default', {...});
  if (res.status === 400) {
    const t = await res.text();
    if (t.includes('virtual')) {/* filter virtual entries out of the picker and re-submit */}
  }
} catch (e) { /* network */ }

Prevention

When it happens

Trigger: POST /api/models/default naming a model that came from multi-key expansion. These entries typically carry a suffixed name (e.g. "my-model #2") and are marked virtual in the list response; picking one from a raw model_list dump triggers the 400.

Common situations: A UI lists every entry of model_list without filtering virtual ones, letting users select a per-key clone; scripts iterating model_list blindly choose the first match which happens to be a virtual expansion entry.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/af2079b42fd1ceb8. Report an issue: GitHub.