unslothai/unsloth · warning · ValueError

Expected numbers, got a boolean.

Error message

Expected numbers, got a boolean.

What it means

Pydantic validation failure (HTTP 422) from the same _no_booleans before-validator, list branch: the field value is a list (in practice gpu_ids) and at least one element is a boolean. Without the guard, [true, 1] would coerce to [1, 1] and silently duplicate GPU 1 instead of 400/422.

Source

Thrown at studio/backend/routes/settings.py:736

        "custom_context_length",
        "spec_draft_n_max",
        "n_parallel",
        "n_batch",
        "n_ubatch",
        "gpu_layers",
        "n_cpu_moe",
        "gpu_ids",
        mode = "before",
    )
    @classmethod
    def _no_booleans(cls, value: Any) -> Any:
        # bool subclasses int and pydantic parses non-strictly, so `true` arrives as 1: a
        # payload could pin GPU 1 or set a one-token context. _bounded_int rejects bools but
        # never sees one, since coercion happens here first. Only bools, so lax parsing stays.
        if isinstance(value, bool):
            raise ValueError("Expected a number, got a boolean.")
        if isinstance(value, list) and any(isinstance(item, bool) for item in value):
            raise ValueError("Expected numbers, got a boolean.")
        return value


class ModelOverridesResponse(BaseModel):
    overrides: dict[str, dict]
    # Filled only when the caller named a model: the entry ITS load would apply,
    # resolved here rather than in the browser. The folding rules are Python's
    # (casefold is not toLowerCase, and an ambiguous fold matches nothing on
    # purpose), so a client mirroring them can only approximate.
    resolved: Optional[dict] = None
    resolved_key: Optional[str] = None


def _upload_limit_response(limit_mb: int) -> UploadLimitResponse:
    return UploadLimitResponse(
        max_upload_size_mb = limit_mb,
        max_upload_size_bytes = upload_limit_bytes(limit_mb),
        max_upload_size_label = upload_limit_label(limit_mb),

View on GitHub (pinned to 203007d190)

Solutions

  1. Send only integers in gpu_ids, e.g. [0, 1].
  2. Map checkbox state to indices client-side: gpus.map((checked, i) => checked ? i : null).filter(v => v !== null).
  3. Assert every element Number.isInteger before submit.

Example fix

// before
await api.put(url, { gpu_ids: [true, false] }); // 422

// after
const gpuIds = gpus.map((checked, i) => (checked ? i : -1)).filter(i => i >= 0);
if (!gpuIds.every(Number.isInteger)) throw new TypeError('gpu_ids must be integers');
await api.put(url, { gpu_ids: gpuIds });
Defensive patterns

Strategy: type-guard

Validate before calling

if (Array.isArray(payload.gpu_ids) && payload.gpu_ids.some(Number.isBoolean)) {
  throw new TypeError('gpu_ids must contain only integers');
}

Type guard

function isGpuIdList(v: unknown): v is number[] {
  return Array.isArray(v) && v.every(n => Number.isInteger(n) && n >= 0);
}

Try / catch

try { await api.put(overridesUrl, payload); }
catch (e) {
  if (e.status === 422 && /Expected numbers/.test(e.detail?.toString() ?? '')) { payload.gpu_ids = payload.gpu_ids.filter(Number.isInteger); return api.put(overridesUrl, payload); }
  throw e;
}

Prevention

When it happens

Trigger: PUT model overrides with gpu_ids: [true] or [0, false, 2]; UI checkboxes feeding directly into the GPU id array.

Common situations: Checkbox-driven GPU selector where checked=true is pushed into the ids array; mixed data from form state; scripted payloads reusing toggle state.

Related errors


AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15). Data as JSON: /api/errors/1cc173f94d8af87d. Report an issue: GitHub.