Astrodyne

Error responses

Every failure returns the same JSON envelope with a customer-safe message. Internal detail — database errors, model-provider identity, stack traces — never appears in a response, with debug mode on or off.

The envelope

Four fields, always these four, on every failure from every endpoint. param names the offending field when one can be identified, and is null otherwise.

JSON
{
  "error": {
    "message": "The model 'gpt-9' does not exist or is not available.",
    "type": "invalid_request_error",
    "param": "model",
    "code": "model_not_found"
  }
}
Branch on code, not on message
type and code come from closed sets that only change with a version. Messages are written for humans and may be reworded at any time. Client logic that matches on message text will break.

Every error, and what to do about it

This table is generated from the same definition the API validates against, so a code listed here is a code the service can actually return.

HTTPcodeWhenWhat to do
400 invalid_request_error
invalid_request_error
The request body or a query parameter is invalid. `param` names the field. Fix the named field. Retrying unchanged will fail identically.
401 authentication_error
authentication_error
No credential was presented, or the wrong kind was. Send `Authorization: Bearer <key>`. Session endpoints refuse an `ak-` token by design.
401 invalid_api_key
authentication_error
The key is not recognised. Check the key, or create a new one.
401 expired_api_key
authentication_error
The key passed its expiry. Rotate the key, or create a new one.
403 beta_access_required
access_denied
Authenticated, but this account is not in the invite-only beta. Retrying cannot help and the credential is not the problem. Request access; nothing was charged.
402 insufficient_funds
insufficient_funds
The balance cannot cover this request's maximum cost. Add funds. Nothing was reserved or charged.
404 model_not_found
invalid_request_error
The model does not exist, or is not available to this key. Call `GET /v1/models`. Unknown and not-permitted answer identically on purpose.
404 api_key_not_found
invalid_request_error
No such key on this account. Check the id. A key belonging to someone else answers the same way.
409 idempotency_conflict
invalid_request_error
This `Idempotency-Key` was used with a different request body. Use a new key, or send the identical body.
409 idempotency_in_progress
invalid_request_error
The original request with this key has not finished. Wait and retry with the same key.
409 api_key_revoked
invalid_request_error
The key is revoked and cannot be rotated. Create a new key.
429 rate_limit_error
rate_limit_error
Too many requests, or too many at once, for this key. Back off. Honour `Retry-After` when present.
429 spending_limit_exceeded
rate_limit_error
The key's own spending limit is reached. Raise the key's limit, or use another key.
500 internal_error
service_unavailable
Something failed on our side. Retry with backoff. Quote `X-Astrodyne-Request-Id`.
500 billing_error
billing_error
The request completed but the charge was not recorded. Do not retry. Quote the request id — this is flagged for review automatically.
503 service_unavailable
service_unavailable
The service is not accepting this request right now. Retry with backoff.
504 upstream_timeout
service_unavailable
The model did not respond in time. Retry. A shorter prompt or a lower `max_tokens` may help.
413 request_too_large
invalid_request_error
The request body exceeds the maximum size. Send less. Split the work across requests.
403 model_not_allowed
invalid_request_error
The key's policy forbids this model. Use a permitted model, or change the key's allowlist.

Which failures are safe to retry

To make a retry safe on a request that may already have run, send an Idempotency-Key.

Handling every case

A complete client that handles each status above, with the retry rules applied.

Python
import os
import time
import httpx

headers = {"Authorization": f"Bearer {os.environ['ASTRODYNE_API_KEY']}"}
body = {"model": "YOUR_MODEL_ID",
        "messages": [{"role": "user", "content": "Hello"}]}


