Errors

Error shapes, status codes, and what to retry.

All AOCore endpoints return errors in a single, consistent shape: the APIError schema. The same shape appears in the API reference on every non-2xx response. This page documents:

  1. The wire shape.
  2. The HTTP status codes and what each one means.
  3. The retry policy.
  4. The auth-error variants (parity-locked to prevent role enumeration).

The APIError shape

Every error response is JSON, wrapped in an "error" object:

{
  "error": {
    "message": "Human-readable description of what went wrong.",
    "type":    "machine-readable error class",
    "code":    "more-specific code within the type",
    "param":   "(optional) request parameter at fault, when applicable"
  }
}

The wrapper preserves OpenAI-SDK compatibility (openai.APIError parses this shape directly). Some fields are optional:

FieldRequiredNotes
messageYesUI-safe English description; never includes internal stack traces
typeYesStable enum, suitable for switch/match
codeYesMore specific identifier; stable across releases
paramNoThe request parameter that caused the error (e.g., "model", "messages.0.content")

Status codes & error types

StatustypeWhen
400validation_errorMalformed request body, missing required fields, invalid enum value
400invalid_request_errorLogically invalid request — wrong shape for the model, unsupported flag, malformed conversation
401authentication_errorMissing / invalid / expired credentials (API key OR session cookie)
403permission_errorAuthenticated but operation is forbidden — e.g. trying to mint a key for another developer
404not_found_errorResource doesn’t exist OR caller’s allowlist doesn’t include it (see the models guide)
409conflict_errorEmail already registered; key name already in use; invitation already consumed
422unprocessable_entityGuardrails rejected the prompt or response (PII / jailbreak / regex / content moderation)
429rate_limit_errorRPM or TPM exceeded — retry after X-AOSentry-RateLimit-Reset
429rate_limit_error (code budget_exceeded)Per-key dollar budget exceeded — NOT retryable on the same key
500api_errorInternal gateway error (DB unavailable, upstream provider hard failure, etc.)
502bad_gatewayUpstream provider returned a non-recoverable error
503service_unavailableGateway temporarily overloaded
504timeout_errorUpstream provider didn’t respond within the gateway’s timeout

The code field is more specific than type. For example, the validation_error type carries codes like missing_field, invalid_format, value_too_long, etc. The list of codes is part of the OpenAPI spec — see the API reference under the relevant endpoint’s response schema.

Quota headers on every response

The six X-AOSentry-* quota headers (see the rate-limits guide) are stamped on error responses too, not just 200s. So when you get a 429, the same response carries X-AOSentry-RateLimit-Reset — you know exactly when to retry without a second probe call. The 400/401/404/5xx responses also carry the headers when the request reaches the rate-limiter (i.e., after API-key authentication succeeds).

Retry policy

The recommended client policy is:

import time
from openai import OpenAI

def call_with_retry(client, **kwargs):
    delays = [1, 2, 4]  # exponential backoff for 5xx
    for attempt in range(len(delays) + 1):
        try:
            return client.chat.completions.create(**kwargs)
        except openai.APIStatusError as e:
            if e.status_code == 429:
                if e.response.json()["error"].get("code") == "budget_exceeded":
                    raise  # No amount of waiting fixes this
                reset = int(e.response.headers.get("X-AOSentry-RateLimit-Reset", "0"))
                time.sleep(max(0.0, reset - time.time()))
                continue
            elif 500 <= e.status_code < 600:
                if attempt < len(delays):
                    time.sleep(delays[attempt])
                    continue
                raise
            else:
                raise  # 4xx — don't retry
    return None

Key points:

StatusRetry?Strategy
400, 404, 422NoCaller error; fix the request
401, 403NoCredential or permission issue; surface to user
409NoIdempotency conflict; usually not retryable
429 (rate_limit_exceeded)YesWait until X-AOSentry-RateLimit-Reset, then retry
429 (budget_exceeded)NoBudget bumps require admin; alert
5xxYesExponential backoff (1s, 2s, 4s, …); cap at 3 retries

/v1/chat/completions is not idempotent by default — each retry creates a new completion (and bills you for it). For mutating management endpoints (POST /developer/v1/keys), an Idempotency-Key header is honored: passing the same key twice within the cache window returns the same response without double-charging. (See the API reference for which endpoints support it.)

Auth error parity

The four authentication-failure paths return byte-identical response bodies to prevent role enumeration:

  1. No cookie / no API key.
  2. Cookie is for a user whose role doesn’t match the endpoint.
  3. Session is expired.
  4. Session is revoked.
{
  "error": {
    "message": "Invalid or expired developer session",
    "type":    "authentication_error",
    "code":    "invalid_session"
  }
}

(/v1/* returns the same shape with a different message mentioning API keys.) This is locked by TestDeveloperSessionMiddleware_ErrorBodyParity and TestAdminMiddleware_RejectsDeveloperRole. An attacker can not distinguish “this email doesn’t exist” from “this email is an admin, not a developer” by comparing 401 bodies. See the authentication guide for context.

The 404-on-unauthorized-model property

Calling GET /v1/models/{model} for a model not in your allowlist returns 404, not 403. This is intentional and locked by spec. See the models guide for the full description (including the verbatim spec language: “Returned both when the model genuinely does not exist AND when the model exists but is not in the caller’s allowlist”).

If you receive a 404 on a model lookup, do not assume the model doesn’t exist on the gateway — only that it’s not in your specific key’s effective allowlist.

Guardrails errors (422)

When a guardrail (PII / jailbreak / content moderation / secrets / regex) blocks a request or response, the error type is unprocessable_entity with a code identifying which guardrail fired:

{
  "error": {
    "message": "Request blocked by content moderation: detected categories: violence",
    "type":    "unprocessable_entity",
    "code":    "guardrail_blocked",
    "param":   "content_moderation"
  }
}

The param field carries the guardrail name. If a guardrail is configured in mode: log (rather than mode: block), the request is not rejected — it flows through with an audit-log entry instead. Your administrator configures guardrail modes per gateway.

Inspecting error payloads from the OpenAI SDK

from openai import OpenAI, APIStatusError
import os

client = OpenAI(
    base_url="https://gateway.core.aocyber.ai/v1",
    api_key=os.environ["AOSENTRY_KEY"],
)

try:
    client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": "..."}],
    )
except APIStatusError as e:
    err = e.response.json()["error"]
    print(f"status={e.status_code} type={err['type']} code={err['code']}")
    print(f"message={err['message']}")
    if "param" in err:
        print(f"param={err['param']}")

The OpenAI SDK exposes e.response.headers so you can correlate with the X-AOSentry-* quota headers in the same handler.

See examples/python-openai-sdk/streaming.py for an error-handling example in the SSE streaming path (where errors arrive as SSE-shaped events rather than HTTP error responses).