unslothai/unsloth · warning · ValueError

Expected a number, got a boolean.

Error message

Expected a number, got a boolean.

What it means

Pydantic validation failure (HTTP 422) from a mode='before' validator applied to the numeric override fields (max_seq_length, custom_context_length, spec_draft_n_max, n_parallel, n_batch, n_ubatch, gpu_layers, n_cpu_moe). Because bool subclasses int, lax pydantic parsing would turn JSON true into 1 — pinning GPU 1 or setting a one-token context. The validator rejects any scalar bool before coercion; downstream _bounded_int never sees one.

Source

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

    @field_validator(
        "max_seq_length",
        "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,

View on GitHub (pinned to 203007d190)

Solutions

  1. Send integers for all numeric fields (e.g. gpu_layers: 40, n_parallel: 2); use null/omit to leave a field unchanged.
  2. Add a client-side typeof check rejecting booleans for these keys.
  3. Validate generated payloads against a JSON schema with type:'integer' before submitting.

Example fix

// before
await api.put(url, { gpu_layers: true }); // 422

// after
const overrides = { gpu_layers: Number(gpuLayersInput.value) };
for (const [k, v] of Object.entries(overrides)) {
  if (typeof v === 'boolean') throw new TypeError(`${k} must be a number`);
}
await api.put(url, overrides);
Defensive patterns

Strategy: type-guard

Validate before calling

const NUMERIC_KEYS = ['max_seq_length','custom_context_length','spec_draft_n_max','n_parallel','n_batch','n_ubatch','gpu_layers','n_cpu_moe'];
for (const k of NUMERIC_KEYS) {
  if (typeof payload[k] === 'boolean') throw new TypeError(`${k} must be a number`);
}

Type guard

function isNumericOverride(v: unknown): v is number {
  return typeof v === 'number' && Number.isInteger(v);
}

Try / catch

try { await api.put(overridesUrl, payload); }
catch (e) {
  if (e.status === 422 && /boolean/.test(e.detail?.toString() ?? '')) { sanitizeNumericFields(); return; }
  throw e;
}

Prevention

When it happens

Trigger: PUT model overrides with gpu_layers: true, n_parallel: false, or any of the listed fields set to a JSON boolean instead of an integer.

Common situations: JS UI binding a toggle to a numeric field; config generated from YAML where 'on'/'off' became true/false; scripts building payloads dynamically with mixed types.

Related errors


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