odysseus-dev/odysseus · error · HTTPException

hidden must be a list of model IDs

Error message

hidden must be a list of model IDs

What it means

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.

Source

Thrown at routes/model_routes.py:2383

        Expects JSON body with optional keys:
          {"hidden": ["model-id-1", ...], "pinned_models": ["deploy-id", ...]}
        Each key is updated only when present, so callers can patch one list
        without clobbering the other.
        """
        require_admin(request)
        db = SessionLocal()
        try:
            ep = db.query(ModelEndpoint).filter(ModelEndpoint.id == ep_id).first()
            if not ep:
                raise HTTPException(404, "Endpoint not found")
            body = await request.json()
            if not isinstance(body, dict):
                raise HTTPException(400, "Body must be a JSON object")
            if "hidden" in body:
                hidden = body.get("hidden")
                if not isinstance(hidden, list):
                    raise HTTPException(400, "hidden must be a list of model IDs")
                base = _normalize_base(ep.base_url)
                kind = _effective_endpoint_kind(ep, base)
                if _picker_requires_pinning(base, kind):
                    # Compatibility for older/admin UI paths that still submit
                    # the previous hide-list shape. API pickers are allow-lists:
                    # convert "unchecked models" into an explicit pinned list so
                    # Settings summary, /api/models, and chat agree.
                    selected = _visible_models(_cached_model_ids(ep), hidden, None)
                    ep.pinned_models = json.dumps(selected)
                    ep.hidden_models = None
                else:
                    ep.hidden_models = json.dumps(hidden) if hidden else None
            # Accept either "pinned" or "pinned_models" for the manual IDs list.
            if "pinned_models" in body or "pinned" in body:
                pinned = _normalize_model_ids(body.get("pinned_models", body.get("pinned")))
                base = _normalize_base(ep.base_url)
                kind = _effective_endpoint_kind(ep, base)
                if _picker_requires_pinning(base, kind):

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Send a JSON array: {"hidden": ["model-id-1", "model-id-2"]}.
  2. For 'no hidden models', omit the 'hidden' key entirely or send an empty array — do not send null.
  3. If the source is a comma-separated string, split it: hidden.split(',').map(s => s.trim()).filter(Boolean).
  4. Remember 'pinned_models' expects a list of deploy ids, same rule.

Example fix

// before
body = { hidden: selectedHidden.join(',') };  // "a,b" -> 400

// after
body = { hidden: selectedHidden };  // ["a","b"]
Defensive patterns

Strategy: validation

Validate before calling

const hidden = Array.isArray(rawHidden) ? rawHidden : null;
if (rawHidden !== undefined && hidden === null) throw new TypeError('hidden must be an array of model ids');

Type guard

function isModelIdList(v: unknown): v is string[] {
  return Array.isArray(v) && v.every(x => typeof x === 'string');
}

Try / catch

try { await api.patchVisibility(epId, {hidden}); }
catch (e) { if (e.status === 400 && /hidden/.test(e.message)) throw new TypeError('wrap in array'); throw e; }

Prevention

When it happens

Trigger: 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"].

Common situations: 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).

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/3f41e17728d96b9c. Report an issue: GitHub.