@ai-sdk/provider-utils
Advanced tools
+6
-0
| # @ai-sdk/provider-utils | ||
| ## 4.0.50 | ||
| ### Patch Changes | ||
| - cc23556: Mark transient network errors that occur while reading successful response bodies as retryable. | ||
| ## 4.0.49 | ||
@@ -4,0 +10,0 @@ |
+1
-1
| { | ||
| "name": "@ai-sdk/provider-utils", | ||
| "version": "4.0.49", | ||
| "version": "4.0.50", | ||
| "license": "Apache-2.0", | ||
@@ -5,0 +5,0 @@ "sideEffects": false, |
@@ -6,3 +6,3 @@ import { APICallError } from '@ai-sdk/provider'; | ||
| const BUN_ERROR_CODES = [ | ||
| const RETRYABLE_NETWORK_ERROR_CODES = new Set([ | ||
| 'ConnectionRefused', | ||
@@ -15,15 +15,29 @@ 'ConnectionClosed', | ||
| 'EPIPE', | ||
| ]; | ||
| 'UND_ERR_SOCKET', | ||
| 'UND_ERR_HEADERS_TIMEOUT', | ||
| 'UND_ERR_BODY_TIMEOUT', | ||
| 'UND_ERR_CONNECT_TIMEOUT', | ||
| ]); | ||
| function isBunNetworkError(error: unknown): error is Error & { code?: string } { | ||
| if (!(error instanceof Error)) { | ||
| return false; | ||
| } | ||
| function findNetworkError( | ||
| error: unknown, | ||
| ): (Error & { code?: unknown }) | undefined { | ||
| const visited = new Set<Error>(); | ||
| let current = error; | ||
| const code = (error as any).code; | ||
| if (typeof code === 'string' && BUN_ERROR_CODES.includes(code)) { | ||
| return true; | ||
| while (current instanceof Error && !visited.has(current)) { | ||
| visited.add(current); | ||
| const errorWithCode = current as Error & { code?: unknown }; | ||
| if ( | ||
| typeof errorWithCode.code === 'string' && | ||
| RETRYABLE_NETWORK_ERROR_CODES.has(errorWithCode.code) | ||
| ) { | ||
| return errorWithCode; | ||
| } | ||
| current = (current as Error & { cause?: unknown }).cause; | ||
| } | ||
| return false; | ||
| return undefined; | ||
| } | ||
@@ -63,5 +77,23 @@ | ||
| if (isBunNetworkError(error)) { | ||
| const networkError = findNetworkError(error); | ||
| if (networkError != null) { | ||
| if (APICallError.isInstance(error)) { | ||
| return new APICallError({ | ||
| message: error.message, | ||
| cause: error.cause, | ||
| url: error.url, | ||
| requestBodyValues: error.requestBodyValues, | ||
| statusCode: error.statusCode, | ||
| responseHeaders: error.responseHeaders, | ||
| responseBody: error.responseBody, | ||
| data: error.data, | ||
| isRetryable: true, | ||
| }); | ||
| } | ||
| return new APICallError({ | ||
| message: `Cannot connect to API: ${error.message}`, | ||
| message: `Cannot connect to API: ${ | ||
| error instanceof Error ? error.message : networkError.message | ||
| }`, | ||
| cause: error, | ||
@@ -68,0 +100,0 @@ url, |
| import { APICallError, EmptyResponseBodyError } from '@ai-sdk/provider'; | ||
| import { extractResponseHeaders } from './extract-response-headers'; | ||
| import { handleFetchError } from './handle-fetch-error'; | ||
| import { isAbortError } from './is-abort-error'; | ||
| import { parseJSON, safeParseJSON, type ParseResult } from './parse-json'; | ||
@@ -20,2 +22,70 @@ import { parseJsonEventStream } from './parse-json-event-stream'; | ||
| function wrapResponseBodyStream({ | ||
| stream, | ||
| url, | ||
| requestBodyValues, | ||
| statusCode, | ||
| responseHeaders, | ||
| }: { | ||
| stream: ReadableStream<Uint8Array>; | ||
| url: string; | ||
| requestBodyValues: unknown; | ||
| statusCode: number; | ||
| responseHeaders: Record<string, string>; | ||
| }): ReadableStream<Uint8Array> { | ||
| const reader = stream.getReader(); | ||
| let readerReleased = false; | ||
| const releaseReader = () => { | ||
| if (!readerReleased) { | ||
| reader.releaseLock(); | ||
| readerReleased = true; | ||
| } | ||
| }; | ||
| return new ReadableStream<Uint8Array>({ | ||
| async pull(controller) { | ||
| try { | ||
| const { done, value } = await reader.read(); | ||
| if (done) { | ||
| releaseReader(); | ||
| controller.close(); | ||
| } else { | ||
| controller.enqueue(value); | ||
| } | ||
| } catch (error) { | ||
| releaseReader(); | ||
| if (isAbortError(error)) { | ||
| controller.error(error); | ||
| return; | ||
| } | ||
| controller.error( | ||
| handleFetchError({ | ||
| error: new APICallError({ | ||
| message: 'Failed to process successful response', | ||
| cause: error, | ||
| statusCode, | ||
| url, | ||
| responseHeaders, | ||
| requestBodyValues, | ||
| }), | ||
| url, | ||
| requestBodyValues, | ||
| }), | ||
| ); | ||
| } | ||
| }, | ||
| async cancel(reason) { | ||
| try { | ||
| await reader.cancel(reason); | ||
| } finally { | ||
| releaseReader(); | ||
| } | ||
| }, | ||
| }); | ||
| } | ||
| async function readResponseBodyAsText({ | ||
@@ -106,3 +176,3 @@ response, | ||
| ): ResponseHandler<ReadableStream<ParseResult<T>>> => | ||
| async ({ response }: { response: Response }) => { | ||
| async ({ response, url, requestBodyValues }) => { | ||
| const responseHeaders = extractResponseHeaders(response); | ||
@@ -117,3 +187,9 @@ | ||
| value: parseJsonEventStream({ | ||
| stream: response.body, | ||
| stream: wrapResponseBodyStream({ | ||
| stream: response.body, | ||
| url, | ||
| requestBodyValues, | ||
| statusCode: response.status, | ||
| responseHeaders, | ||
| }), | ||
| schema: chunkSchema, | ||
@@ -120,0 +196,0 @@ }), |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
1009556
1.57%13757
1.96%