Skip to content

Commit 9e39dbe

Browse files
committed
Stop throwing "NetworkError" for in-stream errors, which are never network errors!
1 parent b9abe54 commit 9e39dbe

File tree

1 file changed

+32
-79
lines changed

1 file changed

+32
-79
lines changed

sdk/src/impl/llm.ts

Lines changed: 32 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,13 @@ import {
2020
} from '@codebuff/internal/openai-compatible/index'
2121
import {
2222
streamText,
23-
APICallError,
2423
generateText,
2524
generateObject,
2625
NoSuchToolError,
27-
InvalidToolInputError,
26+
APICallError,
2827
} from 'ai'
2928

3029
import { WEBSITE_URL } from '../constants'
31-
import { NetworkError, PaymentRequiredError, ErrorCodes } from '../errors'
32-
33-
import type { ErrorCode } from '../errors'
3430
import type { LanguageModelV2 } from '@ai-sdk/provider'
3531
import type { OpenRouterProviderRoutingOptions } from '@codebuff/common/types/agent-template'
3632
import type {
@@ -236,15 +232,21 @@ export async function* promptAiSdkStream(
236232
if (NoSuchToolError.isInstance(error) && 'spawn_agents' in tools) {
237233
// Also check for underscore variant (e.g., "file_picker" -> "file-picker")
238234
const toolNameWithHyphens = toolName.replace(/_/g, '-')
239-
235+
240236
const matchingAgentId = spawnableAgents.find((agentId) => {
241237
const withoutVersion = agentId.split('@')[0]
242238
const parts = withoutVersion.split('/')
243239
const agentName = parts[parts.length - 1]
244-
return agentName === toolName || agentName === toolNameWithHyphens || agentId === toolName
240+
return (
241+
agentName === toolName ||
242+
agentName === toolNameWithHyphens ||
243+
agentId === toolName
244+
)
245245
})
246246
const isSpawnableAgent = matchingAgentId !== undefined
247-
const isLocalAgent = toolName in localAgentTemplates || toolNameWithHyphens in localAgentTemplates
247+
const isLocalAgent =
248+
toolName in localAgentTemplates ||
249+
toolNameWithHyphens in localAgentTemplates
248250

249251
if (isSpawnableAgent || isLocalAgent) {
250252
// Transform agent tool call to spawn_agents
@@ -286,9 +288,12 @@ export async function* promptAiSdkStream(
286288
)
287289

288290
// Use the matching agent ID or corrected name with hyphens
289-
const correctedAgentType = matchingAgentId
290-
?? (toolNameWithHyphens in localAgentTemplates ? toolNameWithHyphens : toolName)
291-
291+
const correctedAgentType =
292+
matchingAgentId ??
293+
(toolNameWithHyphens in localAgentTemplates
294+
? toolNameWithHyphens
295+
: toolName)
296+
292297
const spawnAgentsInput = {
293298
agents: [
294299
{
@@ -345,15 +350,9 @@ export async function* promptAiSdkStream(
345350
}
346351
}
347352
if (chunkValue.type === 'error') {
348-
logger.error(
349-
{
350-
chunk: { ...chunkValue, error: undefined },
351-
error: getErrorObject(chunkValue.error),
352-
model: params.model,
353-
},
354-
'Error from AI SDK',
355-
)
356-
353+
// Error chunks from fullStream are non-network errors (tool failures, model issues, etc.)
354+
// Network errors are thrown, not yielded as chunks.
355+
// Pass all error chunks back to the agent so it can see what went wrong and retry.
357356
const errorBody = APICallError.isInstance(chunkValue.error)
358357
? chunkValue.error.responseBody
359358
: undefined
@@ -365,66 +364,20 @@ export async function* promptAiSdkStream(
365364
: JSON.stringify(chunkValue.error)
366365
const errorMessage = `Error from AI SDK (model ${params.model}): ${buildArray([mainErrorMessage, errorBody]).join('\n')}`
367366

368-
// Determine error code from the error
369-
let errorCode: ErrorCode = ErrorCodes.UNKNOWN_ERROR
370-
let statusCode: number | undefined
371-
372-
if (APICallError.isInstance(chunkValue.error)) {
373-
statusCode = chunkValue.error.statusCode
374-
if (statusCode) {
375-
if (statusCode === 402) {
376-
// Payment required - extract message from JSON response body
377-
let paymentErrorMessage = mainErrorMessage
378-
if (errorBody) {
379-
try {
380-
const parsed = JSON.parse(errorBody)
381-
paymentErrorMessage = parsed.message || errorBody
382-
} catch {
383-
paymentErrorMessage = errorBody
384-
}
385-
}
386-
throw new PaymentRequiredError(paymentErrorMessage)
387-
} else if (statusCode === 503) {
388-
errorCode = ErrorCodes.SERVICE_UNAVAILABLE
389-
} else if (statusCode >= 500) {
390-
errorCode = ErrorCodes.SERVER_ERROR
391-
} else if (statusCode === 408 || statusCode === 429) {
392-
errorCode = ErrorCodes.TIMEOUT
393-
}
394-
}
395-
} else if (chunkValue.error instanceof Error) {
396-
// Check error message for error type indicators (case-insensitive)
397-
const msg = chunkValue.error.message.toLowerCase()
398-
if (msg.includes('service unavailable') || msg.includes('503')) {
399-
errorCode = ErrorCodes.SERVICE_UNAVAILABLE
400-
} else if (
401-
msg.includes('econnrefused') ||
402-
msg.includes('connection refused')
403-
) {
404-
errorCode = ErrorCodes.CONNECTION_REFUSED
405-
} else if (msg.includes('enotfound') || msg.includes('dns')) {
406-
errorCode = ErrorCodes.DNS_FAILURE
407-
} else if (msg.includes('timeout')) {
408-
errorCode = ErrorCodes.TIMEOUT
409-
} else if (
410-
msg.includes('server error') ||
411-
msg.includes('500') ||
412-
msg.includes('502') ||
413-
msg.includes('504')
414-
) {
415-
errorCode = ErrorCodes.SERVER_ERROR
416-
} else if (msg.includes('network') || msg.includes('fetch failed')) {
417-
errorCode = ErrorCodes.NETWORK_ERROR
418-
}
419-
}
420-
421-
// Throw NetworkError so retry logic can handle it
422-
throw new NetworkError(
423-
errorMessage,
424-
errorCode,
425-
statusCode,
426-
chunkValue.error,
367+
logger.warn(
368+
{
369+
chunk: { ...chunkValue, error: undefined },
370+
error: getErrorObject(chunkValue.error),
371+
model: params.model,
372+
},
373+
'Error chunk from AI SDK stream - yielding to agent for retry',
427374
)
375+
376+
yield {
377+
type: 'error',
378+
message: errorMessage,
379+
}
380+
continue
428381
}
429382
if (chunkValue.type === 'reasoning-delta') {
430383
for (const provider of ['openrouter', 'codebuff'] as const) {

0 commit comments

Comments
 (0)