modelmix 4.5.1 → 4.5.4
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/README.md +1 -2
- package/demo/free.js +1 -1
- package/http-client.js +80 -0
- package/index.js +37 -102
- package/multipart.js +111 -0
- package/package.json +1 -2
- package/skills/modelmix/SKILL.md +3 -3
- package/test/live.mcp.js +2 -2
- package/test/live.test.js +4 -38
- package/test/multipart.test.js +99 -0
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ console.log(ETH.price);
|
|
|
87
87
|
```javascript
|
|
88
88
|
ModelMix.new()
|
|
89
89
|
.gptOss()
|
|
90
|
-
.
|
|
90
|
+
.kimiK25think()
|
|
91
91
|
.deepseekR1()
|
|
92
92
|
.hermes3()
|
|
93
93
|
.addText('What is the capital of France?');
|
|
@@ -164,7 +164,6 @@ Here's a comprehensive list of available methods:
|
|
|
164
164
|
| `sonarPro()` | Perplexity | sonar-pro | [\$3.00 / \$15.00][4] |
|
|
165
165
|
| `hermes3()` | Lambda | Hermes-3-Llama-3.1-405B-FP8 | [\$0.80 / \$0.80][8] |
|
|
166
166
|
| `qwen3()` | Together | Qwen3-235B-A22B-fp8-tput | [\$0.20 / \$0.60][7] |
|
|
167
|
-
| `kimiK2()` | Together | Kimi-K2-Instruct | [\$1.00 / \$3.00][7] |
|
|
168
167
|
| `kimiK25think()` | Together | Kimi-K2.5 | [\$0.50 / \$2.80][7] |
|
|
169
168
|
|
|
170
169
|
[1]: https://platform.openai.com/docs/pricing "Pricing | OpenAI"
|
package/demo/free.js
CHANGED
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
|
|
|
@@ -166,12 +102,6 @@ const MODEL_PRICING = {
|
|
|
166
102
|
// Qwen3 (Together/Cerebras)
|
|
167
103
|
'Qwen/Qwen3-235B-A22B-fp8-tput': [0.20, 0.60],
|
|
168
104
|
'qwen-3-32b': [0.20, 0.60],
|
|
169
|
-
// Kimi K2 (Together/Groq/OpenRouter)
|
|
170
|
-
'moonshotai/Kimi-K2-Instruct-0905': [1.00, 3.00],
|
|
171
|
-
'moonshotai/kimi-k2-instruct-0905': [1.00, 3.00],
|
|
172
|
-
'moonshotai/kimi-k2:free': [0, 0],
|
|
173
|
-
'moonshotai/Kimi-K2-Thinking': [1.00, 3.00],
|
|
174
|
-
'moonshotai/kimi-k2-thinking': [1.00, 3.00],
|
|
175
105
|
// Kimi K2.5 (Together/Fireworks/OpenRouter)
|
|
176
106
|
'moonshotai/Kimi-K2.5': [0.50, 2.80],
|
|
177
107
|
'moonshotai/kimi-k2.5': [0.50, 2.80],
|
|
@@ -528,14 +458,6 @@ class ModelMix {
|
|
|
528
458
|
return this;
|
|
529
459
|
}
|
|
530
460
|
|
|
531
|
-
kimiK2({ options = {}, config = {}, mix = {} } = {}) {
|
|
532
|
-
mix = { ...this.mix, ...mix };
|
|
533
|
-
if (mix.together) this.attach('moonshotai/Kimi-K2-Instruct-0905', new MixTogether({ options, config }));
|
|
534
|
-
if (mix.groq) this.attach('moonshotai/kimi-k2-instruct-0905', new MixGroq({ options, config }));
|
|
535
|
-
if (mix.openrouter) this.attach('moonshotai/kimi-k2:free', new MixOpenRouter({ options, config }));
|
|
536
|
-
return this;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
461
|
kimiK25think({ options = {}, config = {}, mix = { together: true } } = {}) {
|
|
540
462
|
mix = { ...this.mix, ...mix };
|
|
541
463
|
if (mix.together) this.attach('moonshotai/Kimi-K2.5', new MixTogether({ options, config }));
|
|
@@ -544,13 +466,6 @@ class ModelMix {
|
|
|
544
466
|
return this;
|
|
545
467
|
}
|
|
546
468
|
|
|
547
|
-
kimiK2think({ options = {}, config = {}, mix = { together: true } } = {}) {
|
|
548
|
-
mix = { ...this.mix, ...mix };
|
|
549
|
-
if (mix.together) this.attach('moonshotai/Kimi-K2-Thinking', new MixTogether({ options, config }));
|
|
550
|
-
if (mix.openrouter) this.attach('moonshotai/kimi-k2-thinking', new MixOpenRouter({ options, config }));
|
|
551
|
-
return this;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
469
|
lmstudio(model = 'lmstudio', { options = {}, config = {} } = {}) {
|
|
555
470
|
return this.attach(model, new MixLMStudio({ options, config }));
|
|
556
471
|
}
|
|
@@ -1389,10 +1304,25 @@ class MixCustom {
|
|
|
1389
1304
|
return MixOpenAI.convertMessages(messages, config);
|
|
1390
1305
|
}
|
|
1391
1306
|
|
|
1307
|
+
static stripContentTypeHeader(headers = {}) {
|
|
1308
|
+
return stripContentTypeHeader(headers);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
static createMultipartFormData({ fields = {}, files = [] } = {}) {
|
|
1312
|
+
return createMultipartFormData({ fields, files });
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
static buildRequestBodyAndHeaders(options, headers) {
|
|
1316
|
+
return buildRequestBodyAndHeaders(options, headers);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1392
1319
|
async create({ config = {}, options = {} } = {}) {
|
|
1393
1320
|
try {
|
|
1321
|
+
if (Array.isArray(options.messages)) {
|
|
1322
|
+
options.messages = this.convertMessages(options.messages, config);
|
|
1323
|
+
}
|
|
1394
1324
|
|
|
1395
|
-
|
|
1325
|
+
const request = buildRequestBodyAndHeaders(options, this.headers);
|
|
1396
1326
|
|
|
1397
1327
|
// debug level 4 (verbose): Full request details
|
|
1398
1328
|
if (config.debug >= 4) {
|
|
@@ -1404,20 +1334,20 @@ class MixCustom {
|
|
|
1404
1334
|
console.log(ModelMix.formatJSON(configToLog));
|
|
1405
1335
|
|
|
1406
1336
|
console.log('\n[OPTIONS]');
|
|
1407
|
-
console.log(ModelMix.formatJSON(options));
|
|
1337
|
+
console.log(ModelMix.formatJSON(request.options));
|
|
1408
1338
|
}
|
|
1409
1339
|
|
|
1410
1340
|
if (options.stream) {
|
|
1411
1341
|
return this.processStream(await fetchStreamResponse(this.config.url, {
|
|
1412
1342
|
method: 'POST',
|
|
1413
|
-
headers:
|
|
1414
|
-
body:
|
|
1343
|
+
headers: request.headers,
|
|
1344
|
+
body: request.body
|
|
1415
1345
|
}));
|
|
1416
1346
|
} else {
|
|
1417
1347
|
return this.processResponse(await fetchJsonResponse(this.config.url, {
|
|
1418
1348
|
method: 'POST',
|
|
1419
|
-
headers:
|
|
1420
|
-
body:
|
|
1349
|
+
headers: request.headers,
|
|
1350
|
+
body: request.body
|
|
1421
1351
|
}));
|
|
1422
1352
|
}
|
|
1423
1353
|
} catch (error) {
|
|
@@ -2236,6 +2166,7 @@ class MixAnthropic extends MixCustom {
|
|
|
2236
2166
|
|
|
2237
2167
|
static extractMessage(data) {
|
|
2238
2168
|
const content = Array.isArray(data?.content) ? data.content : [];
|
|
2169
|
+
const stopReason = data?.stop_reason;
|
|
2239
2170
|
|
|
2240
2171
|
// Anthropic can return text in different positions depending on thinking/tool blocks.
|
|
2241
2172
|
const textBlock = content.find(block => typeof block?.text === 'string' && block.text.trim().length > 0);
|
|
@@ -2243,8 +2174,12 @@ class MixAnthropic extends MixCustom {
|
|
|
2243
2174
|
return textBlock.text;
|
|
2244
2175
|
}
|
|
2245
2176
|
|
|
2177
|
+
// A tool_use turn can legitimately contain no text blocks.
|
|
2178
|
+
if (stopReason === 'tool_use') {
|
|
2179
|
+
return '';
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2246
2182
|
// Empty/non-text content is often due to safety refusal or token limits.
|
|
2247
|
-
const stopReason = data?.stop_reason;
|
|
2248
2183
|
const contentTypes = content.map(block => block?.type || 'unknown').join(', ') || 'none';
|
|
2249
2184
|
|
|
2250
2185
|
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.4",
|
|
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/skills/modelmix/SKILL.md
CHANGED
|
@@ -113,7 +113,7 @@ Thinking variants: append `think` — e.g. `opus46think()` `sonnet46think()` `so
|
|
|
113
113
|
`scout()` `maverick()`
|
|
114
114
|
|
|
115
115
|
### Together
|
|
116
|
-
`qwen3()` `
|
|
116
|
+
`qwen3()` `kimiK25think()` `gptOss()`
|
|
117
117
|
|
|
118
118
|
### MiniMax
|
|
119
119
|
`minimaxM25()` `minimaxM21()` `minimaxM2()` `minimaxM2Stable()`
|
|
@@ -128,7 +128,7 @@ Thinking variants: append `think` — e.g. `opus46think()` `sonnet46think()` `so
|
|
|
128
128
|
`GLM45()`
|
|
129
129
|
|
|
130
130
|
### Multi-provider (auto-fallback across free/paid tiers)
|
|
131
|
-
`deepseekR1()` `hermes3()` `scout()` `maverick()` `
|
|
131
|
+
`deepseekR1()` `hermes3()` `scout()` `maverick()` `kimiK25think()` `GLM47()`
|
|
132
132
|
|
|
133
133
|
### Local
|
|
134
134
|
`lmstudio()` — for LM Studio local models
|
|
@@ -356,7 +356,7 @@ For full debug output, also set: `DEBUG=ModelMix* node script.js`
|
|
|
356
356
|
```javascript
|
|
357
357
|
const model = ModelMix.new()
|
|
358
358
|
.gptOss()
|
|
359
|
-
.
|
|
359
|
+
.kimiK25think()
|
|
360
360
|
.deepseekR1()
|
|
361
361
|
.hermes3()
|
|
362
362
|
.addText("What is the capital of France?");
|
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
|
@@ -192,18 +192,6 @@ describe('Live Integration Tests', function () {
|
|
|
192
192
|
expect(response.toLowerCase()).to.include('scout test successful');
|
|
193
193
|
});
|
|
194
194
|
|
|
195
|
-
it('should work with KimiK2 model', async function () {
|
|
196
|
-
const model = ModelMix.new(setup).kimiK2();
|
|
197
|
-
|
|
198
|
-
model.addText('Say "kimik2 test successful" and nothing else.');
|
|
199
|
-
|
|
200
|
-
const response = await model.message();
|
|
201
|
-
console.log(`KimiK2 response: ${response}`);
|
|
202
|
-
|
|
203
|
-
expect(response).to.be.a('string');
|
|
204
|
-
expect(response.toLowerCase()).to.include('kimik2 test successful');
|
|
205
|
-
});
|
|
206
|
-
|
|
207
195
|
it('should work with GPT-OSS model', async function () {
|
|
208
196
|
const model = ModelMix.new(setup).gptOss();
|
|
209
197
|
|
|
@@ -299,8 +287,8 @@ describe('Live Integration Tests', function () {
|
|
|
299
287
|
expect(result.abilities).to.be.an('array');
|
|
300
288
|
});
|
|
301
289
|
|
|
302
|
-
it('should return structured JSON with
|
|
303
|
-
const model = ModelMix.new(setup).
|
|
290
|
+
it('should return structured JSON with KimiK25 Thinking', async function () {
|
|
291
|
+
const model = ModelMix.new(setup).kimiK25think();
|
|
304
292
|
|
|
305
293
|
model.addText('Generate information about a fictional vehicle.');
|
|
306
294
|
|
|
@@ -311,7 +299,7 @@ describe('Live Integration Tests', function () {
|
|
|
311
299
|
manufacturer: "Future Motors"
|
|
312
300
|
});
|
|
313
301
|
|
|
314
|
-
console.log(`
|
|
302
|
+
console.log(`KimiK25 JSON result:`, result);
|
|
315
303
|
|
|
316
304
|
expect(result).to.be.an('object');
|
|
317
305
|
expect(result).to.have.property('name');
|
|
@@ -321,30 +309,8 @@ describe('Live Integration Tests', function () {
|
|
|
321
309
|
expect(result.features).to.be.an('array');
|
|
322
310
|
});
|
|
323
311
|
|
|
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
312
|
it('should return structured JSON with Grok3Mini', async function () {
|
|
347
|
-
const model = ModelMix.new(setup).
|
|
313
|
+
const model = ModelMix.new(setup).grok41();
|
|
348
314
|
|
|
349
315
|
model.addText('Generate information about a fictional technology.');
|
|
350
316
|
|
|
@@ -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
|
+
});
|