Skip to content

Troubleshooting

Quick reference for the most frequent failure modes. Each section shows the exact error shape, explains what caused it, and gives the remediation step.

missing_api_key — no Authorization header

Section titled “missing_api_key — no Authorization header”
{ "error": { "code": "missing_api_key", "message": "Missing API key", "type": "authentication_error" } }

Cause: The request has no Authorization header.

Fix: Add Authorization: Bearer sk-bve-YOUR_KEY to every request. The header is required for all /v1/* and /admin/* endpoints — only /health and /openapi.json are unauthenticated.


invalid_api_key — wrong scheme, wrong prefix, or key not found

Section titled “invalid_api_key — wrong scheme, wrong prefix, or key not found”
{ "error": { "code": "invalid_api_key", "message": "Invalid API key", "type": "authentication_error" } }

Cause: One of:

  • Authorization scheme is not Bearer (e.g. Token sk-bve-… or bare key)
  • Key does not start with sk-bve- (public API keys) or admin_bve_ (admin keys on /admin/*)
  • Key value is not in the database (never provisioned, or wrong environment)

Fix:

  • Verify the scheme: Authorization: Bearer sk-bve-YOUR_KEY
  • Confirm the key prefix matches the endpoint. Admin keys (admin_bve_…) only work on /admin/*; they are rejected with invalid_api_key on /v1/*.
  • If running locally, confirm the key was created against the local D1 instance (bun run key:new), not production, and that your .dev.vars ADMIN_API_KEY matches the key used to create it.

api_key_expired — key has an expiry date in the past

Section titled “api_key_expired — key has an expiry date in the past”
{ "error": { "code": "api_key_expired", "message": "API key has expired", "type": "authentication_error" } }

Cause: The key has a non-null expires_at date that is before the current UTC time.

Fix: Extend the expiry via the Admin API:

Terminal window
curl -X PATCH "https://api.bve.me/admin/api-keys/KEY_ID" \
-H "Authorization: Bearer admin_bve_YOUR_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"expires_at": "2027-01-01T00:00:00Z"}'
# or "expires_at": null to remove the expiry entirely

{ "error": { "code": "api_key_suspended", "message": "API key is suspended", "type": "authentication_error" } }

Fix: Unsuspend via the Admin API:

Terminal window
curl -X POST "https://api.bve.me/admin/api-keys/KEY_ID/unsuspend" \
-H "Authorization: Bearer admin_bve_YOUR_ADMIN_KEY"

api_key_revoked — key has been permanently revoked

Section titled “api_key_revoked — key has been permanently revoked”
{ "error": { "code": "api_key_revoked", "message": "API key has been revoked", "type": "authentication_error" } }

Fix: Revocation is permanent. Issue a new key:

Terminal window
curl -X POST "https://api.bve.me/admin/api-keys" \
-H "Authorization: Bearer admin_bve_YOUR_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "replacement-key"}'

model_not_allowed — model blocked by per-key allowlist

Section titled “model_not_allowed — model blocked by per-key allowlist”
{ "error": { "code": "model_not_allowed", "message": "Model 'gpt-4o' is not in the allowed models list for this key", "type": "authentication_error" } }

Cause: The key has a non-null allowed_models list and the requested model is not in it.

Fix: Add the model to the key’s allowlist, or clear the restriction entirely:

Terminal window
# Add a model to the allowlist
curl -X PATCH "https://api.bve.me/admin/api-keys/KEY_ID" \
-H "Authorization: Bearer admin_bve_YOUR_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"allowed_models": ["gpt-4o", "gpt-4o-mini"]}'
# Clear the restriction (allow all models)
curl -X PATCH "https://api.bve.me/admin/api-keys/KEY_ID" \
-H "Authorization: Bearer admin_bve_YOUR_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"allowed_models": null}'

model_not_available — model disabled globally

Section titled “model_not_available — model disabled globally”
{ "error": { "code": "model_not_available", "message": "Model 'gpt-4o' is not available", "type": "authentication_error" } }

Cause: The model is either not in the global model allowlist at all, or its status is disabled or unconfirmed.

Fix: Check and update the allowlist via the Admin API. See Model Allowlist for full details.


model_endpoint_mismatch — wrong model type for the endpoint

Section titled “model_endpoint_mismatch — wrong model type for the endpoint”
{
"error": {
"code": "model_endpoint_mismatch",
"message": "Model 'text-embedding-3-large' does not support endpoint '/v1/chat/completions' (embedding model, use POST /v1/embeddings instead)",
"param": "model"
}
}

Cause: The model category does not match the endpoint. Common mistakes:

MistakeFix
Embedding model sent to /v1/chat/completionsUse /v1/embeddings
Chat model sent to /v1/embeddingsUse an embedding model (e.g. text-embedding-3-small)
Chat model sent to /v1/audio/speechUse a TTS model (e.g. tts-1)
Chat model sent to /v1/images/generationsUse an image model (e.g. imagen-4)
Non-OpenAI model sent to /v1/responsesUse gpt-4o or another OpenAI model

The error message always includes a hint about the correct endpoint or model type.


rate_limit_exceeded — RPM, RPD, or monthly cap reached

Section titled “rate_limit_exceeded — RPM, RPD, or monthly cap reached”
{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded: requests per minute",
"type": "rate_limit_error"
}
}

All 429 responses include a Retry-After header:

Retry-After: 42

This is the number of seconds until the current window resets.

Cause and fix by message:

MessageWindowFix
requests per minuteRPMWait Retry-After seconds, then retry. If this is chronic, increase the key’s rpm_limit.
requests per dayRPDWait until UTC midnight, or increase the key’s rpd_limit.
Monthly request limit exceededMonthly requestsWait until next calendar month, or raise monthly_request_limit.
Monthly token limit exceededMonthly tokensWait until next calendar month, or raise monthly_token_limit.

Update a limit via the Admin API:

Terminal window
curl -X PATCH "https://api.bve.me/admin/api-keys/KEY_ID" \
-H "Authorization: Bearer admin_bve_YOUR_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"rpm_limit": 200, "rpd_limit": 5000}'
# set to null to remove a limit entirely

Implementing retry-after in code:

import OpenAI from 'openai';
const client = new OpenAI({ apiKey: 'sk-bve-YOUR_KEY', baseURL: 'https://api.bve.me/v1' });
async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
if (err instanceof OpenAI.RateLimitError && attempt < maxRetries) {
const retryAfter = Number(err.headers?.['retry-after'] ?? 10);
await new Promise(r => setTimeout(r, retryAfter * 1000));
continue;
}
throw err;
}
}
throw new Error('unreachable');
}

missing_required_parameter — required field absent

Section titled “missing_required_parameter — required field absent”
{
"error": {
"code": "missing_required_parameter",
"message": "messages is required",
"param": "messages",
"type": "invalid_request_error"
}
}

Fix: The param field names the missing field. Required fields by endpoint:

EndpointRequired fields
POST /v1/chat/completionsmodel, messages
POST /v1/embeddingsmodel, input
POST /v1/completionsmodel
POST /v1/messagesmodel, max_tokens, messages
POST /v1/responsesmodel, input

invalid_content_type — missing or wrong Content-Type header

Section titled “invalid_content_type — missing or wrong Content-Type header”
{ "error": { "code": "invalid_content_type", "message": "Content-Type must be application/json", "type": "invalid_request_error" } }

Cause: A JSON endpoint (POST /v1/chat/completions, POST /v1/embeddings, POST /v1/messages, POST /v1/responses, POST /v1/completions) received a request with no Content-Type header, or a value other than application/json (e.g. text/plain, application/x-www-form-urlencoded).

Fix: Add Content-Type: application/json to every request that sends a JSON body:

Terminal window
curl -X POST https://api.bve.me/v1/chat/completions \
-H "Authorization: Bearer sk-bve-YOUR_KEY" \
-H "Content-Type: application/json" \ # ← required
-d '{"model":"gpt-4o","messages":[...]}'

Note: multipart/form-data endpoints (POST /v1/audio/transcriptions, POST /v1/images/edits) are exempt — they use a different Content-Type by design.


{ "error": { "code": "request_too_large", "message": "Request body too large", "type": "invalid_request_error" } }

Fix: Reduce the request size. The 10 MB limit applies to the raw request body. Large base64-encoded images or long conversation histories are the usual culprits.


Server and upstream errors (500 / 502 / 503)

Section titled “Server and upstream errors (500 / 502 / 503)”

upstream_error (502) — Fuelix returned an error

Section titled “upstream_error (502) — Fuelix returned an error”
{ "error": { "code": "upstream_error", "message": "Upstream error", "type": "api_error" } }

Cause: Fuelix returned a non-2xx response or was unreachable.

Fix:

  1. Check the X-Request-Id header in the error response and look it up in request logs: GET /admin/request-logs?request_id=<id>
  2. Retry with exponential backoff — Fuelix 5xx errors are usually transient.
  3. If Fuelix returns a model-specific error (e.g. content policy), the upstream error message may be included.

capacity_exceeded (503) — isolate monthly request cap

Section titled “capacity_exceeded (503) — isolate monthly request cap”
{ "error": { "code": "capacity_exceeded", "message": "Worker capacity exceeded", "type": "api_error" } }

Cause: The Cloudflare Worker isolate has reached its monthly per-isolate request hard cap (default: 9,500,000). This is a safety valve, not a per-user limit.

Fix: Contact the gateway operator. See Rate Limits & Quotas for details.


internal_error (500) — unhandled exception

Section titled “internal_error (500) — unhandled exception”
{ "error": { "code": "internal_error", "message": "Internal server error", "type": "api_error" } }

Fix: Include the request_id from the error body or X-Request-Id response header when reporting the issue. All 500 errors are logged in wrangler tail with the stack trace.


Every response — including errors — includes an X-Request-Id header. Record it in your client for support requests:

Terminal window
curl -i 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"}]}'
# Look for: X-Request-Id: ...

Call /v1/usage (no quota consumed) to see how much of your rate-limit window is left:

Terminal window
curl https://api.bve.me/v1/usage \
-H "Authorization: Bearer sk-bve-YOUR_KEY"

Admins can look up any recent request in the sampled log by its X-Request-Id:

Terminal window
curl "https://api.bve.me/admin/request-logs?request_id=REQUEST_ID_HERE" \
-H "Authorization: Bearer admin_bve_YOUR_ADMIN_KEY"
Terminal window
curl https://api.bve.me/health
# {"status":"ok","service":"bve-gateway",...}