def complete(http, attempt=0):
    response = http.post("/v1/chat/completions", headers=headers, json=body)

    # Log this on every outcome, success or failure. It is what support needs.
    request_id = response.headers.get("X-Astrodyne-Request-Id")

    if response.status_code == 200:
        return response.json()

    # Every failure has this exact shape. Branch on code, never on message.
    error = response.json()["error"]
    code = error["code"]

    if response.status_code == 401:
        raise SystemExit(f"Bad credential ({code}). Check ASTRODYNE_API_KEY.")

    if response.status_code == 403:
        # Authenticated, but this account may not use the API. Retrying
        # cannot help and nothing was charged.
        raise SystemExit(f"Access denied ({code}).")

    if response.status_code == 404:
        # The model is unknown OR not permitted for this key — deliberately
        # indistinguishable. Call /v1/models to see what you may use.
        raise SystemExit(f"Not found ({code}): {error['param']}")

    if response.status_code == 409:
        # An Idempotency-Key collision. idempotency_in_progress is worth
        # waiting on; idempotency_conflict means the body changed.
        if code == "idempotency_in_progress" and attempt < 5:
            time.sleep(2 ** attempt)
            return complete(http, attempt + 1)
        raise SystemExit(f"Conflict ({code}).")

    if response.status_code == 429:
        # Honour Retry-After when it is there; back off when it is not.
        wait = float(response.headers.get("Retry-After", 2 ** attempt))
        if attempt < 5:
            time.sleep(wait)
            return complete(http, attempt + 1)
        raise SystemExit("Rate limited; giving up.")

    if response.status_code >= 500:
        # billing_error is the one 5xx you must NOT retry: the request
        # completed and the charge is already flagged for review.
        if code == "billing_error":
            raise SystemExit(f"Billing error; quote request id {request_id}.")
        if attempt < 5:
            time.sleep(2 ** attempt)
            return complete(http, attempt + 1)

    raise SystemExit(f"Unhandled {response.status_code} {code} ({request_id})")


with httpx.Client(base_url="https://api.astrodyne.ai", timeout=60) as http:
    print(complete(http)["choices"][0]["message"]["content"])
JavaScript
const headers = {
  Authorization: `Bearer ${process.env.ASTRODYNE_API_KEY}`,
  "Content-Type": "application/json",
};
const body = JSON.stringify({
  model: "YOUR_MODEL_ID",
  messages: [{ role: "user", content: "Hello" }],
});

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function complete(attempt = 0) {
  const response = await fetch("https://api.astrodyne.ai/v1/chat/completions", {
    method: "POST", headers, body,
  });

  // Log this on every outcome. It is what support needs.
  const requestId = response.headers.get("X-Astrodyne-Request-Id");
  if (response.ok) return response.json();

  // Every failure has this exact shape. Branch on code, never on message.
  const { error } = await response.json();

  if (response.status === 401) throw new Error(`Bad credential (${error.code})`);
  if (response.status === 403) throw new Error(`Access denied (${error.code})`);
  if (response.status === 404) throw new Error(`Not found (${error.code})`);

  if (response.status === 409) {
    if (error.code === "idempotency_in_progress" && attempt < 5) {
      await sleep(2 ** attempt * 1000);
      return complete(attempt + 1);
    }
    throw new Error(`Conflict (${error.code})`);
  }

  if (response.status === 429 && attempt < 5) {
    const wait = Number(response.headers.get("Retry-After") ?? 2 ** attempt);
    await sleep(wait * 1000);
    return complete(attempt + 1);
  }

  if (response.status >= 500) {
    // billing_error is the one 5xx you must NOT retry.
    if (error.code === "billing_error") {
      throw new Error(`Billing error; quote request id ${requestId}`);
    }
    if (attempt < 5) {
      await sleep(2 ** attempt * 1000);
      return complete(attempt + 1);
    }
  }

  throw new Error(`Unhandled ${response.status} ${error.code} (${requestId})`);
}

const result = await complete();
console.log(result.choices[0].message.content);

Failures during a stream

Once streaming has begun the HTTP status can no longer change, so a failure arrives as a single sanitized event in the stream and the stream then closes without data: [DONE]:

SSE
data: {"object": "error", "error": {"message": "...", "type": "service_unavailable", "param": null, "code": "service_unavailable"}}

Treat a stream that ends without [DONE] as failed even if no error event arrived. You are charged only for output actually delivered to you — see Streaming.

Always capture the request id