{"record":{"id":"af2079b42fd1ceb8","repo":"sipeed/picoclaw","slug":"cannot-set-virtual-model-q-as-default","errorCode":null,"errorMessage":"Cannot set virtual model %q as default","messagePattern":"Cannot set virtual model %q as default","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"web/backend/api/models.go","lineNumber":576,"sourceCode":"\t\treturn\n\t}\n\n\t// Verify the model_name exists in model_list and is not a virtual model\n\tfound := false\n\tisVirtual := false\n\tfor _, m := range cfg.ModelList {\n\t\tif m.ModelName == req.ModelName {\n\t\t\tfound = true\n\t\t\tisVirtual = m.IsVirtual()\n\t\t\tbreak\n\t\t}\n\t}\n\tif !found {\n\t\thttp.Error(w, fmt.Sprintf(\"Model %q not found in model_list\", req.ModelName), http.StatusNotFound)\n\t\treturn\n\t}\n\tif isVirtual {\n\t\thttp.Error(w, fmt.Sprintf(\"Cannot set virtual model %q as default\", req.ModelName), http.StatusBadRequest)\n\t\treturn\n\t}\n\tfor _, m := range cfg.ModelList {\n\t\tif m.ModelName == req.ModelName {\n\t\t\tif !defaultModelAllowedForModelConfig(m) {\n\t\t\t\thttp.Error(\n\t\t\t\t\tw,\n\t\t\t\t\tfmt.Sprintf(\"Model %q cannot be used as the default chat model\", req.ModelName),\n\t\t\t\t\thttp.StatusBadRequest,\n\t\t\t\t)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\n\tcfg.Agents.Defaults.ModelName = req.ModelName\n","sourceCodeStart":558,"sourceCodeEnd":594,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/web/backend/api/models.go#L558-L594","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nconst target = models.find(m => m.model_name.includes('gpt-4o'));\n\n// after\nconst target = models.find(m => m.model_name.includes('gpt-4o') && !m.is_virtual);","handlingStrategy":"validation","validationCode":"const models = await (await fetch('/api/models')).json();\nconst eligible = models.filter((m: any) => !m.is_virtual); // exclude multi-key expansion entries\nif (eligible.length === 0) throw new Error('no non-virtual models available to default');\nawait postDefault(eligible[0].model_name);","typeGuard":"function isDefaultEligible(m: { model_name: string; is_virtual?: boolean }): boolean {\n  return m.is_virtual !== true;\n}","tryCatchPattern":"try {\n  const res = await fetch('/api/models/default', {...});\n  if (res.status === 400) {\n    const t = await res.text();\n    if (t.includes('virtual')) {/* filter virtual entries out of the picker and re-submit */}\n  }\n} catch (e) { /* network */ }","preventionTips":["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"],"tags":["http","validation","virtual-model","multi-key","default-model"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}