@ai-sdk/provider-utils 4.0.5 → 4.0.6
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.
- package/CHANGELOG.md +8 -0
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +4 -2
- package/src/__snapshots__/schema.test.ts.snap +346 -0
- package/src/add-additional-properties-to-json-schema.test.ts +289 -0
- package/src/add-additional-properties-to-json-schema.ts +53 -0
- package/src/combine-headers.ts +11 -0
- package/src/convert-async-iterator-to-readable-stream.test.ts +78 -0
- package/src/convert-async-iterator-to-readable-stream.ts +47 -0
- package/src/convert-image-model-file-to-data-uri.test.ts +85 -0
- package/src/convert-image-model-file-to-data-uri.ts +19 -0
- package/src/convert-to-form-data.test.ts +167 -0
- package/src/convert-to-form-data.ts +61 -0
- package/src/create-tool-name-mapping.test.ts +163 -0
- package/src/create-tool-name-mapping.ts +66 -0
- package/src/delay.test.ts +212 -0
- package/src/delay.ts +47 -0
- package/src/delayed-promise.test.ts +132 -0
- package/src/delayed-promise.ts +61 -0
- package/src/download-blob.test.ts +145 -0
- package/src/download-blob.ts +31 -0
- package/src/download-error.ts +39 -0
- package/src/extract-response-headers.ts +9 -0
- package/src/fetch-function.ts +4 -0
- package/src/generate-id.test.ts +31 -0
- package/src/generate-id.ts +57 -0
- package/src/get-error-message.ts +15 -0
- package/src/get-from-api.test.ts +199 -0
- package/src/get-from-api.ts +97 -0
- package/src/get-runtime-environment-user-agent.test.ts +47 -0
- package/src/get-runtime-environment-user-agent.ts +24 -0
- package/src/handle-fetch-error.ts +39 -0
- package/src/index.ts +67 -0
- package/src/inject-json-instruction.test.ts +404 -0
- package/src/inject-json-instruction.ts +63 -0
- package/src/is-abort-error.ts +8 -0
- package/src/is-async-iterable.ts +3 -0
- package/src/is-non-nullable.ts +12 -0
- package/src/is-url-supported.test.ts +282 -0
- package/src/is-url-supported.ts +40 -0
- package/src/load-api-key.ts +45 -0
- package/src/load-optional-setting.ts +30 -0
- package/src/load-setting.ts +62 -0
- package/src/maybe-promise-like.ts +3 -0
- package/src/media-type-to-extension.test.ts +26 -0
- package/src/media-type-to-extension.ts +22 -0
- package/src/normalize-headers.test.ts +64 -0
- package/src/normalize-headers.ts +38 -0
- package/src/parse-json-event-stream.ts +33 -0
- package/src/parse-json.test.ts +191 -0
- package/src/parse-json.ts +122 -0
- package/src/parse-provider-options.ts +32 -0
- package/src/post-to-api.ts +166 -0
- package/src/provider-tool-factory.ts +125 -0
- package/src/remove-undefined-entries.test.ts +57 -0
- package/src/remove-undefined-entries.ts +12 -0
- package/src/resolve.test.ts +125 -0
- package/src/resolve.ts +17 -0
- package/src/response-handler.test.ts +89 -0
- package/src/response-handler.ts +187 -0
- package/src/schema.test-d.ts +11 -0
- package/src/schema.test.ts +502 -0
- package/src/schema.ts +267 -0
- package/src/secure-json-parse.test.ts +59 -0
- package/src/secure-json-parse.ts +92 -0
- package/src/test/convert-array-to-async-iterable.ts +9 -0
- package/src/test/convert-array-to-readable-stream.ts +15 -0
- package/src/test/convert-async-iterable-to-array.ts +9 -0
- package/src/test/convert-readable-stream-to-array.ts +14 -0
- package/src/test/convert-response-stream-to-array.ts +9 -0
- package/src/test/index.ts +7 -0
- package/src/test/is-node-version.ts +4 -0
- package/src/test/mock-id.ts +8 -0
- package/src/to-json-schema/zod3-to-json-schema/LICENSE +16 -0
- package/src/to-json-schema/zod3-to-json-schema/README.md +24 -0
- package/src/to-json-schema/zod3-to-json-schema/get-relative-path.ts +7 -0
- package/src/to-json-schema/zod3-to-json-schema/index.ts +1 -0
- package/src/to-json-schema/zod3-to-json-schema/options.ts +98 -0
- package/src/to-json-schema/zod3-to-json-schema/parse-def.test.ts +224 -0
- package/src/to-json-schema/zod3-to-json-schema/parse-def.ts +109 -0
- package/src/to-json-schema/zod3-to-json-schema/parse-types.ts +57 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/any.ts +5 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/array.test.ts +98 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/array.ts +38 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/bigint.test.ts +51 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/bigint.ts +44 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/boolean.ts +7 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/branded.test.ts +16 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/branded.ts +7 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/catch.test.ts +15 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/catch.ts +7 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/date.test.ts +97 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/date.ts +64 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/default.test.ts +54 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/default.ts +14 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/effects.test.ts +41 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/effects.ts +14 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/enum.ts +13 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/intersection.test.ts +92 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/intersection.ts +52 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/literal.ts +29 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/map.test.ts +48 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/map.ts +47 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/native-enum.test.ts +102 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/native-enum.ts +31 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/never.ts +9 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/null.ts +9 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/nullable.test.ts +67 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/nullable.ts +42 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/number.test.ts +65 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/number.ts +44 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/object.test.ts +149 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/object.ts +88 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/optional.test.ts +147 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/optional.ts +23 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/pipe.test.ts +35 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/pipeline.ts +29 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/promise.test.ts +15 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/promise.ts +11 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/readonly.test.ts +20 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/readonly.ts +7 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/record.test.ts +108 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/record.ts +71 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/set.test.ts +20 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/set.ts +35 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/string.test.ts +438 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/string.ts +426 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/tuple.test.ts +33 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/tuple.ts +61 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/undefined.ts +11 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/union.test.ts +226 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/union.ts +144 -0
- package/src/to-json-schema/zod3-to-json-schema/parsers/unknown.ts +7 -0
- package/src/to-json-schema/zod3-to-json-schema/refs.test.ts +919 -0
- package/src/to-json-schema/zod3-to-json-schema/refs.ts +39 -0
- package/src/to-json-schema/zod3-to-json-schema/select-parser.ts +115 -0
- package/src/to-json-schema/zod3-to-json-schema/zod3-to-json-schema.test.ts +862 -0
- package/src/to-json-schema/zod3-to-json-schema/zod3-to-json-schema.ts +93 -0
- package/src/types/assistant-model-message.ts +39 -0
- package/src/types/content-part.ts +379 -0
- package/src/types/data-content.ts +4 -0
- package/src/types/execute-tool.ts +27 -0
- package/src/types/index.ts +40 -0
- package/src/types/model-message.ts +14 -0
- package/src/types/provider-options.ts +9 -0
- package/src/types/system-model-message.ts +20 -0
- package/src/types/tool-approval-request.ts +16 -0
- package/src/types/tool-approval-response.ts +27 -0
- package/src/types/tool-call.ts +31 -0
- package/src/types/tool-model-message.ts +23 -0
- package/src/types/tool-result.ts +35 -0
- package/src/types/tool.test-d.ts +193 -0
- package/src/types/tool.ts +324 -0
- package/src/types/user-model-message.ts +22 -0
- package/src/uint8-utils.ts +26 -0
- package/src/validate-types.test.ts +105 -0
- package/src/validate-types.ts +81 -0
- package/src/version.ts +6 -0
- package/src/with-user-agent-suffix.test.ts +84 -0
- package/src/with-user-agent-suffix.ts +27 -0
- package/src/without-trailing-slash.ts +3 -0
- package/LICENSE +0 -13
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { JSONSchema7, JSONSchema7Definition } from '@ai-sdk/provider';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Recursively adds additionalProperties: false to the JSON schema. This is necessary because some providers (e.g. OpenAI) do not support additionalProperties: true.
|
|
5
|
+
*/
|
|
6
|
+
export function addAdditionalPropertiesToJsonSchema(
|
|
7
|
+
jsonSchema: JSONSchema7,
|
|
8
|
+
): JSONSchema7 {
|
|
9
|
+
if (
|
|
10
|
+
jsonSchema.type === 'object' ||
|
|
11
|
+
(Array.isArray(jsonSchema.type) && jsonSchema.type.includes('object'))
|
|
12
|
+
) {
|
|
13
|
+
jsonSchema.additionalProperties = false;
|
|
14
|
+
const { properties } = jsonSchema;
|
|
15
|
+
if (properties != null) {
|
|
16
|
+
for (const key of Object.keys(properties)) {
|
|
17
|
+
properties[key] = visit(properties[key]);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (jsonSchema.items != null) {
|
|
23
|
+
jsonSchema.items = Array.isArray(jsonSchema.items)
|
|
24
|
+
? jsonSchema.items.map(visit)
|
|
25
|
+
: visit(jsonSchema.items);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (jsonSchema.anyOf != null) {
|
|
29
|
+
jsonSchema.anyOf = jsonSchema.anyOf.map(visit);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (jsonSchema.allOf != null) {
|
|
33
|
+
jsonSchema.allOf = jsonSchema.allOf.map(visit);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (jsonSchema.oneOf != null) {
|
|
37
|
+
jsonSchema.oneOf = jsonSchema.oneOf.map(visit);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { definitions } = jsonSchema;
|
|
41
|
+
if (definitions != null) {
|
|
42
|
+
for (const key of Object.keys(definitions)) {
|
|
43
|
+
definitions[key] = visit(definitions[key]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return jsonSchema;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function visit(def: JSONSchema7Definition): JSONSchema7Definition {
|
|
51
|
+
if (typeof def === 'boolean') return def;
|
|
52
|
+
return addAdditionalPropertiesToJsonSchema(def);
|
|
53
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function combineHeaders(
|
|
2
|
+
...headers: Array<Record<string, string | undefined> | undefined>
|
|
3
|
+
): Record<string, string | undefined> {
|
|
4
|
+
return headers.reduce(
|
|
5
|
+
(combinedHeaders, currentHeaders) => ({
|
|
6
|
+
...combinedHeaders,
|
|
7
|
+
...(currentHeaders ?? {}),
|
|
8
|
+
}),
|
|
9
|
+
{},
|
|
10
|
+
) as Record<string, string | undefined>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { convertAsyncIteratorToReadableStream } from './convert-async-iterator-to-readable-stream';
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
|
|
4
|
+
async function* makeGenerator(onFinally: () => void) {
|
|
5
|
+
try {
|
|
6
|
+
let i = 0;
|
|
7
|
+
while (true) {
|
|
8
|
+
await new Promise(r => setTimeout(r, 0));
|
|
9
|
+
yield i++;
|
|
10
|
+
}
|
|
11
|
+
} finally {
|
|
12
|
+
onFinally();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('convertAsyncIteratorToReadableStream', () => {
|
|
17
|
+
it('calls iterator.return() on cancel and triggers finally', async () => {
|
|
18
|
+
let finallyCalled = false;
|
|
19
|
+
const it = makeGenerator(() => {
|
|
20
|
+
finallyCalled = true;
|
|
21
|
+
});
|
|
22
|
+
const stream = convertAsyncIteratorToReadableStream(it);
|
|
23
|
+
const reader = stream.getReader();
|
|
24
|
+
|
|
25
|
+
await reader.read();
|
|
26
|
+
|
|
27
|
+
await reader.cancel('stop');
|
|
28
|
+
|
|
29
|
+
// give microtasks a tick for finally to run
|
|
30
|
+
await new Promise(r => setTimeout(r, 0));
|
|
31
|
+
|
|
32
|
+
expect(finallyCalled).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('does not enqueue further values after cancel', async () => {
|
|
36
|
+
const it = makeGenerator(() => {});
|
|
37
|
+
const stream = convertAsyncIteratorToReadableStream(it);
|
|
38
|
+
const reader = stream.getReader();
|
|
39
|
+
|
|
40
|
+
await reader.read();
|
|
41
|
+
await reader.cancel('stop');
|
|
42
|
+
|
|
43
|
+
const { done, value } = await reader.read();
|
|
44
|
+
expect(done).toBe(true);
|
|
45
|
+
expect(value).toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('works with iterator without return() method', async () => {
|
|
49
|
+
const it: AsyncIterator<number> = {
|
|
50
|
+
async next() {
|
|
51
|
+
return { value: 42, done: false };
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
const stream = convertAsyncIteratorToReadableStream(it);
|
|
55
|
+
const reader = stream.getReader();
|
|
56
|
+
|
|
57
|
+
const { value } = await reader.read();
|
|
58
|
+
expect(value).toBe(42);
|
|
59
|
+
|
|
60
|
+
await expect(reader.cancel()).resolves.toBeUndefined();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('ignores errors from iterator.return()', async () => {
|
|
64
|
+
const it: AsyncIterator<number> = {
|
|
65
|
+
async next() {
|
|
66
|
+
return { value: 1, done: false };
|
|
67
|
+
},
|
|
68
|
+
async return() {
|
|
69
|
+
throw new Error('return() failed');
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
const stream = convertAsyncIteratorToReadableStream(it);
|
|
73
|
+
const reader = stream.getReader();
|
|
74
|
+
|
|
75
|
+
await reader.read();
|
|
76
|
+
await expect(reader.cancel()).resolves.toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts an AsyncIterator to a ReadableStream.
|
|
3
|
+
*
|
|
4
|
+
* @template T - The type of elements produced by the AsyncIterator.
|
|
5
|
+
* @param { <T>} iterator - The AsyncIterator to convert.
|
|
6
|
+
* @returns {ReadableStream<T>} - A ReadableStream that provides the same data as the AsyncIterator.
|
|
7
|
+
*/
|
|
8
|
+
export function convertAsyncIteratorToReadableStream<T>(
|
|
9
|
+
iterator: AsyncIterator<T>,
|
|
10
|
+
): ReadableStream<T> {
|
|
11
|
+
let cancelled = false;
|
|
12
|
+
|
|
13
|
+
return new ReadableStream<T>({
|
|
14
|
+
/**
|
|
15
|
+
* Called when the consumer wants to pull more data from the stream.
|
|
16
|
+
*
|
|
17
|
+
* @param {ReadableStreamDefaultController<T>} controller - The controller to enqueue data into the stream.
|
|
18
|
+
* @returns {Promise<void>}
|
|
19
|
+
*/
|
|
20
|
+
async pull(controller) {
|
|
21
|
+
if (cancelled) return;
|
|
22
|
+
try {
|
|
23
|
+
const { value, done } = await iterator.next();
|
|
24
|
+
if (done) {
|
|
25
|
+
controller.close();
|
|
26
|
+
} else {
|
|
27
|
+
controller.enqueue(value);
|
|
28
|
+
}
|
|
29
|
+
} catch (error) {
|
|
30
|
+
controller.error(error);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
/**
|
|
34
|
+
* Called when the consumer cancels the stream.
|
|
35
|
+
*/
|
|
36
|
+
async cancel(reason?: unknown) {
|
|
37
|
+
cancelled = true;
|
|
38
|
+
if (iterator.return) {
|
|
39
|
+
try {
|
|
40
|
+
await iterator.return(reason);
|
|
41
|
+
} catch {
|
|
42
|
+
// intentionally ignore errors during cancellation
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { convertImageModelFileToDataUri } from './convert-image-model-file-to-data-uri';
|
|
3
|
+
|
|
4
|
+
describe('convertImageModelFileToDataUri()', () => {
|
|
5
|
+
describe('URL files', () => {
|
|
6
|
+
it('should return the URL as-is for URL type files', () => {
|
|
7
|
+
const result = convertImageModelFileToDataUri({
|
|
8
|
+
type: 'url',
|
|
9
|
+
url: 'https://example.com/image.png',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
expect(result).toBe('https://example.com/image.png');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should handle URLs with query parameters', () => {
|
|
16
|
+
const result = convertImageModelFileToDataUri({
|
|
17
|
+
type: 'url',
|
|
18
|
+
url: 'https://example.com/image.png?width=100&height=200',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
expect(result).toBe('https://example.com/image.png?width=100&height=200');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('base64 string files', () => {
|
|
26
|
+
it('should return a data URI for base64 string data', () => {
|
|
27
|
+
const result = convertImageModelFileToDataUri({
|
|
28
|
+
type: 'file',
|
|
29
|
+
mediaType: 'image/png',
|
|
30
|
+
data: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(result).toBe(
|
|
34
|
+
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should handle different media types', () => {
|
|
39
|
+
const result = convertImageModelFileToDataUri({
|
|
40
|
+
type: 'file',
|
|
41
|
+
mediaType: 'image/jpeg',
|
|
42
|
+
data: 'base64data',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(result).toBe('data:image/jpeg;base64,base64data');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('Uint8Array files', () => {
|
|
50
|
+
it('should convert Uint8Array to base64 and return a data URI', () => {
|
|
51
|
+
// "Hello" in bytes
|
|
52
|
+
const data = new Uint8Array([72, 101, 108, 108, 111]);
|
|
53
|
+
|
|
54
|
+
const result = convertImageModelFileToDataUri({
|
|
55
|
+
type: 'file',
|
|
56
|
+
mediaType: 'image/png',
|
|
57
|
+
data,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(result).toBe('data:image/png;base64,SGVsbG8=');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should handle empty Uint8Array', () => {
|
|
64
|
+
const result = convertImageModelFileToDataUri({
|
|
65
|
+
type: 'file',
|
|
66
|
+
mediaType: 'image/png',
|
|
67
|
+
data: new Uint8Array([]),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(result).toBe('data:image/png;base64,');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should handle different media types with Uint8Array', () => {
|
|
74
|
+
const data = new Uint8Array([72, 101, 108, 108, 111]);
|
|
75
|
+
|
|
76
|
+
const result = convertImageModelFileToDataUri({
|
|
77
|
+
type: 'file',
|
|
78
|
+
mediaType: 'image/webp',
|
|
79
|
+
data,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(result).toBe('data:image/webp;base64,SGVsbG8=');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ImageModelV3File } from '@ai-sdk/provider';
|
|
2
|
+
import { convertUint8ArrayToBase64 } from './uint8-utils';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Convert an ImageModelV3File to a URL or data URI string.
|
|
6
|
+
*
|
|
7
|
+
* If the file is a URL, it returns the URL as-is.
|
|
8
|
+
* If the file is base64 data, it returns a data URI with the base64 data.
|
|
9
|
+
* If the file is a Uint8Array, it converts it to base64 and returns a data URI.
|
|
10
|
+
*/
|
|
11
|
+
export function convertImageModelFileToDataUri(file: ImageModelV3File): string {
|
|
12
|
+
if (file.type === 'url') return file.url;
|
|
13
|
+
|
|
14
|
+
return `data:${file.mediaType};base64,${
|
|
15
|
+
typeof file.data === 'string'
|
|
16
|
+
? file.data
|
|
17
|
+
: convertUint8ArrayToBase64(file.data)
|
|
18
|
+
}`;
|
|
19
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { convertToFormData } from './convert-to-form-data';
|
|
3
|
+
|
|
4
|
+
describe('convertToFormData()', () => {
|
|
5
|
+
describe('basic values', () => {
|
|
6
|
+
it('should add string values to form data', () => {
|
|
7
|
+
const formData = convertToFormData({
|
|
8
|
+
model: 'gpt-image-1',
|
|
9
|
+
prompt: 'A cute cat',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
expect(formData.get('model')).toBe('gpt-image-1');
|
|
13
|
+
expect(formData.get('prompt')).toBe('A cute cat');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should add number values as strings', () => {
|
|
17
|
+
const formData = convertToFormData({
|
|
18
|
+
n: 2,
|
|
19
|
+
seed: 42,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(formData.get('n')).toBe('2');
|
|
23
|
+
expect(formData.get('seed')).toBe('42');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should add Blob values to form data', () => {
|
|
27
|
+
const blob = new Blob(['test'], { type: 'image/png' });
|
|
28
|
+
const formData = convertToFormData({
|
|
29
|
+
image: blob,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(formData.get('image')).toBeInstanceOf(Blob);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('null and undefined values', () => {
|
|
37
|
+
it('should skip null values', () => {
|
|
38
|
+
const formData = convertToFormData({
|
|
39
|
+
model: 'gpt-image-1',
|
|
40
|
+
mask: null,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(formData.get('model')).toBe('gpt-image-1');
|
|
44
|
+
expect(formData.has('mask')).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should skip undefined values', () => {
|
|
48
|
+
const formData = convertToFormData({
|
|
49
|
+
model: 'gpt-image-1',
|
|
50
|
+
size: undefined,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(formData.get('model')).toBe('gpt-image-1');
|
|
54
|
+
expect(formData.has('size')).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('array values', () => {
|
|
59
|
+
it('should add single-element arrays as single value without [] suffix', () => {
|
|
60
|
+
const blob = new Blob(['test'], { type: 'image/png' });
|
|
61
|
+
const formData = convertToFormData({
|
|
62
|
+
image: [blob],
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(formData.get('image')).toBeInstanceOf(Blob);
|
|
66
|
+
expect(formData.has('image[]')).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should add multi-element arrays with [] suffix', () => {
|
|
70
|
+
const blob1 = new Blob(['test1'], { type: 'image/png' });
|
|
71
|
+
const blob2 = new Blob(['test2'], { type: 'image/jpeg' });
|
|
72
|
+
const formData = convertToFormData({
|
|
73
|
+
image: [blob1, blob2],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(formData.has('image')).toBe(false);
|
|
77
|
+
const images = formData.getAll('image[]');
|
|
78
|
+
expect(images).toHaveLength(2);
|
|
79
|
+
expect(images[0]).toBeInstanceOf(Blob);
|
|
80
|
+
expect(images[1]).toBeInstanceOf(Blob);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should add multi-element arrays without [] suffix when useArrayBrackets is false', () => {
|
|
84
|
+
const blob1 = new Blob(['test1'], { type: 'image/png' });
|
|
85
|
+
const blob2 = new Blob(['test2'], { type: 'image/jpeg' });
|
|
86
|
+
const formData = convertToFormData(
|
|
87
|
+
{
|
|
88
|
+
image: [blob1, blob2],
|
|
89
|
+
},
|
|
90
|
+
{ useArrayBrackets: false },
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
expect(formData.has('image[]')).toBe(false);
|
|
94
|
+
const images = formData.getAll('image');
|
|
95
|
+
expect(images).toHaveLength(2);
|
|
96
|
+
expect(images[0]).toBeInstanceOf(Blob);
|
|
97
|
+
expect(images[1]).toBeInstanceOf(Blob);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should handle empty arrays by not adding any values', () => {
|
|
101
|
+
const formData = convertToFormData({
|
|
102
|
+
model: 'test',
|
|
103
|
+
images: [],
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(formData.get('model')).toBe('test');
|
|
107
|
+
expect(formData.has('images')).toBe(false);
|
|
108
|
+
expect(formData.has('images[]')).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should add string arrays with [] suffix', () => {
|
|
112
|
+
const formData = convertToFormData({
|
|
113
|
+
tags: ['cat', 'cute', 'animal'],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const tags = formData.getAll('tags[]');
|
|
117
|
+
expect(tags).toHaveLength(3);
|
|
118
|
+
expect(tags).toEqual(['cat', 'cute', 'animal']);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('type validation', () => {
|
|
123
|
+
it('should accept typed input objects', () => {
|
|
124
|
+
type ImageInput = {
|
|
125
|
+
model: string;
|
|
126
|
+
prompt: string;
|
|
127
|
+
n: number;
|
|
128
|
+
size: `${number}x${number}` | undefined;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const formData = convertToFormData<ImageInput>({
|
|
132
|
+
model: 'dall-e-3',
|
|
133
|
+
prompt: 'A sunset',
|
|
134
|
+
n: 1,
|
|
135
|
+
size: '1024x1024',
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(formData.get('model')).toBe('dall-e-3');
|
|
139
|
+
expect(formData.get('prompt')).toBe('A sunset');
|
|
140
|
+
expect(formData.get('n')).toBe('1');
|
|
141
|
+
expect(formData.get('size')).toBe('1024x1024');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('mixed values', () => {
|
|
146
|
+
it('should handle a complex input with various types', () => {
|
|
147
|
+
const blob = new Blob(['image data'], { type: 'image/png' });
|
|
148
|
+
const formData = convertToFormData({
|
|
149
|
+
model: 'gpt-image-1',
|
|
150
|
+
prompt: 'Edit this image',
|
|
151
|
+
image: [blob],
|
|
152
|
+
mask: null,
|
|
153
|
+
n: 1,
|
|
154
|
+
size: '1024x1024',
|
|
155
|
+
quality: 'high',
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(formData.get('model')).toBe('gpt-image-1');
|
|
159
|
+
expect(formData.get('prompt')).toBe('Edit this image');
|
|
160
|
+
expect(formData.get('image')).toBeInstanceOf(Blob);
|
|
161
|
+
expect(formData.has('mask')).toBe(false);
|
|
162
|
+
expect(formData.get('n')).toBe('1');
|
|
163
|
+
expect(formData.get('size')).toBe('1024x1024');
|
|
164
|
+
expect(formData.get('quality')).toBe('high');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts an input object to FormData for multipart/form-data requests.
|
|
3
|
+
*
|
|
4
|
+
* Handles the following cases:
|
|
5
|
+
* - `null` or `undefined` values are skipped
|
|
6
|
+
* - Arrays with a single element are appended as a single value
|
|
7
|
+
* - Arrays with multiple elements are appended with `[]` suffix (e.g., `image[]`)
|
|
8
|
+
* unless `useArrayBrackets` is set to `false`
|
|
9
|
+
* - All other values are appended directly
|
|
10
|
+
*
|
|
11
|
+
* @param input - The input object to convert. Use a generic type for type validation.
|
|
12
|
+
* @param options - Optional configuration object.
|
|
13
|
+
* @param options.useArrayBrackets - Whether to add `[]` suffix for multi-element arrays.
|
|
14
|
+
* Defaults to `true`. Set to `false` for APIs that expect repeated keys without brackets.
|
|
15
|
+
* @returns A FormData object containing the input values.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* type MyInput = {
|
|
20
|
+
* model: string;
|
|
21
|
+
* prompt: string;
|
|
22
|
+
* images: Blob[];
|
|
23
|
+
* };
|
|
24
|
+
*
|
|
25
|
+
* const formData = convertToFormData<MyInput>({
|
|
26
|
+
* model: 'gpt-image-1',
|
|
27
|
+
* prompt: 'A cat',
|
|
28
|
+
* images: [blob1, blob2],
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function convertToFormData<T extends Record<string, unknown>>(
|
|
33
|
+
input: T,
|
|
34
|
+
options: { useArrayBrackets?: boolean } = {},
|
|
35
|
+
): FormData {
|
|
36
|
+
const { useArrayBrackets = true } = options;
|
|
37
|
+
const formData = new FormData();
|
|
38
|
+
|
|
39
|
+
for (const [key, value] of Object.entries(input)) {
|
|
40
|
+
if (value == null) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (Array.isArray(value)) {
|
|
45
|
+
if (value.length === 1) {
|
|
46
|
+
formData.append(key, value[0] as string | Blob);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const arrayKey = useArrayBrackets ? `${key}[]` : key;
|
|
51
|
+
for (const item of value) {
|
|
52
|
+
formData.append(arrayKey, item as string | Blob);
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
formData.append(key, value as string | Blob);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return formData;
|
|
61
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LanguageModelV3FunctionTool,
|
|
3
|
+
LanguageModelV3ProviderTool,
|
|
4
|
+
} from '@ai-sdk/provider';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import { createToolNameMapping } from './create-tool-name-mapping';
|
|
7
|
+
|
|
8
|
+
describe('createToolNameMapping', () => {
|
|
9
|
+
it('should create mappings for provider-defined tools', () => {
|
|
10
|
+
const tools: Array<
|
|
11
|
+
LanguageModelV3FunctionTool | LanguageModelV3ProviderTool
|
|
12
|
+
> = [
|
|
13
|
+
{
|
|
14
|
+
type: 'provider',
|
|
15
|
+
id: 'anthropic.computer-use',
|
|
16
|
+
name: 'custom-computer-tool',
|
|
17
|
+
args: {},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
type: 'provider',
|
|
21
|
+
id: 'openai.code-interpreter',
|
|
22
|
+
name: 'custom-code-tool',
|
|
23
|
+
args: {},
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const providerToolNames: Record<`${string}.${string}`, string> = {
|
|
28
|
+
'anthropic.computer-use': 'computer_use',
|
|
29
|
+
'openai.code-interpreter': 'code_interpreter',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const mapping = createToolNameMapping({ tools, providerToolNames });
|
|
33
|
+
|
|
34
|
+
expect(mapping.toProviderToolName('custom-computer-tool')).toBe(
|
|
35
|
+
'computer_use',
|
|
36
|
+
);
|
|
37
|
+
expect(mapping.toProviderToolName('custom-code-tool')).toBe(
|
|
38
|
+
'code_interpreter',
|
|
39
|
+
);
|
|
40
|
+
expect(mapping.toCustomToolName('computer_use')).toBe(
|
|
41
|
+
'custom-computer-tool',
|
|
42
|
+
);
|
|
43
|
+
expect(mapping.toCustomToolName('code_interpreter')).toBe(
|
|
44
|
+
'custom-code-tool',
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should ignore function tools', () => {
|
|
49
|
+
const tools: Array<
|
|
50
|
+
LanguageModelV3FunctionTool | LanguageModelV3ProviderTool
|
|
51
|
+
> = [
|
|
52
|
+
{
|
|
53
|
+
type: 'function',
|
|
54
|
+
name: 'my-function-tool',
|
|
55
|
+
description: 'A function tool',
|
|
56
|
+
inputSchema: { type: 'object' },
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const providerToolNames: Record<`${string}.${string}`, string> = {};
|
|
61
|
+
|
|
62
|
+
const mapping = createToolNameMapping({ tools, providerToolNames });
|
|
63
|
+
|
|
64
|
+
expect(mapping.toProviderToolName('my-function-tool')).toBe(
|
|
65
|
+
'my-function-tool',
|
|
66
|
+
);
|
|
67
|
+
expect(mapping.toCustomToolName('my-function-tool')).toBe(
|
|
68
|
+
'my-function-tool',
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should return input name when tool is not in providerToolNames', () => {
|
|
73
|
+
const tools: Array<
|
|
74
|
+
LanguageModelV3FunctionTool | LanguageModelV3ProviderTool
|
|
75
|
+
> = [
|
|
76
|
+
{
|
|
77
|
+
type: 'provider',
|
|
78
|
+
id: 'unknown.tool',
|
|
79
|
+
name: 'custom-tool',
|
|
80
|
+
args: {},
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const providerToolNames: Record<`${string}.${string}`, string> = {};
|
|
85
|
+
|
|
86
|
+
const mapping = createToolNameMapping({ tools, providerToolNames });
|
|
87
|
+
|
|
88
|
+
expect(mapping.toProviderToolName('custom-tool')).toBe('custom-tool');
|
|
89
|
+
expect(mapping.toCustomToolName('unknown-name')).toBe('unknown-name');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should return input name when mapping does not exist', () => {
|
|
93
|
+
const tools: Array<
|
|
94
|
+
LanguageModelV3FunctionTool | LanguageModelV3ProviderTool
|
|
95
|
+
> = [
|
|
96
|
+
{
|
|
97
|
+
type: 'provider',
|
|
98
|
+
id: 'anthropic.computer-use',
|
|
99
|
+
name: 'custom-computer-tool',
|
|
100
|
+
args: {},
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const providerToolNames: Record<`${string}.${string}`, string> = {
|
|
105
|
+
'anthropic.computer-use': 'computer_use',
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const mapping = createToolNameMapping({ tools, providerToolNames });
|
|
109
|
+
|
|
110
|
+
expect(mapping.toProviderToolName('non-existent-tool')).toBe(
|
|
111
|
+
'non-existent-tool',
|
|
112
|
+
);
|
|
113
|
+
expect(mapping.toCustomToolName('non-existent-provider-tool')).toBe(
|
|
114
|
+
'non-existent-provider-tool',
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should handle empty tools array', () => {
|
|
119
|
+
const tools: Array<
|
|
120
|
+
LanguageModelV3FunctionTool | LanguageModelV3ProviderTool
|
|
121
|
+
> = [];
|
|
122
|
+
|
|
123
|
+
const providerToolNames: Record<`${string}.${string}`, string> = {};
|
|
124
|
+
|
|
125
|
+
const mapping = createToolNameMapping({ tools, providerToolNames });
|
|
126
|
+
|
|
127
|
+
expect(mapping.toProviderToolName('any-tool')).toBe('any-tool');
|
|
128
|
+
expect(mapping.toCustomToolName('any-tool')).toBe('any-tool');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should handle mixed function and provider-defined tools', () => {
|
|
132
|
+
const tools: Array<
|
|
133
|
+
LanguageModelV3FunctionTool | LanguageModelV3ProviderTool
|
|
134
|
+
> = [
|
|
135
|
+
{
|
|
136
|
+
type: 'function',
|
|
137
|
+
name: 'function-tool',
|
|
138
|
+
description: 'A function tool',
|
|
139
|
+
inputSchema: { type: 'object' },
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
type: 'provider',
|
|
143
|
+
id: 'anthropic.computer-use',
|
|
144
|
+
name: 'provider-tool',
|
|
145
|
+
args: {},
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
const providerToolNames: Record<`${string}.${string}`, string> = {
|
|
150
|
+
'anthropic.computer-use': 'computer_use',
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const mapping = createToolNameMapping({ tools, providerToolNames });
|
|
154
|
+
|
|
155
|
+
// Function tool should not be mapped
|
|
156
|
+
expect(mapping.toProviderToolName('function-tool')).toBe('function-tool');
|
|
157
|
+
expect(mapping.toCustomToolName('function-tool')).toBe('function-tool');
|
|
158
|
+
|
|
159
|
+
// Provider-defined tool should be mapped
|
|
160
|
+
expect(mapping.toProviderToolName('provider-tool')).toBe('computer_use');
|
|
161
|
+
expect(mapping.toCustomToolName('computer_use')).toBe('provider-tool');
|
|
162
|
+
});
|
|
163
|
+
});
|