modelmix 1.2.2 → 2.0.0

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/index.js CHANGED
@@ -1,12 +1,11 @@
1
1
  const axios = require('axios');
2
- const fs = require('fs').promises;
2
+ const fs = require('fs');
3
3
  const mime = require('mime-types');
4
4
 
5
5
  class ModelMix {
6
6
  constructor(args = { options: {}, config: {} }) {
7
7
  this.models = {};
8
8
  this.defaultOptions = {
9
- model: 'gpt-4o',
10
9
  max_tokens: 2000,
11
10
  temperature: 1,
12
11
  top_p: 1,
@@ -22,7 +21,7 @@ class ModelMix {
22
21
  }
23
22
 
24
23
  attach(modelInstance) {
25
- const key = modelInstance.constructor.name.replace('Model', '').toLowerCase();
24
+ const key = modelInstance.config.prefix.join("_");
26
25
  this.models[key] = modelInstance;
27
26
  modelInstance.queue = [];
28
27
  modelInstance.active_requests = 0;
@@ -37,7 +36,12 @@ class ModelMix {
37
36
  throw new Error(`Model with prefix matching ${modelKey} is not attached.`);
38
37
  }
39
38
 
40
- const options = { ...this.defaultOptions, ...modelEntry.options, ...overOptions, model: modelKey };
39
+ const options = {
40
+ ...this.defaultOptions,
41
+ ...modelEntry.options,
42
+ ...overOptions,
43
+ model: modelKey
44
+ };
41
45
  const config = { ...this.config, ...modelEntry.config };
42
46
 
43
47
  return new MessageHandler(this, modelEntry, options, config);
@@ -91,9 +95,9 @@ class MessageHandler {
91
95
  return this;
92
96
  }
93
97
 
94
- async addImage(filePath, config = { role: "user" }) {
98
+ addImage(filePath, config = { role: "user" }) {
95
99
  try {
96
- const imageBuffer = await fs.readFile(filePath);
100
+ const imageBuffer = fs.readFileSync(filePath);
97
101
  const mimeType = mime.lookup(filePath);
98
102
 
99
103
  if (!mimeType || !mimeType.startsWith('image/')) {
@@ -125,13 +129,20 @@ class MessageHandler {
125
129
  }
126
130
 
127
131
  async message() {
132
+ this.options.stream = false;
128
133
  const response = await this.execute();
129
134
  return response.message;
130
135
  }
131
136
 
132
137
  async raw() {
133
- const data = await this.execute();
134
- return data.response;
138
+ this.options.stream = false;
139
+ return this.execute();
140
+ }
141
+
142
+ async stream(callback) {
143
+ this.options.stream = true;
144
+ this.modelEntry.streamCallback = callback;
145
+ return this.execute();
135
146
  }
136
147
 
137
148
  groupByRoles(messages) {
@@ -147,10 +158,9 @@ class MessageHandler {
147
158
  }
148
159
 
149
160
  async execute() {
150
-
151
161
  this.messages = this.groupByRoles(this.messages);
152
162
 
153
- if (this.messages.length === 0) { // Only system message is present
163
+ if (this.messages.length === 0) {
154
164
  throw new Error("No user messages have been added. Use addMessage(prompt) to add a message.");
155
165
  }
156
166
 
@@ -171,30 +181,112 @@ class MessageHandler {
171
181
  }
172
182
  }
173
183
 
174
- class OpenAIModel {
175
- constructor(openai, args = { options: {}, config: {} }) {
176
- this.openai = openai;
184
+ class MixCustom {
185
+ constructor(args = { config: {}, options: {}, headers: {} }) {
186
+ this.config = this.getDefaultConfig(args.config);
187
+ this.options = this.getDefaultOptions(args.options);
188
+ this.headers = this.getDefaultHeaders(args.headers);
189
+ this.streamCallback = null; // Definimos streamCallback aquí
190
+ }
177
191
 
178
- this.options = {
179
- frequency_penalty: 0,
180
- presence_penalty: 0,
181
- stream: false,
182
- ...args.options
183
- }
192
+ getDefaultOptions(customOptions) {
193
+ return {
194
+ ...customOptions
195
+ };
196
+ }
184
197
 
185
- this.config = {
186
- prefix: ["gpt"],
187
- max_request: 1,
188
- ...args.config || {}
198
+ getDefaultConfig(customConfig) {
199
+ return {
200
+ url: '',
201
+ apiKey: '',
202
+ prefix: [],
203
+ ...customConfig
204
+ };
205
+ }
206
+
207
+ getDefaultHeaders(customHeaders) {
208
+ return {
209
+ 'accept': 'application/json',
210
+ 'content-type': 'application/json',
211
+ 'authorization': `Bearer ${this.config.apiKey}`,
212
+ ...customHeaders
213
+ };
214
+ }
215
+
216
+ async create(args = { config: {}, options: {} }) {
217
+
218
+ if (args.options.stream) {
219
+ return this.processStream(await axios.post(this.config.url, args.options, {
220
+ headers: this.headers,
221
+ responseType: 'stream'
222
+ }));
223
+ } else {
224
+ return this.processResponse(await axios.post(this.config.url, args.options, {
225
+ headers: this.headers
226
+ }));
189
227
  }
190
228
  }
191
229
 
192
- async create(args = { options: {}, config: {} }) {
230
+ processStream(response) {
231
+ return new Promise((resolve, reject) => {
232
+ let raw = [];
233
+ let message = '';
234
+ let buffer = '';
235
+
236
+ response.data.on('data', chunk => {
237
+ buffer += chunk.toString();
238
+
239
+ let boundary;
240
+ while ((boundary = buffer.indexOf('\n')) !== -1) {
241
+ const dataStr = buffer.slice(0, boundary).trim();
242
+ buffer = buffer.slice(boundary + 1);
243
+
244
+ const firstBraceIndex = dataStr.indexOf('{');
245
+ if (dataStr === '[DONE]' || firstBraceIndex === -1) continue;
246
+
247
+ const jsonStr = dataStr.slice(firstBraceIndex);
248
+ try {
249
+ const data = JSON.parse(jsonStr);
250
+ if (this.streamCallback) {
251
+ const delta = this.extractDelta(data);
252
+ message += delta;
253
+ this.streamCallback({ response: data, message, delta });
254
+ raw.push(data);
255
+ }
256
+ } catch (error) {
257
+ console.error('Error parsing JSON:', error);
258
+ }
259
+ }
260
+ });
261
+
262
+ response.data.on('end', () => resolve({ response: raw, message: message.trim() }));
263
+ response.data.on('error', reject);
264
+ });
265
+ }
266
+
267
+ extractDelta(data) {
268
+ if (data.choices && data.choices[0].delta.content) return data.choices[0].delta.content;
269
+ return '';
270
+ }
271
+
272
+ processResponse(response) {
273
+ return { response: response.data, message: response.data.choices[0].message.content };
274
+ }
275
+ }
193
276
 
277
+ class MixOpenAI extends MixCustom {
278
+ getDefaultConfig(customConfig) {
279
+ return {
280
+ ...super.getDefaultConfig(customConfig),
281
+ url: 'https://api.openai.com/v1/chat/completions',
282
+ prefix: ['gpt']
283
+ };
284
+ }
285
+
286
+ create(args = { config: {}, options: {} }) {
194
287
  args.options.messages = [{ role: 'system', content: args.config.system }, ...args.options.messages || []];
195
288
  args.options.messages = this.convertMessages(args.options.messages);
196
- const response = await this.openai.chat.completions.create(args.options);
197
- return { response, message: response.choices[0].message.content };
289
+ return super.create(args);
198
290
  }
199
291
 
200
292
  convertMessages(messages) {
@@ -218,65 +310,92 @@ class OpenAIModel {
218
310
  }
219
311
  }
220
312
 
221
- class AnthropicModel {
222
- constructor(anthropic, args = { options: {}, config: {} }) {
223
- this.anthropic = anthropic;
224
- this.options = {
225
- temperature: 0.5,
226
- ...args.options || {}
227
- }
228
-
229
- this.config = {
230
- prefix: ["claude"],
231
- max_request: 1,
232
- ...args.config || {}
233
- }
313
+ class MixAnthropic extends MixCustom {
314
+ getDefaultConfig(customConfig) {
315
+ return {
316
+ ...super.getDefaultConfig(customConfig),
317
+ url: 'https://api.anthropic.com/v1/messages',
318
+ prefix: ['claude']
319
+ };
234
320
  }
235
321
 
236
- async create(args = { config: {}, options: {} }) {
237
-
238
- args.options.system = args.config.system;
322
+ getDefaultHeaders() {
323
+ return {
324
+ ...super.getDefaultHeaders(),
325
+ 'x-api-key': this.config.apiKey,
326
+ 'anthropic-version': '2023-06-01'
327
+ };
328
+ }
239
329
 
240
- const response = await this.anthropic.messages.create(args.options);
241
- const responseText = response.content[0].text;
330
+ extractDelta(data) {
331
+ if (data.delta && data.delta.text) return data.delta.text;
332
+ return '';
333
+ }
242
334
 
243
- return { response, message: responseText.trim() };
335
+ processResponse(response) {
336
+ return { response: response.data, message: response.data.content[0].text };
244
337
  }
245
338
  }
246
339
 
247
- class CustomModel {
248
- constructor(args = { config: {}, options: {} }) {
249
- this.config = {
340
+ class MixPerplexity extends MixCustom {
341
+ getDefaultConfig(customConfig) {
342
+ return {
343
+ ...super.getDefaultConfig(customConfig),
250
344
  url: 'https://api.perplexity.ai/chat/completions',
251
- bearer: '',
252
- prefix: ["pplx", "llama", "mixtral"],
253
- max_request: 1,
254
- ...args.config
345
+ prefix: ['pplx', 'llama', 'mixtral']
255
346
  };
347
+ }
256
348
 
257
- this.options = {
258
- return_citations: false,
259
- return_images: false,
260
- stream: false,
261
- presence_penalty: 0,
262
- frequency_penalty: 1,
263
- ...args.options
349
+ create(args = { config: {}, options: {} }) {
350
+ args.options.messages = [{ role: 'system', content: args.config.system }, ...args.options.messages || []];
351
+ return super.create(args);
352
+ }
353
+ }
354
+
355
+ class MixOllama extends MixCustom {
356
+
357
+ getDefaultOptions(customOptions) {
358
+ return {
359
+ options: customOptions,
264
360
  };
265
361
  }
266
362
 
267
- async create(args = { config: {}, options: {} }) {
363
+ extractDelta(data) {
364
+ if (data.message && data.message.content) return data.message.content;
365
+ return '';
366
+ }
367
+
368
+ create(args = { config: {}, options: {} }) {
369
+
370
+ args.options.messages = this.convertMessages(args.options.messages);
268
371
  args.options.messages = [{ role: 'system', content: args.config.system }, ...args.options.messages || []];
269
-
270
- const response = await axios.post(this.config.url, args.options, {
271
- headers: {
272
- 'accept': 'application/json',
273
- 'authorization': `Bearer ${this.config.bearer}`,
274
- 'content-type': 'application/json'
275
- }
276
- });
372
+ return super.create(args);
373
+ }
277
374
 
278
- return { response: response.data, message: response.data.choices[0].message.content };
375
+ processResponse(response) {
376
+ return { response: response.data, message: response.data.message.content.trim() };
377
+ }
378
+
379
+ convertMessages(messages) {
380
+ return messages.map(entry => {
381
+ let content = '';
382
+ let images = [];
383
+
384
+ entry.content.forEach(item => {
385
+ if (item.type === 'text') {
386
+ content += item.text + ' ';
387
+ } else if (item.type === 'image') {
388
+ images.push(item.source.data);
389
+ }
390
+ });
391
+
392
+ return {
393
+ role: entry.role,
394
+ content: content.trim(),
395
+ images: images
396
+ };
397
+ });
279
398
  }
280
399
  }
281
400
 
282
- module.exports = { OpenAIModel, AnthropicModel, CustomModel, ModelMix };
401
+ module.exports = { MixCustom, ModelMix, MixAnthropic, MixOpenAI, MixPerplexity, MixOllama };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modelmix",
3
- "version": "1.2.2",
3
+ "version": "2.0.0",
4
4
  "description": "🧬 ModelMix - Unified API for Diverse AI Language Models.",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -20,7 +20,11 @@
20
20
  "llama",
21
21
  "mixtral",
22
22
  "nlp",
23
- "chat"
23
+ "chat",
24
+ "multimodal",
25
+ "omni",
26
+ "4o",
27
+ "ollama"
24
28
  ],
25
29
  "author": "Martin Clasen",
26
30
  "license": "MIT",