@ai-sdk/provider-utils 4.0.5 → 4.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.d.mts +1 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +1 -1
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +4 -2
  9. package/src/__snapshots__/schema.test.ts.snap +346 -0
  10. package/src/add-additional-properties-to-json-schema.test.ts +289 -0
  11. package/src/add-additional-properties-to-json-schema.ts +53 -0
  12. package/src/combine-headers.ts +11 -0
  13. package/src/convert-async-iterator-to-readable-stream.test.ts +78 -0
  14. package/src/convert-async-iterator-to-readable-stream.ts +47 -0
  15. package/src/convert-image-model-file-to-data-uri.test.ts +85 -0
  16. package/src/convert-image-model-file-to-data-uri.ts +19 -0
  17. package/src/convert-to-form-data.test.ts +167 -0
  18. package/src/convert-to-form-data.ts +61 -0
  19. package/src/create-tool-name-mapping.test.ts +163 -0
  20. package/src/create-tool-name-mapping.ts +66 -0
  21. package/src/delay.test.ts +212 -0
  22. package/src/delay.ts +47 -0
  23. package/src/delayed-promise.test.ts +132 -0
  24. package/src/delayed-promise.ts +61 -0
  25. package/src/download-blob.test.ts +145 -0
  26. package/src/download-blob.ts +31 -0
  27. package/src/download-error.ts +39 -0
  28. package/src/extract-response-headers.ts +9 -0
  29. package/src/fetch-function.ts +4 -0
  30. package/src/generate-id.test.ts +31 -0
  31. package/src/generate-id.ts +57 -0
  32. package/src/get-error-message.ts +15 -0
  33. package/src/get-from-api.test.ts +199 -0
  34. package/src/get-from-api.ts +97 -0
  35. package/src/get-runtime-environment-user-agent.test.ts +47 -0
  36. package/src/get-runtime-environment-user-agent.ts +24 -0
  37. package/src/handle-fetch-error.ts +39 -0
  38. package/src/index.ts +67 -0
  39. package/src/inject-json-instruction.test.ts +404 -0
  40. package/src/inject-json-instruction.ts +63 -0
  41. package/src/is-abort-error.ts +8 -0
  42. package/src/is-async-iterable.ts +3 -0
  43. package/src/is-non-nullable.ts +12 -0
  44. package/src/is-url-supported.test.ts +282 -0
  45. package/src/is-url-supported.ts +40 -0
  46. package/src/load-api-key.ts +45 -0
  47. package/src/load-optional-setting.ts +30 -0
  48. package/src/load-setting.ts +62 -0
  49. package/src/maybe-promise-like.ts +3 -0
  50. package/src/media-type-to-extension.test.ts +26 -0
  51. package/src/media-type-to-extension.ts +22 -0
  52. package/src/normalize-headers.test.ts +64 -0
  53. package/src/normalize-headers.ts +38 -0
  54. package/src/parse-json-event-stream.ts +33 -0
  55. package/src/parse-json.test.ts +191 -0
  56. package/src/parse-json.ts +122 -0
  57. package/src/parse-provider-options.ts +32 -0
  58. package/src/post-to-api.ts +166 -0
  59. package/src/provider-tool-factory.ts +125 -0
  60. package/src/remove-undefined-entries.test.ts +57 -0
  61. package/src/remove-undefined-entries.ts +12 -0
  62. package/src/resolve.test.ts +125 -0
  63. package/src/resolve.ts +17 -0
  64. package/src/response-handler.test.ts +89 -0
  65. package/src/response-handler.ts +187 -0
  66. package/src/schema.test-d.ts +11 -0
  67. package/src/schema.test.ts +502 -0
  68. package/src/schema.ts +267 -0
  69. package/src/secure-json-parse.test.ts +59 -0
  70. package/src/secure-json-parse.ts +92 -0
  71. package/src/test/convert-array-to-async-iterable.ts +9 -0
  72. package/src/test/convert-array-to-readable-stream.ts +15 -0
  73. package/src/test/convert-async-iterable-to-array.ts +9 -0
  74. package/src/test/convert-readable-stream-to-array.ts +14 -0
  75. package/src/test/convert-response-stream-to-array.ts +9 -0
  76. package/src/test/index.ts +7 -0
  77. package/src/test/is-node-version.ts +4 -0
  78. package/src/test/mock-id.ts +8 -0
  79. package/src/to-json-schema/zod3-to-json-schema/LICENSE +16 -0
  80. package/src/to-json-schema/zod3-to-json-schema/README.md +24 -0
  81. package/src/to-json-schema/zod3-to-json-schema/get-relative-path.ts +7 -0
  82. package/src/to-json-schema/zod3-to-json-schema/index.ts +1 -0
  83. package/src/to-json-schema/zod3-to-json-schema/options.ts +98 -0
  84. package/src/to-json-schema/zod3-to-json-schema/parse-def.test.ts +224 -0
  85. package/src/to-json-schema/zod3-to-json-schema/parse-def.ts +109 -0
  86. package/src/to-json-schema/zod3-to-json-schema/parse-types.ts +57 -0
  87. package/src/to-json-schema/zod3-to-json-schema/parsers/any.ts +5 -0
  88. package/src/to-json-schema/zod3-to-json-schema/parsers/array.test.ts +98 -0
  89. package/src/to-json-schema/zod3-to-json-schema/parsers/array.ts +38 -0
  90. package/src/to-json-schema/zod3-to-json-schema/parsers/bigint.test.ts +51 -0
  91. package/src/to-json-schema/zod3-to-json-schema/parsers/bigint.ts +44 -0
  92. package/src/to-json-schema/zod3-to-json-schema/parsers/boolean.ts +7 -0
  93. package/src/to-json-schema/zod3-to-json-schema/parsers/branded.test.ts +16 -0
  94. package/src/to-json-schema/zod3-to-json-schema/parsers/branded.ts +7 -0
  95. package/src/to-json-schema/zod3-to-json-schema/parsers/catch.test.ts +15 -0
  96. package/src/to-json-schema/zod3-to-json-schema/parsers/catch.ts +7 -0
  97. package/src/to-json-schema/zod3-to-json-schema/parsers/date.test.ts +97 -0
  98. package/src/to-json-schema/zod3-to-json-schema/parsers/date.ts +64 -0
  99. package/src/to-json-schema/zod3-to-json-schema/parsers/default.test.ts +54 -0
  100. package/src/to-json-schema/zod3-to-json-schema/parsers/default.ts +14 -0
  101. package/src/to-json-schema/zod3-to-json-schema/parsers/effects.test.ts +41 -0
  102. package/src/to-json-schema/zod3-to-json-schema/parsers/effects.ts +14 -0
  103. package/src/to-json-schema/zod3-to-json-schema/parsers/enum.ts +13 -0
  104. package/src/to-json-schema/zod3-to-json-schema/parsers/intersection.test.ts +92 -0
  105. package/src/to-json-schema/zod3-to-json-schema/parsers/intersection.ts +52 -0
  106. package/src/to-json-schema/zod3-to-json-schema/parsers/literal.ts +29 -0
  107. package/src/to-json-schema/zod3-to-json-schema/parsers/map.test.ts +48 -0
  108. package/src/to-json-schema/zod3-to-json-schema/parsers/map.ts +47 -0
  109. package/src/to-json-schema/zod3-to-json-schema/parsers/native-enum.test.ts +102 -0
  110. package/src/to-json-schema/zod3-to-json-schema/parsers/native-enum.ts +31 -0
  111. package/src/to-json-schema/zod3-to-json-schema/parsers/never.ts +9 -0
  112. package/src/to-json-schema/zod3-to-json-schema/parsers/null.ts +9 -0
  113. package/src/to-json-schema/zod3-to-json-schema/parsers/nullable.test.ts +67 -0
  114. package/src/to-json-schema/zod3-to-json-schema/parsers/nullable.ts +42 -0
  115. package/src/to-json-schema/zod3-to-json-schema/parsers/number.test.ts +65 -0
  116. package/src/to-json-schema/zod3-to-json-schema/parsers/number.ts +44 -0
  117. package/src/to-json-schema/zod3-to-json-schema/parsers/object.test.ts +149 -0
  118. package/src/to-json-schema/zod3-to-json-schema/parsers/object.ts +88 -0
  119. package/src/to-json-schema/zod3-to-json-schema/parsers/optional.test.ts +147 -0
  120. package/src/to-json-schema/zod3-to-json-schema/parsers/optional.ts +23 -0
  121. package/src/to-json-schema/zod3-to-json-schema/parsers/pipe.test.ts +35 -0
  122. package/src/to-json-schema/zod3-to-json-schema/parsers/pipeline.ts +29 -0
  123. package/src/to-json-schema/zod3-to-json-schema/parsers/promise.test.ts +15 -0
  124. package/src/to-json-schema/zod3-to-json-schema/parsers/promise.ts +11 -0
  125. package/src/to-json-schema/zod3-to-json-schema/parsers/readonly.test.ts +20 -0
  126. package/src/to-json-schema/zod3-to-json-schema/parsers/readonly.ts +7 -0
  127. package/src/to-json-schema/zod3-to-json-schema/parsers/record.test.ts +108 -0
  128. package/src/to-json-schema/zod3-to-json-schema/parsers/record.ts +71 -0
  129. package/src/to-json-schema/zod3-to-json-schema/parsers/set.test.ts +20 -0
  130. package/src/to-json-schema/zod3-to-json-schema/parsers/set.ts +35 -0
  131. package/src/to-json-schema/zod3-to-json-schema/parsers/string.test.ts +438 -0
  132. package/src/to-json-schema/zod3-to-json-schema/parsers/string.ts +426 -0
  133. package/src/to-json-schema/zod3-to-json-schema/parsers/tuple.test.ts +33 -0
  134. package/src/to-json-schema/zod3-to-json-schema/parsers/tuple.ts +61 -0
  135. package/src/to-json-schema/zod3-to-json-schema/parsers/undefined.ts +11 -0
  136. package/src/to-json-schema/zod3-to-json-schema/parsers/union.test.ts +226 -0
  137. package/src/to-json-schema/zod3-to-json-schema/parsers/union.ts +144 -0
  138. package/src/to-json-schema/zod3-to-json-schema/parsers/unknown.ts +7 -0
  139. package/src/to-json-schema/zod3-to-json-schema/refs.test.ts +919 -0
  140. package/src/to-json-schema/zod3-to-json-schema/refs.ts +39 -0
  141. package/src/to-json-schema/zod3-to-json-schema/select-parser.ts +115 -0
  142. package/src/to-json-schema/zod3-to-json-schema/zod3-to-json-schema.test.ts +862 -0
  143. package/src/to-json-schema/zod3-to-json-schema/zod3-to-json-schema.ts +93 -0
  144. package/src/types/assistant-model-message.ts +39 -0
  145. package/src/types/content-part.ts +379 -0
  146. package/src/types/data-content.ts +4 -0
  147. package/src/types/execute-tool.ts +27 -0
  148. package/src/types/index.ts +40 -0
  149. package/src/types/model-message.ts +14 -0
  150. package/src/types/provider-options.ts +9 -0
  151. package/src/types/system-model-message.ts +20 -0
  152. package/src/types/tool-approval-request.ts +16 -0
  153. package/src/types/tool-approval-response.ts +27 -0
  154. package/src/types/tool-call.ts +31 -0
  155. package/src/types/tool-model-message.ts +23 -0
  156. package/src/types/tool-result.ts +35 -0
  157. package/src/types/tool.test-d.ts +228 -0
  158. package/src/types/tool.ts +324 -0
  159. package/src/types/user-model-message.ts +22 -0
  160. package/src/uint8-utils.ts +26 -0
  161. package/src/validate-types.test.ts +105 -0
  162. package/src/validate-types.ts +81 -0
  163. package/src/version.ts +6 -0
  164. package/src/with-user-agent-suffix.test.ts +84 -0
  165. package/src/with-user-agent-suffix.ts +27 -0
  166. package/src/without-trailing-slash.ts +3 -0
  167. package/LICENSE +0 -13
