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
| Status | Typical class | First action | Retry unchanged? |
|---|---|---|---|
| 400 | Invalid payload or unsupported parameter | Correct the request | No |
| 401 | Missing or invalid credential | Fix key/base URL | No |
| 403 | Key lacks permission or model access | Check allowlist/account | No |
| 404 | Resource or model route not found | Verify endpoint and ID | No |
| 429 | Rate or quota limit | Back off or queue | Sometimes |
| 5xx | Gateway or upstream failure | Bounded retry/fallback | Sometimes |
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
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:
{
“model”: “gpt-5.4-mini”,
“messages”: [{“role”: “user”, “content”: “Hello”}],
}Correct 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:
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.jsonA 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:
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.
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.
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 errorsThe 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:
temperatureor sampling controls;- output-token limit;
- streaming;
- tools and tool choice;
- structured response format;
- 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
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
- Official OpenAI Python library for
BadRequestError, request IDs and retry behavior. - LumeAPI docs, model catalog, and OpenAI-compatible API for current routes and IDs.
- Stack Overflow invalid JSON question and Reddit invalid message example as community demand signals, not technical authorities.
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.