Skip to content

Commit 2999724

Browse files
committed
Document retry-wrapped API errors
1 parent f95a2ce commit 2999724

1 file changed

Lines changed: 53 additions & 29 deletions

File tree

docs/error-schema.md

Lines changed: 53 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ The server returns JSON error responses with an HTTP status code. There are two
1616

1717
Used for:
1818

19-
| Status | Example message |
20-
|--------|----------------|
21-
| 400 | `"Invalid JSON in request body"` |
22-
| 400 | `"No runId found in request body"` |
23-
| 401 | `"Unauthorized"` |
24-
| 401 | `"Invalid Codebuff API key"` |
25-
| 402 | `"Out of credits. Please add credits at https://codebuff.com/usage. Your free credits reset in 3 hours."` |
19+
| Status | Example message |
20+
| ------ | --------------------------------------------------------------------------------------------------------- |
21+
| 400 | `"Invalid JSON in request body"` |
22+
| 400 | `"No runId found in request body"` |
23+
| 401 | `"Unauthorized"` |
24+
| 401 | `"Invalid Codebuff API key"` |
25+
| 402 | `"Out of credits. Please add credits at https://codebuff.com/usage. Your free credits reset in 3 hours."` |
2626

2727
### Typed errors (error code + message)
2828

@@ -32,11 +32,13 @@ Used for:
3232

3333
Used for errors that the client needs to identify programmatically:
3434

35-
| Status | `error` code | Example `message` |
36-
|--------|-------------|-------------------|
37-
| 403 | `account_suspended` | `"Your account has been suspended. Please contact support@codebuff.com if you did not expect this."` |
38-
| 403 | `free_mode_unavailable` | `"Free mode is not available in your country."` (Freebuff: `"Freebuff is not available in your country."`) |
39-
| 429 | `rate_limit_exceeded` | `"Subscription weekly limit reached. Your limit resets in 2 hours. Enable 'Continue with credits' in the CLI to use a-la-carte credits."` |
35+
| Status | `error` code | Example `message` |
36+
| ------ | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
37+
| 403 | `account_suspended` | `"Your account has been suspended. Please contact support@codebuff.com if you did not expect this."` |
38+
| 403 | `free_mode_unavailable` | `"Free mode is not available in your country."` (Freebuff: `"Freebuff is not available in your country."`) |
39+
| 409 | `session_superseded` | `"Another instance of freebuff has taken over this session. Only one instance per account is allowed."` |
40+
| 409 | `session_model_mismatch` | `"This session is bound to <model>; restart freebuff to switch models."` |
41+
| 429 | `rate_limit_exceeded` | `"Subscription weekly limit reached. Your limit resets in 2 hours. Enable 'Continue with credits' in the CLI to use a-la-carte credits."` |
4042

4143
### Catch-all server error
4244

