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.
Authentication errors (401 / 403)
Section titled “Authentication errors (401 / 403)”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) oradmin_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 withinvalid_api_keyon/v1/*. - If running locally, confirm the key was created against the local D1 instance (
bun run key:new), not production, and that your.dev.varsADMIN_API_KEYmatches 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:
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 entirelyapi_key_suspended — key is suspended
Section titled “api_key_suspended — key is suspended”{ "error": { "code": "api_key_suspended", "message": "API key is suspended", "type": "authentication_error" } }Fix: Unsuspend via the Admin API:
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:
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 errors (400 / 403)
Section titled “Model errors (400 / 403)”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:
# Add a model to the allowlistcurl -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:
| Mistake | Fix |
|---|---|
Embedding model sent to /v1/chat/completions | Use /v1/embeddings |
Chat model sent to /v1/embeddings | Use an embedding model (e.g. text-embedding-3-small) |
Chat model sent to /v1/audio/speech | Use a TTS model (e.g. tts-1) |
Chat model sent to /v1/images/generations | Use an image model (e.g. imagen-4) |
Non-OpenAI model sent to /v1/responses | Use gpt-4o or another OpenAI model |
The error message always includes a hint about the correct endpoint or model type.
Rate limit errors (429)
Section titled “Rate limit errors (429)”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: 42This is the number of seconds until the current window resets.
Cause and fix by message:
| Message | Window | Fix |
|---|---|---|
requests per minute | RPM | Wait Retry-After seconds, then retry. If this is chronic, increase the key’s rpm_limit. |
requests per day | RPD | Wait until UTC midnight, or increase the key’s rpd_limit. |
Monthly request limit exceeded | Monthly requests | Wait until next calendar month, or raise monthly_request_limit. |
Monthly token limit exceeded | Monthly tokens | Wait until next calendar month, or raise monthly_token_limit. |
Update a limit via the Admin API:
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 entirelyImplementing 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');}import timefrom openai import OpenAI, RateLimitError
client = OpenAI(api_key="sk-bve-YOUR_KEY", base_url="https://api.bve.me/v1")
def with_retry(fn, max_retries=3): for attempt in range(max_retries + 1): try: return fn() except RateLimitError as e: if attempt < max_retries: retry_after = int(e.response.headers.get("retry-after", 10)) time.sleep(retry_after) continue raiseValidation errors (400)
Section titled “Validation errors (400)”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:
| Endpoint | Required fields |
|---|---|
POST /v1/chat/completions | model, messages |
POST /v1/embeddings | model, input |
POST /v1/completions | model |
POST /v1/messages | model, max_tokens, messages |
POST /v1/responses | model, 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:
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.
request_too_large — body exceeds 10 MB
Section titled “request_too_large — body exceeds 10 MB”{ "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:
- Check the
X-Request-Idheader in the error response and look it up in request logs:GET /admin/request-logs?request_id=<id> - Retry with exponential backoff — Fuelix 5xx errors are usually transient.
- 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.
Debugging tips
Section titled “Debugging tips”Capture the request ID
Section titled “Capture the request ID”Every response — including errors — includes an X-Request-Id header. Record it in your client for support requests:
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: ...Check your current quota
Section titled “Check your current quota”Call /v1/usage (no quota consumed) to see how much of your rate-limit window is left:
curl https://api.bve.me/v1/usage \ -H "Authorization: Bearer sk-bve-YOUR_KEY"Inspect sampled request logs
Section titled “Inspect sampled request logs”Admins can look up any recent request in the sampled log by its X-Request-Id:
curl "https://api.bve.me/admin/request-logs?request_id=REQUEST_ID_HERE" \ -H "Authorization: Bearer admin_bve_YOUR_ADMIN_KEY"Confirm the gateway is reachable
Section titled “Confirm the gateway is reachable”curl https://api.bve.me/health# {"status":"ok","service":"bve-gateway",...}