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
- Set the default to the parent (non-virtual) model entry instead of the per-key expansion
- Filter the picker: exclude entries where the list response marks the model as virtual
- 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
- Filter virtual/multi-key expansion entries out of the default-model picker UI
- Prefer parent models when using multi-key configurations
- Label virtual entries in the UI so users understand why they are excluded
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
- model_name is required
- Model %q cannot be used as the default chat model
- build request: %w
- Invalid JSON: %v
- port %d is out of range (1-65535)
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/af2079b42fd1ceb8.
Report an issue: GitHub.