Skip to content

Commit 6a72817

Browse files
committed
Use ai sdk error codes instead of our own error code schema
1 parent 95d2c01 commit 6a72817

File tree

17 files changed

+300
-503
lines changed

17 files changed

+300
-503
lines changed

cli/src/__tests__/integration/api-integration.test.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import {
2-
AuthenticationError,
3-
NetworkError,
42
getUserInfoFromApiKey,
53
} from '@codebuff/sdk'
64
import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'
@@ -143,7 +141,7 @@ describe('API Integration', () => {
143141
fields: ['id'],
144142
logger: testLogger,
145143
}),
146-
).rejects.toBeInstanceOf(AuthenticationError)
144+
).rejects.toMatchObject({ statusCode: 401 })
147145

148146
// 401s are now logged as auth failures
149147
expect(testLogger.error.mock.calls.length).toBeGreaterThan(0)
@@ -161,7 +159,7 @@ describe('API Integration', () => {
161159
fields: ['id'],
162160
logger: testLogger,
163161
}),
164-
).rejects.toBeInstanceOf(AuthenticationError)
162+
).rejects.toMatchObject({ statusCode: 401 })
165163

166164
expect(testLogger.error.mock.calls.length).toBeGreaterThan(0)
167165
})
@@ -180,7 +178,7 @@ describe('API Integration', () => {
180178
fields: ['id'],
181179
logger: testLogger,
182180
}),
183-
).rejects.toBeInstanceOf(NetworkError)
181+
).rejects.toMatchObject({ statusCode: expect.any(Number) })
184182

185183
expect(testLogger.error.mock.calls.length).toBeGreaterThan(0)
186184
})
@@ -197,7 +195,7 @@ describe('API Integration', () => {
197195
fields: ['id'],
198196
logger: testLogger,
199197
}),
200-
).rejects.toBeInstanceOf(NetworkError)
198+
).rejects.toMatchObject({ statusCode: expect.any(Number) })
201199

202200
expect(
203201
testLogger.error.mock.calls.some(([payload]) =>
@@ -218,7 +216,7 @@ describe('API Integration', () => {
218216
fields: ['id'],
219217
logger: testLogger,
220218
}),
221-
).rejects.toBeInstanceOf(NetworkError)
219+
).rejects.toMatchObject({ statusCode: expect.any(Number) })
222220

223221
expect(testLogger.error.mock.calls.length).toBeGreaterThan(0)
224222
})
@@ -239,7 +237,7 @@ describe('API Integration', () => {
239237
fields: ['id'],
240238
logger: testLogger,
241239
}),
242-
).rejects.toBeInstanceOf(NetworkError)
240+
).rejects.toMatchObject({ statusCode: expect.any(Number) })
243241

244242
expect(fetchMock.mock.calls.length).toBe(1)
245243
expect(
@@ -263,7 +261,7 @@ describe('API Integration', () => {
263261
fields: ['id'],
264262
logger: testLogger,
265263
}),
266-
).rejects.toBeInstanceOf(NetworkError)
264+
).rejects.toMatchObject({ statusCode: expect.any(Number) })
267265

