Developers

Error handling

The SchneeAI Gateway returns OpenAI-compatible error objects with SchneeAI-specific extensions (budget, PII, routing). Here is how to parse them, when to retry, and how to make retries safe.

Error response shape

Every non-2xx response has the same envelope:

{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded. Retry after 2 seconds.",
    "param": null,
    "request_id": "req_01HQ..."
  }
}
FieldPurpose
typeOpenAI-compatible category (invalid_request_error, authentication_error, rate_limit_error, api_error)
codeMachine-readable sub-code (see SchneeAI-specific codes)
messageHuman-readable explanation
paramOffending parameter, if applicable
request_idInclude this when contacting support — we can trace the exact call

HTTP status → action

StatusMeaningRetry?Action
400Malformed JSON or missing required fieldNoFix the request body
401API key invalid or missingNoCheck the Authorization header
403Tenant scope or role forbiddenNoVerify tenant and role on the token
404model or prompt name not foundNoCheck the Model Directory
422Valid JSON but semantically rejectedNoRead error.code — usually context length, unsupported parameter, or content policy
429Rate limit or budget exceededYes (with backoff)Honor Retry-After if present
500Internal Gateway errorYesExponential backoff
502Upstream provider errorYesExponential backoff
503Gateway temporarily unavailableYesExponential backoff
504Upstream provider timeoutYesExponential backoff

SchneeAI-specific error codes

The OpenAI-compatible envelope carries SchneeAI-specific sub-codes in error.code. The ones you will actually hit:

CodeHTTPMeaning
budget_exceeded429The tenant or feature budget is exhausted. Retry only after the budget resets (next hour / day / month, depending on the policy).
pii_detected422Your tenant’s PII policy is set to block and the scanner found a critical category. Redact the input or switch the policy to mask / flag.
context_length_exceeded422The combined prompt + max_tokens exceeds the model’s context window. Truncate or switch to a longer-context model.
unsupported_parameter422The selected provider does not support a parameter you sent (e.g., temperature with reasoning models).
provider_error502The upstream LLM provider returned an error. Retry with backoff.

Retry strategy

Retry only on 429, 500, 502, 503, 504. Use exponential backoff with jitter:

wait = min(base * 2^attempt, max_wait) + random_jitter
  base      = 1 second
  max_wait  = 60 seconds
  jitter    = 0–25% of wait
  attempts  = max 5

Honor the Retry-After header when present. 429 responses include it as an integer number of seconds.

When not to retry

  • 400 / 401 / 403 / 404 / 422 — the request itself is wrong; retrying produces the same error.
  • 429 with code: budget_exceeded — retrying burns the budget faster. Wait for the reset window.

Idempotency

For POST /v1/chat/completions, send an Idempotency-Key header so retries do not double-charge or double-count:

curl https://api.schneeai.com/v1/chat/completions \
  -H "Authorization: Bearer $SCHNEEAI_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "model": "auto", "messages": [...] }'
  • Format: any string ≤ 255 chars. UUIDv4 recommended.
  • TTL: 24 hours. After that the same key is treated as a new request.
  • Scope: scoped to your service + tenant, so two tenants using the same key do not collide.
  • What is deduplicated: the charge and the usage record. The actual LLM call still happens if the original request never reached the provider.

Client examples

Python (requests + tenacity)

import uuid
import requests
from tenacity import retry, stop_after_attempt, wait_exponential_jitter, retry_if_exception_type

class RetryableError(Exception): pass

@retry(stop=stop_after_attempt(5),
       wait=wait_exponential_jitter(initial=1, max=60, jitter=2),
       retry=retry_if_exception_type(RetryableError))
def chat(messages, model="auto"):
    res = requests.post(
        "https://api.schneeai.com/v1/chat/completions",
        headers={
            "Authorization": f"Bearer {API_KEY}",
            "Idempotency-Key": str(uuid.uuid4()),
            "Content-Type": "application/json",
        },
        json={"model": model, "messages": messages},
        timeout=60,
    )
    if res.status_code in {429, 500, 502, 503, 504}:
        raise RetryableError(res.text)
    res.raise_for_status()
    return res.json()

TypeScript (fetch)

async function chat(messages: Msg[], model = "auto") {
  const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
  for (let attempt = 0; attempt < 5; attempt++) {
    const res = await fetch("https://api.schneeai.com/v1/chat/completions", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${API_KEY}`,
        "Idempotency-Key": crypto.randomUUID(),
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ model, messages }),
    });
    if (res.ok) return res.json();
    if (![429, 500, 502, 503, 504].includes(res.status)) {
      const body = await res.json();
      throw new Error(`schneeai ${res.status}: ${body.error?.code}`);
    }
    const retryAfter = Number(res.headers.get("Retry-After")) || 0;
    const wait = retryAfter || Math.min(2 ** attempt + Math.random(), 60);
    await sleep(wait * 1000);
  }
  throw new Error("schneeai: max retries exceeded");
}

Go (net/http + backoff)

var buf bytes.Buffer
json.NewEncoder(&buf).Encode(map[string]any{
    "model":    "auto",
    "messages": messages,
})
req, _ := http.NewRequest("POST", "https://api.schneeai.com/v1/chat/completions", &buf)
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Idempotency-Key", uuid.NewString())
req.Header.Set("Content-Type", "application/json")

bo := backoff.NewExponentialBackOff()
bo.MaxElapsedTime = 60 * time.Second
backoff.Retry(func() error {
    resp, err := http.DefaultClient.Do(req)
    if err != nil { return err }
    defer resp.Body.Close()
    switch {
    case resp.StatusCode >= 500, resp.StatusCode == 429:
        return errors.New("retry: " + resp.Status)
    case resp.StatusCode >= 400:
        return backoff.Permanent(parseError(resp))
    }
    return json.NewDecoder(resp.Body).Decode(&result)
}, bo)

Support

If an error persists after correct retrying, email [email protected] with the request_id from the error response. We can trace the exact call within minutes.


Note: SchneeAI is in design-partner stage. The error envelope above is stable and OpenAI-compatible; the budget / PII sub-codes are aligned with the platform features currently shipping. If you hit a code not listed here, send us the request_id and we will document it.