{"record":{"id":"8e86a8d080e955a9","repo":"sipeed/picoclaw","slug":"provider-is-required","errorCode":null,"errorMessage":"provider is required","messagePattern":"provider is required","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"web/backend/api/models.go","lineNumber":654,"sourceCode":"\tif err != nil {\n\t\thttp.Error(w, \"Failed to read request body\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tdefer r.Body.Close()\n\n\tvar req struct {\n\t\tProvider   string `json:\"provider\"`\n\t\tAPIKey     string `json:\"api_key\"`\n\t\tAPIBase    string `json:\"api_base\"`\n\t\tModelIndex *int   `json:\"model_index,omitempty\"`\n\t}\n\tif err = json.Unmarshal(body, &req); err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"Invalid JSON: %v\", err), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tif req.Provider == \"\" {\n\t\thttp.Error(w, \"provider is required\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tif !providers.IsModelProviderFetchable(req.Provider) {\n\t\thttp.Error(w, fmt.Sprintf(\"provider %q does not support model listing\", req.Provider), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tapiKey := strings.TrimSpace(req.APIKey)\n\tapiBase := strings.TrimSpace(req.APIBase)\n\n\tif apiKey == \"\" && req.ModelIndex != nil {\n\t\tif stored := h.lookupStoredAPIKey(*req.ModelIndex, req.Provider, apiBase); stored != \"\" {\n\t\t\tapiKey = stored\n\t\t}\n\t}\n\n\tif apiBase == \"\" {","sourceCodeStart":636,"sourceCodeEnd":672,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/web/backend/api/models.go#L636-L672","documentation":"Returned by POST /api/models/fetch when the parsed JSON has an empty (or missing) provider field. Provider determines which upstream model-list API to call, so an empty value is rejected with 400 before any key/base fallback logic runs.","triggerScenarios":"POST /api/models/fetch with {}, {\"provider\": \"\"}, or {\"api_key\": \"sk-...\"} only. Also when the client sends a differently-named field (e.g. \"provider_id\") so Provider stays empty after unmarshal.","commonSituations":"Provider dropdown left on a placeholder option with value \"\"; state variable initialized as empty string and submitted before selection; field renamed on the frontend but not the backend.","solutions":["Include a non-empty provider, e.g. {\"provider\": \"openai\", ...}","Disable the submit button until the provider selector has a concrete value","Fetch the provider list from the API (e.g. provider options endpoint) and use its exact IDs"],"exampleFix":"// before\nbody: JSON.stringify({ provider: providerId ?? '', api_key: key })\n\n// after\nif (!providerId) throw new Error('select a provider first');\nbody: JSON.stringify({ provider: providerId, api_key: key })","handlingStrategy":"validation","validationCode":"function providerPayload(provider: string | undefined | null): { provider: string } {\n  const p = (provider ?? '').trim();\n  if (!p) throw new Error('provider is required');\n  return { provider: p };\n}","typeGuard":"function hasProvider(v: unknown): v is { provider: string } {\n  return typeof v === 'object' && v !== null && typeof (v as any).provider === 'string' && (v as any).provider.trim() !== '';\n}","tryCatchPattern":"try {\n  const res = await fetch('/api/models/fetch', {...});\n  if (res.status === 400 && (await res.text()) === 'provider is required') {\n    /* keep the Fetch button disabled until a provider is selected */\n  }\n} catch (e) { /* network */ }","preventionTips":["Use a placeholder-free select or disable submit until provider is chosen","Populate provider options from the backend so ids are always valid and non-empty","Trim and assert provider before serializing"],"tags":["http","validation","provider","api"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}