Errors & retries

Standard HTTP semantics, stable error codes, idempotency keys, and retry patterns.

FIG.
FIG. 00 · ERRORS & RETRIESSTATUS SPECTRUM

Synapse Garden uses standard HTTP semantics. Every error is returned as JSON with a stable code, a human-readable message, and (when useful) a details object. The AI SDK's streamText throws on these automatically — pair it with Idempotency-Key for safe retries.

FIG. 01RETRY LOOP
SCHEMATIC
On 5xx, timeouts, and 429, retry with backoff. Pass `Idempotency-Key` and a duplicate retry returns the cached first response with no extra tokens billed. Spend caps return 402 — surface that to the user instead of retrying.

Error codes

CodeHTTPMeaningRecoverable?
INVALID_API_KEY401Key is malformed, revoked, or doesn't existRe-issue a key
EXPIRED_API_KEY401Key passed expires_atRe-issue a key
MODEL_NOT_ALLOWED403Project allowlist blocks this modelUpdate allowlist or pick a different model
MODEL_NOT_FOUND404Model id doesn't existCheck spelling against /models
RATE_LIMITED429RPM or TPM cap hitHonor Retry-After header
BUDGET_EXCEEDED402Project spend cap reachedRaise the cap or wait for next period
INVALID_REQUEST400Body failed schema validationFix the request
UPSTREAM_ERROR502Provider returned an error we couldn't recover fromRetry; consider a fallback model
UPSTREAM_TIMEOUT504Provider didn't respond in timeRetry; consider a fallback model
INTERNAL500Our bugEmail us with the X-Request-Id

Error response shape

{
  "error": {
    "code": "MODEL_NOT_ALLOWED",
    "message": "Model 'anthropic/claude-opus-4.6' is not in the allowlist for project 'production'.",
    "details": {
      "modelId": "anthropic/claude-opus-4.6",
      "projectId": "prj_2KY4…",
      "allowlist": ["openai/gpt-5.4", "openai/gpt-5.4-mini", "anthropic/claude-sonnet-4.6"]
    }
  }
}

Every response — success or error — carries an X-Request-Id header. Include it when you report issues.

Retry semantics

We retry idempotent failures (5xx, timeouts) up to 3× internally with exponential backoff. Customer-side retry is also safe — every request takes an Idempotency-Key header for deduping at our edge:

const res = await fetch("https://synapse.garden/api/v1/chat/completions", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${MG_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": `req_${Date.now()}_${crypto.randomUUID()}`,
  },
  body: JSON.stringify({
    model: "openai/gpt-5.4",
    messages: [{ role: "user", content: "..." }],
  }),
})
const res = await client.chat.completions.create(
  {
    model: "openai/gpt-5.4",
    messages: [{ role: "user", content: "..." }],
  },
  {
    headers: {
      "Idempotency-Key": `req_${Date.now()}_${crypto.randomUUID()}`,
    },
  },
)
Idempotency window

We dedupe on Idempotency-Key for 24 hours. If you POST the same key twice within that window, the second request returns the first response without hitting the upstream — saves you tokens.

Streaming errors

For streaming requests, errors mid-stream arrive as a final SSE event:

data: {"error": {"code": "UPSTREAM_TIMEOUT", "message": "..."}}

data: [DONE]

The AI SDK and OpenAI SDK throw on these automatically; with raw fetch, parse the data: line and check for error.

Common patterns

Retry with fallback model

If your primary model is overloaded, fall back to a different model — not just a different provider:

import { streamText } from "ai"

const result = streamText({
  model: "openai/gpt-5.4",
  providerOptions: {
    gateway: {
      models: ["anthropic/claude-opus-4.6", "google/gemini-3.1-pro-preview"],
    },
  },
})

The response's providerMetadata.gateway.modelAttempts array tells you which model actually answered.

Catch budget exceeded gracefully

try {
  const { text } = await generateText({ model: "openai/gpt-5.4", prompt })
  return Response.json({ text })
} catch (err: any) {
  if (err.statusCode === 402) {
    return Response.json(
      { error: "Spend cap reached for this billing period." },
      { status: 402 },
    )
  }
  throw err
}

Honor Retry-After

async function callWithRetry(req: Request) {
  for (let attempt = 0; attempt < 3; attempt++) {
    const res = await fetch(req)
    if (res.status !== 429) return res
    const retryAfter = Number(res.headers.get("Retry-After") ?? "1")
    await new Promise((r) => setTimeout(r, retryAfter * 1000))
  }
  throw new Error("Rate limit retries exhausted")
}

Debugging tips

Check the request ID

X-Request-Id on every response. Look it up in Dashboard → Usage → Recent requests for the full trace.

Inspect provider metadata

result.providerMetadata.gateway.modelAttempts shows every provider attempted, with response times and error codes.

Reproduce with curl

Errors that look weird in the AI SDK often resolve in a curl call. We auto-render a curl reproduction in the dashboard for any logged request.

Email us

hi@synapse.garden with the X-Request-Id and we'll trace it from our side. Same-day reply during the beta.