{"record":{"id":"3f41e17728d96b9c","repo":"odysseus-dev/odysseus","slug":"hidden-must-be-a-list-of-model-ids","errorCode":null,"errorMessage":"hidden must be a list of model IDs","messagePattern":"hidden must be a list of model IDs","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"routes/model_routes.py","lineNumber":2383,"sourceCode":"\n        Expects JSON body with optional keys:\n          {\"hidden\": [\"model-id-1\", ...], \"pinned_models\": [\"deploy-id\", ...]}\n        Each key is updated only when present, so callers can patch one list\n        without clobbering the other.\n        \"\"\"\n        require_admin(request)\n        db = SessionLocal()\n        try:\n            ep = db.query(ModelEndpoint).filter(ModelEndpoint.id == ep_id).first()\n            if not ep:\n                raise HTTPException(404, \"Endpoint not found\")\n            body = await request.json()\n            if not isinstance(body, dict):\n                raise HTTPException(400, \"Body must be a JSON object\")\n            if \"hidden\" in body:\n                hidden = body.get(\"hidden\")\n                if not isinstance(hidden, list):\n                    raise HTTPException(400, \"hidden must be a list of model IDs\")\n                base = _normalize_base(ep.base_url)\n                kind = _effective_endpoint_kind(ep, base)\n                if _picker_requires_pinning(base, kind):\n                    # Compatibility for older/admin UI paths that still submit\n                    # the previous hide-list shape. API pickers are allow-lists:\n                    # convert \"unchecked models\" into an explicit pinned list so\n                    # Settings summary, /api/models, and chat agree.\n                    selected = _visible_models(_cached_model_ids(ep), hidden, None)\n                    ep.pinned_models = json.dumps(selected)\n                    ep.hidden_models = None\n                else:\n                    ep.hidden_models = json.dumps(hidden) if hidden else None\n            # Accept either \"pinned\" or \"pinned_models\" for the manual IDs list.\n            if \"pinned_models\" in body or \"pinned\" in body:\n                pinned = _normalize_model_ids(body.get(\"pinned_models\", body.get(\"pinned\")))\n                base = _normalize_base(ep.base_url)\n                kind = _effective_endpoint_kind(ep, base)\n                if _picker_requires_pinning(base, kind):","sourceCodeStart":2365,"sourceCodeEnd":2401,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/model_routes.py#L2365-L2401","documentation":"400 raised at routes/model_routes.py:2383 in the bulk visibility update handler when the body contains a 'hidden' key but its value is not a JSON list. The handler checks `isinstance(hidden, list)` because it must iterate model ids and later json.dumps them; a string, object, number, or null fails the guard. Element types are not checked here — only the container.","triggerScenarios":"Sending {\"hidden\": \"gpt-4o\"} (single id as a bare string), {\"hidden\": {\"gpt-4o\": true}} (an object/map), or {\"hidden\": null}. Sending a comma-separated string \"a,b,c\" instead of [\"a\",\"b\",\"c\"].","commonSituations":"Form serialization that maps unchecked checkboxes to a map or single value. Client building the payload from a text input without splitting into an array. Passing undefined/null because the caller 'had no hidden models' — the correct move is to omit the key entirely (each key is only updated when present).","solutions":["Send a JSON array: {\"hidden\": [\"model-id-1\", \"model-id-2\"]}.","For 'no hidden models', omit the 'hidden' key entirely or send an empty array — do not send null.","If the source is a comma-separated string, split it: hidden.split(',').map(s => s.trim()).filter(Boolean).","Remember 'pinned_models' expects a list of deploy ids, same rule."],"exampleFix":"// before\nbody = { hidden: selectedHidden.join(',') };  // \"a,b\" -> 400\n\n// after\nbody = { hidden: selectedHidden };  // [\"a\",\"b\"]","handlingStrategy":"validation","validationCode":"const hidden = Array.isArray(rawHidden) ? rawHidden : null;\nif (rawHidden !== undefined && hidden === null) throw new TypeError('hidden must be an array of model ids');","typeGuard":"function isModelIdList(v: unknown): v is string[] {\n  return Array.isArray(v) && v.every(x => typeof x === 'string');\n}","tryCatchPattern":"try { await api.patchVisibility(epId, {hidden}); }\ncatch (e) { if (e.status === 400 && /hidden/.test(e.message)) throw new TypeError('wrap in array'); throw e; }","preventionTips":["Omit the 'hidden' key entirely rather than sending null.","Split comma-separated strings into arrays before sending.","Type the payload in the client (TS interface) so shape errors surface at compile time."],"tags":["fastapi","http-400","json-validation","request-body"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}