BerriAI/litellm · warning · HTTPException

Discount for {provider} must be a number

Error message

Discount for {provider} must be a number

What it means

Validation error (HTTP 400) from PATCH /config/cost_discount_config: the value for each provider key must be an int or float. Python's isinstance check rejects strings like "0.1", booleans aside, None, lists, or nested objects.

Source

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

    # Validate that all providers are valid LiteLLM providers
    invalid_providers: Final = []
    for provider in cost_discount_config:
        if provider not in LlmProvidersSet:
            invalid_providers.append(provider)

    if invalid_providers:
        raise HTTPException(
            status_code=400,
            detail={
                "error": f"Invalid provider(s): {', '.join(invalid_providers)}. Must be valid LiteLLM providers. See https://docs.litellm.ai/docs/providers for the full list."
            },
        )

    # Validate discount values are between 0 and 1
    for provider, discount in cost_discount_config.items():
        if not isinstance(discount, (int, float)):
            raise HTTPException(status_code=400, detail=f"Discount for {provider} must be a number")
        if not (0 <= discount <= 1):
            raise HTTPException(
                status_code=400,
                detail=f"Discount for {provider} must be between 0 and 1 (0% to 100%)",
            )

    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_discount_config
        config["litellm_settings"]["cost_discount_config"] = cost_discount_config

        # Save the updated config to DB

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. Send a bare JSON number: {"openai": 0.10} not {"openai": "0.10"}.
  2. If generating the payload programmatically, cast with float()/int() before serializing.
  3. Check for accidental nulls or nested dicts in the payload.

Example fix

# before
{"openai": "0.10"}   # 400 Discount for openai must be a number

# after
{"openai": 0.10}
Defensive patterns

Strategy: type-guard

Validate before calling

cleaned = {}
for provider, discount in raw_config.items():
    if isinstance(discount, str):
        discount = float(discount)  # coerce stringified numbers, or fail loudly
    if not isinstance(discount, (int, float)) or isinstance(discount, bool):
        raise TypeError(f"{provider}: discount must be a number, got {type(discount).__name__}")
    cleaned[provider] = discount

Type guard

def is_numeric_discount(cfg: dict) -> bool:
    return all(
        isinstance(v, (int, float)) and not isinstance(v, bool)
        for v in cfg.values()
    )

Prevention

When it happens

Trigger: Sending the discount as a quoted string ("discount": "0.10") — very common when values come from environment variables or YAML-as-string; sending null or an object {"discount": 0.1} instead of a bare number.

Common situations: Values round-tripped through env vars or templates that stringify numbers; JSON built by string concatenation; config pipelines that emit strings for all scalars.

Related errors


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