{"record":{"id":"8c750c4f41fd62e5","repo":"sipeed/picoclaw","slug":"validation-error-v","errorCode":null,"errorMessage":"Validation error: %v","messagePattern":"Validation error: (.+?)","errorType":"http","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"web/backend/api/models.go","lineNumber":330,"sourceCode":"\t\treturn\n\t}\n\tdefer r.Body.Close()\n\n\ttype custom struct {\n\t\tconfig.ModelConfig\n\t\tAPIKey string `json:\"api_key\"`\n\t}\n\n\tvar mc custom\n\tif err = json.Unmarshal(body, &mc); err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"Invalid JSON: %v\", err), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tnormalizeIncomingModelConfig(&mc.ModelConfig)\n\n\tif err = validateIncomingModelConfig(&mc.ModelConfig, nil); err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"Validation error: %v\", err), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tif mc.APIKey != \"\" {\n\t\tmc.ModelConfig.SetAPIKey(mc.APIKey)\n\t}\n\n\tcfg, err := config.LoadConfig(h.configPath)\n\tif err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"Failed to load config: %v\", err), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tcfg.ModelList = append(cfg.ModelList, &mc.ModelConfig)\n\tnormalizeStoredModelProviders(cfg)\n\n\tif err := config.SaveConfig(h.configPath, cfg); err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"Failed to save config: %v\", err), http.StatusInternalServerError)","sourceCodeStart":312,"sourceCodeEnd":348,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/web/backend/api/models.go#L312-L348","documentation":"HTTP 400 returned by POST /api/models (handleAddModel) when validateIncomingModelConfig rejects the normalized payload. Rejections come from ModelConfig.Validate (model_name required, model required, whitespace in model id, leading '/', consecutive '//', bad tool_schema_transform), a missing or unsupported provider, an ElevenLabs model other than the single supported id, or a provider not creatable right now (e.g. claude-cli/codex-cli when the CLI executable is not installed/authed).","triggerScenarios":"POST /api/models with empty model_name or model; provider \"foo\" (not in providers.IsSupportedModelProvider); model \"gpt-4 o\" or \"/gpt-4o\" or \"openai//gpt-4o\"; provider elevenlabs with model \"elevenlabs-turbo\" instead of the supported id; provider claude-cli when the claude binary is missing so createAllowedForProvider is false.","commonSituations":"Form submitted with required fields blank; provider name typed freehand instead of picked from the provider_options list; users assuming any model id works for ElevenLabs; CLI providers (claude-cli, codex-cli) used before installing or authenticating the underlying CLI; copy-pasting model ids that contain spaces or leading slashes.","solutions":["Read the %v detail — it names the exact failing rule (e.g. 'model_name is required', 'provider \"x\" is not supported')","Supply non-empty model_name and model, and pick provider from the provider_options array returned by GET /api/models","Normalize the model id: trim whitespace, no leading '/', no '//' — the provider prefix should come from the provider field, not be glued twice","For elevenlabs use the exact supported model id from the error message; for claude-cli/codex-cli install and log in to the CLI first, then retry"],"exampleFix":"// before: rejected — no model_name, provider typo, spaced model\n{ \"model\": \"openai/gpt 4o\", \"provider\": \"opennai\" }\n\n// after: accepted\n{ \"model_name\": \"gpt4o\", \"model\": \"gpt-4o\", \"provider\": \"openai\", \"api_base\": \"https://api.openai.com/v1\", \"api_key\": \"sk-...\" }","handlingStrategy":"validation","validationCode":"function validateModelDraft(draft, providerOptions) {\n  if (!draft.model_name?.trim()) throw new Error('model_name is required');\n  if (!draft.model?.trim()) throw new Error('model is required');\n  if (/\\s/.test(draft.model)) throw new Error('model id must not contain whitespace');\n  if (draft.model.startsWith('/') || draft.model.includes('//')) throw new Error('model id must not start with / or contain //');\n  if (!providerOptions.some(p => p.id === draft.provider)) throw new Error(`unsupported provider ${draft.provider}`);\n}","typeGuard":"function isCreatableModelDraft(draft, providerOptions) {\n  const opt = providerOptions.find(p => p.id === draft.provider);\n  return Boolean(opt) && opt.create_allowed === true\n    && typeof draft.model_name === 'string' && draft.model_name.trim() !== ''\n    && typeof draft.model === 'string' && /^[^\\s/]/.test(draft.model) && !draft.model.includes('//');\n}","tryCatchPattern":"const res = await fetch('/api/models', { method: 'POST', body });\nif (!res.ok && (await res.text()).startsWith('Validation error')) {\n  // show the rule detail to the user; fix fields and resubmit — do not blind-retry\n}","preventionTips":["Populate the provider dropdown from provider_options in the GET /api/models response instead of free text","Require model_name and model in the form before enabling submit","Trim whitespace from model ids client-side; reject leading '/' and '//'","For CLI providers, confirm the CLI is installed and authenticated before creating entries"],"tags":["go","http","validation","models","bad-request"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}