← Back to research
Guides11 min readPublished 2026-07-20

How to Fix an LLM API 400 Bad Request

Fix an LLM API 400 Bad Request by validating JSON, model IDs, messages, endpoints, parameters, token limits, and per-model feature support before retrying.

By LumeAPI Engineering Team

OpenAI-Compatible API hub →

Last verified: July 20, 2026

An LLM API returns 400 Bad Request when it can read the HTTP request but cannot accept its payload or parameters. Fix it by preserving the original status, request ID and sanitized error fields, then check valid JSON, the exact endpoint, model ID, messages shape, token budget and model-specific parameters in that order. Do not retry an unchanged 400 request.

Community questions repeatedly show the same underlying jobs: a JSON body that cannot be parsed, an invalid message value, a missing model, or a model sent to the wrong endpoint. The wording differs, but all belong to one troubleshooting owner because the diagnostic workflow is the same.

First separate 400 from other failures

StatusTypical classFirst actionRetry unchanged?
400Invalid payload or unsupported parameterCorrect the requestNo
401Missing or invalid credentialFix key/base URLNo
403Key lacks permission or model accessCheck allowlist/accountNo
404Resource or model route not foundVerify endpoint and IDNo
429Rate or quota limitBack off or queueSometimes
5xxGateway or upstream failureBounded retry/fallbackSometimes

OpenAI's official SDKs map a 400 response to BadRequestError. Compatible gateways can use different error messages and machine codes, so branch on the status and documented fields rather than matching a full English sentence.

Capture a safe diagnostic record

python
import hashlib
import json
from typing import Any

def payload_fingerprint(payload: dict[str, Any]) -> str:
    encoded = json.dumps(
        payload, sort_keys=True, separators=(',', ':'), ensure_ascii=False
    ).encode('utf-8')
    return hashlib.sha256(encoded).hexdigest()[:16]

def diagnostic_record(payload: dict[str, Any], error: Exception) -> dict[str, Any]:
    return {
        'status': getattr(error, 'status_code', None),
        'error_type': type(error).__name__,
        'request_id': getattr(error, 'request_id', None),
        'model': payload.get('model'),
        'message_count': len(payload.get('messages', [])),
        'payload_hash': payload_fingerprint(payload),
    }

A hash lets engineers compare whether two failures used the same request without storing prompts. Never log the API key, authorization header or sensitive message content.

Check 1: Is the body valid JSON?

Smart quotes, trailing commas, raw control characters and accidentally double-encoded JSON cause common failures.

Broken JSON:

json
{
  “model”: “gpt-5.4-mini”,
  “messages”: [{“role”: “user”, “content”: “Hello”}],
}

Correct JSON:

json
{
  "model": "gpt-5.4-mini",
  "messages": [{"role": "user", "content": "Hello"}]
}

Let the SDK serialize objects where possible. When using curl, validate the file locally before sending it:

bash
python -m json.tool request.json
curl https://api.lumeapi.site/v1/chat/completions \
  -H "Authorization: Bearer $LUMEAPI_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @request.json

A Stack Overflow question viewed more than 16,000 times describes this exact invalid-JSON 400 pattern. The durable lesson is to validate the serialized bytes, not only the object shown in an editor.

Check 2: Does the endpoint match the request?

LumeAPI text chat uses POST /v1/chat/completions. Image and video models use their documented modality endpoints. A valid chat model sent to a legacy completions route, or an image request sent to chat without documented support, can be rejected even when the JSON parses.

Confirm the actual client instance uses:

python
from openai import OpenAI

client = OpenAI(
    api_key='read-from-a-secret-manager',
    base_url='https://api.lumeapi.site/v1',
)

Do not hard-code a real key as shown by the placeholder. In JavaScript, the option is baseURL; Python uses base_url.

Check 3: Is the exact model ID available?

