modelmix 3.8.0 → 3.8.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/.claude/settings.local.json +5 -1
- package/README.md +5 -3
- package/demo/demo.mjs +8 -8
- package/demo/images.mjs +9 -0
- package/demo/img.png +0 -0
- package/demo/json.mjs +4 -2
- package/index.js +143 -28
- package/package.json +20 -6
- package/test/README.md +158 -0
- package/test/bottleneck.test.js +483 -0
- package/test/fallback.test.js +387 -0
- package/test/fixtures/data.json +36 -0
- package/test/fixtures/img.png +0 -0
- package/test/fixtures/template.txt +15 -0
- package/test/images.test.js +87 -0
- package/test/json.test.js +295 -0
- package/test/live.test.js +356 -0
- package/test/mocha.opts +5 -0
- package/test/setup.js +176 -0
- package/test/templates.test.js +473 -0
- package/test/test-runner.js +73 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
const { expect } = require('chai');
|
|
2
|
+
const sinon = require('sinon');
|
|
3
|
+
const nock = require('nock');
|
|
4
|
+
const { ModelMix } = require('../index.js');
|
|
5
|
+
|
|
6
|
+
describe('Provider Fallback Chain Tests', () => {
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
nock.cleanAll();
|
|
10
|
+
sinon.restore();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('Basic Fallback Chain', () => {
|
|
14
|
+
let model;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
model = ModelMix.new({
|
|
18
|
+
config: { debug: false }
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should use primary provider when available', async () => {
|
|
23
|
+
model.gpt5mini().sonnet4().addText('Hello');
|
|
24
|
+
|
|
25
|
+
// Mock successful OpenAI response
|
|
26
|
+
nock('https://api.openai.com')
|
|
27
|
+
.post('/v1/chat/completions')
|
|
28
|
+
.reply(200, {
|
|
29
|
+
choices: [{
|
|
30
|
+
message: {
|
|
31
|
+
role: 'assistant',
|
|
32
|
+
content: 'Hello from GPT-5 mini!'
|
|
33
|
+
}
|
|
34
|
+
}]
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const response = await model.message();
|
|
38
|
+
|
|
39
|
+
expect(response).to.include('Hello from GPT-5 mini!');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should fallback to secondary provider when primary fails', async () => {
|
|
43
|
+
model.gpt5mini().sonnet4().addText('Hello');
|
|
44
|
+
|
|
45
|
+
// Mock failed OpenAI response (GPT-5 mini)
|
|
46
|
+
nock('https://api.openai.com')
|
|
47
|
+
.post('/v1/chat/completions')
|
|
48
|
+
.reply(500, { error: 'Server error' });
|
|
49
|
+
|
|
50
|
+
// Mock successful Anthropic response (Sonnet 4)
|
|
51
|
+
nock('https://api.anthropic.com')
|
|
52
|
+
.post('/v1/messages')
|
|
53
|
+
.reply(200, {
|
|
54
|
+
content: [{
|
|
55
|
+
type: 'text',
|
|
56
|
+
text: 'Hello from Claude Sonnet 4!'
|
|
57
|
+
}]
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const response = await model.message();
|
|
61
|
+
|
|
62
|
+
expect(response).to.include('Hello from Claude Sonnet 4!');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should cascade through multiple fallbacks', async () => {
|
|
66
|
+
model.gpt5mini().sonnet4().gemini25flash().addText('Hello');
|
|
67
|
+
|
|
68
|
+
// Mock failed OpenAI response
|
|
69
|
+
nock('https://api.openai.com')
|
|
70
|
+
.post('/v1/chat/completions')
|
|
71
|
+
.reply(429, { error: 'Rate limit exceeded' });
|
|
72
|
+
|
|
73
|
+
// Mock failed Anthropic response
|
|
74
|
+
nock('https://api.anthropic.com')
|
|
75
|
+
.post('/v1/messages')
|
|
76
|
+
.reply(401, { error: 'Unauthorized' });
|
|
77
|
+
|
|
78
|
+
// Mock successful Google response
|
|
79
|
+
nock('https://generativelanguage.googleapis.com')
|
|
80
|
+
.post(/.*generateContent/)
|
|
81
|
+
.reply(200, {
|
|
82
|
+
candidates: [{
|
|
83
|
+
content: {
|
|
84
|
+
parts: [{
|
|
85
|
+
text: 'Hello from Google Gemini 2.5 Flash!'
|
|
86
|
+
}]
|
|
87
|
+
}
|
|
88
|
+
}]
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const response = await model.message();
|
|
92
|
+
|
|
93
|
+
expect(response).to.include('Hello from Google Gemini 2.5 Flash!');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should throw error when all providers fail', async () => {
|
|
97
|
+
model.gpt5mini().sonnet4().addText('Hello');
|
|
98
|
+
|
|
99
|
+
// Mock all providers failing
|
|
100
|
+
nock('https://api.openai.com')
|
|
101
|
+
.post('/v1/chat/completions')
|
|
102
|
+
.reply(500, { error: 'All servers down' });
|
|
103
|
+
|
|
104
|
+
nock('https://api.anthropic.com')
|
|
105
|
+
.post('/v1/messages')
|
|
106
|
+
.reply(500, { error: 'All servers down' });
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
await model.message();
|
|
110
|
+
expect.fail('Should have thrown an error');
|
|
111
|
+
} catch (error) {
|
|
112
|
+
expect(error.message).to.include('500');
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('Cross-Provider Fallback', () => {
|
|
118
|
+
let model;
|
|
119
|
+
|
|
120
|
+
beforeEach(() => {
|
|
121
|
+
model = ModelMix.new({
|
|
122
|
+
config: { debug: false }
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should fallback from OpenAI to Anthropic', async () => {
|
|
127
|
+
model.gpt5mini().sonnet4().addText('Test message');
|
|
128
|
+
|
|
129
|
+
// Mock OpenAI failure
|
|
130
|
+
nock('https://api.openai.com')
|
|
131
|
+
.post('/v1/chat/completions')
|
|
132
|
+
.reply(503, { error: 'Service unavailable' });
|
|
133
|
+
|
|
134
|
+
// Mock Anthropic success
|
|
135
|
+
nock('https://api.anthropic.com')
|
|
136
|
+
.post('/v1/messages')
|
|
137
|
+
.reply(200, {
|
|
138
|
+
content: [{
|
|
139
|
+
type: 'text',
|
|
140
|
+
text: 'Response from Anthropic'
|
|
141
|
+
}]
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const response = await model.message();
|
|
145
|
+
|
|
146
|
+
expect(response).to.include('Response from Anthropic');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should fallback from Anthropic to Google', async () => {
|
|
150
|
+
model.sonnet4().gemini25flash().addText('Test message');
|
|
151
|
+
|
|
152
|
+
// Mock Anthropic failure
|
|
153
|
+
nock('https://api.anthropic.com')
|
|
154
|
+
.post('/v1/messages')
|
|
155
|
+
.reply(401, { error: 'Unauthorized' });
|
|
156
|
+
|
|
157
|
+
// Mock Google success
|
|
158
|
+
nock('https://generativelanguage.googleapis.com')
|
|
159
|
+
.post(/.*generateContent/)
|
|
160
|
+
.reply(200, {
|
|
161
|
+
candidates: [{
|
|
162
|
+
content: {
|
|
163
|
+
parts: [{
|
|
164
|
+
text: 'Response from Google Gemini 2.5 Flash'
|
|
165
|
+
}]
|
|
166
|
+
}
|
|
167
|
+
}]
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const response = await model.message();
|
|
171
|
+
|
|
172
|
+
expect(response).to.include('Response from Google Gemini 2.5 Flash');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should handle network timeout fallback', async () => {
|
|
176
|
+
model.gpt5mini().sonnet4().addText('Hello');
|
|
177
|
+
|
|
178
|
+
// Mock timeout error on first provider (using 408 Request Timeout)
|
|
179
|
+
nock('https://api.openai.com')
|
|
180
|
+
.post('/v1/chat/completions')
|
|
181
|
+
.reply(408, { error: 'Request timeout' });
|
|
182
|
+
|
|
183
|
+
// Mock successful response on fallback (Anthropic)
|
|
184
|
+
nock('https://api.anthropic.com')
|
|
185
|
+
.post('/v1/messages')
|
|
186
|
+
.reply(200, {
|
|
187
|
+
content: [{
|
|
188
|
+
type: 'text',
|
|
189
|
+
text: 'Quick fallback response from Claude'
|
|
190
|
+
}]
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const response = await model.message();
|
|
194
|
+
|
|
195
|
+
expect(response).to.include('Quick fallback response from Claude');
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('Fallback with Different Response Formats', () => {
|
|
200
|
+
let model;
|
|
201
|
+
|
|
202
|
+
beforeEach(() => {
|
|
203
|
+
model = ModelMix.new({
|
|
204
|
+
config: { debug: false }
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should handle JSON fallback correctly', async () => {
|
|
209
|
+
const schema = { name: 'Alice', age: 30 };
|
|
210
|
+
model.gpt5mini().sonnet4().addText('Generate user data');
|
|
211
|
+
|
|
212
|
+
// Mock OpenAI failure
|
|
213
|
+
nock('https://api.openai.com')
|
|
214
|
+
.post('/v1/chat/completions')
|
|
215
|
+
.reply(400, { error: 'Bad request' });
|
|
216
|
+
|
|
217
|
+
// Mock Anthropic success with JSON
|
|
218
|
+
nock('https://api.anthropic.com')
|
|
219
|
+
.post('/v1/messages')
|
|
220
|
+
.reply(200, {
|
|
221
|
+
content: [{
|
|
222
|
+
type: 'text',
|
|
223
|
+
text: JSON.stringify({ name: 'Bob', age: 25 })
|
|
224
|
+
}]
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const result = await model.json(schema);
|
|
228
|
+
|
|
229
|
+
expect(result).to.have.property('name');
|
|
230
|
+
expect(result).to.have.property('age');
|
|
231
|
+
expect(result.name).to.equal('Bob');
|
|
232
|
+
expect(result.age).to.equal(25);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should preserve message history through fallbacks', async () => {
|
|
236
|
+
model.gpt5mini().sonnet4()
|
|
237
|
+
.addText('First message')
|
|
238
|
+
.addText('Second message');
|
|
239
|
+
|
|
240
|
+
// Mock first provider failure
|
|
241
|
+
nock('https://api.openai.com')
|
|
242
|
+
.post('/v1/chat/completions')
|
|
243
|
+
.reply(500, { error: 'Server error' });
|
|
244
|
+
|
|
245
|
+
// Mock second provider success (Anthropic)
|
|
246
|
+
nock('https://api.anthropic.com')
|
|
247
|
+
.post('/v1/messages')
|
|
248
|
+
.reply(200, {
|
|
249
|
+
content: [{
|
|
250
|
+
type: 'text',
|
|
251
|
+
text: 'Fallback response with context from Claude'
|
|
252
|
+
}]
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const response = await model.message();
|
|
256
|
+
|
|
257
|
+
expect(response).to.include('Fallback response with context from Claude');
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe('Fallback Configuration', () => {
|
|
262
|
+
it('should respect custom provider configurations in fallback', async () => {
|
|
263
|
+
const model = ModelMix.new({
|
|
264
|
+
config: { debug: false },
|
|
265
|
+
options: { temperature: 0.5 }
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Configure with custom temperature for fallback
|
|
269
|
+
model.gpt5mini({ options: { temperature: 0.6 } })
|
|
270
|
+
.sonnet4({ options: { temperature: 0.7 } })
|
|
271
|
+
.addText('Creative response');
|
|
272
|
+
|
|
273
|
+
// Mock first provider failure
|
|
274
|
+
nock('https://api.openai.com')
|
|
275
|
+
.post('/v1/chat/completions')
|
|
276
|
+
.reply(503, { error: 'Service unavailable' });
|
|
277
|
+
|
|
278
|
+
// Mock second provider success - verify temperature in request (Anthropic)
|
|
279
|
+
nock('https://api.anthropic.com')
|
|
280
|
+
.post('/v1/messages')
|
|
281
|
+
.reply(function(uri, requestBody) {
|
|
282
|
+
const body = typeof requestBody === 'string' ? JSON.parse(requestBody) : requestBody;
|
|
283
|
+
expect(body.temperature).to.equal(0.7);
|
|
284
|
+
return [200, {
|
|
285
|
+
content: [{
|
|
286
|
+
type: 'text',
|
|
287
|
+
text: 'Creative fallback response from Claude'
|
|
288
|
+
}]
|
|
289
|
+
}];
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const response = await model.message();
|
|
293
|
+
|
|
294
|
+
expect(response).to.include('Creative fallback response from Claude');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should handle provider-specific options in fallback chain', async () => {
|
|
298
|
+
const model = ModelMix.new({
|
|
299
|
+
config: { debug: false }
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
model.gpt5mini({ options: { max_tokens: 100 } })
|
|
303
|
+
.sonnet4({ options: { max_tokens: 200 } })
|
|
304
|
+
.addText('Generate text');
|
|
305
|
+
|
|
306
|
+
// Mock OpenAI failure
|
|
307
|
+
nock('https://api.openai.com')
|
|
308
|
+
.post('/v1/chat/completions')
|
|
309
|
+
.reply(429, { error: 'Rate limited' });
|
|
310
|
+
|
|
311
|
+
// Mock Anthropic success - verify max_tokens
|
|
312
|
+
nock('https://api.anthropic.com')
|
|
313
|
+
.post('/v1/messages')
|
|
314
|
+
.reply(function (uri, body) {
|
|
315
|
+
expect(body.max_tokens).to.equal(200);
|
|
316
|
+
return [200, {
|
|
317
|
+
content: [{
|
|
318
|
+
type: 'text',
|
|
319
|
+
text: 'Fallback with correct max_tokens'
|
|
320
|
+
}]
|
|
321
|
+
}];
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const response = await model.message();
|
|
325
|
+
|
|
326
|
+
expect(response).to.include('Fallback with correct max_tokens');
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe('Error Handling in Fallback', () => {
|
|
331
|
+
let model;
|
|
332
|
+
|
|
333
|
+
beforeEach(() => {
|
|
334
|
+
model = ModelMix.new({
|
|
335
|
+
config: { debug: false }
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should provide detailed error information when all fallbacks fail', async () => {
|
|
340
|
+
model.gpt5mini().sonnet4().gemini25flash().addText('Test');
|
|
341
|
+
|
|
342
|
+
// Mock all providers failing with different errors
|
|
343
|
+
nock('https://api.openai.com')
|
|
344
|
+
.post('/v1/chat/completions')
|
|
345
|
+
.reply(500, { error: 'OpenAI server error' });
|
|
346
|
+
|
|
347
|
+
nock('https://api.anthropic.com')
|
|
348
|
+
.post('/v1/messages')
|
|
349
|
+
.reply(401, { error: 'Anthropic auth error' });
|
|
350
|
+
|
|
351
|
+
nock('https://generativelanguage.googleapis.com')
|
|
352
|
+
.post(/.*generateContent/)
|
|
353
|
+
.reply(403, { error: 'Google forbidden' });
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
await model.message();
|
|
357
|
+
expect.fail('Should have thrown an error');
|
|
358
|
+
} catch (error) {
|
|
359
|
+
// Should contain information about the final error
|
|
360
|
+
expect(error.message).to.be.a('string');
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('should handle malformed responses in fallback', async () => {
|
|
365
|
+
model.gpt5mini().sonnet4().addText('Test');
|
|
366
|
+
|
|
367
|
+
// Mock malformed response from first provider
|
|
368
|
+
nock('https://api.openai.com')
|
|
369
|
+
.post('/v1/chat/completions')
|
|
370
|
+
.reply(200, { invalid: 'response' });
|
|
371
|
+
|
|
372
|
+
// Mock valid response from fallback (Anthropic)
|
|
373
|
+
nock('https://api.anthropic.com')
|
|
374
|
+
.post('/v1/messages')
|
|
375
|
+
.reply(200, {
|
|
376
|
+
content: [{
|
|
377
|
+
type: 'text',
|
|
378
|
+
text: 'Valid fallback response from Claude'
|
|
379
|
+
}]
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const response = await model.message();
|
|
383
|
+
|
|
384
|
+
expect(response).to.include('Valid fallback response from Claude');
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"users": [
|
|
3
|
+
{
|
|
4
|
+
"id": 1,
|
|
5
|
+
"name": "Alice Smith",
|
|
6
|
+
"email": "alice@example.com",
|
|
7
|
+
"role": "admin",
|
|
8
|
+
"active": true
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": 2,
|
|
12
|
+
"name": "Bob Johnson",
|
|
13
|
+
"email": "bob@example.com",
|
|
14
|
+
"role": "user",
|
|
15
|
+
"active": false
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": 3,
|
|
19
|
+
"name": "Carol Davis",
|
|
20
|
+
"email": "carol@example.com",
|
|
21
|
+
"role": "moderator",
|
|
22
|
+
"active": true
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"settings": {
|
|
26
|
+
"theme": "dark",
|
|
27
|
+
"notifications": true,
|
|
28
|
+
"language": "en",
|
|
29
|
+
"timezone": "UTC"
|
|
30
|
+
},
|
|
31
|
+
"metadata": {
|
|
32
|
+
"version": "1.0.0",
|
|
33
|
+
"last_updated": "2023-12-01T10:00:00Z",
|
|
34
|
+
"total_users": 3
|
|
35
|
+
}
|
|
36
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Hello {{name}}, welcome to {{platform}}!
|
|
2
|
+
|
|
3
|
+
Your account details:
|
|
4
|
+
- Username: {{username}}
|
|
5
|
+
- Role: {{role}}
|
|
6
|
+
- Created: {{created_date}}
|
|
7
|
+
|
|
8
|
+
Please visit {{website}} for more information.
|
|
9
|
+
|
|
10
|
+
{{#if premium}}
|
|
11
|
+
Thank you for being a premium member!
|
|
12
|
+
{{/if}}
|
|
13
|
+
|
|
14
|
+
Best regards,
|
|
15
|
+
The {{company}} Team
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const { expect } = require('chai');
|
|
2
|
+
const sinon = require('sinon');
|
|
3
|
+
const nock = require('nock');
|
|
4
|
+
const { ModelMix } = require('../index.js');
|
|
5
|
+
|
|
6
|
+
describe('Image Processing and Multimodal Support Tests', () => {
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
nock.cleanAll();
|
|
10
|
+
sinon.restore();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('Image Data Handling', () => {
|
|
14
|
+
let model;
|
|
15
|
+
|
|
16
|
+
const max_history = 2;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
model = ModelMix.new({
|
|
20
|
+
config: { debug: false },
|
|
21
|
+
config: { max_history }
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should handle base64 image data correctly', async () => {
|
|
26
|
+
const base64Image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFUlEQVR42mP8z8BQz0AEYBxVSF+FABJADveWkH6oAAAAAElFTkSuQmCC';
|
|
27
|
+
|
|
28
|
+
model.gpt4o()
|
|
29
|
+
.addText('What do you see in this image?')
|
|
30
|
+
.addImageFromUrl(base64Image);
|
|
31
|
+
|
|
32
|
+
nock('https://api.openai.com')
|
|
33
|
+
.post('/v1/chat/completions')
|
|
34
|
+
.reply(function (uri, body) {
|
|
35
|
+
expect(body.messages[1].content).to.be.an('array');
|
|
36
|
+
expect(body.messages[1].content).to.have.length(max_history);
|
|
37
|
+
expect(body.messages[1].content[max_history - 1].image_url.url).to.equal(base64Image);
|
|
38
|
+
|
|
39
|
+
return [200, {
|
|
40
|
+
choices: [{
|
|
41
|
+
message: {
|
|
42
|
+
role: 'assistant',
|
|
43
|
+
content: 'I can see a small test image'
|
|
44
|
+
}
|
|
45
|
+
}]
|
|
46
|
+
}];
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const response = await model.message();
|
|
50
|
+
expect(response).to.include('I can see a small test image');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should support multimodal with sonnet4()', async () => {
|
|
54
|
+
const base64Image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFUlEQVR42mP8z8BQz0AEYBxVSF+FABJADveWkH6oAAAAAElFTkSuQmCC';
|
|
55
|
+
|
|
56
|
+
model.sonnet4()
|
|
57
|
+
.addText('Describe this image')
|
|
58
|
+
.addImageFromUrl(base64Image);
|
|
59
|
+
|
|
60
|
+
// Claude expects images as base64 in the content array
|
|
61
|
+
// We'll check that the message is formatted as expected
|
|
62
|
+
nock('https://api.anthropic.com')
|
|
63
|
+
.post('/v1/messages')
|
|
64
|
+
.reply(function (uri, body) {
|
|
65
|
+
console.log(body.messages);
|
|
66
|
+
// body is already parsed as JSON by nock
|
|
67
|
+
expect(body.messages).to.be.an('array');
|
|
68
|
+
// Find the message with the image
|
|
69
|
+
const userMsg = body.messages.find(m => m.role === 'user');
|
|
70
|
+
expect(userMsg).to.exist;
|
|
71
|
+
const imageContent = userMsg.content.find(c => c.type === 'image');
|
|
72
|
+
expect(imageContent).to.exist;
|
|
73
|
+
expect(imageContent.source.type).to.equal('base64');
|
|
74
|
+
expect(imageContent.source.data).to.equal(base64Image.split(',')[1]);
|
|
75
|
+
expect(imageContent.source.media_type).to.equal('image/png');
|
|
76
|
+
return [200, {
|
|
77
|
+
content: [{ type: "text", text: "This is a small PNG test image." }],
|
|
78
|
+
role: "assistant"
|
|
79
|
+
}];
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const response = await model.message();
|
|
83
|
+
expect(response).to.include('small PNG test image');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
});
|
|
87
|
+
});
|