modelmix 4.5.1 → 4.5.2
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/http-client.js +80 -0
- package/index.js +37 -81
- package/multipart.js +111 -0
- package/package.json +1 -2
- package/test/live.mcp.js +2 -2
- package/test/live.test.js +3 -25
- package/test/multipart.test.js +99 -0
package/http-client.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const { Readable } = require('stream');
|
|
2
|
+
|
|
3
|
+
function headersToObject(headers) {
|
|
4
|
+
return Object.fromEntries(headers.entries());
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
async function parseResponseBody(response) {
|
|
8
|
+
try {
|
|
9
|
+
return await response.json();
|
|
10
|
+
} catch {
|
|
11
|
+
try {
|
|
12
|
+
return await response.text();
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function parseJsonBody(response) {
|
|
20
|
+
const text = await response.text();
|
|
21
|
+
if (!text) return null;
|
|
22
|
+
return JSON.parse(text);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function buildHttpError(url, response) {
|
|
26
|
+
const details = await parseResponseBody(response);
|
|
27
|
+
const error = new Error(`Request to ${url} failed with status code ${response.status}`);
|
|
28
|
+
error.isHttpError = true;
|
|
29
|
+
error.statusCode = response.status;
|
|
30
|
+
error.details = details;
|
|
31
|
+
error.response = { status: response.status, data: details };
|
|
32
|
+
return error;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function fetchJsonResponse(url, { method = 'POST', headers = {}, body } = {}) {
|
|
36
|
+
const response = await fetch(url, { method, headers, body });
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw await buildHttpError(url, response);
|
|
39
|
+
}
|
|
40
|
+
const data = await parseJsonBody(response);
|
|
41
|
+
return {
|
|
42
|
+
data,
|
|
43
|
+
status: response.status,
|
|
44
|
+
headers: headersToObject(response.headers)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function fetchBinaryResponse(url, { method = 'GET', headers = {}, body } = {}) {
|
|
49
|
+
const response = await fetch(url, { method, headers, body });
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
throw await buildHttpError(url, response);
|
|
52
|
+
}
|
|
53
|
+
const data = Buffer.from(await response.arrayBuffer());
|
|
54
|
+
return {
|
|
55
|
+
data,
|
|
56
|
+
status: response.status,
|
|
57
|
+
headers: headersToObject(response.headers)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function fetchStreamResponse(url, { method = 'POST', headers = {}, body } = {}) {
|
|
62
|
+
const response = await fetch(url, { method, headers, body });
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
throw await buildHttpError(url, response);
|
|
65
|
+
}
|
|
66
|
+
if (!response.body) {
|
|
67
|
+
throw new Error(`Request to ${url} did not return a readable stream`);
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
data: Readable.fromWeb(response.body),
|
|
71
|
+
status: response.status,
|
|
72
|
+
headers: headersToObject(response.headers)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
fetchJsonResponse,
|
|
78
|
+
fetchBinaryResponse,
|
|
79
|
+
fetchStreamResponse
|
|
80
|
+
};
|
package/index.js
CHANGED
|
@@ -2,7 +2,6 @@ const fs = require('fs');
|
|
|
2
2
|
const fileType = require('file-type');
|
|
3
3
|
const detectFileTypeFromBuffer = fileType.fileTypeFromBuffer || fileType.fromBuffer;
|
|
4
4
|
const { inspect } = require('util');
|
|
5
|
-
const { Readable } = require('stream');
|
|
6
5
|
const log = require('lemonlog')('ModelMix');
|
|
7
6
|
const Bottleneck = require('bottleneck');
|
|
8
7
|
const path = require('path');
|
|
@@ -11,79 +10,16 @@ const generateJsonSchema = require('./schema');
|
|
|
11
10
|
const { Client } = require("@modelcontextprotocol/sdk/client/index.js");
|
|
12
11
|
const { StdioClientTransport } = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
13
12
|
const { MCPToolsManager } = require('./mcp-tools');
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return await response.text();
|
|
25
|
-
} catch {
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async function parseJsonBody(response) {
|
|
32
|
-
const text = await response.text();
|
|
33
|
-
if (!text) return null;
|
|
34
|
-
return JSON.parse(text);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function buildHttpError(url, response) {
|
|
38
|
-
const details = await parseResponseBody(response);
|
|
39
|
-
const error = new Error(`Request to ${url} failed with status code ${response.status}`);
|
|
40
|
-
error.isHttpError = true;
|
|
41
|
-
error.statusCode = response.status;
|
|
42
|
-
error.details = details;
|
|
43
|
-
error.response = { status: response.status, data: details };
|
|
44
|
-
return error;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function fetchJsonResponse(url, { method = 'POST', headers = {}, body } = {}) {
|
|
48
|
-
const response = await fetch(url, { method, headers, body });
|
|
49
|
-
if (!response.ok) {
|
|
50
|
-
throw await buildHttpError(url, response);
|
|
51
|
-
}
|
|
52
|
-
const data = await parseJsonBody(response);
|
|
53
|
-
return {
|
|
54
|
-
data,
|
|
55
|
-
status: response.status,
|
|
56
|
-
headers: headersToObject(response.headers)
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function fetchBinaryResponse(url, { method = 'GET', headers = {}, body } = {}) {
|
|
61
|
-
const response = await fetch(url, { method, headers, body });
|
|
62
|
-
if (!response.ok) {
|
|
63
|
-
throw await buildHttpError(url, response);
|
|
64
|
-
}
|
|
65
|
-
const data = Buffer.from(await response.arrayBuffer());
|
|
66
|
-
return {
|
|
67
|
-
data,
|
|
68
|
-
status: response.status,
|
|
69
|
-
headers: headersToObject(response.headers)
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function fetchStreamResponse(url, { method = 'POST', headers = {}, body } = {}) {
|
|
74
|
-
const response = await fetch(url, { method, headers, body });
|
|
75
|
-
if (!response.ok) {
|
|
76
|
-
throw await buildHttpError(url, response);
|
|
77
|
-
}
|
|
78
|
-
if (!response.body) {
|
|
79
|
-
throw new Error(`Request to ${url} did not return a readable stream`);
|
|
80
|
-
}
|
|
81
|
-
return {
|
|
82
|
-
data: Readable.fromWeb(response.body),
|
|
83
|
-
status: response.status,
|
|
84
|
-
headers: headersToObject(response.headers)
|
|
85
|
-
};
|
|
86
|
-
}
|
|
13
|
+
const {
|
|
14
|
+
stripContentTypeHeader,
|
|
15
|
+
createMultipartFormData,
|
|
16
|
+
buildRequestBodyAndHeaders
|
|
17
|
+
} = require('./multipart');
|
|
18
|
+
const {
|
|
19
|
+
fetchJsonResponse,
|
|
20
|
+
fetchBinaryResponse,
|
|
21
|
+
fetchStreamResponse
|
|
22
|
+
} = require('./http-client');
|
|
87
23
|
|
|
88
24
|
const DEFAULT_RETRYABLE_STATUS_CODES = [408, 425, 429, 500, 502, 503, 504, 529];
|
|
89
25
|
|
|
@@ -1389,10 +1325,25 @@ class MixCustom {
|
|
|
1389
1325
|
return MixOpenAI.convertMessages(messages, config);
|
|
1390
1326
|
}
|
|
1391
1327
|
|
|
1328
|
+
static stripContentTypeHeader(headers = {}) {
|
|
1329
|
+
return stripContentTypeHeader(headers);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
static createMultipartFormData({ fields = {}, files = [] } = {}) {
|
|
1333
|
+
return createMultipartFormData({ fields, files });
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
static buildRequestBodyAndHeaders(options, headers) {
|
|
1337
|
+
return buildRequestBodyAndHeaders(options, headers);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1392
1340
|
async create({ config = {}, options = {} } = {}) {
|
|
1393
1341
|
try {
|
|
1342
|
+
if (Array.isArray(options.messages)) {
|
|
1343
|
+
options.messages = this.convertMessages(options.messages, config);
|
|
1344
|
+
}
|
|
1394
1345
|
|
|
1395
|
-
|
|
1346
|
+
const request = buildRequestBodyAndHeaders(options, this.headers);
|
|
1396
1347
|
|
|
1397
1348
|
// debug level 4 (verbose): Full request details
|
|
1398
1349
|
if (config.debug >= 4) {
|
|
@@ -1404,20 +1355,20 @@ class MixCustom {
|
|
|
1404
1355
|
console.log(ModelMix.formatJSON(configToLog));
|
|
1405
1356
|
|
|
1406
1357
|
console.log('\n[OPTIONS]');
|
|
1407
|
-
console.log(ModelMix.formatJSON(options));
|
|
1358
|
+
console.log(ModelMix.formatJSON(request.options));
|
|
1408
1359
|
}
|
|
1409
1360
|
|
|
1410
1361
|
if (options.stream) {
|
|
1411
1362
|
return this.processStream(await fetchStreamResponse(this.config.url, {
|
|
1412
1363
|
method: 'POST',
|
|
1413
|
-
headers:
|
|
1414
|
-
body:
|
|
1364
|
+
headers: request.headers,
|
|
1365
|
+
body: request.body
|
|
1415
1366
|
}));
|
|
1416
1367
|
} else {
|
|
1417
1368
|
return this.processResponse(await fetchJsonResponse(this.config.url, {
|
|
1418
1369
|
method: 'POST',
|
|
1419
|
-
headers:
|
|
1420
|
-
body:
|
|
1370
|
+
headers: request.headers,
|
|
1371
|
+
body: request.body
|
|
1421
1372
|
}));
|
|
1422
1373
|
}
|
|
1423
1374
|
} catch (error) {
|
|
@@ -2236,6 +2187,7 @@ class MixAnthropic extends MixCustom {
|
|
|
2236
2187
|
|
|
2237
2188
|
static extractMessage(data) {
|
|
2238
2189
|
const content = Array.isArray(data?.content) ? data.content : [];
|
|
2190
|
+
const stopReason = data?.stop_reason;
|
|
2239
2191
|
|
|
2240
2192
|
// Anthropic can return text in different positions depending on thinking/tool blocks.
|
|
2241
2193
|
const textBlock = content.find(block => typeof block?.text === 'string' && block.text.trim().length > 0);
|
|
@@ -2243,8 +2195,12 @@ class MixAnthropic extends MixCustom {
|
|
|
2243
2195
|
return textBlock.text;
|
|
2244
2196
|
}
|
|
2245
2197
|
|
|
2198
|
+
// A tool_use turn can legitimately contain no text blocks.
|
|
2199
|
+
if (stopReason === 'tool_use') {
|
|
2200
|
+
return '';
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2246
2203
|
// Empty/non-text content is often due to safety refusal or token limits.
|
|
2247
|
-
const stopReason = data?.stop_reason;
|
|
2248
2204
|
const contentTypes = content.map(block => block?.type || 'unknown').join(', ') || 'none';
|
|
2249
2205
|
|
|
2250
2206
|
if (stopReason === 'refusal') {
|
package/multipart.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function stripContentTypeHeader(headers = {}) {
|
|
5
|
+
const sanitizedHeaders = {};
|
|
6
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
7
|
+
if (name.toLowerCase() !== 'content-type') {
|
|
8
|
+
sanitizedHeaders[name] = value;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return sanitizedHeaders;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function createMultipartFormData({ fields = {}, files = [] } = {}) {
|
|
15
|
+
if (typeof FormData !== 'function' || typeof Blob !== 'function') {
|
|
16
|
+
throw new Error('Native FormData/Blob are not available in this Node.js runtime');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const formData = new FormData();
|
|
20
|
+
|
|
21
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
22
|
+
if (value === undefined || value === null) continue;
|
|
23
|
+
const normalizedValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
24
|
+
formData.append(name, normalizedValue);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const file of files) {
|
|
28
|
+
if (!file || !file.name) {
|
|
29
|
+
throw new Error('Each multipart file must include a "name" property');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let data = file.data;
|
|
33
|
+
let filename = file.filename;
|
|
34
|
+
|
|
35
|
+
if (file.path) {
|
|
36
|
+
const absolutePath = path.resolve(file.path);
|
|
37
|
+
data = fs.readFileSync(absolutePath);
|
|
38
|
+
filename = filename || path.basename(absolutePath);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (typeof data === 'string') {
|
|
42
|
+
data = Buffer.from(data);
|
|
43
|
+
} else if (data instanceof ArrayBuffer) {
|
|
44
|
+
data = Buffer.from(data);
|
|
45
|
+
} else if (ArrayBuffer.isView(data)) {
|
|
46
|
+
data = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!Buffer.isBuffer(data)) {
|
|
50
|
+
throw new Error(`Invalid multipart file data for "${file.name}" - expected Buffer, ArrayBuffer, typed array, string, or file path`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const blob = new Blob([data], {
|
|
54
|
+
type: file.contentType || 'application/octet-stream'
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
formData.append(file.name, blob, filename || 'file');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return formData;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildRequestBodyAndHeaders(options, headers) {
|
|
64
|
+
if (options?.body instanceof FormData) {
|
|
65
|
+
const requestOptions = { ...options };
|
|
66
|
+
delete requestOptions.body;
|
|
67
|
+
return {
|
|
68
|
+
options: requestOptions,
|
|
69
|
+
body: options.body,
|
|
70
|
+
headers: stripContentTypeHeader(headers)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (options?.multipart) {
|
|
75
|
+
const requestOptions = { ...options };
|
|
76
|
+
const multipartConfig = requestOptions.multipart || {};
|
|
77
|
+
delete requestOptions.multipart;
|
|
78
|
+
|
|
79
|
+
const serializedFields = {};
|
|
80
|
+
for (const [key, value] of Object.entries(requestOptions)) {
|
|
81
|
+
if (value === undefined) continue;
|
|
82
|
+
serializedFields[key] = value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const body = createMultipartFormData({
|
|
86
|
+
fields: {
|
|
87
|
+
...serializedFields,
|
|
88
|
+
...(multipartConfig.fields || {})
|
|
89
|
+
},
|
|
90
|
+
files: multipartConfig.files || []
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
options: requestOptions,
|
|
95
|
+
body,
|
|
96
|
+
headers: stripContentTypeHeader(headers)
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
options,
|
|
102
|
+
body: JSON.stringify(options),
|
|
103
|
+
headers
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
stripContentTypeHeader,
|
|
109
|
+
createMultipartFormData,
|
|
110
|
+
buildRequestBodyAndHeaders
|
|
111
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modelmix",
|
|
3
|
-
"version": "4.5.
|
|
3
|
+
"version": "4.5.2",
|
|
4
4
|
"description": "🧬 Reliable interface with automatic fallback for AI LLMs.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -50,7 +50,6 @@
|
|
|
50
50
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
51
51
|
"bottleneck": "^2.19.5",
|
|
52
52
|
"file-type": "^21.3.3",
|
|
53
|
-
"form-data": "^4.0.4",
|
|
54
53
|
"lemonlog": "^1.2.0",
|
|
55
54
|
"ws": "^8.19.0"
|
|
56
55
|
},
|
package/test/live.mcp.js
CHANGED
|
@@ -60,8 +60,8 @@ describe('Live MCP Integration Tests', function () {
|
|
|
60
60
|
|
|
61
61
|
describe('Basic MCP Tool Integration', function () {
|
|
62
62
|
|
|
63
|
-
it('should use custom MCP tools with GPT-5.
|
|
64
|
-
const model = ModelMix.new(setup).
|
|
63
|
+
it('should use custom MCP tools with GPT-5.4', async function () {
|
|
64
|
+
const model = ModelMix.new(setup).gpt54();
|
|
65
65
|
|
|
66
66
|
// Add custom calculator tool
|
|
67
67
|
model.addTool({
|
package/test/live.test.js
CHANGED
|
@@ -299,8 +299,8 @@ describe('Live Integration Tests', function () {
|
|
|
299
299
|
expect(result.abilities).to.be.an('array');
|
|
300
300
|
});
|
|
301
301
|
|
|
302
|
-
it('should return structured JSON with
|
|
303
|
-
const model = ModelMix.new(setup).
|
|
302
|
+
it('should return structured JSON with KimiK25 Thinking', async function () {
|
|
303
|
+
const model = ModelMix.new(setup).kimiK25think();
|
|
304
304
|
|
|
305
305
|
model.addText('Generate information about a fictional vehicle.');
|
|
306
306
|
|
|
@@ -321,30 +321,8 @@ describe('Live Integration Tests', function () {
|
|
|
321
321
|
expect(result.features).to.be.an('array');
|
|
322
322
|
});
|
|
323
323
|
|
|
324
|
-
it('should return structured JSON with GPT-OSS', async function () {
|
|
325
|
-
const model = ModelMix.new(setup).gptOss();
|
|
326
|
-
|
|
327
|
-
model.addText('Generate information about a fictional planet.');
|
|
328
|
-
|
|
329
|
-
const result = await model.json({
|
|
330
|
-
name: "Nova Prime",
|
|
331
|
-
type: "Gas Giant",
|
|
332
|
-
moons: ["Alpha", "Beta", "Gamma"],
|
|
333
|
-
atmosphere: "Hydrogen and Helium"
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
console.log(`GPT-OSS JSON result:`, result);
|
|
337
|
-
|
|
338
|
-
expect(result).to.be.an('object');
|
|
339
|
-
expect(result).to.have.property('name');
|
|
340
|
-
expect(result).to.have.property('type');
|
|
341
|
-
expect(result).to.have.property('moons');
|
|
342
|
-
expect(result).to.have.property('atmosphere');
|
|
343
|
-
expect(result.moons).to.be.an('array');
|
|
344
|
-
});
|
|
345
|
-
|
|
346
324
|
it('should return structured JSON with Grok3Mini', async function () {
|
|
347
|
-
const model = ModelMix.new(setup).
|
|
325
|
+
const model = ModelMix.new(setup).grok41();
|
|
348
326
|
|
|
349
327
|
model.addText('Generate information about a fictional technology.');
|
|
350
328
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const { expect } = require('chai');
|
|
2
|
+
const { MixCustom } = require('../index.js');
|
|
3
|
+
|
|
4
|
+
describe('Native multipart support', () => {
|
|
5
|
+
let originalFetch;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
originalFetch = global.fetch;
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
global.fetch = originalFetch;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should create FormData from fields and files', () => {
|
|
16
|
+
const formData = MixCustom.createMultipartFormData({
|
|
17
|
+
fields: {
|
|
18
|
+
model: 'whisper-1',
|
|
19
|
+
metadata: { source: 'test' }
|
|
20
|
+
},
|
|
21
|
+
files: [{
|
|
22
|
+
name: 'file',
|
|
23
|
+
data: Buffer.from('hello'),
|
|
24
|
+
filename: 'hello.txt',
|
|
25
|
+
contentType: 'text/plain'
|
|
26
|
+
}]
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(formData).to.be.instanceOf(FormData);
|
|
30
|
+
expect(formData.get('model')).to.equal('whisper-1');
|
|
31
|
+
expect(formData.get('metadata')).to.equal('{"source":"test"}');
|
|
32
|
+
|
|
33
|
+
const uploadedFile = formData.get('file');
|
|
34
|
+
expect(uploadedFile).to.exist;
|
|
35
|
+
expect(uploadedFile.name).to.equal('hello.txt');
|
|
36
|
+
expect(uploadedFile.type).to.equal('text/plain');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should remove content-type header for multipart requests', () => {
|
|
40
|
+
const request = MixCustom.buildRequestBodyAndHeaders({
|
|
41
|
+
multipart: {
|
|
42
|
+
fields: { model: 'whisper-1' }
|
|
43
|
+
}
|
|
44
|
+
}, {
|
|
45
|
+
accept: 'application/json',
|
|
46
|
+
'content-type': 'application/json',
|
|
47
|
+
authorization: 'Bearer test'
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(request.body).to.be.instanceOf(FormData);
|
|
51
|
+
expect(request.headers).to.deep.equal({
|
|
52
|
+
accept: 'application/json',
|
|
53
|
+
authorization: 'Bearer test'
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should send multipart body in create()', async () => {
|
|
58
|
+
let capturedRequest = null;
|
|
59
|
+
|
|
60
|
+
global.fetch = async (url, request) => {
|
|
61
|
+
capturedRequest = { url, request };
|
|
62
|
+
return new Response(JSON.stringify({
|
|
63
|
+
choices: [{
|
|
64
|
+
message: { content: 'ok' }
|
|
65
|
+
}]
|
|
66
|
+
}), {
|
|
67
|
+
status: 200,
|
|
68
|
+
headers: { 'content-type': 'application/json' }
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const mix = new MixCustom({
|
|
73
|
+
config: {
|
|
74
|
+
url: 'https://api.example.com/v1/upload',
|
|
75
|
+
apiKey: 'token'
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const result = await mix.create({
|
|
80
|
+
options: {
|
|
81
|
+
stream: false,
|
|
82
|
+
multipart: {
|
|
83
|
+
fields: { purpose: 'test' },
|
|
84
|
+
files: [{
|
|
85
|
+
name: 'file',
|
|
86
|
+
data: Buffer.from('sample'),
|
|
87
|
+
filename: 'sample.txt',
|
|
88
|
+
contentType: 'text/plain'
|
|
89
|
+
}]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(result.message).to.equal('ok');
|
|
95
|
+
expect(capturedRequest.url).to.equal('https://api.example.com/v1/upload');
|
|
96
|
+
expect(capturedRequest.request.body).to.be.instanceOf(FormData);
|
|
97
|
+
expect(capturedRequest.request.headers).to.not.have.property('content-type');
|
|
98
|
+
});
|
|
99
|
+
});
|