UNPKG

@ai-sdk/provider-utils

Version:
1 lines 210 kB
{"version":3,"sources":["../src/combine-headers.ts","../src/convert-async-iterator-to-readable-stream.ts","../src/create-tool-name-mapping.ts","../src/delay.ts","../src/delayed-promise.ts","../src/extract-response-headers.ts","../src/uint8-utils.ts","../src/convert-image-model-file-to-data-uri.ts","../src/convert-to-form-data.ts","../src/cancel-response-body.ts","../src/download-error.ts","../src/is-browser-runtime.ts","../src/validate-download-url.ts","../src/fetch-with-validated-redirects.ts","../src/read-response-with-size-limit.ts","../src/download-blob.ts","../src/generate-id.ts","../src/get-error-message.ts","../src/get-from-api.ts","../src/handle-fetch-error.ts","../src/is-abort-error.ts","../src/get-runtime-environment-user-agent.ts","../src/normalize-headers.ts","../src/with-user-agent-suffix.ts","../src/version.ts","../src/inject-json-instruction.ts","../src/is-non-nullable.ts","../src/is-same-origin.ts","../src/is-url-supported.ts","../src/load-api-key.ts","../src/load-optional-setting.ts","../src/load-setting.ts","../src/media-type-to-extension.ts","../src/parse-json.ts","../src/secure-json-parse.ts","../src/validate-types.ts","../src/schema.ts","../src/add-additional-properties-to-json-schema.ts","../src/to-json-schema/zod3-to-json-schema/options.ts","../src/to-json-schema/zod3-to-json-schema/select-parser.ts","../src/to-json-schema/zod3-to-json-schema/parsers/any.ts","../src/to-json-schema/zod3-to-json-schema/parsers/array.ts","../src/to-json-schema/zod3-to-json-schema/parsers/bigint.ts","../src/to-json-schema/zod3-to-json-schema/parsers/boolean.ts","../src/to-json-schema/zod3-to-json-schema/parsers/branded.ts","../src/to-json-schema/zod3-to-json-schema/parsers/catch.ts","../src/to-json-schema/zod3-to-json-schema/parsers/date.ts","../src/to-json-schema/zod3-to-json-schema/parsers/default.ts","../src/to-json-schema/zod3-to-json-schema/parsers/effects.ts","../src/to-json-schema/zod3-to-json-schema/parsers/enum.ts","../src/to-json-schema/zod3-to-json-schema/parsers/intersection.ts","../src/to-json-schema/zod3-to-json-schema/parsers/literal.ts","../src/to-json-schema/zod3-to-json-schema/parsers/record.ts","../src/to-json-schema/zod3-to-json-schema/parsers/string.ts","../src/to-json-schema/zod3-to-json-schema/parsers/map.ts","../src/to-json-schema/zod3-to-json-schema/parsers/native-enum.ts","../src/to-json-schema/zod3-to-json-schema/parsers/never.ts","../src/to-json-schema/zod3-to-json-schema/parsers/null.ts","../src/to-json-schema/zod3-to-json-schema/parsers/union.ts","../src/to-json-schema/zod3-to-json-schema/parsers/nullable.ts","../src/to-json-schema/zod3-to-json-schema/parsers/number.ts","../src/to-json-schema/zod3-to-json-schema/parsers/object.ts","../src/to-json-schema/zod3-to-json-schema/parsers/optional.ts","../src/to-json-schema/zod3-to-json-schema/parsers/pipeline.ts","../src/to-json-schema/zod3-to-json-schema/parsers/promise.ts","../src/to-json-schema/zod3-to-json-schema/parsers/set.ts","../src/to-json-schema/zod3-to-json-schema/parsers/tuple.ts","../src/to-json-schema/zod3-to-json-schema/parsers/undefined.ts","../src/to-json-schema/zod3-to-json-schema/parsers/unknown.ts","../src/to-json-schema/zod3-to-json-schema/parsers/readonly.ts","../src/to-json-schema/zod3-to-json-schema/get-relative-path.ts","../src/to-json-schema/zod3-to-json-schema/parse-def.ts","../src/to-json-schema/zod3-to-json-schema/refs.ts","../src/to-json-schema/zod3-to-json-schema/zod3-to-json-schema.ts","../src/parse-json-event-stream.ts","../src/parse-provider-options.ts","../src/post-to-api.ts","../src/types/tool.ts","../src/provider-tool-factory.ts","../src/remove-undefined-entries.ts","../src/resolve.ts","../src/response-handler.ts","../src/strip-file-extension.ts","../src/without-trailing-slash.ts","../src/is-async-iterable.ts","../src/types/execute-tool.ts","../src/index.ts"],"sourcesContent":["export function combineHeaders(\n ...headers: Array<Record<string, string | undefined> | undefined>\n): Record<string, string | undefined> {\n return headers.reduce(\n (combinedHeaders, currentHeaders) => ({\n ...combinedHeaders,\n ...(currentHeaders ?? {}),\n }),\n {},\n ) as Record<string, string | undefined>;\n}\n","/**\n * Converts an AsyncIterator to a ReadableStream.\n *\n * @template T - The type of elements produced by the AsyncIterator.\n * @param { <T>} iterator - The AsyncIterator to convert.\n * @returns {ReadableStream<T>} - A ReadableStream that provides the same data as the AsyncIterator.\n */\nexport function convertAsyncIteratorToReadableStream<T>(\n iterator: AsyncIterator<T>,\n): ReadableStream<T> {\n let cancelled = false;\n\n return new ReadableStream<T>({\n /**\n * Called when the consumer wants to pull more data from the stream.\n *\n * @param {ReadableStreamDefaultController<T>} controller - The controller to enqueue data into the stream.\n * @returns {Promise<void>}\n */\n async pull(controller) {\n if (cancelled) return;\n try {\n const { value, done } = await iterator.next();\n if (done) {\n controller.close();\n } else {\n controller.enqueue(value);\n }\n } catch (error) {\n controller.error(error);\n }\n },\n /**\n * Called when the consumer cancels the stream.\n */\n async cancel(reason?: unknown) {\n cancelled = true;\n if (iterator.return) {\n try {\n await iterator.return(reason);\n } catch {\n // intentionally ignore errors during cancellation\n }\n }\n },\n });\n}\n","import type {\n LanguageModelV3FunctionTool,\n LanguageModelV3ProviderTool,\n} from '@ai-sdk/provider';\n\n/**\n * Interface for mapping between custom tool names and provider tool names.\n */\nexport interface ToolNameMapping {\n /**\n * Maps a custom tool name (used by the client) to the provider's tool name.\n * If the custom tool name does not have a mapping, returns the input name.\n *\n * @param customToolName - The custom name of the tool defined by the client.\n * @returns The corresponding provider tool name, or the input name if not mapped.\n */\n toProviderToolName: (customToolName: string) => string;\n\n /**\n * Maps a provider tool name to the custom tool name used by the client.\n * If the provider tool name does not have a mapping, returns the input name.\n *\n * @param providerToolName - The name of the tool as understood by the provider.\n * @returns The corresponding custom tool name, or the input name if not mapped.\n */\n toCustomToolName: (providerToolName: string) => string;\n}\n\n/**\n * @param tools - Tools that were passed to the language model.\n * @param providerToolNames - Maps the provider tool ids to the provider tool names.\n */\nexport function createToolNameMapping({\n tools = [],\n providerToolNames,\n resolveProviderToolName,\n}: {\n /**\n * Tools that were passed to the language model.\n */\n tools:\n | Array<LanguageModelV3FunctionTool | LanguageModelV3ProviderTool>\n | undefined;\n\n /**\n * Maps the provider tool ids to the provider tool names.\n */\n providerToolNames: Record<`${string}.${string}`, string>;\n\n /**\n * Optional resolver for provider tool names that cannot be represented as\n * static id -> name mappings (e.g. dynamic provider names).\n */\n resolveProviderToolName?: (\n tool: LanguageModelV3ProviderTool,\n ) => string | undefined;\n}): ToolNameMapping {\n const customToolNameToProviderToolName: Record<string, string> = {};\n const providerToolNameToCustomToolName: Record<string, string> = {};\n\n for (const tool of tools) {\n if (tool.type === 'provider') {\n const providerToolName =\n resolveProviderToolName?.(tool) ??\n (tool.id in providerToolNames ? providerToolNames[tool.id] : undefined);\n\n if (providerToolName == null) {\n continue;\n }\n\n customToolNameToProviderToolName[tool.name] = providerToolName;\n providerToolNameToCustomToolName[providerToolName] = tool.name;\n }\n }\n\n return {\n toProviderToolName: (customToolName: string) =>\n customToolNameToProviderToolName[customToolName] ?? customToolName,\n toCustomToolName: (providerToolName: string) =>\n providerToolNameToCustomToolName[providerToolName] ?? providerToolName,\n };\n}\n","/**\n * Creates a Promise that resolves after a specified delay\n * @param delayInMs - The delay duration in milliseconds. If null or undefined, resolves immediately.\n * @param signal - Optional AbortSignal to cancel the delay\n * @returns A Promise that resolves after the specified delay\n * @throws {DOMException} When the signal is aborted\n */\nexport async function delay(\n delayInMs?: number | null,\n options?: {\n abortSignal?: AbortSignal;\n },\n): Promise<void> {\n if (delayInMs == null) {\n return Promise.resolve();\n }\n\n const signal = options?.abortSignal;\n\n return new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n reject(createAbortError());\n return;\n }\n\n const timeoutId = setTimeout(() => {\n cleanup();\n resolve();\n }, delayInMs);\n\n const cleanup = () => {\n clearTimeout(timeoutId);\n signal?.removeEventListener('abort', onAbort);\n };\n\n const onAbort = () => {\n cleanup();\n reject(createAbortError());\n };\n\n signal?.addEventListener('abort', onAbort);\n });\n}\n\nfunction createAbortError(): DOMException {\n return new DOMException('Delay was aborted', 'AbortError');\n}\n","/**\n * Delayed promise. It is only constructed once the value is accessed.\n * This is useful to avoid unhandled promise rejections when the promise is created\n * but not accessed.\n */\nexport class DelayedPromise<T> {\n private status:\n | { type: 'pending' }\n | { type: 'resolved'; value: T }\n | { type: 'rejected'; error: unknown } = { type: 'pending' };\n private _promise: Promise<T> | undefined;\n private _resolve: undefined | ((value: T) => void) = undefined;\n private _reject: undefined | ((error: unknown) => void) = undefined;\n\n get promise(): Promise<T> {\n if (this._promise) {\n return this._promise;\n }\n\n this._promise = new Promise<T>((resolve, reject) => {\n if (this.status.type === 'resolved') {\n resolve(this.status.value);\n } else if (this.status.type === 'rejected') {\n reject(this.status.error);\n }\n\n this._resolve = resolve;\n this._reject = reject;\n });\n\n return this._promise;\n }\n\n resolve(value: T): void {\n this.status = { type: 'resolved', value };\n\n if (this._promise) {\n this._resolve?.(value);\n }\n }\n\n reject(error: unknown): void {\n this.status = { type: 'rejected', error };\n\n if (this._promise) {\n this._reject?.(error);\n }\n }\n\n isResolved(): boolean {\n return this.status.type === 'resolved';\n }\n\n isRejected(): boolean {\n return this.status.type === 'rejected';\n }\n\n isPending(): boolean {\n return this.status.type === 'pending';\n }\n}\n","/**\n * Extracts the headers from a response object and returns them as a key-value object.\n *\n * @param response - The response object to extract headers from.\n * @returns The headers as a key-value object.\n */\nexport function extractResponseHeaders(response: Response) {\n return Object.fromEntries<string>([...response.headers]);\n}\n","// btoa and atob need to be invoked as a function call, not as a method call.\n// Otherwise CloudFlare will throw a\n// \"TypeError: Illegal invocation: function called with incorrect this reference\"\nconst { btoa, atob } = globalThis;\n\nexport function convertBase64ToUint8Array(base64String: string) {\n const base64Url = base64String.replace(/-/g, '+').replace(/_/g, '/');\n const latin1string = atob(base64Url);\n return Uint8Array.from(latin1string, byte => byte.codePointAt(0)!);\n}\n\nexport function convertUint8ArrayToBase64(array: Uint8Array): string {\n let latin1string = '';\n\n // Note: regular for loop to support older JavaScript versions that\n // do not support for..of on Uint8Array\n for (let i = 0; i < array.length; i++) {\n latin1string += String.fromCodePoint(array[i]);\n }\n\n return btoa(latin1string);\n}\n\nexport function convertToBase64(value: string | Uint8Array): string {\n return value instanceof Uint8Array ? convertUint8ArrayToBase64(value) : value;\n}\n","import type { ImageModelV3File } from '@ai-sdk/provider';\nimport { convertUint8ArrayToBase64 } from './uint8-utils';\n\n/**\n * Convert an ImageModelV3File to a URL or data URI string.\n *\n * If the file is a URL, it returns the URL as-is.\n * If the file is base64 data, it returns a data URI with the base64 data.\n * If the file is a Uint8Array, it converts it to base64 and returns a data URI.\n */\nexport function convertImageModelFileToDataUri(file: ImageModelV3File): string {\n if (file.type === 'url') return file.url;\n\n return `data:${file.mediaType};base64,${\n typeof file.data === 'string'\n ? file.data\n : convertUint8ArrayToBase64(file.data)\n }`;\n}\n","/**\n * Converts an input object to FormData for multipart/form-data requests.\n *\n * Handles the following cases:\n * - `null` or `undefined` values are skipped\n * - Arrays with a single element are appended as a single value\n * - Arrays with multiple elements are appended with `[]` suffix (e.g., `image[]`)\n * unless `useArrayBrackets` is set to `false`\n * - All other values are appended directly\n *\n * @param input - The input object to convert. Use a generic type for type validation.\n * @param options - Optional configuration object.\n * @param options.useArrayBrackets - Whether to add `[]` suffix for multi-element arrays.\n * Defaults to `true`. Set to `false` for APIs that expect repeated keys without brackets.\n * @returns A FormData object containing the input values.\n *\n * @example\n * ```ts\n * type MyInput = {\n * model: string;\n * prompt: string;\n * images: Blob[];\n * };\n *\n * const formData = convertToFormData<MyInput>({\n * model: 'gpt-image-1',\n * prompt: 'A cat',\n * images: [blob1, blob2],\n * });\n * ```\n */\nexport function convertToFormData<T extends Record<string, unknown>>(\n input: T,\n options: { useArrayBrackets?: boolean } = {},\n): FormData {\n const { useArrayBrackets = true } = options;\n const formData = new FormData();\n\n for (const [key, value] of Object.entries(input)) {\n if (value == null) {\n continue;\n }\n\n if (Array.isArray(value)) {\n if (value.length === 1) {\n formData.append(key, value[0] as string | Blob);\n continue;\n }\n\n const arrayKey = useArrayBrackets ? `${key}[]` : key;\n for (const item of value) {\n formData.append(arrayKey, item as string | Blob);\n }\n continue;\n }\n\n formData.append(key, value as string | Blob);\n }\n\n return formData;\n}\n","/**\n * Cancels a response body to release the underlying connection.\n *\n * When a fetch Response is rejected without consuming its body (e.g. a failed\n * status code, an open-redirect rejection, or a Content-Length that exceeds the\n * size limit), the underlying TCP socket is not returned to the connection pool\n * and may stay open until the process runs out of file descriptors. Cancelling\n * the body avoids this leak.\n *\n * Errors thrown while cancelling are ignored: the body may already be locked,\n * disturbed, or absent, none of which should mask the original rejection.\n */\nexport async function cancelResponseBody(response: Response): Promise<void> {\n try {\n await response.body?.cancel();\n } catch {\n // Ignore cancel errors so the original rejection is preserved.\n }\n}\n","import { AISDKError } from '@ai-sdk/provider';\n\nconst name = 'AI_DownloadError';\nconst marker = `vercel.ai.error.${name}`;\nconst symbol = Symbol.for(marker);\n\nexport class DownloadError extends AISDKError {\n private readonly [symbol] = true; // used in isInstance\n\n readonly url: string;\n readonly statusCode?: number;\n readonly statusText?: string;\n\n constructor({\n url,\n statusCode,\n statusText,\n cause,\n message = cause == null\n ? `Failed to download ${url}: ${statusCode} ${statusText}`\n : `Failed to download ${url}: ${cause}`,\n }: {\n url: string;\n statusCode?: number;\n statusText?: string;\n message?: string;\n cause?: unknown;\n }) {\n super({ name, message, cause });\n\n this.url = url;\n this.statusCode = statusCode;\n this.statusText = statusText;\n }\n\n static isInstance(error: unknown): error is DownloadError {\n return AISDKError.hasMarker(error, marker);\n }\n}\n","/**\n * Returns `true` when running in a browser.\n *\n * Detection keys on the presence of a global `window`, matching the browser\n * check used elsewhere in this package (see `getRuntimeEnvironmentUserAgent`)\n * so the SDK has a single, consistent definition of \"browser\". Server runtimes\n * (Node.js, Deno, Bun, edge/workers) do not define `window`.\n */\nexport function isBrowserRuntime(\n globalThisAny: any = globalThis as any,\n): boolean {\n return globalThisAny.window != null;\n}\n","import { DownloadError } from './download-error';\n\n/**\n * Validates that a URL is safe to download from, blocking private/internal addresses\n * to prevent SSRF attacks.\n *\n * Note: this performs string/literal-IP checks only. It does not resolve DNS, so a\n * hostname that resolves to a private address is not blocked here (see callers, which\n * should additionally constrain egress at the network layer when handling untrusted URLs).\n *\n * @param url - The URL string to validate.\n * @throws DownloadError if the URL is unsafe.\n */\nexport function validateDownloadUrl(url: string): void {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new DownloadError({\n url,\n message: `Invalid URL: ${url}`,\n });\n }\n\n // data: URLs are inline content, so they do not trigger a network fetch or SSRF risk.\n if (parsed.protocol === 'data:') {\n return;\n }\n\n // Only allow http and https network protocols\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new DownloadError({\n url,\n message: `URL scheme must be http, https, or data, got ${parsed.protocol}`,\n });\n }\n\n // Strip a trailing dot so a fully-qualified name like `localhost.` (which resolves\n // identically to `localhost`) cannot bypass the hostname blocklist below.\n const hostname = parsed.hostname.toLowerCase().replace(/\\.+$/, '');\n\n // Block empty hostname\n if (!hostname) {\n throw new DownloadError({\n url,\n message: `URL must have a hostname`,\n });\n }\n\n // Block localhost and .local domains\n if (\n hostname === 'localhost' ||\n hostname.endsWith('.local') ||\n hostname.endsWith('.localhost')\n ) {\n throw new DownloadError({\n url,\n message: `URL with hostname ${hostname} is not allowed`,\n });\n }\n\n // Check for IPv6 addresses (enclosed in brackets in URLs)\n if (hostname.startsWith('[') && hostname.endsWith(']')) {\n const ipv6 = hostname.slice(1, -1);\n if (isPrivateIPv6(ipv6)) {\n throw new DownloadError({\n url,\n message: `URL with IPv6 address ${hostname} is not allowed`,\n });\n }\n return;\n }\n\n // Check for IPv4 addresses\n if (isIPv4(hostname)) {\n if (isPrivateIPv4(hostname)) {\n throw new DownloadError({\n url,\n message: `URL with IP address ${hostname} is not allowed`,\n });\n }\n return;\n }\n}\n\nfunction isIPv4(hostname: string): boolean {\n const parts = hostname.split('.');\n if (parts.length !== 4) return false;\n return parts.every(part => {\n const num = Number(part);\n return (\n Number.isInteger(num) && num >= 0 && num <= 255 && String(num) === part\n );\n });\n}\n\nfunction isPrivateIPv4(ip: string): boolean {\n const parts = ip.split('.').map(Number);\n const [a, b, c] = parts;\n\n // 0.0.0.0/8\n if (a === 0) return true;\n // 10.0.0.0/8\n if (a === 10) return true;\n // 100.64.0.0/10 (CGNAT, used by some cloud providers for internal traffic)\n if (a === 100 && b >= 64 && b <= 127) return true;\n // 127.0.0.0/8\n if (a === 127) return true;\n // 169.254.0.0/16\n if (a === 169 && b === 254) return true;\n // 172.16.0.0/12\n if (a === 172 && b >= 16 && b <= 31) return true;\n // 192.0.0.0/24 (IETF protocol assignments)\n if (a === 192 && b === 0 && c === 0) return true;\n // 192.168.0.0/16\n if (a === 192 && b === 168) return true;\n // 198.18.0.0/15 (benchmarking)\n if (a === 198 && (b === 18 || b === 19)) return true;\n // 240.0.0.0/4 (reserved, includes 255.255.255.255 broadcast)\n if (a >= 240) return true;\n\n return false;\n}\n\n/**\n * Expands an IPv6 address string into its 8 16-bit groups, handling `::`\n * compression and an optional dotted-decimal IPv4 tail (e.g. `::ffff:127.0.0.1`).\n *\n * @returns the 8 groups, or null if the input is not a parseable IPv6 address.\n */\nfunction parseIPv6(ip: string): number[] | null {\n // Strip an optional zone id (e.g. `fe80::1%eth0`).\n let address = ip.toLowerCase();\n const zoneIndex = address.indexOf('%');\n if (zoneIndex !== -1) {\n address = address.slice(0, zoneIndex);\n }\n\n // At most one `::` compression marker is allowed.\n const halves = address.split('::');\n if (halves.length > 2) return null;\n\n const toGroups = (segment: string): number[] | null => {\n if (segment === '') return [];\n const groups: number[] = [];\n const parts = segment.split(':');\n for (let i = 0; i < parts.length; i++) {\n const part = parts[i];\n // A dotted-decimal IPv4 tail is only valid as the final part.\n if (part.includes('.')) {\n if (i !== parts.length - 1 || !isIPv4(part)) return null;\n const [a, b, c, d] = part.split('.').map(Number);\n groups.push((a << 8) | b, (c << 8) | d);\n continue;\n }\n if (!/^[0-9a-f]{1,4}$/.test(part)) return null;\n groups.push(parseInt(part, 16));\n }\n return groups;\n };\n\n const head = toGroups(halves[0]);\n if (head === null) return null;\n\n if (halves.length === 2) {\n const tail = toGroups(halves[1]);\n if (tail === null) return null;\n const fill = 8 - head.length - tail.length;\n if (fill < 0) return null;\n return [...head, ...new Array<number>(fill).fill(0), ...tail];\n }\n\n // No `::` compression: the address must contain exactly 8 groups.\n return head.length === 8 ? head : null;\n}\n\nfunction isPrivateIPv6(ip: string): boolean {\n const groups = parseIPv6(ip);\n\n // Fail closed: if the address cannot be parsed, treat it as unsafe.\n if (groups === null) return true;\n\n const topZero = (count: number) =>\n groups.slice(0, count).every(group => group === 0);\n\n // ::1 (loopback) and :: (unspecified)\n if (topZero(7) && (groups[7] === 0 || groups[7] === 1)) return true;\n\n // fc00::/7 (unique local addresses)\n if ((groups[0] & 0xfe00) === 0xfc00) return true;\n\n // fe80::/10 (link-local)\n if ((groups[0] & 0xffc0) === 0xfe80) return true;\n\n // fec0::/10 (site-local, deprecated but still routable internally)\n if ((groups[0] & 0xffc0) === 0xfec0) return true;\n\n // ff00::/8 (multicast)\n if ((groups[0] & 0xff00) === 0xff00) return true;\n\n // Addresses that embed an IPv4 address in their last 32 bits. For these we\n // extract the embedded IPv4 and reuse the IPv4 private-range checks, so that\n // e.g. ::ffff:127.0.0.1 or 64:ff9b::169.254.169.254 are blocked.\n const embedsIPv4 =\n // ::/96 — IPv4-compatible (deprecated)\n topZero(6) ||\n // ::ffff:0:0/96 — IPv4-mapped (ffff in group 5)\n (topZero(5) && groups[5] === 0xffff) ||\n // ::ffff:0:0/96 — IPv4-translated form (ffff in group 4, group 5 zero)\n (topZero(4) && groups[4] === 0xffff && groups[5] === 0) ||\n // 64:ff9b::/96 — NAT64 well-known prefix\n (groups[0] === 0x0064 &&\n groups[1] === 0xff9b &&\n groups[2] === 0 &&\n groups[3] === 0 &&\n groups[4] === 0 &&\n groups[5] === 0) ||\n // 64:ff9b:1::/48 — NAT64 local-use prefix\n (groups[0] === 0x0064 && groups[1] === 0xff9b && groups[2] === 0x0001);\n\n if (embedsIPv4) {\n const a = (groups[6] >> 8) & 0xff;\n const b = groups[6] & 0xff;\n const c = (groups[7] >> 8) & 0xff;\n const d = groups[7] & 0xff;\n return isPrivateIPv4(`${a}.${b}.${c}.${d}`);\n }\n\n return false;\n}\n","import { cancelResponseBody } from './cancel-response-body';\nimport { DownloadError } from './download-error';\nimport { isBrowserRuntime } from './is-browser-runtime';\nimport { validateDownloadUrl } from './validate-download-url';\n\nconst MAX_DOWNLOAD_REDIRECTS = 10;\n\n/**\n * Fetches a URL while enforcing the SSRF download guard on every hop.\n *\n * Redirects are followed manually (`redirect: 'manual'`) so each hop is\n * validated with {@link validateDownloadUrl} *before* it is requested. Relying\n * on the default `redirect: 'follow'` would issue the request to a redirect\n * target (e.g. an internal address) before we ever see its URL, defeating the\n * SSRF guard.\n *\n * A `redirect: 'manual'` request yields an unreadable opaque response in the\n * browser (and in other spec-compliant fetch implementations), so the redirect\n * target cannot be validated here. In a real browser this is safe to follow\n * natively because SSRF is not reachable (fetch is constrained by CORS and\n * cannot reach a server's internal network or cloud-metadata). On any other\n * runtime we cannot validate the hop, so we fail closed rather than follow it\n * blindly and bypass the SSRF guard.\n *\n * The returned response is the final (non-redirect) response. The caller is\n * responsible for checking `response.ok` and reading the body.\n *\n * @throws DownloadError if a hop is unsafe, the redirect limit is exceeded, or\n * a redirect cannot be validated on a non-browser runtime.\n */\nexport async function fetchWithValidatedRedirects({\n url,\n headers,\n abortSignal,\n maxRedirects = MAX_DOWNLOAD_REDIRECTS,\n}: {\n url: string;\n headers?: HeadersInit;\n abortSignal?: AbortSignal;\n maxRedirects?: number;\n}): Promise<Response> {\n // Per-hop request options. Only the `redirect` mode varies between hops, so\n // the rest is assembled once. `headers` is omitted entirely when not provided\n // so callers that send none issue a bare request.\n const baseInit: RequestInit = { signal: abortSignal };\n if (headers !== undefined) {\n baseInit.headers = headers;\n }\n\n let currentUrl = url;\n // The bound also acts as a backstop against an unterminated redirect chain.\n for (let redirectCount = 0; redirectCount <= maxRedirects; redirectCount++) {\n validateDownloadUrl(currentUrl);\n\n const response = await fetch(currentUrl, {\n ...baseInit,\n redirect: 'manual',\n });\n\n if (response.type === 'opaqueredirect') {\n if (!isBrowserRuntime()) {\n throw new DownloadError({\n url,\n message: `Redirect from ${currentUrl} could not be validated and was blocked`,\n });\n }\n return await fetch(currentUrl, { ...baseInit, redirect: 'follow' });\n }\n\n const location = response.headers.get('location');\n if (response.status >= 300 && response.status < 400 && location) {\n // Release the redirect response's connection before moving to the next\n // hop. Whether that hop is followed or rejected by the SSRF guard, an\n // unconsumed 3xx body would leak the underlying socket.\n await cancelResponseBody(response);\n currentUrl = new URL(location, currentUrl).toString();\n continue;\n }\n\n return response;\n }\n\n throw new DownloadError({\n url,\n message: `Too many redirects (max ${maxRedirects})`,\n });\n}\n","import { cancelResponseBody } from './cancel-response-body';\nimport { DownloadError } from './download-error';\n\n/**\n * Default maximum download size: 2 GiB.\n *\n * `fetch().arrayBuffer()` has ~2x peak memory overhead (undici buffers the\n * body internally, then creates the JS ArrayBuffer), so very large downloads\n * risk exceeding the default V8 heap limit on 64-bit systems and terminating\n * the process with an out-of-memory error.\n *\n * Setting this limit converts an unrecoverable OOM crash into a catchable\n * `DownloadError`.\n */\nexport const DEFAULT_MAX_DOWNLOAD_SIZE = 2 * 1024 * 1024 * 1024;\n\n/**\n * Reads a fetch Response body with a size limit to prevent memory exhaustion.\n *\n * Checks the Content-Length header for early rejection, then reads the body\n * incrementally via ReadableStream and aborts with a DownloadError when the\n * limit is exceeded.\n *\n * @param response - The fetch Response to read.\n * @param url - The URL being downloaded (used in error messages).\n * @param maxBytes - Maximum allowed bytes. Defaults to DEFAULT_MAX_DOWNLOAD_SIZE.\n * @returns A Uint8Array containing the response body.\n * @throws DownloadError if the response exceeds maxBytes.\n */\nexport async function readResponseWithSizeLimit({\n response,\n url,\n maxBytes = DEFAULT_MAX_DOWNLOAD_SIZE,\n}: {\n response: Response;\n url: string;\n maxBytes?: number;\n}): Promise<Uint8Array> {\n // Early rejection based on Content-Length header\n const contentLength = response.headers.get('content-length');\n if (contentLength != null) {\n const length = parseInt(contentLength, 10);\n if (!isNaN(length) && length > maxBytes) {\n // Cancel the body so the underlying connection is released back to the\n // pool instead of being left open until the socket is exhausted.\n await cancelResponseBody(response);\n throw new DownloadError({\n url,\n message: `Download of ${url} exceeded maximum size of ${maxBytes} bytes (Content-Length: ${length}).`,\n });\n }\n }\n\n const body = response.body;\n\n // Handle missing body (empty responses)\n if (body == null) {\n return new Uint8Array(0);\n }\n\n const reader = body.getReader();\n const chunks: Uint8Array[] = [];\n let totalBytes = 0;\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n\n if (done) {\n break;\n }\n\n totalBytes += value.length;\n\n if (totalBytes > maxBytes) {\n throw new DownloadError({\n url,\n message: `Download of ${url} exceeded maximum size of ${maxBytes} bytes.`,\n });\n }\n\n chunks.push(value);\n }\n } finally {\n try {\n await reader.cancel();\n } finally {\n reader.releaseLock();\n }\n }\n\n // Concatenate chunks into a single Uint8Array\n const result = new Uint8Array(totalBytes);\n let offset = 0;\n for (const chunk of chunks) {\n result.set(chunk, offset);\n offset += chunk.length;\n }\n\n return result;\n}\n","import { cancelResponseBody } from './cancel-response-body';\nimport { DownloadError } from './download-error';\nimport { fetchWithValidatedRedirects } from './fetch-with-validated-redirects';\nimport {\n readResponseWithSizeLimit,\n DEFAULT_MAX_DOWNLOAD_SIZE,\n} from './read-response-with-size-limit';\n\n/**\n * Download a file from a URL and return it as a Blob.\n *\n * @param url - The URL to download from.\n * @param options - Optional settings for the download.\n * @param options.maxBytes - Maximum allowed download size in bytes. Defaults to 100 MiB.\n * @param options.abortSignal - An optional abort signal to cancel the download.\n * @returns A Promise that resolves to the downloaded Blob.\n *\n * @throws DownloadError if the download fails or exceeds maxBytes.\n */\nexport async function downloadBlob(\n url: string,\n options?: { maxBytes?: number; abortSignal?: AbortSignal },\n): Promise<Blob> {\n try {\n const response = await fetchWithValidatedRedirects({\n url,\n abortSignal: options?.abortSignal,\n });\n\n if (!response.ok) {\n // Release the connection before rejecting so an error status from an\n // attacker-controlled origin cannot leak open sockets.\n await cancelResponseBody(response);\n throw new DownloadError({\n url,\n statusCode: response.status,\n statusText: response.statusText,\n });\n }\n\n const data = await readResponseWithSizeLimit({\n response,\n url,\n maxBytes: options?.maxBytes ?? DEFAULT_MAX_DOWNLOAD_SIZE,\n });\n\n const contentType = response.headers.get('content-type') ?? undefined;\n return new Blob([data], contentType ? { type: contentType } : undefined);\n } catch (error) {\n if (DownloadError.isInstance(error)) {\n throw error;\n }\n\n throw new DownloadError({ url, cause: error });\n }\n}\n","import { InvalidArgumentError } from '@ai-sdk/provider';\n\n/**\n * Creates an ID generator.\n * The total length of the ID is the sum of the prefix, separator, and random part length.\n * Not cryptographically secure.\n *\n * @param alphabet - The alphabet to use for the ID. Default: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.\n * @param prefix - The prefix of the ID to generate. Optional.\n * @param separator - The separator between the prefix and the random part of the ID. Default: '-'.\n * @param size - The size of the random part of the ID to generate. Default: 16.\n */\nexport const createIdGenerator = ({\n prefix,\n size = 16,\n alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',\n separator = '-',\n}: {\n prefix?: string;\n separator?: string;\n size?: number;\n alphabet?: string;\n} = {}): IdGenerator => {\n const generator = () => {\n const alphabetLength = alphabet.length;\n const chars = new Array(size);\n for (let i = 0; i < size; i++) {\n chars[i] = alphabet[(Math.random() * alphabetLength) | 0];\n }\n return chars.join('');\n };\n\n if (prefix == null) {\n return generator;\n }\n\n // check that the prefix is not part of the alphabet (otherwise prefix checking can fail randomly)\n if (alphabet.includes(separator)) {\n throw new InvalidArgumentError({\n argument: 'separator',\n message: `The separator \"${separator}\" must not be part of the alphabet \"${alphabet}\".`,\n });\n }\n\n return () => `${prefix}${separator}${generator()}`;\n};\n\n/**\n * A function that generates an ID.\n */\nexport type IdGenerator = () => string;\n\n/**\n * Generates a 16-character random string to use for IDs.\n * Not cryptographically secure.\n */\nexport const generateId = createIdGenerator();\n","export function getErrorMessage(error: unknown | undefined) {\n if (error == null) {\n return 'unknown error';\n }\n\n if (typeof error === 'string') {\n return error;\n }\n\n if (error instanceof Error) {\n return error.message;\n }\n\n return JSON.stringify(error);\n}\n","import { APICallError } from '@ai-sdk/provider';\nimport { extractResponseHeaders } from './extract-response-headers';\nimport type { FetchFunction } from './fetch-function';\nimport { handleFetchError } from './handle-fetch-error';\nimport { isAbortError } from './is-abort-error';\nimport type { ResponseHandler } from './response-handler';\nimport { getRuntimeEnvironmentUserAgent } from './get-runtime-environment-user-agent';\nimport { withUserAgentSuffix } from './with-user-agent-suffix';\nimport { VERSION } from './version';\n\n// use function to allow for mocking in tests:\nconst getOriginalFetch = () => globalThis.fetch;\n\nexport const getFromApi = async <T>({\n url,\n headers = {},\n successfulResponseHandler,\n failedResponseHandler,\n abortSignal,\n fetch = getOriginalFetch(),\n}: {\n url: string;\n headers?: Record<string, string | undefined>;\n failedResponseHandler: ResponseHandler<Error>;\n successfulResponseHandler: ResponseHandler<T>;\n abortSignal?: AbortSignal;\n fetch?: FetchFunction;\n}) => {\n try {\n const response = await fetch(url, {\n method: 'GET',\n headers: withUserAgentSuffix(\n headers,\n `ai-sdk/provider-utils/${VERSION}`,\n getRuntimeEnvironmentUserAgent(),\n ),\n signal: abortSignal,\n });\n\n const responseHeaders = extractResponseHeaders(response);\n\n if (!response.ok) {\n let errorInformation: {\n value: Error;\n responseHeaders?: Record<string, string> | undefined;\n };\n\n try {\n errorInformation = await failedResponseHandler({\n response,\n url,\n requestBodyValues: {},\n });\n } catch (error) {\n if (isAbortError(error) || APICallError.isInstance(error)) {\n throw error;\n }\n\n throw new APICallError({\n message: 'Failed to process error response',\n cause: error,\n statusCode: response.status,\n url,\n responseHeaders,\n requestBodyValues: {},\n });\n }\n\n throw errorInformation.value;\n }\n\n try {\n return await successfulResponseHandler({\n response,\n url,\n requestBodyValues: {},\n });\n } catch (error) {\n if (error instanceof Error) {\n if (isAbortError(error) || APICallError.isInstance(error)) {\n throw error;\n }\n }\n\n throw new APICallError({\n message: 'Failed to process successful response',\n cause: error,\n statusCode: response.status,\n url,\n responseHeaders,\n requestBodyValues: {},\n });\n }\n } catch (error) {\n throw handleFetchError({ error, url, requestBodyValues: {} });\n }\n};\n","import { APICallError } from '@ai-sdk/provider';\nimport { isAbortError } from './is-abort-error';\n\nconst FETCH_FAILED_ERROR_MESSAGES = ['fetch failed', 'failed to fetch'];\n\nconst BUN_ERROR_CODES = [\n 'ConnectionRefused',\n 'ConnectionClosed',\n 'FailedToOpenSocket',\n 'ECONNRESET',\n 'ECONNREFUSED',\n 'ETIMEDOUT',\n 'EPIPE',\n];\n\nfunction isBunNetworkError(error: unknown): error is Error & { code?: string } {\n if (!(error instanceof Error)) {\n return false;\n }\n\n const code = (error as any).code;\n if (typeof code === 'string' && BUN_ERROR_CODES.includes(code)) {\n return true;\n }\n\n return false;\n}\n\nexport function handleFetchError({\n error,\n url,\n requestBodyValues,\n}: {\n error: unknown;\n url: string;\n requestBodyValues: unknown;\n}) {\n if (isAbortError(error)) {\n return error;\n }\n\n // unwrap original error when fetch failed (for easier debugging):\n if (\n error instanceof TypeError &&\n FETCH_FAILED_ERROR_MESSAGES.includes(error.message.toLowerCase())\n ) {\n const cause = (error as any).cause;\n\n if (cause != null) {\n // Failed to connect to server:\n return new APICallError({\n message: `Cannot connect to API: ${cause.message}`,\n cause,\n url,\n requestBodyValues,\n isRetryable: true, // retry when network error\n });\n }\n }\n\n if (isBunNetworkError(error)) {\n return new APICallError({\n message: `Cannot connect to API: ${error.message}`,\n cause: error,\n url,\n requestBodyValues,\n isRetryable: true,\n });\n }\n\n return error;\n}\n","export function isAbortError(error: unknown): error is Error {\n return (\n (error instanceof Error || error instanceof DOMException) &&\n (error.name === 'AbortError' ||\n error.name === 'ResponseAborted' || // Next.js\n error.name === 'TimeoutError')\n );\n}\n","export function getRuntimeEnvironmentUserAgent(\n globalThisAny: any = globalThis as any,\n): string {\n // Browsers\n if (globalThisAny.window) {\n return `runtime/browser`;\n }\n\n // Cloudflare Workers / Deno / Bun / Node.js >= 21.1\n if (globalThisAny.navigator?.userAgent) {\n return `runtime/${globalThisAny.navigator.userAgent.toLowerCase()}`;\n }\n\n // Nodes.js < 21.1\n if (globalThisAny.process?.versions?.node) {\n return `runtime/node.js/${globalThisAny.process.version.substring(0)}`;\n }\n\n if (globalThisAny.EdgeRuntime) {\n return `runtime/vercel-edge`;\n }\n\n return 'runtime/unknown';\n}\n","/**\n * Normalizes different header inputs into a plain record with lower-case keys.\n * Entries with `undefined` or `null` values are removed.\n *\n * @param headers - Input headers (`Headers`, tuples array, plain record) to normalize.\n * @returns A record containing the normalized header entries.\n */\nexport function normalizeHeaders(\n headers:\n | HeadersInit\n | Record<string, string | undefined>\n | Array<[string, string | undefined]>\n | undefined,\n): Record<string, string> {\n if (headers == null) {\n return {};\n }\n\n const normalized: Record<string, string> = {};\n\n if (headers instanceof Headers) {\n headers.forEach((value, key) => {\n normalized[key.toLowerCase()] = value;\n });\n } else {\n if (!Array.isArray(headers)) {\n headers = Object.entries(headers);\n }\n\n for (const [key, value] of headers) {\n if (value != null) {\n normalized[key.toLowerCase()] = value;\n }\n }\n }\n\n return normalized;\n}\n","import { normalizeHeaders } from './normalize-headers';\n\n/**\n * Appends suffix parts to the `user-agent` header.\n * If a `user-agent` header already exists, the suffix parts are appended to it.\n * If no `user-agent` header exists, a new one is created with the suffix parts.\n * Automatically removes undefined entries from the headers.\n *\n * @param headers - The original headers.\n * @param userAgentSuffixParts - The parts to append to the `user-agent` header.\n * @returns The new headers with the `user-agent` header set or updated.\n */\nexport function withUserAgentSuffix(\n headers: HeadersInit | Record<string, string | undefined> | undefined,\n ...userAgentSuffixParts: string[]\n): Record<string, string> {\n const normalizedHeaders = new Headers(normalizeHeaders(headers));\n\n const currentUserAgentHeader = normalizedHeaders.get('user-agent') || '';\n\n normalizedHeaders.set(\n 'user-agent',\n [currentUserAgentHeader, ...userAgentSuffixParts].filter(Boolean).join(' '),\n );\n\n return Object.fromEntries(normalizedHeaders.entries());\n}\n","// Version string of this package injected at build time.\ndeclare const __PACKAGE_VERSION__: string | undefined;\nexport const VERSION: string =\n typeof __PACKAGE_VERSION__ !== 'undefined'\n ? __PACKAGE_VERSION__\n : '0.0.0-test';\n","import type {\n JSONSchema7,\n LanguageModelV3Message,\n LanguageModelV3Prompt,\n} from '@ai-sdk/provider';\n\nconst DEFAULT_SCHEMA_PREFIX = 'JSON schema:';\nconst DEFAULT_SCHEMA_SUFFIX =\n 'You MUST answer with a JSON object that matches the JSON schema above.';\nconst DEFAULT_GENERIC_SUFFIX = 'You MUST answer with JSON.';\n\nexport function injectJsonInstruction({\n prompt,\n schema,\n schemaPrefix = schema != null ? DEFAULT_SCHEMA_PREFIX : undefined,\n schemaSuffix = schema != null\n ? DEFAULT_SCHEMA_SUFFIX\n : DEFAULT_GENERIC_SUFFIX,\n}: {\n prompt?: string;\n schema?: JSONSchema7;\n schemaPrefix?: string;\n schemaSuffix?: string;\n}): string {\n return [\n prompt != null && prompt.length > 0 ? prompt : undefined,\n prompt != null && prompt.length > 0 ? '' : undefined, // add a newline if prompt is not null\n schemaPrefix,\n schema != null ? JSON.stringify(schema) : undefined,\n schemaSuffix,\n ]\n .filter(line => line != null)\n .join('\\n');\n}\n\nexport function injectJsonInstructionIntoMessages({\n messages,\n schema,\n schemaPrefix,\n schemaSuffix,\n}: {\n messages: LanguageModelV3Prompt;\n schema?: JSONSchema7;\n schemaPrefix?: string;\n schemaSuffix?: string;\n}): LanguageModelV3Prompt {\n const systemMessage: LanguageModelV3Message =\n messages[0]?.role === 'system'\n ? { ...messages[0] }\n : { role: 'system', content: '' };\n\n systemMessage.content = injectJsonInstruction({\n prompt: systemMessage.content,\n schema,\n schemaPrefix,\n schemaSuffix,\n });\n\n return [\n systemMessage,\n ...(messages[0]?.role === 'system' ? messages.slice(1) : messages),\n ];\n}\n","/**\n * Type guard that checks whether a value is not `null` or `undefined`.\n *\n * @template T - The type of the value to check.\n * @param value - The value to check.\n * @returns `true` if the value is neither `null` nor `undefined`, otherwise `false`.\n */\nexport function isNonNullable<T>(\n value: T | undefined | null,\n): value is NonNullable<T> {\n return value != null;\n}\n","/**\n * Returns true when `url` has the same origin (scheme + host + port) as\n * `baseUrl`.\n *\n * Used to decide whether provider credentials may be attached to a request to a\n * URL taken from a provider response (e.g. a polling or media-download URL).\n * Credentials must only be sent to the provider's own origin; a response that\n * names a foreign host (a CDN, or an attacker-controlled host if the response\n * is tampered with) must not receive the API key.\n *\n * Returns false if either value is not a valid absolute URL (fail-closed).\n */\nexport function isSameOrigin(url: string, baseUrl: string): boolean {\n try {\n return new URL(url).origin === new URL(baseUrl).origin;\n } catch {\n return false;\n }\n}\n","/**\n * Checks if the given URL is supported natively by the model.\n *\n * @param mediaType - The media type of the URL. Case-sensitive.\n * @param url - The URL to check.\n * @param supportedUrls - A record where keys are case-sensitive media types (or '*')\n * and values are arrays of RegExp patterns for URLs.\n *\n * @returns `true` if the URL matches a pattern under the specific media type\n * or the wildcard '*', `false` otherwise.\n */\nexport function isUrlSupported({\n mediaType,\n url,\n supportedUrls,\n}: {\n mediaType: string;\n url: string;\n supportedUrls: Record<string, RegExp[]>;\n}): boolean {\n // standardize media type and url to lower case\n url = url.toLowerCase();\n mediaType = mediaType.toLowerCase();\n\n return (\n Object.entries(supportedUrls)\n // standardize supported url map into lowercase prefixes:\n .map(([key, value]) => {\n const mediaType = key.toLowerCase();\n return mediaType === '*' || mediaType === '*/*'\n ? { mediaTypePrefix: '', regexes: value }\n : { mediaTypePrefix: mediaType.replace(/\\*/, ''), regexes: value };\n })\n // gather all regexp pattern from matched media type prefixes:\n .filter(({ mediaTypePrefix }) => mediaType.startsWith(mediaTypePrefix))\n .flatMap(({ regexes }) => regexes)\n // check if any pattern matches the url:\n .some(pattern => pattern.test(url))\n );\n}\n","import { LoadAPIKeyError } from '@ai-sdk/provider';\n\nexport function loadApiKey({\n apiKey,\n environmentVariableName,\n apiKeyParameterName = 'apiKey',\n description,\n}: {\n apiKey: string | undefined;\n environmentVariableName: string;\n apiKeyParameterName?: string;\n description: string;\n}): string {\n if (typeof apiKey === 'string') {\n return apiKey;\n }\n\n if (apiKey != null) {\n throw new LoadAPIKeyError({\n message: `${description} API key must be a string.`,\n });\n }\n\n if (typeof process === 'undefined') {\n throw new LoadAPIKeyError({\n message: `${description} API key is missing. Pass it using the '${apiKeyParameterName}' parameter. Environment variables are not supported in this environment.`,\n });\n }\n\n apiKey = process.env[environmentVariableName];\n\n if (apiKey == null) {\n throw new LoadAPIKeyError({\n message: `${description} API key is missing. Pass it using the '${apiKeyParameterName}' parameter or the ${environmentVariableName} environment variable.`,\n });\n }\n\n if (typeof apiKey !== 'string') {\n throw new LoadAPIKeyError({\n message: `${description} API key must be a string. The value of the ${environmentVariableName} environment variable is not a string.`,\n });\n }\n\n return apiKey;\n}\n","/**\n * Loads an optional `string` setting from the environment or a parameter.\n *\n * @param settingValue - The setting value.\n * @param environmentVariableName - The environment variable name.\n * @returns The setting value.\n */\nexport function loadOptionalSetting({\n settingValue,\n environmentVariableName,\n}: {\n settingValue: string | undefined;\n environmentVariableName: string;\n}): string | undefined {\n if (typeof settingValue === 'string') {\n return settingValue;\n }\n\n if (settingValue != null || typeof process === 'undefined') {\n return undefined;\n }\n\n settingValue = process.env[environmentVariableName];\n\n if (settingValue == null || typeof settingValue !== 'string') {\n return undefined;\n }\n\n return settingValue;\n}\n","import { LoadSettingError } from '@ai-sdk/provider';\n\n/**\n * Loads a `string` setting from the environment or a parameter.\n *\n * @param settingValue - The setting value.\n * @param environmentVariableName - The environment variable name.\n * @param settingName - The setting name.\n * @param description - The description of the setting.\n * @returns The setting value.\n */\nexport function loadSetting({\n settingValue,\n environmentVariableName,\n settingName,\n description,\n}: {\n settingValue: string | undefined;\n environmentVariableName: string;\n settingName: string;\n description: string;\n}): string {\n if (typeof settingValue === 'string') {\n return settingValue;\n }\n\n if (settingValue != null) {\n throw new LoadSettingError({\n message: `${description} setting must be a string.`,\n });\