268266
expect(fetchMock.mock.calls.length).toBe(1)
269267
expect(

cli/src/app.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { NetworkError, RETRYABLE_ERROR_CODES } from '@codebuff/sdk'
1+
import { isRetryableStatusCode, getErrorStatusCode } from '@codebuff/sdk'
22
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
33
import { useShallow } from 'zustand/react/shallow'
44

@@ -213,15 +213,13 @@ export const App = ({
213213

214214
// Derive auth reachability + retrying state inline from authQuery error
215215
const authError = authQuery.error
216-
const networkError =
217-
authError && authError instanceof NetworkError ? authError : null
218-
const isRetryableNetworkError = Boolean(
219-
networkError && RETRYABLE_ERROR_CODES.has(networkError.code),
220-
)
216+
const authErrorStatusCode = authError ? getErrorStatusCode(authError) : undefined
217+
const isRetryableNetworkError = authErrorStatusCode !== undefined && isRetryableStatusCode(authErrorStatusCode)
221218

222219
let authStatus: AuthStatus = 'ok'
223220
if (authQuery.isError) {
224-
if (!networkError) {
221+
// Only show network status if it's a server/network error (5xx)
222+
if (authErrorStatusCode === undefined || authErrorStatusCode < 500) {
225223
authStatus = 'ok'
226224
} else if (isRetryableNetworkError) {
227225
authStatus = 'retrying'

cli/src/hooks/helpers/__tests__/send-message.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ const { setupStreamingContext, handleRunError } = await import(
3434
const { createBatchedMessageUpdater } = await import(
3535
'../../../utils/message-updater'
3636
)
37-
const { PaymentRequiredError } = await import('@codebuff/sdk')
37+
import { createPaymentRequiredError } from '@codebuff/sdk'
3838

3939
const createMockTimerController = (): SendMessageTimerController & {
4040
startCalls: string[]
@@ -375,7 +375,7 @@ describe('handleRunError', () => {
375375
expect(mockInvalidateQueries).not.toHaveBeenCalled()
376376
})
377377

378-
test('PaymentRequiredError uses setError, invalidates queries, and switches input mode', () => {
378+
test('Payment required error (402) uses setError, invalidates queries, and switches input mode', () => {
379379
let messages: ChatMessage[] = [
380380
{
381381
id: 'ai-1',
@@ -397,7 +397,7 @@ describe('handleRunError', () => {
397397
setInputMode: setInputModeMock,
398398
})
399399

400-
const paymentError = new PaymentRequiredError('Out of credits')
400+
const paymentError = createPaymentRequiredError('Out of credits')
401401

402402
handleRunError({
403403
error: paymentError,

cli/src/hooks/use-auth-query.ts

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import { createHash } from 'crypto'
22

33
import { getCiEnv } from '@codebuff/common/env-ci'
44
import {
5-
AuthenticationError,
6-
ErrorCodes,
75
getUserInfoFromApiKey as defaultGetUserInfoFromApiKey,
8-
NetworkError,
9-
RETRYABLE_ERROR_CODES,
6+
isRetryableStatusCode,
7+
getErrorStatusCode,
8+
createAuthError,
9+
createServerError,
1010
MAX_RETRIES_PER_MESSAGE,
1111
RETRY_BACKOFF_BASE_DELAY_MS,
1212
} from '@codebuff/sdk'
@@ -47,6 +47,14 @@ type ValidatedUserInfo = {
4747
email: string
4848
}
4949

50+
/**
51+
* Check if an error is an authentication error (401, 403)
52+
*/
53+
function isAuthenticationError(error: unknown): boolean {
54+
const statusCode = getErrorStatusCode(error)
55+
return statusCode === 401 || statusCode === 403
56+
}
57+
5058
/**
5159
* Validates an API key by calling the backend
5260
*
@@ -69,42 +77,39 @@ export async function validateApiKey({
6977

7078
if (!authResult) {
7179
logger.error('❌ API key validation failed - invalid credentials')
72-
throw new AuthenticationError('Invalid API key', 401)
80+
throw createAuthError('Invalid API key')
7381
}
7482

7583
return authResult
7684
} catch (error) {
77-
if (error instanceof AuthenticationError) {
85+
const statusCode = getErrorStatusCode(error)
86+
87+
if (isAuthenticationError(error)) {
7888
logger.error('❌ API key validation failed - authentication error')
79-
// Rethrow the original error to preserve error type for higher layers
89+
// Rethrow the original error to preserve statusCode for higher layers
8090
throw error
8191
}
8292

83-
if (error instanceof NetworkError) {
93+
if (statusCode !== undefined && isRetryableStatusCode(statusCode)) {
8494
logger.error(
8595
{
86-
error: error.message,
87-
code: error.code,
96+
error: error instanceof Error ? error.message : String(error),
97+
statusCode,
8898
},
8999
'❌ API key validation failed - network error',
90100
)
91-
// Rethrow the original error to preserve error type for higher layers
101+
// Rethrow the original error to preserve statusCode for higher layers
92102
throw error
93103
}
94104

95-
// Unknown error - wrap in NetworkError for consistency
105+
// Unknown error - wrap with statusCode for consistency
96106
logger.error(
97107
{
98108
error: error instanceof Error ? error.message : String(error),
99109
},
100110
'❌ API key validation failed - unknown error',
101111
)
102-
throw new NetworkError(
103-
'Authentication failed',
104-
ErrorCodes.UNKNOWN_ERROR,
105-
undefined,
106-
error,
107-
)
112+
throw createServerError('Authentication failed')
108113
}
109114
}
110115

@@ -139,12 +144,13 @@ export function useAuthQuery(deps: UseAuthQueryDeps = {}) {
139144
// Retry only for retryable network errors (5xx, timeouts, etc.)
140145
// Don't retry authentication errors (invalid credentials)
141146
retry: (failureCount, error) => {
147+
const statusCode = getErrorStatusCode(error)
142148
// Don't retry authentication errors - user needs to update credentials
143-
if (error instanceof AuthenticationError) {
149+
if (isAuthenticationError(error)) {
144150
return false
145151
}
146152
// Retry network errors if they're retryable and we haven't exceeded max retries
147-
if (error instanceof NetworkError && RETRYABLE_ERROR_CODES.has(error.code)) {
153+
if (statusCode !== undefined && isRetryableStatusCode(statusCode)) {
148154
return failureCount < MAX_RETRIES_PER_MESSAGE
149155
}
150156
// Don't retry other errors

cli/src/utils/create-run-config.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ export type CreateRunConfigParams = {
3030
type RetryArgs = {
3131
attempt: number
3232
delayMs: number
33-
errorCode?: string
33+
statusCode?: number
3434
}
3535

3636
type RetryExhaustedArgs = {
3737
totalAttempts: number
38-
errorCode?: string
38+
statusCode?: number
3939
}
4040

4141
export const createRunConfig = (params: CreateRunConfigParams) => {
@@ -63,19 +63,19 @@ export const createRunConfig = (params: CreateRunConfigParams) => {
6363
maxRetries: MAX_RETRIES_PER_MESSAGE,
6464
backoffBaseMs: RETRY_BACKOFF_BASE_DELAY_MS,
6565
backoffMaxMs: RETRY_BACKOFF_MAX_DELAY_MS,
66-
onRetry: async ({ attempt, delayMs, errorCode }: RetryArgs) => {
66+
onRetry: async ({ attempt, delayMs, statusCode }: RetryArgs) => {
6767
logger.warn(
68-
{ sdkAttempt: attempt, delayMs, errorCode },
68+
{ sdkAttempt: attempt, delayMs, statusCode },
6969
'SDK retrying after error',
7070
)
7171
setIsRetrying(true)
7272
setStreamStatus('waiting')
7373
},
7474
onRetryExhausted: async ({
7575
totalAttempts,
76-
errorCode,
76+
statusCode,
7777
}: RetryExhaustedArgs) => {
78-
logger.warn({ totalAttempts, errorCode }, 'SDK exhausted all retries')
78+
logger.warn({ totalAttempts, statusCode }, 'SDK exhausted all retries')
7979
},
8080
},
8181
agentDefinitions,

cli/src/utils/error-handling.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { env } from '@codebuff/common/env'
2-
import { ErrorCodes, isPaymentRequiredError } from '@codebuff/sdk'
32

43
import type { ChatMessage } from '../types/chat'
54

@@ -14,7 +13,7 @@ const extractErrorMessage = (error: unknown, fallback: string): string => {
1413
return error.message + (error.stack ? `\n\n${error.stack}` : '')
1514
}
1615
if (error && typeof error === 'object' && 'message' in error) {
17-
const candidate = (error as any).message
16+
const candidate = (error as { message: unknown }).message
1817
if (typeof candidate === 'string' && candidate.length > 0) {
1918
return candidate
2019
}
@@ -27,16 +26,14 @@ const extractErrorMessage = (error: unknown, fallback: string): string => {
2726
* Standardized on statusCode === 402 for payment required detection.
2827
*/
2928
export const isOutOfCreditsError = (error: unknown): boolean => {
30-
// Check for error output with errorCode property (from agent run results)
3129
if (
3230
error &&
3331
typeof error === 'object' &&
3432
'statusCode' in error &&
35-
error.statusCode === 402
33+
(error as { statusCode: unknown }).statusCode === 402
3634
) {
3735
return true
3836
}
39-
4037
return false
4138
}
4239

@@ -55,6 +52,3 @@ export const createErrorMessage = (
5552
isComplete: true,
5653
}
5754
}
58-
59-
// Re-export for convenience in helpers
60-
export { isPaymentRequiredError }

cli/src/utils/error-messages.ts

Lines changed: 25 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,35 @@
1-
import {
2-
AuthenticationError,
3-
NetworkError,
4-
ErrorCodes,
5-
isErrorWithCode,
6-
sanitizeErrorMessage,
7-
} from '@codebuff/sdk'
1+
import { sanitizeErrorMessage, getErrorStatusCode } from '@codebuff/sdk'
82

93
/**
104
* Formats an unknown error into a user-facing markdown string.
115
*
12-
* The goal is to provide clear, consistent messaging across the CLI while
13-
* reusing the SDK error typing and sanitization logic.
6+
* The goal is to provide clear, consistent messaging across the CLI.
147
*/
158
export function formatErrorForDisplay(error: unknown, fallbackTitle: string): string {
16-
// Authentication-specific messaging
17-
if (error instanceof AuthenticationError) {
18-
if (error.status === 401) {
19-
return `${fallbackTitle}: Authentication failed. Please check your API key.`
20-
}
21-
22-
if (error.status === 403) {
23-
return `${fallbackTitle}: Access forbidden. You do not have permission to access this resource.`
24-
}
9+
const statusCode = getErrorStatusCode(error)
2510

26-
return `${fallbackTitle}: Invalid API key. Please check your credentials.`
11+
// Authentication-specific messaging based on statusCode
12+
if (statusCode === 401) {
13+
return `${fallbackTitle}: Authentication failed. Please check your API key.`
2714
}
28-
29-
// Network-specific messaging
30-
if (error instanceof NetworkError) {
31-
let detail: string
32-
33-
switch (error.code) {
34-
case ErrorCodes.TIMEOUT:
35-
detail = 'Request timed out. Please check your internet connection.'
36-
break
37-
case ErrorCodes.CONNECTION_REFUSED:
38-
detail = 'Connection refused. The server may be down.'
39-
break
40-
case ErrorCodes.DNS_FAILURE:
41-
detail = 'DNS resolution failed. Please check your internet connection.'
42-
break
43-
case ErrorCodes.SERVER_ERROR:
44-
case ErrorCodes.SERVICE_UNAVAILABLE:
45-
detail = 'Server error. Please try again later.'
46-
break
47-
case ErrorCodes.NETWORK_ERROR:
48-
default:
49-
detail = 'Network error. Please check your internet connection.'
50-
break
51-
}
52-
53-
return `${fallbackTitle}: ${detail}`
15+
if (statusCode === 403) {
16+
return `${fallbackTitle}: Access forbidden. You do not have permission to access this resource.`
5417
}
5518

56-
// Any other typed error that exposes a code
57-
if (isErrorWithCode(error)) {
58-
const safeMessage = sanitizeErrorMessage(error)
59-
return `${fallbackTitle}: ${safeMessage}`
19+
// Network/server error messaging based on statusCode
20+
if (statusCode !== undefined) {
21+
if (statusCode === 408) {
22+
return `${fallbackTitle}: Request timed out. Please check your internet connection.`
23+
}
24+
if (statusCode === 503) {
25+
return `${fallbackTitle}: Service unavailable. The server may be down.`
26+
}
27+
if (statusCode >= 500) {
28+
return `${fallbackTitle}: Server error. Please try again later.`
29+
}
30+
if (statusCode === 429) {
31+
return `${fallbackTitle}: Rate limited. Please try again later.`
32+
}
6033
}
6134

6235
// Generic Error instance
@@ -65,8 +38,9 @@ export function formatErrorForDisplay(error: unknown, fallbackTitle: string): st
6538
return `${fallbackTitle}: ${message}`
6639
}
6740

68-
// Fallback for unknown values
69-
return `${fallbackTitle}: ${String(error)}`
41+
// Try sanitizeErrorMessage for other cases
42+
const safeMessage = sanitizeErrorMessage(error)
43+
return `${fallbackTitle}: ${safeMessage}`
7044
}
7145

7246
/**

0 commit comments

Comments
 (0)