@@ -65,20 +67,38 @@ AI SDK creates: APICallError {
6567
}
6668
```
6769

68-
The server's human-readable `message` and machine-readable `error` code are buried inside `responseBody` as a JSON string. The `APICallError.message` is just the HTTP status text ("Forbidden", "Payment Required", etc.).
70+
The server's human-readable `message` and machine-readable `error` code are buried inside `responseBody` as a JSON string. The `APICallError.message` is often just the HTTP status text ("Forbidden", "Payment Required", "Conflict", etc.).
71+
72+
Some statuses that the AI SDK considers retryable, including HTTP 409, can be retried and then wrapped in an `AI_RetryError`:
73+
74+
```
75+
AI_RetryError {
76+
message: "Failed after 4 attempts. Last error: Conflict",
77+
lastError: APICallError { statusCode: 409, responseBody: "{\"error\":\"session_superseded\",...}" },
78+
errors: [APICallError, ...]
79+
}
80+
```
81+
82+
In this case the structured server response is no longer on the top-level error. It must be recovered from `lastError` or `errors`.
6983

7084
## Client-Side Error Recovery
7185

72-
To recover the server's structured error details, we use `parseApiErrorResponseBody()` from `common/src/util/error.ts`:
86+
To recover the server's structured error details, callers use `extractApiErrorDetails()` from `common/src/util/error.ts`:
7387

7488
```typescript
75-
export function parseApiErrorResponseBody(responseBody: unknown): {
89+
export function extractApiErrorDetails(error: unknown): {
90+
statusCode?: number
7691
errorCode?: string
7792
message?: string
93+
countryCode?: string
94+
countryBlockReason?: string
95+
ipPrivacySignals?: string[]
7896
}
7997
```
8098

81-
This is called in two places:
99+
`extractApiErrorDetails()` checks the top-level error and nested retry wrapper fields (`lastError`, `errors`, and `cause`). For each candidate it extracts `statusCode`/`status` and parses any API `responseBody` with `parseApiErrorResponseBody()`.
100+
101+
This helper is called in two places:
82102

83103
### 1. Agent Runtime catch block
84104

@@ -88,18 +108,17 @@ This is the **primary** error handler. Most API errors are caught here because t
88108

89109
```typescript
90110
catch (error) {
91-
if (error instanceof APICallError) {
92-
const parsed = parseApiErrorResponseBody(error.responseBody)
93-
// parsed.errorCode = 'free_mode_unavailable'
94-
// parsed.message = 'Free mode is not available in your country.'
95-
}
111+
const apiErrorDetails = extractApiErrorDetails(error)
112+
// apiErrorDetails.errorCode = 'free_mode_unavailable'
113+
// apiErrorDetails.message = 'Free mode is not available in your country.'
114+
// apiErrorDetails.statusCode = 403
96115
// ...
97116
return {
98117
output: {
99118
type: 'error',
100119
message: hasServerMessage ? errorMessage : 'Agent run error: ' + errorMessage,
101-
statusCode,
102-
error: errorCode, // ← machine-readable code for client matching
120+
statusCode: apiErrorDetails.statusCode,
121+
error: apiErrorDetails.errorCode, // ← machine-readable code for client matching
103122
},
104123
}
105124
}
@@ -111,6 +130,8 @@ catch (error) {
111130

112131
This is a **fallback** handler for errors that escape the agent runtime (e.g., errors during setup before the agent loop starts).
113132

133+
It also calls `extractApiErrorDetails()` so retry-wrapped setup errors preserve the same `statusCode`, `error`, and `message` fields as agent-loop errors.
134+
114135
## Error Output Schema
115136

116137
**File:** `common/src/types/session-state.ts`
@@ -122,7 +143,7 @@ z.object({
122143
type: z.literal('error'),
123144
message: z.string(),
124145
statusCode: z.number().optional(),
125-
error: z.string().optional(), // machine-readable error code
146+
error: z.string().optional(), // machine-readable error code
126147
})
127148
```
128149

@@ -152,12 +173,13 @@ For all other errors, the raw `output.message` is displayed in the `UserErrorBan
152173
│ HTTP 403 │ │ │ │
153174
│ { error, message } │ │ │ │
154175
│────────────────────────▶│ │ │ │
155-
│ │ APICallError │ │ │
156-
│ │ .message="Forbidden" │ │ │
176+
│ │ APICallError or │ │ │
177+
│ │ AI_RetryError │ │ │
157178
│ │ .responseBody="{...}" │ │ │
179+
│ │ or .lastError │ │ │
158180
│ │────────────────────────▶│ │ │
159-
│ │ │ catch (APICallError) │ │
160-
│ │ │ parseResponseBody() │ │
181+
│ │ │ catch (error) │ │
182+
│ │ │ extractApiError...() │ │
161183
│ │ │ extract error code │ │
162184
│ │ │ extract message │ │
163185
│ │ │─────────────────────▶ │ │
@@ -177,6 +199,7 @@ For all other errors, the raw `output.message` is displayed in the `UserErrorBan
177199
To add a new error type that the CLI can identify and handle specially:
178200

179201
1. **Server** (`web/src/app/api/v1/chat/completions/_post.ts`): Return a typed error:
202+
180203
```typescript
181204
return NextResponse.json(
182205
{ error: 'your_error_code', message: 'User-friendly message.' },
@@ -185,6 +208,7 @@ To add a new error type that the CLI can identify and handle specially:
185208
```
186209

187210
2. **CLI error detection** (`cli/src/utils/error-handling.ts`): Add a checker:
211+
188212
```typescript
189213
export const isYourError = (error: unknown): boolean => {
190214
if (
@@ -210,4 +234,4 @@ To add a new error type that the CLI can identify and handle specially:
210234
}
211235
```
212236

213-
No changes needed in the agent runtime or SDK — `parseApiErrorResponseBody` automatically extracts any `error` and `message` fields from the server's response body.
237+
No changes needed in the agent runtime or SDK — `extractApiErrorDetails()` automatically extracts any `error` and `message` fields from the server's response body, including when the API error is nested inside an AI SDK retry wrapper.

0 commit comments

Comments
 (0)