Display names are not API identifiers. Copy the ID from the current LumeAPI model catalog or list authenticated models. For example, the catalog listed gpt-5.4-mini, claude-sonnet-4-6 and gemini-3.5-flash on July 20, 2026.

python
def assert_model_available(client: OpenAI, model_id: str) -> None:
    available = {model.id for model in client.models.list().data}
    if model_id not in available:
        raise ValueError(f'Model is unavailable to this key: {model_id}')

If the error specifically says model not found, follow the dedicated model-not-found diagnosis.

Check 4: Validate the messages array

A minimal text request needs a non-empty model ID and messages with supported roles and content. This local linter catches common structural failures before the network call.

python
ALLOWED_ROLES = {'system', 'developer', 'user', 'assistant', 'tool'}

def validate_chat_payload(payload: dict) -> list[str]:
    errors: list[str] = []
    model = payload.get('model')
    if not isinstance(model, str) or not model.strip():
        errors.append('model must be a non-empty string')

    messages = payload.get('messages')
    if not isinstance(messages, list) or not messages:
        errors.append('messages must be a non-empty list')
        return errors

    for index, message in enumerate(messages):
        if not isinstance(message, dict):
            errors.append(f'messages[{index}] must be an object')
            continue
        if message.get('role') not in ALLOWED_ROLES:
            errors.append(f'messages[{index}].role is unsupported')
        if 'content' not in message and 'tool_calls' not in message:
            errors.append(f'messages[{index}] needs content or tool_calls')
    return errors

The allowed role/part combinations can be model-specific. This linter rejects obvious mistakes; it does not replace the selected model's current documentation.

Check 5: Remove unsupported optional parameters

Start with only model and messages, then add one feature at a time:

  1. temperature or sampling controls;
  2. output-token limit;
  3. streaming;
  4. tools and tool choice;
  5. structured response format;
  6. multimodal parts.

If the minimal request works and adding one field produces 400, you have isolated the compatibility boundary. Do not conclude that the entire endpoint is incompatible. Keep a capability matrix by exact model ID.

Check 6: Recalculate context and output limits

Some providers return 400 when input plus reserved output exceeds the model context window. Reduce duplicated history, retrieval and tool schemas, then set a task-sized output limit. The context-length troubleshooting guide contains a loss-aware trimming order.

A bounded Python error handler

python
import openai

def send_chat(client: OpenAI, payload: dict) -> str:
    local_errors = validate_chat_payload(payload)
    if local_errors:
        raise ValueError('; '.join(local_errors))

    try:
        response = client.chat.completions.create(**payload)
    except openai.BadRequestError as exc:
        record = diagnostic_record(payload, exc)
        raise RuntimeError(f'Correct request payload: {record}') from exc
    return response.choices[0].message.content or ''

Do not put this call inside an automatic fallback chain. The same malformed payload will probably fail on the next model and create noise or extra cost.

Why LumeAPI helps diagnose the request

LumeAPI uses one OpenAI-compatible base URL for supported GPT, Claude and Gemini text models, with exact IDs in one catalog. That reduces differences caused by maintaining separate clients. Per-call usage records also expose the selected model, tokens, cost, latency, stream flag, request ID and error detail where available, helping distinguish a request-building bug from an operational failure.

The same account and USD wallet cover listed image and video models, but their endpoints and parameters remain modality-specific. One platform is not one universal payload.

Final 400 checklist

  • Preserve status, request ID and sanitized error code.
  • Validate the serialized JSON bytes.
  • Confirm HTTP method, base URL and endpoint.
  • Copy the exact model ID from the authenticated list or current catalog.
  • Validate each message role and content shape.
  • Remove optional fields, then add them back one at a time.
  • Recalculate context and output limits.
  • Do not retry an unchanged 400.
  • Record the new case in a regression fixture.

Sources and verification

The local linter and all Python blocks were syntax-checked on July 20, 2026. No customer inference key was used, so exact error messages must be verified with the selected model and key.