Skip to content

Errors

All BVE Gateway errors use the OpenAI error envelope format.

{
"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"); null for auth, quota, and server errors.
  • request_id — present on all error responses (not just 500s). Matches the X-Request-Id response header. Include it when filing a bug report or opening a support ticket.
StatusMeaning
400Bad request (invalid JSON, unsupported operation)
401Missing or invalid credentials
403Valid credentials but key is suspended, revoked, or model is restricted
404Route not found
413Request body exceeds 10 MB
429Rate limit exceeded
500Internal server error
502Upstream Fuelix error or unreachable
503Worker request capacity exceeded (global per-isolate hard cap)
CodeStatusDescription
missing_api_key401No Authorization header present
invalid_api_key401Wrong scheme, wrong prefix, or key not in DB
api_key_expired401Key exists but its expires_at date is in the past
api_key_suspended403Key is suspended by an admin
api_key_revoked403Key has been revoked
CodeStatusDescription
model_not_allowed403Model is not in the key’s allowed_models list
model_not_available403Model is blocked by the gateway’s live availability policy, broken-model registry, or global allowlist
model_endpoint_mismatch400Model 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:

  1. Global allowlist policy — models disabled via POST /admin/model-allowlist are removed from GET /v1/models and rejected everywhere with 403 model_not_available.
  2. Per-key allowlist — if a key has a non-null allowed_models array, only those models are permitted. Requests for other models receive model_not_allowed, and those models are also omitted from that key’s GET /v1/models response.
  3. Live availability snapshotGET /v1/models mirrors the live Fuelix catalog, so it can still show Fuelix-listed models with bve_availability != "callable". Direct requests still return 403 model_not_available for rows the gateway currently classifies as listed_unavailable, registry_broken, or absent from the live Fuelix catalog.
  4. Endpoint-model capability — models are strictly restricted to their capability category (e.g. text-embedding-3-large can only be used with POST /v1/embeddings). Using them on a mismatched endpoint returns model_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:

EndpointHint 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 categoryHint 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)
CodeStatusDescription
rate_limit_exceeded429RPM, 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’s monthly_token_limit reached
CodeStatusDescription
missing_required_parameter400A required field is absent (e.g. model, messages, input, max_tokens)
invalid_type400A field has the wrong JavaScript type (e.g. messages is not an array, n is a string, body is not a JSON object)
invalid_value400A field has the right type but fails a constraint (e.g. n is 11 when max is 10, encoding_format is "binary")
invalid_content_type400Content-Type header is missing or is not application/json (on JSON endpoints)
invalid_json400Request body is not valid JSON (Content-Type is correct but body cannot be parsed)
unsupported_parameter400Parameter sent but not supported by the endpoint (e.g. best_of or logprobs for POST /v1/completions)
unsupported_value400Parameter 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_error400Admin API input failed schema validation (e.g. missing name, negative limit)
request_too_large413Body 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.

CodeStatusDescription
internal_error500Unhandled exception in the gateway
upstream_error502Fuelix is unreachable or returned 5xx
capacity_exceeded503Worker 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.

{
"error": {
"message": "Route GET /v1/unknown not found",
"type": "invalid_request_error",
"param": null,
"code": "route_not_found"
}
}
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
}
}