@@ -0,0 +1,39 @@
1
+ import { AISDKError } from '@ai-sdk/provider';
2
+
3
+ const name = 'AI_DownloadError';
4
+ const marker = `vercel.ai.error.${name}`;
5
+ const symbol = Symbol.for(marker);
6
+
7
+ export class DownloadError extends AISDKError {
8
+ private readonly [symbol] = true; // used in isInstance
9
+
10
+ readonly url: string;
11
+ readonly statusCode?: number;
12
+ readonly statusText?: string;
13
+
14
+ constructor({
15
+ url,
16
+ statusCode,
17
+ statusText,
18
+ cause,
19
+ message = cause == null
20
+ ? `Failed to download ${url}: ${statusCode} ${statusText}`
21
+ : `Failed to download ${url}: ${cause}`,
22
+ }: {
23
+ url: string;
24
+ statusCode?: number;
25
+ statusText?: string;
26
+ message?: string;
27
+ cause?: unknown;
28
+ }) {
29
+ super({ name, message, cause });
30
+
31
+ this.url = url;
32
+ this.statusCode = statusCode;
33
+ this.statusText = statusText;
34
+ }
35
+
36
+ static isInstance(error: unknown): error is DownloadError {
37
+ return AISDKError.hasMarker(error, marker);
38
+ }
39
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ Extracts the headers from a response object and returns them as a key-value object.
3
+
4
+ @param response - The response object to extract headers from.
5
+ @returns The headers as a key-value object.
6
+ */
7
+ export function extractResponseHeaders(response: Response) {
8
+ return Object.fromEntries<string>([...response.headers]);
9
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Fetch function type (standardizes the version of fetch used).
3
+ */
4
+ export type FetchFunction = typeof globalThis.fetch;
@@ -0,0 +1,31 @@
1
+ import { InvalidArgumentError } from '@ai-sdk/provider';
2
+ import { expect, it } from 'vitest';
3
+ import { createIdGenerator, generateId } from './generate-id';
4
+ import { describe } from 'vitest';
5
+
6
+ describe('createIdGenerator', () => {
7
+ it('should generate an ID with the correct length', () => {
8
+ const idGenerator = createIdGenerator({ size: 10 });
9
+ expect(idGenerator()).toHaveLength(10);
10
+ });
11
+
12
+ it('should generate an ID with the correct default length', () => {
13
+ const idGenerator = createIdGenerator();
14
+ expect(idGenerator()).toHaveLength(16);
15
+ });
16
+
17
+ it('should throw an error if the separator is part of the alphabet', () => {
18
+ expect(() => createIdGenerator({ separator: 'a', prefix: 'b' })).toThrow(
19
+ InvalidArgumentError,
20
+ );
21
+ });
22
+ });
23
+
24
+ describe('generateId', () => {
25
+ it('should generate unique IDs', () => {
26
+ const id1 = generateId();
27
+ const id2 = generateId();
28
+
29
+ expect(id1).not.toBe(id2);
30
+ });
31
+ });
@@ -0,0 +1,57 @@
1
+ import { InvalidArgumentError } from '@ai-sdk/provider';
2
+
3
+ /**
4
+ Creates an ID generator.
5
+ The total length of the ID is the sum of the prefix, separator, and random part length.
6
+ Not cryptographically secure.
7
+
8
+ @param alphabet - The alphabet to use for the ID. Default: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.
9
+ @param prefix - The prefix of the ID to generate. Optional.
10
+ @param separator - The separator between the prefix and the random part of the ID. Default: '-'.
11
+ @param size - The size of the random part of the ID to generate. Default: 16.
12
+ */
13
+ export const createIdGenerator = ({
14
+ prefix,
15
+ size = 16,
16
+ alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
17
+ separator = '-',
18
+ }: {
19
+ prefix?: string;
20
+ separator?: string;
21
+ size?: number;
22
+ alphabet?: string;
23
+ } = {}): IdGenerator => {
24
+ const generator = () => {
25
+ const alphabetLength = alphabet.length;
26
+ const chars = new Array(size);
27
+ for (let i = 0; i < size; i++) {
28
+ chars[i] = alphabet[(Math.random() * alphabetLength) | 0];
29
+ }
30
+ return chars.join('');
31
+ };
32
+
33
+ if (prefix == null) {
34
+ return generator;
35
+ }
36
+
37
+ // check that the prefix is not part of the alphabet (otherwise prefix checking can fail randomly)
38
+ if (alphabet.includes(separator)) {
39
+ throw new InvalidArgumentError({
40
+ argument: 'separator',
41
+ message: `The separator "${separator}" must not be part of the alphabet "${alphabet}".`,
42
+ });
43
+ }
44
+
45
+ return () => `${prefix}${separator}${generator()}`;
46
+ };
47
+
48
+ /**
49
+ A function that generates an ID.
50
+ */
51
+ export type IdGenerator = () => string;
52
+
53
+ /**
54
+ Generates a 16-character random string to use for IDs.
55
+ Not cryptographically secure.
56
+ */
57
+ export const generateId = createIdGenerator();
@@ -0,0 +1,15 @@
1
+ export function getErrorMessage(error: unknown | undefined) {
2
+ if (error == null) {
3
+ return 'unknown error';
4
+ }
5
+
6
+ if (typeof error === 'string') {
7
+ return error;
8
+ }
9
+
10
+ if (error instanceof Error) {
11
+ return error.message;
12
+ }
13
+
14
+ return JSON.stringify(error);
15
+ }
@@ -0,0 +1,199 @@
1
+ import { APICallError } from '@ai-sdk/provider';
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import { getFromApi } from './get-from-api';
4
+ import {
5
+ createJsonResponseHandler,
6
+ createStatusCodeErrorResponseHandler,
7
+ } from './response-handler';
8
+ import { z } from 'zod/v4';
9
+ import { getRuntimeEnvironmentUserAgent } from './get-runtime-environment-user-agent';
10
+ import { withUserAgentSuffix } from './with-user-agent-suffix';
11
+
12
+ vi.mock('./get-runtime-environment-user-agent', async () => {
13
+ const actual = await vi.importActual('./get-runtime-environment-user-agent');
14
+ return {
15
+ ...actual,
16
+ getRuntimeEnvironmentUserAgent: () => 'runtime/test-env',
17
+ };
18
+ });
19
+
20
+ describe('getFromApi', () => {
21
+ const mockSuccessResponse = {
22
+ name: 'test',
23
+ value: 123,
24
+ };
25
+
26
+ const mockResponseSchema = z.object({
27
+ name: z.string(),
28
+ value: z.number(),
29
+ });
30
+
31
+ const mockHeaders = {
32
+ 'Content-Type': 'application/json',
33
+ Authorization: 'Bearer test',
34
+ 'user-agent': 'runtime/test-env',
35
+ };
36
+
37
+ it('should successfully fetch and parse data', async () => {
38
+ const mockFetch = vi.fn().mockResolvedValue(
39
+ new Response(JSON.stringify(mockSuccessResponse), {
40
+ status: 200,
41
+ headers: withUserAgentSuffix(
42
+ mockHeaders,
43
+ getRuntimeEnvironmentUserAgent(),
44
+ ),
45
+ }),
46
+ );
47
+
48
+ const result = await getFromApi({
49
+ url: 'https://api.test.com/data',
50
+ headers: { Authorization: 'Bearer test' },
51
+ successfulResponseHandler: createJsonResponseHandler(mockResponseSchema),
52
+ failedResponseHandler: createStatusCodeErrorResponseHandler(),
53
+ fetch: mockFetch,
54
+ });
55
+
56
+ expect(result.value).toEqual(mockSuccessResponse);
57
+ expect(mockFetch).toHaveBeenCalledWith(
58
+ 'https://api.test.com/data',
59
+ expect.objectContaining({
60
+ method: 'GET',
61
+ headers: {
62
+ authorization: 'Bearer test',
63
+ 'user-agent': 'ai-sdk/provider-utils/0.0.0-test runtime/test-env',
64
+ },
65
+ }),
66
+ );
67
+ });
68
+
69
+ it('should handle API errors', async () => {
70
+ const errorResponse = { error: 'Not Found' };
71
+ const mockFetch = vi.fn().mockResolvedValue(
72
+ new Response(JSON.stringify(errorResponse), {
73
+ status: 404,
74
+ statusText: 'Not Found',
75
+ headers: mockHeaders,
76
+ }),
77
+ );
78
+
79
+ await expect(
80
+ getFromApi({
81
+ url: 'https://api.test.com/data',
82
+ successfulResponseHandler:
83
+ createJsonResponseHandler(mockResponseSchema),
84
+ failedResponseHandler: createStatusCodeErrorResponseHandler(),
85
+ fetch: mockFetch,
86
+ }),
87
+ ).rejects.toThrow(APICallError);
88
+ });
89
+
90
+ it('should handle network errors', async () => {
91
+ const mockFetch = vi.fn().mockRejectedValue(
92
+ Object.assign(new TypeError('fetch failed'), {
93
+ cause: new Error('Failed to connect'),
94
+ }),
95
+ );
96
+
97
+ await expect(
98
+ getFromApi({
99
+ url: 'https://api.test.com/data',
100
+ successfulResponseHandler:
101
+ createJsonResponseHandler(mockResponseSchema),
102
+ failedResponseHandler: createStatusCodeErrorResponseHandler(),
103
+ fetch: mockFetch,
104
+ }),
105
+ ).rejects.toThrow('Cannot connect to API: Failed to connect');
106
+ });
107
+
108
+ it('should handle abort signals', async () => {
109
+ const abortController = new AbortController();
110
+ const mockFetch = vi.fn().mockImplementation(() => {
111
+ abortController.abort();
112
+ return Promise.reject(new DOMException('Aborted', 'AbortError'));
113
+ });
114
+
115
+ await expect(
116
+ getFromApi({
117
+ url: 'https://api.test.com/data',
118
+ successfulResponseHandler:
119
+ createJsonResponseHandler(mockResponseSchema),
120
+ failedResponseHandler: createStatusCodeErrorResponseHandler(),
121
+ fetch: mockFetch,
122
+ abortSignal: abortController.signal,
123
+ }),
124
+ ).rejects.toThrow('Aborted');
125
+ });
126
+
127
+ it('should remove undefined header entries', async () => {
128
+ const mockFetch = vi.fn().mockResolvedValue(
129
+ new Response(JSON.stringify(mockSuccessResponse), {
130
+ status: 200,
131
+ headers: mockHeaders,
132
+ }),
133
+ );
134
+
135
+ await getFromApi({
136
+ url: 'https://api.test.com/data',
137
+ headers: {
138
+ Authorization: 'Bearer test',
139
+ 'X-Custom-Header': undefined,
140
+ },
141
+ successfulResponseHandler: createJsonResponseHandler(mockResponseSchema),
142
+ failedResponseHandler: createStatusCodeErrorResponseHandler(),
143
+ fetch: mockFetch,
144
+ });
145
+
146
+ expect(mockFetch).toHaveBeenCalledWith(
147
+ 'https://api.test.com/data',
148
+ expect.objectContaining({
149
+ headers: {
150
+ authorization: 'Bearer test',
151
+ 'user-agent': 'ai-sdk/provider-utils/0.0.0-test runtime/test-env',
152
+ },
153
+ }),
154
+ );
155
+ });
156
+
157
+ it('should handle errors in response handlers', async () => {
158
+ const mockFetch = vi.fn().mockResolvedValue(
159
+ new Response('invalid json', {
160
+ status: 200,
161
+ headers: mockHeaders,
162
+ }),
163
+ );
164
+
165
+ await expect(
166
+ getFromApi({
167
+ url: 'https://api.test.com/data',
168
+ successfulResponseHandler:
169
+ createJsonResponseHandler(mockResponseSchema),
170
+ failedResponseHandler: createStatusCodeErrorResponseHandler(),
171
+ fetch: mockFetch,
172
+ }),
173
+ ).rejects.toThrow(APICallError);
174
+ });
175
+
176
+ it('should use default fetch when not provided', async () => {
177
+ const originalFetch = global.fetch;
178
+ const mockFetch = vi.fn().mockResolvedValue(
179
+ new Response(JSON.stringify(mockSuccessResponse), {
180
+ status: 200,
181
+ headers: mockHeaders,
182
+ }),
183
+ );
184
+ global.fetch = mockFetch;
185
+
186
+ try {
187
+ await getFromApi({
188
+ url: 'https://api.test.com/data',
189
+ successfulResponseHandler:
190
+ createJsonResponseHandler(mockResponseSchema),
191
+ failedResponseHandler: createStatusCodeErrorResponseHandler(),
192
+ });
193
+
194
+ expect(mockFetch).toHaveBeenCalled();
195
+ } finally {
196
+ global.fetch = originalFetch;
197
+ }
198
+ });
199
+ });
@@ -0,0 +1,97 @@
1
+ import { APICallError } from '@ai-sdk/provider';
2
+ import { extractResponseHeaders } from './extract-response-headers';
3
+ import { FetchFunction } from './fetch-function';
4
+ import { handleFetchError } from './handle-fetch-error';
5
+ import { isAbortError } from './is-abort-error';
6
+ import { ResponseHandler } from './response-handler';
7
+ import { getRuntimeEnvironmentUserAgent } from './get-runtime-environment-user-agent';
8
+ import { withUserAgentSuffix } from './with-user-agent-suffix';
9
+ import { VERSION } from './version';
10
+
11
+ // use function to allow for mocking in tests:
12
+ const getOriginalFetch = () => globalThis.fetch;
13
+
14
+ export const getFromApi = async <T>({
15
+ url,
16
+ headers = {},
17
+ successfulResponseHandler,
18
+ failedResponseHandler,
19
+ abortSignal,
20
+ fetch = getOriginalFetch(),
21
+ }: {
22
+ url: string;
23
+ headers?: Record<string, string | undefined>;
24
+ failedResponseHandler: ResponseHandler<Error>;
25
+ successfulResponseHandler: ResponseHandler<T>;
26
+ abortSignal?: AbortSignal;
27
+ fetch?: FetchFunction;
28
+ }) => {
29
+ try {
30
+ const response = await fetch(url, {
31
+ method: 'GET',
32
+ headers: withUserAgentSuffix(
33
+ headers,
34
+ `ai-sdk/provider-utils/${VERSION}`,
35
+ getRuntimeEnvironmentUserAgent(),
36
+ ),
37
+ signal: abortSignal,
38
+ });
39
+
40
+ const responseHeaders = extractResponseHeaders(response);
41
+
42
+ if (!response.ok) {
43
+ let errorInformation: {
44
+ value: Error;
45
+ responseHeaders?: Record<string, string> | undefined;
46
+ };
47
+
48
+ try {
49
+ errorInformation = await failedResponseHandler({
50
+ response,
51
+ url,
52
+ requestBodyValues: {},
53
+ });
54
+ } catch (error) {
55
+ if (isAbortError(error) || APICallError.isInstance(error)) {
56
+ throw error;
57
+ }
58
+
59
+ throw new APICallError({
60
+ message: 'Failed to process error response',
61
+ cause: error,
62
+ statusCode: response.status,
63
+ url,
64
+ responseHeaders,
65
+ requestBodyValues: {},
66
+ });
67
+ }
68
+
69
+ throw errorInformation.value;
70
+ }
71
+
72
+ try {
73
+ return await successfulResponseHandler({
74
+ response,
75
+ url,
76
+ requestBodyValues: {},
77
+ });
78
+ } catch (error) {
79
+ if (error instanceof Error) {
80
+ if (isAbortError(error) || APICallError.isInstance(error)) {
81
+ throw error;
82
+ }
83
+ }
84
+
85
+ throw new APICallError({
86
+ message: 'Failed to process successful response',
87
+ cause: error,
88
+ statusCode: response.status,
89
+ url,
90
+ responseHeaders,
91
+ requestBodyValues: {},
92
+ });
93
+ }
94
+ } catch (error) {
95
+ throw handleFetchError({ error, url, requestBodyValues: {} });
96
+ }
97
+ };
@@ -0,0 +1,47 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ // Stabilize provider utils version used inside UA string construction
4
+ vi.mock('./version', () => ({
5
+ VERSION: '0.0.0-test',
6
+ }));
7
+
8
+ import { getRuntimeEnvironmentUserAgent } from './get-runtime-environment-user-agent';
9
+
10
+ describe('getRuntimeEnvironmentUserAgent', () => {
11
+ it('should return the correct user agent for browsers', () => {
12
+ expect(
13
+ getRuntimeEnvironmentUserAgent({
14
+ window: true,
15
+ }),
16
+ ).toBe('runtime/browser');
17
+ });
18
+
19
+ it('should return the correct user agent for test', () => {
20
+ expect(
21
+ getRuntimeEnvironmentUserAgent({
22
+ navigator: {
23
+ userAgent: 'test',
24
+ },
25
+ }),
26
+ ).toBe('runtime/test');
27
+ });
28
+
29
+ it('should return the correct user agent for Edge Runtime', () => {
30
+ expect(
31
+ getRuntimeEnvironmentUserAgent({
32
+ EdgeRuntime: true,
33
+ }),
34
+ ).toBe('runtime/vercel-edge');
35
+ });
36
+
37
+ it('should return the correct user agent for Node.js', () => {
38
+ expect(
39
+ getRuntimeEnvironmentUserAgent({
40
+ process: {
41
+ versions: { node: 'test' },
42
+ version: 'test',
43
+ },
44
+ }),
45
+ ).toBe('runtime/node.js/test');
46
+ });
47
+ });
@@ -0,0 +1,24 @@
1
+ export function getRuntimeEnvironmentUserAgent(
2
+ globalThisAny: any = globalThis as any,
3
+ ): string {
4
+ // Browsers
5
+ if (globalThisAny.window) {
6
+ return `runtime/browser`;
7
+ }
8
+
9
+ // Cloudflare Workers / Deno / Bun / Node.js >= 21.1
10
+ if (globalThisAny.navigator?.userAgent) {
11
+ return `runtime/${globalThisAny.navigator.userAgent.toLowerCase()}`;
12
+ }
13
+
14
+ // Nodes.js < 21.1
15
+ if (globalThisAny.process?.versions?.node) {
16
+ return `runtime/node.js/${globalThisAny.process.version.substring(0)}`;
17
+ }
18
+
19
+ if (globalThisAny.EdgeRuntime) {
20
+ return `runtime/vercel-edge`;
21
+ }
22
+
23
+ return 'runtime/unknown';
24
+ }
@@ -0,0 +1,39 @@
1
+ import { APICallError } from '@ai-sdk/provider';
2
+ import { isAbortError } from './is-abort-error';
3
+
4
+ const FETCH_FAILED_ERROR_MESSAGES = ['fetch failed', 'failed to fetch'];
5
+
6
+ export function handleFetchError({
7
+ error,
8
+ url,
9
+ requestBodyValues,
10
+ }: {
11
+ error: unknown;
12
+ url: string;
13
+ requestBodyValues: unknown;
14
+ }) {
15
+ if (isAbortError(error)) {
16
+ return error;
17
+ }
18
+
19
+ // unwrap original error when fetch failed (for easier debugging):
20
+ if (
21
+ error instanceof TypeError &&
22
+ FETCH_FAILED_ERROR_MESSAGES.includes(error.message.toLowerCase())
23
+ ) {
24
+ const cause = (error as any).cause;
25
+
26
+ if (cause != null) {
27
+ // Failed to connect to server:
28
+ return new APICallError({
29
+ message: `Cannot connect to API: ${cause.message}`,
30
+ cause,
31
+ url,
32
+ requestBodyValues,
33
+ isRetryable: true, // retry when network error
34
+ });
35
+ }
36
+ }
37
+
38
+ return error;
39
+ }
package/src/index.ts ADDED
@@ -0,0 +1,67 @@
1
+ export * from './combine-headers';
2
+ export { convertAsyncIteratorToReadableStream } from './convert-async-iterator-to-readable-stream';
3
+ export {
4
+ createToolNameMapping,
5
+ type ToolNameMapping,
6
+ } from './create-tool-name-mapping';
7
+ export * from './delay';
8
+ export { DelayedPromise } from './delayed-promise';
9
+ export * from './extract-response-headers';
10
+ export { convertImageModelFileToDataUri } from './convert-image-model-file-to-data-uri';
11
+ export { convertToFormData } from './convert-to-form-data';
12
+ export { downloadBlob } from './download-blob';
13
+ export { DownloadError } from './download-error';
14
+ export * from './fetch-function';
15
+ export { createIdGenerator, generateId, type IdGenerator } from './generate-id';
16
+ export * from './get-error-message';
17
+ export * from './get-from-api';
18
+ export { getRuntimeEnvironmentUserAgent } from './get-runtime-environment-user-agent';
19
+ export { injectJsonInstructionIntoMessages } from './inject-json-instruction';
20
+ export * from './is-abort-error';
21
+ export { isNonNullable } from './is-non-nullable';
22
+ export { isUrlSupported } from './is-url-supported';
23
+ export * from './load-api-key';
24
+ export { loadOptionalSetting } from './load-optional-setting';
25
+ export { loadSetting } from './load-setting';
26
+ export { type MaybePromiseLike } from './maybe-promise-like';
27
+ export { mediaTypeToExtension } from './media-type-to-extension';
28
+ export { normalizeHeaders } from './normalize-headers';
29
+ export * from './parse-json';
30
+ export { parseJsonEventStream } from './parse-json-event-stream';
31
+ export { parseProviderOptions } from './parse-provider-options';
32
+ export * from './post-to-api';
33
+ export {
34
+ createProviderToolFactory,
35
+ createProviderToolFactoryWithOutputSchema,
36
+ type ProviderToolFactory,
37
+ type ProviderToolFactoryWithOutputSchema,
38
+ } from './provider-tool-factory';
39
+ export * from './remove-undefined-entries';
40
+ export * from './resolve';
41
+ export * from './response-handler';
42
+ export {
43
+ asSchema,
44
+ jsonSchema,
45
+ lazySchema,
46
+ zodSchema,
47
+ type FlexibleSchema,
48
+ type InferSchema,
49
+ type LazySchema,
50
+ type Schema,
51
+ type ValidationResult,
52
+ } from './schema';
53
+ export * from './uint8-utils';
54
+ export * from './validate-types';
55
+ export { VERSION } from './version';
56
+ export { withUserAgentSuffix } from './with-user-agent-suffix';
57
+ export * from './without-trailing-slash';
58
+
59
+ // folder re-exports
60
+ export * from './types';
61
+
62
+ // external re-exports
63
+ export * from '@standard-schema/spec';
64
+ export {
65
+ EventSourceParserStream,
66
+ type EventSourceMessage,
67
+ } from 'eventsource-parser/stream';