modelmix 3.8.2 → 3.8.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.
@@ -0,0 +1,295 @@
1
+ const { expect } = require('chai');
2
+ const sinon = require('sinon');
3
+ const nock = require('nock');
4
+ const { ModelMix } = require('../index.js');
5
+ const generateJsonSchema = require('../schema.js');
6
+
7
+
8
+ describe('JSON Schema and Structured Output Tests', () => {
9
+
10
+ // Setup test hooks
11
+ if (global.setupTestHooks) {
12
+ global.setupTestHooks();
13
+ }
14
+
15
+ afterEach(() => {
16
+ nock.cleanAll();
17
+ sinon.restore();
18
+ });
19
+
20
+ describe('JSON Schema Generation', () => {
21
+ it('should generate schema for simple object', () => {
22
+ const example = {
23
+ name: 'Alice',
24
+ age: 30,
25
+ isAdmin: false
26
+ };
27
+
28
+ const schema = generateJsonSchema(example);
29
+
30
+ expect(schema).to.deep.equal({
31
+ type: 'object',
32
+ properties: {
33
+ name: { type: 'string' },
34
+ age: { type: 'integer' },
35
+ isAdmin: { type: 'boolean' }
36
+ },
37
+ required: ['name', 'age', 'isAdmin']
38
+ });
39
+ });
40
+
41
+ it('should generate schema with email format detection', () => {
42
+ const example = { email: 'test@example.com' };
43
+ const schema = generateJsonSchema(example);
44
+
45
+ expect(schema.properties.email).to.deep.equal({
46
+ type: 'string',
47
+ format: 'email'
48
+ });
49
+ });
50
+
51
+ it('should generate schema with date format detection', () => {
52
+ const example = { birthDate: '1990-01-01' };
53
+ const schema = generateJsonSchema(example);
54
+
55
+ expect(schema.properties.birthDate).to.deep.equal({
56
+ type: 'string',
57
+ format: 'date',
58
+ description: 'Date in format YYYY-MM-DD'
59
+ });
60
+ });
61
+
62
+ it('should generate schema with time format detection', () => {
63
+ const example = {
64
+ time1: '14:30',
65
+ time2: '09:15:45'
66
+ };
67
+ const schema = generateJsonSchema(example);
68
+
69
+ expect(schema.properties.time1).to.deep.equal({
70
+ type: 'string',
71
+ format: 'time',
72
+ description: 'Time in format HH:MM'
73
+ });
74
+
75
+ expect(schema.properties.time2).to.deep.equal({
76
+ type: 'string',
77
+ format: 'time',
78
+ description: 'Time in format HH:MM:SS'
79
+ });
80
+ });
81
+
82
+ it('should handle nested objects', () => {
83
+ const example = {
84
+ user: {
85
+ name: 'Bob',
86
+ preferences: {
87
+ theme: 'dark'
88
+ }
89
+ }
90
+ };
91
+
92
+ const schema = generateJsonSchema(example);
93
+
94
+ expect(schema.properties.user.type).to.equal('object');
95
+ expect(schema.properties.user.properties.name).to.deep.equal({ type: 'string' });
96
+ expect(schema.properties.user.properties.preferences.type).to.equal('object');
97
+ });
98
+
99
+ it('should handle arrays of objects', () => {
100
+ const example = {
101
+ users: [{
102
+ name: 'Alice',
103
+ age: 30
104
+ }]
105
+ };
106
+
107
+ const schema = generateJsonSchema(example);
108
+
109
+ expect(schema.properties.users).to.deep.equal({
110
+ type: 'array',
111
+ items: {
112
+ type: 'object',
113
+ properties: {
114
+ name: { type: 'string' },
115
+ age: { type: 'integer' }
116
+ },
117
+ required: ['name', 'age']
118
+ }
119
+ });
120
+ });
121
+
122
+ it('should handle arrays of primitives', () => {
123
+ const example = {
124
+ tags: ['admin', 'user'],
125
+ numbers: [1, 2, 3]
126
+ };
127
+
128
+ const schema = generateJsonSchema(example);
129
+
130
+ expect(schema.properties.tags).to.deep.equal({
131
+ type: 'array',
132
+ items: { type: 'string' }
133
+ });
134
+
135
+ expect(schema.properties.numbers).to.deep.equal({
136
+ type: 'array',
137
+ items: { type: 'integer' }
138
+ });
139
+ });
140
+
141
+ it('should handle custom descriptions', () => {
142
+ const example = { name: 'Alice', age: 30 };
143
+ const descriptions = {
144
+ name: 'Full name of the user',
145
+ age: 'User age in years'
146
+ };
147
+
148
+ const schema = generateJsonSchema(example, descriptions);
149
+
150
+ expect(schema.properties.name.description).to.equal('Full name of the user');
151
+ expect(schema.properties.age.description).to.equal('User age in years');
152
+ });
153
+
154
+ it('should handle null values', () => {
155
+ const example = { data: null };
156
+ const schema = generateJsonSchema(example);
157
+
158
+ expect(schema.properties.data).to.deep.equal({ type: 'null' });
159
+ });
160
+
161
+ it('should distinguish between integer and float', () => {
162
+ const example = {
163
+ count: 42,
164
+ price: 19.99
165
+ };
166
+
167
+ const schema = generateJsonSchema(example);
168
+
169
+ expect(schema.properties.count).to.deep.equal({ type: 'integer' });
170
+ expect(schema.properties.price).to.deep.equal({ type: 'number' });
171
+ });
172
+
173
+ it('should handle empty arrays', () => {
174
+ const example = { items: [] };
175
+ const schema = generateJsonSchema(example);
176
+
177
+ expect(schema.properties.items).to.deep.equal({
178
+ type: 'array',
179
+ items: {}
180
+ });
181
+ });
182
+ });
183
+
184
+ describe('ModelMix JSON Output', () => {
185
+ let model;
186
+
187
+ beforeEach(() => {
188
+ model = ModelMix.new({
189
+ config: { debug: false }
190
+ });
191
+ });
192
+
193
+ it('should prepare JSON schema for structured output', async () => {
194
+ const example = {
195
+ countries: [{
196
+ name: 'France',
197
+ capital: 'Paris'
198
+ }]
199
+ };
200
+
201
+ model.gpt4o().addText('List 3 countries');
202
+
203
+ // Mock the API response
204
+ nock('https://api.openai.com')
205
+ .post('/v1/chat/completions')
206
+ .reply(200, {
207
+ choices: [{
208
+ message: {
209
+ role: 'assistant',
210
+ content: JSON.stringify({
211
+ countries: [
212
+ { name: 'France', capital: 'Paris' },
213
+ { name: 'Germany', capital: 'Berlin' },
214
+ { name: 'Spain', capital: 'Madrid' }
215
+ ]
216
+ })
217
+ }
218
+ }]
219
+ });
220
+
221
+ const result = await model.json(example);
222
+
223
+ expect(result).to.have.property('countries');
224
+ expect(result.countries).to.be.an('array');
225
+ expect(result.countries).to.have.length(3);
226
+ expect(result.countries[0]).to.have.property('name');
227
+ expect(result.countries[0]).to.have.property('capital');
228
+ });
229
+
230
+ it('should handle complex nested JSON schema', async () => {
231
+ const example = {
232
+ user: {
233
+ personal: {
234
+ name: 'John',
235
+ age: 25
236
+ },
237
+ contact: {
238
+ email: 'john@example.com',
239
+ phone: '123-456-7890'
240
+ },
241
+ preferences: {
242
+ notifications: true,
243
+ theme: 'dark'
244
+ }
245
+ },
246
+ metadata: {
247
+ createdAt: '2023-01-01',
248
+ tags: ['premium', 'verified']
249
+ }
250
+ };
251
+
252
+ model.sonnet4().addText('Generate user data');
253
+
254
+ // Mock the API response
255
+ nock('https://api.anthropic.com')
256
+ .post('/v1/messages')
257
+ .reply(200, {
258
+ content: [{
259
+ type: 'text',
260
+ text: JSON.stringify(example)
261
+ }]
262
+ });
263
+
264
+ const result = await model.json(example);
265
+
266
+ expect(result.user.personal.name).to.equal('John');
267
+ expect(result.user.contact.email).to.equal('john@example.com');
268
+ expect(result.metadata.tags).to.be.an('array');
269
+ expect(result.metadata.tags).to.deep.equal(['premium', 'verified']);
270
+ });
271
+
272
+ it('should handle JSON parsing errors gracefully', async () => {
273
+ model.gpt4o().addText('Generate invalid JSON');
274
+
275
+ // Mock invalid JSON response
276
+ nock('https://api.openai.com')
277
+ .post('/v1/chat/completions')
278
+ .reply(200, {
279
+ choices: [{
280
+ message: {
281
+ role: 'assistant',
282
+ content: 'This is not valid JSON'
283
+ }
284
+ }]
285
+ });
286
+
287
+ try {
288
+ await model.json({ name: 'test' });
289
+ expect.fail('Should have thrown an error');
290
+ } catch (error) {
291
+ expect(error.message).to.include('JSON');
292
+ }
293
+ });
294
+ });
295
+ });