BerriAI/litellm · warning · HTTPException

Margin for {provider} must be a number (percentage) or dict

Error message

Margin for {provider} must be a number (percentage) or dict with 'percentage' and/or 'fixed_amount'

What it means

Validation error (HTTP 400) from PATCH /config/cost_margin_config: a provider's margin value is neither a number (simple percentage format) nor a dict (complex format with 'percentage'/'fixed_amount') — e.g. a string, list, or null. This is the fallback branch after the isinstance checks for (int, float) and dict both fail.

Source

Thrown at litellm/proxy/management_endpoints/cost_tracking_settings.py:405

            if "fixed_amount" in margin_value:
                fixed_amount = margin_value["fixed_amount"]
                if not isinstance(fixed_amount, (int, float)):
                    raise HTTPException(
                        status_code=400,
                        detail=f"Fixed margin amount for {provider} must be a number",
                    )
                if fixed_amount < 0:
                    raise HTTPException(
                        status_code=400,
                        detail=f"Fixed margin amount for {provider} must be non-negative",
                    )
            if not margin_value:  # Empty dict
                raise HTTPException(
                    status_code=400,
                    detail=f"Margin config for {provider} cannot be empty. Must include 'percentage' and/or 'fixed_amount'",
                )
        else:
            raise HTTPException(
                status_code=400,
                detail=f"Margin for {provider} must be a number (percentage) or dict with 'percentage' and/or 'fixed_amount'",
            )

    try:
        # Load existing config
        config: Final = await proxy_config.get_config()

        # Ensure litellm_settings exists
        if "litellm_settings" not in config:
            config["litellm_settings"] = {}

        # Update cost_margin_config
        config["litellm_settings"]["cost_margin_config"] = cost_margin_config

        # Save the updated config to DB
        await proxy_config.save_config(new_config=config)

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. Send either a bare number ({"openai": 0.08}) or a dict ({"openai": {"percentage": 0.08}}).
  2. Never send null, strings, or arrays as the margin value.
  3. Run the payload through a JSON schema check before submitting (see type guard).

Example fix

# before
{"openai": "0.08"}

# after
{"openai": 0.08}
Defensive patterns

Strategy: type-guard

Validate before calling

def normalize_margin(v):
    if isinstance(v, str):
        v = float(v)
    if isinstance(v, dict):
        assert {"percentage", "fixed_amount"} & set(v), "dict needs percentage/fixed_amount"
    else:
        assert isinstance(v, (int, float)) and not isinstance(v, bool), "must be number or dict"
    return v

cost_margin_config = {k: normalize_margin(v) for k, v in cost_margin_config.items()}

Type guard

from typing import Union

def is_margin_value(v: object) -> bool:
    if isinstance(v, bool) or v is None:
        return False
    if isinstance(v, (int, float)):
        return True
    return isinstance(v, dict) and bool({"percentage", "fixed_amount"} & set(v))

Prevention

When it happens

Trigger: Sending {"openai": "0.08"}, {"openai": null}, {"openai": [0.08]}, or a boolean; stringified values from config pipelines.

Common situations: JSON payloads built from untyped sources where scalars become strings or nulls; booleans sneaking in from feature-flag systems (bool is not int-checked here for the dict path but True would pass the numeric branch as 1 — the string/list cases are the real hits).

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18). Data as JSON: /api/errors/a70f8df695e1fe74. Report an issue: GitHub.