Errors
All BVE Gateway errors use the OpenAI error envelope format.
Error envelope
Section titled “Error envelope”{ "error": { "message": "Human-readable description of the error.", "type": "error_type", "param": null, "code": "machine_readable_code", "request_id": "550e8400-e29b-41d4-a716-446655440000" }}Every error response includes param and request_id.
param— names the invalid field for validation errors (e.g.,"model","messages");nullfor auth, quota, and server errors.request_id— present on all error responses (not just 500s). Matches theX-Request-Idresponse header. Include it when filing a bug report or opening a support ticket.
HTTP status codes
Section titled “HTTP status codes”| Status | Meaning |
|---|---|
400 | Bad request (invalid JSON, unsupported operation) |
401 | Missing or invalid credentials |
403 | Valid credentials but key is suspended, revoked, or model is restricted |
404 | Route not found |
413 | Request body exceeds 10 MB |
429 | Rate limit exceeded |
500 | Internal server error |
502 | Upstream Fuelix error or unreachable |
503 | Worker request capacity exceeded (global per-isolate hard cap) |
Error codes
Section titled “Error codes”Auth errors (401 / 403)
Section titled “Auth errors (401 / 403)”| Code | Status | Description |
|---|---|---|
missing_api_key | 401 | No Authorization header present |
invalid_api_key | 401 | Wrong scheme, wrong prefix, or key not in DB |
api_key_expired | 401 | Key exists but its expires_at date is in the past |
api_key_suspended | 403 | Key is suspended by an admin |
api_key_revoked | 403 | Key has been revoked |
Model errors (400 / 403)
Section titled “Model errors (400 / 403)”| Code | Status | Description |
|---|---|---|
model_not_allowed | 403 | Model is not in the key’s allowed_models list |
model_not_available | 403 | Model is blocked by the gateway’s live availability policy, broken-model registry, or global allowlist |
model_endpoint_mismatch | 400 | Model sent to an endpoint it does not support (e.g. embedding model to chat completions) |
Expired key response example:
{ "error": { "message": "API key has expired", "type": "authentication_error", "param": null, "code": "api_key_expired", "request_id": "550e8400-e29b-41d4-a716-446655440000" }}An expired key retains status: active in the database and can be reactivated by PATCHing expires_at to a future date or null via PATCH /admin/api-keys/:id. See Admin API: API Keys for details.
The gateway enforces four layers of model filtering:
- Global allowlist policy — models disabled via
POST /admin/model-allowlistare removed fromGET /v1/modelsand rejected everywhere with403 model_not_available. - Per-key allowlist — if a key has a non-null
allowed_modelsarray, only those models are permitted. Requests for other models receivemodel_not_allowed, and those models are also omitted from that key’sGET /v1/modelsresponse. - Live availability snapshot —
GET /v1/modelsmirrors the live Fuelix catalog, so it can still show Fuelix-listed models withbve_availability != "callable". Direct requests still return403 model_not_availablefor rows the gateway currently classifies aslisted_unavailable,registry_broken, or absent from the live Fuelix catalog. - Endpoint-model capability — models are strictly restricted to their capability category (e.g.
text-embedding-3-largecan only be used withPOST /v1/embeddings). Using them on a mismatched endpoint returnsmodel_endpoint_mismatch.
The model_endpoint_mismatch error message always includes a contextual hint suggesting the correct endpoint or model type.
Wrong model category for the endpoint — e.g. sending an embedding model to chat completions:
{ "error": { "message": "Model 'text-embedding-3-large' does not support endpoint '/v1/chat/completions' (embedding model, use POST /v1/embeddings instead)", "type": "invalid_request_error", "param": "model", "code": "model_endpoint_mismatch", "request_id": "550e8400-e29b-41d4-a716-446655440000" }}Chat or reasoning model sent to a specialized endpoint — the hint names the correct model type to use:
| Endpoint | Hint in error message |
|---|---|
POST /v1/audio/speech | (chat model — this endpoint requires a text-to-speech model such as tts-1) |
POST /v1/audio/transcriptions | (chat model — this endpoint requires a speech-to-text model such as whisper-1) |
POST /v1/images/generations | (chat model — this endpoint requires an image generation model such as imagen-4) |
POST /v1/embeddings | (chat model — this endpoint requires an embedding model such as text-embedding-3-small) |
POST /v1/responses | (non-OpenAI model — this endpoint only supports OpenAI GPT and O-series models such as gpt-4o or o3) |
Full capability-mismatch hint catalog:
| Model category | Hint suffix |
|---|---|
| Embedding model on wrong endpoint | (embedding model, use POST /v1/embeddings instead) |
| TTS model on wrong endpoint | (text-to-speech model, use POST /v1/audio/speech instead) |
| STT model on wrong endpoint | (speech-to-text model, use POST /v1/audio/transcriptions instead) |
| Image model on wrong endpoint | (image generation model, use POST /v1/images/generations instead) |
| Legacy base model on non-completions endpoint | (legacy base model, use POST /v1/completions instead) |
| Realtime model (any endpoint) | (realtime model, requires WebSocket connection instead) |
| OCR model on non-chat endpoint | (OCR model, use POST /v1/chat/completions with image_url content blocks instead) |
Non-OpenAI model on /v1/responses | (non-OpenAI model — this endpoint only supports OpenAI GPT and O-series models such as gpt-4o or o3) |
Rate limit errors (429)
Section titled “Rate limit errors (429)”| Code | Status | Description |
|---|---|---|
rate_limit_exceeded | 429 | RPM, RPD, or monthly limit hit |
Rate limit error messages indicate which limit was exceeded:
{ "error": { "message": "Rate limit exceeded: requests per minute", "type": "rate_limit_error", "param": null, "code": "rate_limit_exceeded" }}All 429 responses include a Retry-After header with the number of seconds until the window resets.
Possible messages:
"Rate limit exceeded: requests per minute"— RPM limit hit"Rate limit exceeded: requests per day"— RPD limit hit"Monthly request limit exceeded"— monthly request cap hit"Monthly token limit exceeded"— key’smonthly_token_limitreached
Request errors (400 / 413)
Section titled “Request errors (400 / 413)”| Code | Status | Description |
|---|---|---|
missing_required_parameter | 400 | A required field is absent (e.g. model, messages, input, max_tokens) |
invalid_type | 400 | A field has the wrong JavaScript type (e.g. messages is not an array, n is a string, body is not a JSON object) |
invalid_value | 400 | A field has the right type but fails a constraint (e.g. n is 11 when max is 10, encoding_format is "binary") |
invalid_content_type | 400 | Content-Type header is missing or is not application/json (on JSON endpoints) |
invalid_json | 400 | Request body is not valid JSON (Content-Type is correct but body cannot be parsed) |
unsupported_parameter | 400 | Parameter sent but not supported by the endpoint (e.g. best_of or logprobs for POST /v1/completions) |
unsupported_value | 400 | Parameter is supported but its value is not allowed for the specific model (e.g. temperature ≠ 1 or n > 1 for o1-family reasoning models) |
validation_error | 400 | Admin API input failed schema validation (e.g. missing name, negative limit) |
request_too_large | 413 | Body exceeds 10 MB |
missing_required_parameter and invalid_value errors include a param field naming the invalid field:
{ "error": { "message": "messages is required", "type": "invalid_request_error", "param": "messages", "code": "missing_required_parameter" }}invalid_content_type is returned when the Content-Type header is missing or not application/json on any JSON endpoint (POST /v1/chat/completions, POST /v1/embeddings, POST /v1/messages, POST /v1/responses, POST /v1/completions):
{ "error": { "message": "Content-Type must be application/json", "type": "invalid_request_error", "param": null, "code": "invalid_content_type", "request_id": "550e8400-e29b-41d4-a716-446655440000" }}The gateway validates required fields for /v1/chat/completions (model, messages), /v1/embeddings (model, input), /v1/messages (model, max_tokens, messages), /v1/responses (model, input), and /v1/completions (model). Invalid requests are rejected before reaching the upstream, returning a standard error instead of Fuelix’s non-OpenAI-compatible 422 format.
Server errors (500 / 502 / 503)
Section titled “Server errors (500 / 502 / 503)”| Code | Status | Description |
|---|---|---|
internal_error | 500 | Unhandled exception in the gateway |
upstream_error | 502 | Fuelix is unreachable or returned 5xx |
capacity_exceeded | 503 | Worker isolate’s monthly request hard cap reached |
500 error responses look like:
{ "error": { "message": "Internal server error", "type": "api_error", "param": null, "code": "internal_error", "request_id": "550e8400-e29b-41d4-a716-446655440000" }}request_id is present on every error response (see Error envelope above). Use it when filing a bug report.
503 responses are returned by the globalRateLimiter middleware when the per-isolate request counter reaches MONTHLY_WORKER_REQUEST_HARD_CAP (default: 9,500,000 requests). This is a safety valve against runaway isolates — it is per-isolate, not a global cluster-wide limit. See Rate Limits & Quotas for full details.
Not found (404)
Section titled “Not found (404)”{ "error": { "message": "Route GET /v1/unknown not found", "type": "invalid_request_error", "param": null, "code": "route_not_found" }}Error handling example
Section titled “Error handling example”import OpenAI from 'openai';
const client = new OpenAI({ apiKey: 'sk-bve-YOUR_KEY', baseURL: 'https://api.bve.me/v1',});
try { const response = await client.chat.completions.create({ model: 'gpt-4o', messages: [{ role: 'user', content: 'Hello' }], });} catch (err) { if (err instanceof OpenAI.APIError) { console.error(err.status, err.message); // err.error.code — machine-readable error code (e.g. "rate_limit_exceeded") // err.error.param — invalid field name for validation errors, null otherwise }}from openai import OpenAI, APIError
client = OpenAI( api_key="sk-bve-YOUR_KEY", base_url="https://api.bve.me/v1",)
try: response = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "Hello"}], )except APIError as e: print(e.status_code, e.message) # e.code — machine-readable error code (e.g. "rate_limit_exceeded") # e.param — invalid field name for validation errors, None otherwiseHTTP_STATUS=$(curl -s -o /tmp/bve_response.json -w "%{http_code}" \ -X POST https://api.bve.me/v1/chat/completions \ -H "Authorization: Bearer sk-bve-YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4o","messages":[{"role":"user","content":"Hello"}]}')
if [ "$HTTP_STATUS" -ge 400 ]; then # {"error":{"code":"...","message":"...","param":...,"request_id":"..."}} cat /tmp/bve_response.jsonfi