Errors & retries
Standard HTTP semantics, stable error codes, idempotency keys, and retry patterns.
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.
Error codes
| Code | HTTP | Meaning | Recoverable? |
|---|---|---|---|
INVALID_API_KEY | 401 | Key is malformed, revoked, or doesn't exist | Re-issue a key |
EXPIRED_API_KEY | 401 | Key passed expires_at | Re-issue a key |
MODEL_NOT_ALLOWED | 403 | Project allowlist blocks this model | Update allowlist or pick a different model |
MODEL_NOT_FOUND | 404 | Model id doesn't exist | Check spelling against /models |
RATE_LIMITED | 429 | RPM or TPM cap hit | Honor Retry-After header |
BUDGET_EXCEEDED | 402 | Project spend cap reached | Raise the cap or wait for next period |
INVALID_REQUEST | 400 | Body failed schema validation | Fix the request |
UPSTREAM_ERROR | 502 | Provider returned an error we couldn't recover from | Retry; consider a fallback model |
UPSTREAM_TIMEOUT | 504 | Provider didn't respond in time | Retry; consider a fallback model |
INTERNAL | 500 | Our bug | Email 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()}`,
},
},
)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.