@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,282 @@
1
+ import { isUrlSupported } from './is-url-supported';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ describe('isUrlSupported', () => {
5
+ describe('when the model does not support any URLs', () => {
6
+ it('should return false', async () => {
7
+ expect(
8
+ isUrlSupported({
9
+ mediaType: 'text/plain',
10
+ url: 'https://example.com',
11
+ supportedUrls: {},
12
+ }),
13
+ ).toBe(false);
14
+ });
15
+ });
16
+
17
+ describe('when the model supports specific media types and URLs', () => {
18
+ it('should return true for exact media type and exact URL match', async () => {
19
+ expect(
20
+ isUrlSupported({
21
+ mediaType: 'text/plain',
22
+ url: 'https://example.com',
23
+ supportedUrls: { 'text/plain': [/https:\/\/example\.com/] },
24
+ }),
25
+ ).toBe(true);
26
+ });
27
+
28
+ it('should return true for exact media type and regex URL match', async () => {
29
+ expect(
30
+ isUrlSupported({
31
+ mediaType: 'image/png',
32
+ url: 'https://images.example.com/cat.png',
33
+ supportedUrls: {
34
+ 'image/png': [/https:\/\/images\.example\.com\/.+/],
35
+ },
36
+ }),
37
+ ).toBe(true);
38
+ });
39
+
40
+ it('should return true for exact media type and one of multiple regex URLs match', async () => {
41
+ expect(
42
+ isUrlSupported({
43
+ mediaType: 'image/png',
44
+ url: 'https://another.com/img.png',
45
+ supportedUrls: {
46
+ 'image/png': [
47
+ /https:\/\/images\.example\.com\/.+/,
48
+ /https:\/\/another\.com\/img\.png/,
49
+ ],
50
+ },
51
+ }),
52
+ ).toBe(true);
53
+ });
54
+
55
+ it('should return false for exact media type but URL mismatch', async () => {
56
+ expect(
57
+ isUrlSupported({
58
+ mediaType: 'text/plain',
59
+ url: 'https://another.com',
60
+ supportedUrls: { 'text/plain': [/https:\/\/example\.com/] },
61
+ }),
62
+ ).toBe(false);
63
+ });
64
+
65
+ it('should return false for URL match but media type mismatch', async () => {
66
+ expect(
67
+ isUrlSupported({
68
+ mediaType: 'image/png', // Different media type
69
+ url: 'https://example.com',
70
+ supportedUrls: { 'text/plain': [/https:\/\/example\.com/] },
71
+ }),
72
+ ).toBe(false);
73
+ });
74
+ });
75
+
76
+ describe('when the model supports URLs via wildcard media type (*)', () => {
77
+ it('should return true for wildcard media type and exact URL match', async () => {
78
+ expect(
79
+ isUrlSupported({
80
+ mediaType: 'text/plain',
81
+ url: 'https://example.com',
82
+ supportedUrls: { '*': [/https:\/\/example\.com/] },
83
+ }),
84
+ ).toBe(true);
85
+ });
86
+
87
+ it('should return true for wildcard media type and regex URL match', async () => {
88
+ expect(
89
+ isUrlSupported({
90
+ mediaType: 'image/jpeg',
91
+ url: 'https://images.example.com/dog.jpg',
92
+ supportedUrls: { '*': [/https:\/\/images\.example\.com\/.+/] },
93
+ }),
94
+ ).toBe(true);
95
+ });
96
+
97
+ it('should return false for wildcard media type but URL mismatch', async () => {
98
+ expect(
99
+ isUrlSupported({
100
+ mediaType: 'video/mp4',
101
+ url: 'https://another.com',
102
+ supportedUrls: { '*': [/https:\/\/example\.com/] },
103
+ }),
104
+ ).toBe(false);
105
+ });
106
+ });
107
+
108
+ describe('when both specific and wildcard media types are defined', () => {
109
+ const supportedUrls = {
110
+ 'text/plain': [/https:\/\/text\.com/],
111
+ '*': [/https:\/\/any\.com/],
112
+ };
113
+
114
+ it('should return true if URL matches under specific media type', async () => {
115
+ expect(
116
+ isUrlSupported({
117
+ mediaType: 'text/plain',
118
+ url: 'https://text.com',
119
+ supportedUrls,
120
+ }),
121
+ ).toBe(true);
122
+ });
123
+
124
+ it('should return true if URL matches under wildcard media type even if specific exists', async () => {
125
+ // Assumes the logic checks specific first, then falls back to wildcard
126
+ expect(
127
+ isUrlSupported({
128
+ mediaType: 'text/plain', // Specific type exists
129
+ url: 'https://any.com', // Matches wildcard
130
+ supportedUrls,
131
+ }),
132
+ ).toBe(true);
133
+ });
134
+
135
+ it('should return true if URL matches under wildcard for a non-specified media type', async () => {
136
+ expect(
137
+ isUrlSupported({
138
+ mediaType: 'image/png', // No specific entry for this type
139
+ url: 'https://any.com', // Matches wildcard
140
+ supportedUrls,
141
+ }),
142
+ ).toBe(true);
143
+ });
144
+
145
+ it('should return false if URL matches neither specific nor wildcard', async () => {
146
+ expect(
147
+ isUrlSupported({
148
+ mediaType: 'text/plain',
149
+ url: 'https://other.com',
150
+ supportedUrls,
151
+ }),
152
+ ).toBe(false);
153
+ });
154
+
155
+ it('should return false if URL does not match wildcard for a non-specified media type', async () => {
156
+ expect(
157
+ isUrlSupported({
158
+ mediaType: 'image/png',
159
+ url: 'https://other.com',
160
+ supportedUrls,
161
+ }),
162
+ ).toBe(false);
163
+ });
164
+ });
165
+
166
+ describe('edge cases', () => {
167
+ it('should return true if an empty URL matches a pattern', async () => {
168
+ expect(
169
+ isUrlSupported({
170
+ mediaType: 'text/plain',
171
+ url: '',
172
+ supportedUrls: { 'text/plain': [/.*/] }, // Matches any string, including empty
173
+ }),
174
+ ).toBe(true);
175
+ });
176
+
177
+ it('should return false if an empty URL does not match a pattern', async () => {
178
+ expect(
179
+ isUrlSupported({
180
+ mediaType: 'text/plain',
181
+ url: '',
182
+ supportedUrls: { 'text/plain': [/https:\/\/.+/] }, // Requires non-empty string
183
+ }),
184
+ ).toBe(false);
185
+ });
186
+ });
187
+
188
+ describe('case sensitivity', () => {
189
+ it('should be case-insensitive for media types', async () => {
190
+ expect(
191
+ isUrlSupported({
192
+ mediaType: 'TEXT/PLAIN', // Uppercase
193
+ url: 'https://example.com',
194
+ supportedUrls: { 'text/plain': [/https:\/\/example\.com/] }, // Lowercase
195
+ }),
196
+ ).toBe(true);
197
+ });
198
+
199
+ it('should handle case-insensitive regex for URLs if specified', async () => {
200
+ expect(
201
+ isUrlSupported({
202
+ mediaType: 'text/plain',
203
+ url: 'https://EXAMPLE.com/path', // Uppercase domain
204
+ supportedUrls: { 'text/plain': [/https:\/\/example\.com\/path/] },
205
+ }),
206
+ ).toBe(true);
207
+ });
208
+
209
+ it('should be case-insensitive for URL paths by default regex', async () => {
210
+ expect(
211
+ isUrlSupported({
212
+ mediaType: 'text/plain',
213
+ url: 'https://example.com/PATH', // Uppercase path
214
+ supportedUrls: { 'text/plain': [/https:\/\/example\.com\/path/] }, // Lowercase path in regex
215
+ }),
216
+ ).toBe(true);
217
+ });
218
+ });
219
+
220
+ describe('wildcard subtypes in media types', () => {
221
+ it('should return true for wildcard subtype match', async () => {
222
+ expect(
223
+ isUrlSupported({
224
+ mediaType: 'image/png',
225
+ url: 'https://example.com',
226
+ supportedUrls: { 'image/*': [/https:\/\/example\.com/] },
227
+ }),
228
+ ).toBe(true);
229
+ });
230
+
231
+ it('should use full wildcard "*" if subtype wildcard is not matched or supported', async () => {
232
+ expect(
233
+ isUrlSupported({
234
+ mediaType: 'image/png',
235
+ url: 'https://any.com',
236
+ supportedUrls: {
237
+ 'image/*': [/https:\/\/images\.com/], // Doesn't match URL
238
+ '*': [/https:\/\/any\.com/], // Matches URL
239
+ },
240
+ }),
241
+ ).toBe(true);
242
+ });
243
+ });
244
+
245
+ describe('empty URL arrays for a media type', () => {
246
+ it('should return false if the specific media type has an empty URL array', async () => {
247
+ expect(
248
+ isUrlSupported({
249
+ mediaType: 'text/plain',
250
+ url: 'https://example.com',
251
+ supportedUrls: { 'text/plain': [] },
252
+ }),
253
+ ).toBe(false);
254
+ });
255
+
256
+ it('should fall back to wildcard "*" if specific media type has empty array but wildcard matches', async () => {
257
+ expect(
258
+ isUrlSupported({
259
+ mediaType: 'text/plain',
260
+ url: 'https://any.com',
261
+ supportedUrls: {
262
+ 'text/plain': [],
263
+ '*': [/https:\/\/any\.com/],
264
+ },
265
+ }),
266
+ ).toBe(true);
267
+ });
268
+
269
+ it('should return false if specific media type has empty array and wildcard does not match', async () => {
270
+ expect(
271
+ isUrlSupported({
272
+ mediaType: 'text/plain',
273
+ url: 'https://another.com',
274
+ supportedUrls: {
275
+ 'text/plain': [],
276
+ '*': [/https:\/\/any\.com/],
277
+ },
278
+ }),
279
+ ).toBe(false);
280
+ });
281
+ });
282
+ });
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Checks if the given URL is supported natively by the model.
3
+ *
4
+ * @param mediaType - The media type of the URL. Case-sensitive.
5
+ * @param url - The URL to check.
6
+ * @param supportedUrls - A record where keys are case-sensitive media types (or '*')
7
+ * and values are arrays of RegExp patterns for URLs.
8
+ *
9
+ * @returns `true` if the URL matches a pattern under the specific media type
10
+ * or the wildcard '*', `false` otherwise.
11
+ */
12
+ export function isUrlSupported({
13
+ mediaType,
14
+ url,
15
+ supportedUrls,
16
+ }: {
17
+ mediaType: string;
18
+ url: string;
19
+ supportedUrls: Record<string, RegExp[]>;
20
+ }): boolean {
21
+ // standardize media type and url to lower case
22
+ url = url.toLowerCase();
23
+ mediaType = mediaType.toLowerCase();
24
+
25
+ return (
26
+ Object.entries(supportedUrls)
27
+ // standardize supported url map into lowercase prefixes:
28
+ .map(([key, value]) => {
29
+ const mediaType = key.toLowerCase();
30
+ return mediaType === '*' || mediaType === '*/*'
31
+ ? { mediaTypePrefix: '', regexes: value }
32
+ : { mediaTypePrefix: mediaType.replace(/\*/, ''), regexes: value };
33
+ })
34
+ // gather all regexp pattern from matched media type prefixes:
35
+ .filter(({ mediaTypePrefix }) => mediaType.startsWith(mediaTypePrefix))
36
+ .flatMap(({ regexes }) => regexes)
37
+ // check if any pattern matches the url:
38
+ .some(pattern => pattern.test(url))
39
+ );
40
+ }
@@ -0,0 +1,45 @@
1
+ import { LoadAPIKeyError } from '@ai-sdk/provider';
2
+
3
+ export function loadApiKey({
4
+ apiKey,
5
+ environmentVariableName,
6
+ apiKeyParameterName = 'apiKey',
7
+ description,
8
+ }: {
9
+ apiKey: string | undefined;
10
+ environmentVariableName: string;
11
+ apiKeyParameterName?: string;
12
+ description: string;
13
+ }): string {
14
+ if (typeof apiKey === 'string') {
15
+ return apiKey;
16
+ }
17
+
18
+ if (apiKey != null) {
19
+ throw new LoadAPIKeyError({
20
+ message: `${description} API key must be a string.`,
21
+ });
22
+ }
23
+
24
+ if (typeof process === 'undefined') {
25
+ throw new LoadAPIKeyError({
26
+ message: `${description} API key is missing. Pass it using the '${apiKeyParameterName}' parameter. Environment variables is not supported in this environment.`,
27
+ });
28
+ }
29
+
30
+ apiKey = process.env[environmentVariableName];
31
+
32
+ if (apiKey == null) {
33
+ throw new LoadAPIKeyError({
34
+ message: `${description} API key is missing. Pass it using the '${apiKeyParameterName}' parameter or the ${environmentVariableName} environment variable.`,
35
+ });
36
+ }
37
+
38
+ if (typeof apiKey !== 'string') {
39
+ throw new LoadAPIKeyError({
40
+ message: `${description} API key must be a string. The value of the ${environmentVariableName} environment variable is not a string.`,
41
+ });
42
+ }
43
+
44
+ return apiKey;
45
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Loads an optional `string` setting from the environment or a parameter.
3
+ *
4
+ * @param settingValue - The setting value.
5
+ * @param environmentVariableName - The environment variable name.
6
+ * @returns The setting value.
7
+ */
8
+ export function loadOptionalSetting({
9
+ settingValue,
10
+ environmentVariableName,
11
+ }: {
12
+ settingValue: string | undefined;
13
+ environmentVariableName: string;
14
+ }): string | undefined {
15
+ if (typeof settingValue === 'string') {
16
+ return settingValue;
17
+ }
18
+
19
+ if (settingValue != null || typeof process === 'undefined') {
20
+ return undefined;
21
+ }
22
+
23
+ settingValue = process.env[environmentVariableName];
24
+
25
+ if (settingValue == null || typeof settingValue !== 'string') {
26
+ return undefined;
27
+ }
28
+
29
+ return settingValue;
30
+ }
@@ -0,0 +1,62 @@
1
+ import { LoadSettingError } from '@ai-sdk/provider';
2
+
3
+ /**
4
+ * Loads a `string` setting from the environment or a parameter.
5
+ *
6
+ * @param settingValue - The setting value.
7
+ * @param environmentVariableName - The environment variable name.
8
+ * @param settingName - The setting name.
9
+ * @param description - The description of the setting.
10
+ * @returns The setting value.
11
+ */
12
+ export function loadSetting({
13
+ settingValue,
14
+ environmentVariableName,
15
+ settingName,
16
+ description,
17
+ }: {
18
+ settingValue: string | undefined;
19
+ environmentVariableName: string;
20
+ settingName: string;
21
+ description: string;
22
+ }): string {
23
+ if (typeof settingValue === 'string') {
24
+ return settingValue;
25
+ }
26
+
27
+ if (settingValue != null) {
28
+ throw new LoadSettingError({
29
+ message: `${description} setting must be a string.`,
30
+ });
31
+ }
32
+
33
+ if (typeof process === 'undefined') {
34
+ throw new LoadSettingError({
35
+ message:
36
+ `${description} setting is missing. ` +
37
+ `Pass it using the '${settingName}' parameter. ` +
38
+ `Environment variables is not supported in this environment.`,
39
+ });
40
+ }
41
+
42
+ settingValue = process.env[environmentVariableName];
43
+
44
+ if (settingValue == null) {
45
+ throw new LoadSettingError({
46
+ message:
47
+ `${description} setting is missing. ` +
48
+ `Pass it using the '${settingName}' parameter ` +
49
+ `or the ${environmentVariableName} environment variable.`,
50
+ });
51
+ }
52
+
53
+ if (typeof settingValue !== 'string') {
54
+ throw new LoadSettingError({
55
+ message:
56
+ `${description} setting must be a string. ` +
57
+ `The value of the ${environmentVariableName} environment variable is not a string.`,
58
+ });
59
+ }
60
+
61
+ return settingValue;
62
+ }
@@ -0,0 +1,3 @@
1
+ export type MaybePromiseLike<T> =
2
+ | T // Raw value
3
+ | PromiseLike<T>; // Promise of value
@@ -0,0 +1,26 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mediaTypeToExtension } from './media-type-to-extension';
3
+
4
+ describe('mediaTypeToExtension()', () => {
5
+ it.each([
6
+ // most common
7
+ ['audio/mpeg', 'mp3'],
8
+ ['audio/mp3', 'mp3'],
9
+ ['audio/wav', 'wav'],
10
+ ['audio/x-wav', 'wav'],
11
+ ['audio/webm', 'webm'],
12
+ ['audio/ogg', 'ogg'],
13
+ ['audio/opus', 'ogg'],
14
+ ['audio/mp4', 'm4a'],
15
+ ['audio/x-m4a', 'm4a'],
16
+ ['audio/flac', 'flac'],
17
+ ['audio/aac', 'aac'],
18
+ // upper case
19
+ ['AUDIO/MPEG', 'mp3'],
20
+ ['AUDIO/MP3', 'mp3'],
21
+ // invalid
22
+ ['nope', ''],
23
+ ])('should map %s to %s', (mediaType, expectedExtension) => {
24
+ expect(mediaTypeToExtension(mediaType)).toBe(expectedExtension);
25
+ });
26
+ });
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Maps a media type to its corresponding file extension.
3
+ * It was originally introduced to set a filename for audio file uploads
4
+ * in https://github.com/vercel/ai/pull/8159.
5
+ *
6
+ * @param mediaType The media type to map.
7
+ * @returns The corresponding file extension
8
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types/Common_types
9
+ */
10
+ export function mediaTypeToExtension(mediaType: string) {
11
+ const [_type, subtype = ''] = mediaType.toLowerCase().split('/');
12
+
13
+ return (
14
+ {
15
+ mpeg: 'mp3',
16
+ 'x-wav': 'wav',
17
+ opus: 'ogg',
18
+ mp4: 'm4a',
19
+ 'x-m4a': 'm4a',
20
+ }[subtype] ?? subtype
21
+ );
22
+ }
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { normalizeHeaders } from './normalize-headers';
4
+
5
+ describe('normalizeHeaders', () => {
6
+ it('returns empty object for undefined', () => {
7
+ expect(normalizeHeaders(undefined)).toEqual({});
8
+ });
9
+
10
+ it('converts Headers instance to record', () => {
11
+ const headers = new Headers({
12
+ Authorization: 'Bearer token',
13
+ 'X-Feature': 'beta',
14
+ });
15
+
16
+ expect(normalizeHeaders(headers)).toEqual({
17
+ authorization: 'Bearer token',
18
+ 'x-feature': 'beta',
19
+ });
20
+ });
21
+
22
+ it('converts tuple array', () => {
23
+ const headers: HeadersInit = [
24
+ ['Authorization', 'Bearer token'],
25
+ ['X-Feature', 'beta'],
26
+ ['X-Ignore', undefined as unknown as string],
27
+ ];
28
+
29
+ expect(normalizeHeaders(headers)).toEqual({
30
+ authorization: 'Bearer token',
31
+ 'x-feature': 'beta',
32
+ });
33
+ });
34
+
35
+ it('converts plain record and filters nullish values', () => {
36
+ expect(
37
+ normalizeHeaders({
38
+ Authorization: 'Bearer token',
39
+ 'X-Feature': undefined,
40
+ 'Content-Type': 'application/json',
41
+ }),
42
+ ).toEqual({
43
+ authorization: 'Bearer token',
44
+ 'content-type': 'application/json',
45
+ });
46
+ });
47
+
48
+ it('handles empty Headers instance', () => {
49
+ const headers = new Headers();
50
+ expect(normalizeHeaders(headers)).toEqual({});
51
+ });
52
+
53
+ it('converts uppercase keys to lowercase', () => {
54
+ expect(
55
+ normalizeHeaders({
56
+ 'CONTENT-TYPE': 'application/json',
57
+ 'X-CUSTOM-HEADER': 'test-value',
58
+ }),
59
+ ).toEqual({
60
+ 'content-type': 'application/json',
61
+ 'x-custom-header': 'test-value',
62
+ });
63
+ });
64
+ });
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Normalizes different header inputs into a plain record with lower-case keys.
3
+ * Entries with `undefined` or `null` values are removed.
4
+ *
5
+ * @param headers - Input headers (`Headers`, tuples array, plain record) to normalize.
6
+ * @returns A record containing the normalized header entries.
7
+ */
8
+ export function normalizeHeaders(
9
+ headers:
10
+ | HeadersInit
11
+ | Record<string, string | undefined>
12
+ | Array<[string, string | undefined]>
13
+ | undefined,
14
+ ): Record<string, string> {
15
+ if (headers == null) {
16
+ return {};
17
+ }
18
+
19
+ const normalized: Record<string, string> = {};
20
+
21
+ if (headers instanceof Headers) {
22
+ headers.forEach((value, key) => {
23
+ normalized[key.toLowerCase()] = value;
24
+ });
25
+ } else {
26
+ if (!Array.isArray(headers)) {
27
+ headers = Object.entries(headers);
28
+ }
29
+
30
+ for (const [key, value] of headers) {
31
+ if (value != null) {
32
+ normalized[key.toLowerCase()] = value;
33
+ }
34
+ }
35
+ }
36
+
37
+ return normalized;
38
+ }
@@ -0,0 +1,33 @@
1
+ import {
2
+ EventSourceMessage,
3
+ EventSourceParserStream,
4
+ } from 'eventsource-parser/stream';
5
+ import { ParseResult, safeParseJSON } from './parse-json';
6
+ import { FlexibleSchema } from './schema';
7
+
8
+ /**
9
+ * Parses a JSON event stream into a stream of parsed JSON objects.
10
+ */
11
+ export function parseJsonEventStream<T>({
12
+ stream,
13
+ schema,
14
+ }: {
15
+ stream: ReadableStream<Uint8Array>;
16
+ schema: FlexibleSchema<T>;
17
+ }): ReadableStream<ParseResult<T>> {
18
+ return stream
19
+ .pipeThrough(new TextDecoderStream())
20
+ .pipeThrough(new EventSourceParserStream())
21
+ .pipeThrough(
22
+ new TransformStream<EventSourceMessage, ParseResult<T>>({
23
+ async transform({ data }, controller) {
24
+ // ignore the 'DONE' event that e.g. OpenAI sends:
25
+ if (data === '[DONE]') {
26
+ return;
27
+ }
28
+
29
+ controller.enqueue(await safeParseJSON({ text: data, schema }));
30
+ },
31
+ }),
32
+ );
33
+ }