modelmix 4.3.0 β†’ 4.3.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 CHANGED
@@ -92,6 +92,7 @@ This pattern allows you to:
92
92
  - Chain multiple models together
93
93
  - Automatically fall back to the next model if one fails
94
94
  - Get structured JSON responses when needed
95
+ - Track token usage across all providers
95
96
  - Keep your code clean and maintainable
96
97
 
97
98
  ## πŸ”§ Model Context Protocol (MCP) Integration
@@ -138,6 +139,7 @@ Here's a comprehensive list of available methods:
138
139
  | `gpt41mini()` | OpenAI | gpt-4.1-mini | [\$0.40 / \$1.60][1] |
139
140
  | `gpt41nano()` | OpenAI | gpt-4.1-nano | [\$0.10 / \$0.40][1] |
140
141
  | `gptOss()` | Together | gpt-oss-120B | [\$0.15 / \$0.60][7] |
142
+ | `opus46[think]()` | Anthropic | claude-opus-4-6 | [\$5.00 / \$25.00][2] |
141
143
  | `opus45[think]()` | Anthropic | claude-opus-4-5-20251101 | [\$5.00 / \$25.00][2] |
142
144
  | `opus41[think]()` | Anthropic | claude-opus-4-1-20250805 | [\$15.00 / \$75.00][2] |
143
145
  | `sonnet45[think]()`| Anthropic | claude-sonnet-4-5-20250929 | [\$3.00 / \$15.00][2] |
@@ -291,6 +293,24 @@ const result = await model.json(
291
293
 
292
294
  These options give you fine-grained control over how much guidance you provide to the model for generating properly formatted JSON responses.
293
295
 
296
+ ## πŸ“Š Token Usage Tracking
297
+
298
+ ModelMix automatically tracks token usage for all requests across different providers, providing a unified format regardless of the underlying API.
299
+
300
+ ### How it works
301
+
302
+ Every response from `raw()` now includes a `tokens` object with the following structure:
303
+
304
+ ```javascript
305
+ {
306
+ tokens: {
307
+ input: 150, // Number of tokens in the prompt/input
308
+ output: 75, // Number of tokens in the completion/output
309
+ total: 225 // Total tokens used (input + output)
310
+ }
311
+ }
312
+ ```
313
+
294
314
  ## πŸ› Enabling Debug Mode
295
315
 
296
316
  To activate debug mode in ModelMix and view detailed request information, follow these two steps:
@@ -375,7 +395,12 @@ new ModelMix(args = { options: {}, config: {} })
375
395
  - `addImage(filePath, config = { role: "user" })`: Adds an image message from a file path.
376
396
  - `addImageFromUrl(url, config = { role: "user" })`: Adds an image message from URL.
377
397
  - `message()`: Sends the message and returns the response.
378
- - `raw()`: Sends the message and returns the raw response data.
398
+ - `raw()`: Sends the message and returns the complete response data including:
399
+ - `message`: The text response from the model
400
+ - `think`: Reasoning/thinking content (if available)
401
+ - `toolCalls`: Array of tool calls made by the model (if any)
402
+ - `tokens`: Object with `input`, `output`, and `total` token counts
403
+ - `response`: The raw API response
379
404
  - `stream(callback)`: Sends the message and streams the response, invoking the callback with each streamed part.
380
405
  - `json(schemaExample, descriptions = {})`: Forces the model to return a response in a specific JSON format.
381
406
  - `schemaExample`: Optional example of the JSON structure to be returned.
@@ -0,0 +1,18 @@
1
+ process.loadEnvFile();
2
+ import { ModelMix } from '../index.js';
3
+
4
+ // Ejemplo simple: obtener informaciΓ³n de tokens
5
+ const model = ModelMix.new()
6
+ .gpt5nano()
7
+ .addText('What is 2+2?');
8
+
9
+ const result = await model.raw();
10
+
11
+ console.log('\nπŸ“Š Token Usage Information:');
12
+ console.log('━'.repeat(50));
13
+ console.log(`Input tokens: ${result.tokens.input}`);
14
+ console.log(`Output tokens: ${result.tokens.output}`);
15
+ console.log(`Total tokens: ${result.tokens.total}`);
16
+ console.log('━'.repeat(50));
17
+ console.log('\nπŸ’¬ Response:', result.message);
18
+ console.log();
package/demo/tokens.js ADDED
@@ -0,0 +1,109 @@
1
+ process.loadEnvFile();
2
+ import { ModelMix } from '../index.js';
3
+
4
+ console.log('\nπŸ”’ Token Usage Tracking Demo\n');
5
+ console.log('='.repeat(60));
6
+
7
+ // Example 1: Get token usage from a simple request
8
+ console.log('\nπŸ“ Example 1: Basic token usage tracking');
9
+ console.log('-'.repeat(60));
10
+
11
+ const model1 = ModelMix.new({ config: { debug: 1 } })
12
+ .gpt5nano()
13
+ .addText('What is 2+2?');
14
+
15
+ const result1 = await model1.raw();
16
+ console.log('\nπŸ“Š Token Usage:');
17
+ console.log(' Input tokens:', result1.tokens.input);
18
+ console.log(' Output tokens:', result1.tokens.output);
19
+ console.log(' Total tokens:', result1.tokens.total);
20
+ console.log('\nπŸ’¬ Response:', result1.message);
21
+
22
+ // Example 2: Compare token usage across different providers
23
+ console.log('\n\nπŸ“ Example 2: Token usage across providers');
24
+ console.log('-'.repeat(60));
25
+
26
+ const providers = [
27
+ { name: 'OpenAI GPT-5-nano', fn: (m) => m.gpt5nano() },
28
+ { name: 'Anthropic Haiku', fn: (m) => m.haiku35() },
29
+ { name: 'Google Gemini', fn: (m) => m.gemini25flash() }
30
+ ];
31
+
32
+ const prompt = 'Explain quantum computing in one sentence.';
33
+
34
+ for (const provider of providers) {
35
+ try {
36
+ const model = ModelMix.new({ config: { debug: 0 } });
37
+ provider.fn(model).addText(prompt);
38
+
39
+ const result = await model.raw();
40
+
41
+ console.log(`\nπŸ€– ${provider.name}`);
42
+ console.log(` Input: ${result.tokens.input} | Output: ${result.tokens.output} | Total: ${result.tokens.total}`);
43
+ console.log(` Response: ${result.message.substring(0, 80)}...`);
44
+ } catch (error) {
45
+ console.log(`\n❌ ${provider.name}: ${error.message}`);
46
+ }
47
+ }
48
+
49
+ // Example 3: Track tokens in a conversation
50
+ console.log('\n\nπŸ“ Example 3: Token usage in conversation history');
51
+ console.log('-'.repeat(60));
52
+
53
+ const conversation = ModelMix.new({ config: { debug: 0, max_history: 10 } })
54
+ .gpt5nano();
55
+
56
+ let totalInput = 0;
57
+ let totalOutput = 0;
58
+
59
+ // First message
60
+ conversation.addText('Hi! My name is Alice.');
61
+ let result = await conversation.raw();
62
+ totalInput += result.tokens.input;
63
+ totalOutput += result.tokens.output;
64
+ console.log(`\nπŸ’¬ Turn 1: ${result.tokens.input} in, ${result.tokens.output} out`);
65
+
66
+ // Second message (includes history)
67
+ conversation.addText('What is my name?');
68
+ result = await conversation.raw();
69
+ totalInput += result.tokens.input;
70
+ totalOutput += result.tokens.output;
71
+ console.log(`πŸ’¬ Turn 2: ${result.tokens.input} in, ${result.tokens.output} out`);
72
+
73
+ // Third message (includes more history)
74
+ conversation.addText('Tell me a joke about my name.');
75
+ result = await conversation.raw();
76
+ totalInput += result.tokens.input;
77
+ totalOutput += result.tokens.output;
78
+ console.log(`πŸ’¬ Turn 3: ${result.tokens.input} in, ${result.tokens.output} out`);
79
+
80
+ console.log('\nπŸ“Š Conversation totals:');
81
+ console.log(` Total input tokens: ${totalInput}`);
82
+ console.log(` Total output tokens: ${totalOutput}`);
83
+ console.log(` Grand total: ${totalInput + totalOutput}`);
84
+
85
+ // Example 4: JSON response with token tracking
86
+ console.log('\n\nπŸ“ Example 4: JSON response with token tracking');
87
+ console.log('-'.repeat(60));
88
+
89
+ const jsonModel = ModelMix.new({ config: { debug: 0 } })
90
+ .gpt5nano()
91
+ .addText('List 3 programming languages');
92
+
93
+ const jsonResult = await jsonModel.json(
94
+ { languages: [{ name: '', year: 0 }] }
95
+ );
96
+
97
+ // Get raw result for token info
98
+ const rawJsonModel = ModelMix.new({ config: { debug: 0 } })
99
+ .gpt5nano()
100
+ .addText('List 3 programming languages');
101
+
102
+ const rawJsonResult = await rawJsonModel.raw();
103
+
104
+ console.log('\nπŸ“Š Token Usage for JSON response:');
105
+ console.log(` Input: ${rawJsonResult.tokens.input} | Output: ${rawJsonResult.tokens.output} | Total: ${rawJsonResult.tokens.total}`);
106
+ console.log('\nπŸ“‹ JSON Result:', jsonResult);
107
+
108
+ console.log('\n' + '='.repeat(60));
109
+ console.log('βœ… Token tracking demo complete!\n');
package/index.js CHANGED
@@ -190,6 +190,17 @@ class ModelMix {
190
190
  if (mix.openrouter) this.attach('openai/gpt-oss-120b:free', new MixOpenRouter({ options, config }));
191
191
  return this;
192
192
  }
193
+ opus46think({ options = {}, config = {} } = {}) {
194
+ options = { ...MixAnthropic.thinkingOptions, ...options };
195
+ return this.attach('claude-opus-4-6', new MixAnthropic({ options, config }));
196
+ }
197
+ opus45think({ options = {}, config = {} } = {}) {
198
+ options = { ...MixAnthropic.thinkingOptions, ...options };
199
+ return this.attach('claude-opus-4-5-20251101', new MixAnthropic({ options, config }));
200
+ }
201
+ opus46({ options = {}, config = {} } = {}) {
202
+ return this.attach('claude-opus-4-6', new MixAnthropic({ options, config }));
203
+ }
193
204
  opus45({ options = {}, config = {} } = {}) {
194
205
  return this.attach('claude-opus-4-5-20251101', new MixAnthropic({ options, config }));
195
206
  }
@@ -1135,7 +1146,8 @@ class MixCustom {
1135
1146
  response: raw,
1136
1147
  message: message.trim(),
1137
1148
  toolCalls: [],
1138
- think: null
1149
+ think: null,
1150
+ tokens: raw.length > 0 ? MixCustom.extractTokens(raw[raw.length - 1]) : { input: 0, output: 0, total: 0 }
1139
1151
  }));
1140
1152
  response.data.on('error', reject);
1141
1153
  });
@@ -1181,11 +1193,28 @@ class MixCustom {
1181
1193
  })) || []
1182
1194
  }
1183
1195
 
1196
+ static extractTokens(data) {
1197
+ // OpenAI/Groq/Together/Lambda/Cerebras/Fireworks format
1198
+ if (data.usage) {
1199
+ return {
1200
+ input: data.usage.prompt_tokens || 0,
1201
+ output: data.usage.completion_tokens || 0,
1202
+ total: data.usage.total_tokens || 0
1203
+ };
1204
+ }
1205
+ return {
1206
+ input: 0,
1207
+ output: 0,
1208
+ total: 0
1209
+ };
1210
+ }
1211
+
1184
1212
  processResponse(response) {
1185
1213
  return {
1186
1214
  message: MixCustom.extractMessage(response.data),
1187
1215
  think: MixCustom.extractThink(response.data),
1188
1216
  toolCalls: MixCustom.extractToolCalls(response.data),
1217
+ tokens: MixCustom.extractTokens(response.data),
1189
1218
  response: response.data
1190
1219
  }
1191
1220
  }
@@ -1331,7 +1360,7 @@ class MixAnthropic extends MixCustom {
1331
1360
  static thinkingOptions = {
1332
1361
  thinking: {
1333
1362
  "type": "enabled",
1334
- "budget_tokens": 1024
1363
+ "budget_tokens": 1638
1335
1364
  },
1336
1365
  temperature: 1
1337
1366
  };
@@ -1478,11 +1507,28 @@ class MixAnthropic extends MixCustom {
1478
1507
  return data.content[0]?.signature || null;
1479
1508
  }
1480
1509
 
1510
+ static extractTokens(data) {
1511
+ // Anthropic format
1512
+ if (data.usage) {
1513
+ return {
1514
+ input: data.usage.input_tokens || 0,
1515
+ output: data.usage.output_tokens || 0,
1516
+ total: (data.usage.input_tokens || 0) + (data.usage.output_tokens || 0)
1517
+ };
1518
+ }
1519
+ return {
1520
+ input: 0,
1521
+ output: 0,
1522
+ total: 0
1523
+ };
1524
+ }
1525
+
1481
1526
  processResponse(response) {
1482
1527
  return {
1483
1528
  message: MixAnthropic.extractMessage(response.data),
1484
1529
  think: MixAnthropic.extractThink(response.data),
1485
1530
  toolCalls: MixAnthropic.extractToolCalls(response.data),
1531
+ tokens: MixAnthropic.extractTokens(response.data),
1486
1532
  response: response.data,
1487
1533
  signature: MixAnthropic.extractSignature(response.data)
1488
1534
  }
@@ -1706,6 +1752,7 @@ class MixLMStudio extends MixCustom {
1706
1752
  message: MixLMStudio.extractMessage(response.data),
1707
1753
  think: MixLMStudio.extractThink(response.data),
1708
1754
  toolCalls: MixCustom.extractToolCalls(response.data),
1755
+ tokens: MixCustom.extractTokens(response.data),
1709
1756
  response: response.data
1710
1757
  };
1711
1758
  }
@@ -1926,6 +1973,7 @@ class MixGoogle extends MixCustom {
1926
1973
  message: MixGoogle.extractMessage(response.data),
1927
1974
  think: null,
1928
1975
  toolCalls: MixGoogle.extractToolCalls(response.data),
1976
+ tokens: MixGoogle.extractTokens(response.data),
1929
1977
  response: response.data
1930
1978
  }
1931
1979
  }
@@ -1951,6 +1999,22 @@ class MixGoogle extends MixCustom {
1951
1999
  return data.candidates?.[0]?.content?.parts?.[0]?.text;
1952
2000
  }
1953
2001
 
2002
+ static extractTokens(data) {
2003
+ // Google Gemini format
2004
+ if (data.usageMetadata) {
2005
+ return {
2006
+ input: data.usageMetadata.promptTokenCount || 0,
2007
+ output: data.usageMetadata.candidatesTokenCount || 0,
2008
+ total: data.usageMetadata.totalTokenCount || 0
2009
+ };
2010
+ }
2011
+ return {
2012
+ input: 0,
2013
+ output: 0,
2014
+ total: 0
2015
+ };
2016
+ }
2017
+
1954
2018
  static getOptionsTools(tools) {
1955
2019
  const functionDeclarations = [];
1956
2020
  for (const tool in tools) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modelmix",
3
- "version": "4.3.0",
3
+ "version": "4.3.4",
4
4
  "description": "🧬 Reliable interface with automatic fallback for AI LLMs.",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "homepage": "https://github.com/clasen/ModelMix#readme",
48
48
  "dependencies": {
49
- "@modelcontextprotocol/sdk": "^1.25.3",
49
+ "@modelcontextprotocol/sdk": "^1.26.0",
50
50
  "axios": "^1.12.2",
51
51
  "bottleneck": "^2.19.5",
52
52
  "file-type": "^16.5.4",
@@ -69,6 +69,7 @@
69
69
  "test:images": "mocha test/images.test.js --timeout 10000 --require test/setup.js",
70
70
  "test:bottleneck": "mocha test/bottleneck.test.js --timeout 10000 --require test/setup.js",
71
71
  "test:live": "mocha test/live.test.js --timeout 10000 --require dotenv/config --require test/setup.js",
72
- "test:live.mcp": "mocha test/live.mcp.js --timeout 60000 --require dotenv/config --require test/setup.js"
72
+ "test:live.mcp": "mocha test/live.mcp.js --timeout 60000 --require dotenv/config --require test/setup.js",
73
+ "test:tokens": "mocha test/tokens.test.js --timeout 10000 --require dotenv/config --require test/setup.js"
73
74
  }
74
75
  }
@@ -0,0 +1,135 @@
1
+ import { expect } from 'chai';
2
+ import { ModelMix } from '../index.js';
3
+
4
+ describe('Token Usage Tracking', () => {
5
+
6
+ it('should track tokens in OpenAI response', async function () {
7
+ this.timeout(30000);
8
+
9
+ const model = ModelMix.new()
10
+ .gpt5nano()
11
+ .addText('Say hi');
12
+
13
+ const result = await model.raw();
14
+
15
+ expect(result).to.have.property('tokens');
16
+ expect(result.tokens).to.have.property('input');
17
+ expect(result.tokens).to.have.property('output');
18
+ expect(result.tokens).to.have.property('total');
19
+
20
+ expect(result.tokens.input).to.be.a('number');
21
+ expect(result.tokens.output).to.be.a('number');
22
+ expect(result.tokens.total).to.be.a('number');
23
+
24
+ expect(result.tokens.input).to.be.greaterThan(0);
25
+ expect(result.tokens.output).to.be.greaterThan(0);
26
+ expect(result.tokens.total).to.be.greaterThan(0);
27
+ });
28
+
29
+ it('should track tokens in Anthropic response', async function () {
30
+ this.timeout(30000);
31
+
32
+ const model = ModelMix.new()
33
+ .haiku35()
34
+ .addText('Say hi');
35
+
36
+ const result = await model.raw();
37
+
38
+ expect(result).to.have.property('tokens');
39
+ expect(result.tokens).to.have.property('input');
40
+ expect(result.tokens).to.have.property('output');
41
+ expect(result.tokens).to.have.property('total');
42
+
43
+ expect(result.tokens.input).to.be.greaterThan(0);
44
+ expect(result.tokens.output).to.be.greaterThan(0);
45
+ expect(result.tokens.total).to.equal(result.tokens.input + result.tokens.output);
46
+ });
47
+
48
+ it('should track tokens in Google Gemini response', async function () {
49
+ this.timeout(30000);
50
+
51
+ const model = ModelMix.new()
52
+ .gemini25flash()
53
+ .addText('Say hi');
54
+
55
+ const result = await model.raw();
56
+
57
+ expect(result).to.have.property('tokens');
58
+ expect(result.tokens).to.have.property('input');
59
+ expect(result.tokens).to.have.property('output');
60
+ expect(result.tokens).to.have.property('total');
61
+
62
+ expect(result.tokens.input).to.be.greaterThan(0);
63
+ expect(result.tokens.output).to.be.greaterThan(0);
64
+ expect(result.tokens.total).to.be.greaterThan(0);
65
+ });
66
+
67
+ it('should accumulate tokens across conversation turns', async function () {
68
+ this.timeout(60000);
69
+
70
+ const conversation = ModelMix.new({ config: { max_history: 10 } })
71
+ .gpt5nano();
72
+
73
+ // First turn
74
+ conversation.addText('My name is Alice');
75
+ const result1 = await conversation.raw();
76
+
77
+ expect(result1.tokens.input).to.be.greaterThan(0);
78
+ expect(result1.tokens.output).to.be.greaterThan(0);
79
+
80
+ // Second turn (should have more input tokens due to history)
81
+ conversation.addText('What is my name?');
82
+ const result2 = await conversation.raw();
83
+
84
+ expect(result2.tokens.input).to.be.greaterThan(result1.tokens.input);
85
+ expect(result2.tokens.output).to.be.greaterThan(0);
86
+
87
+ // Verify both results have valid token counts
88
+ expect(result1.tokens.total).to.equal(result1.tokens.input + result1.tokens.output);
89
+ expect(result2.tokens.total).to.be.greaterThan(0);
90
+ });
91
+
92
+ it('should track tokens with JSON responses', async function () {
93
+ this.timeout(30000);
94
+
95
+ const model = ModelMix.new()
96
+ .gpt5nano()
97
+ .addText('Return a simple greeting');
98
+
99
+ // Using raw() to get token info
100
+ const result = await model.raw();
101
+
102
+ expect(result).to.have.property('tokens');
103
+ expect(result.tokens.input).to.be.greaterThan(0);
104
+ expect(result.tokens.output).to.be.greaterThan(0);
105
+ expect(result.tokens.total).to.be.greaterThan(0);
106
+ });
107
+
108
+ it('should have consistent token format across providers', async function () {
109
+ this.timeout(90000);
110
+
111
+ const providers = [
112
+ { name: 'OpenAI', create: (m) => m.gpt5nano() },
113
+ { name: 'Anthropic', create: (m) => m.haiku35() },
114
+ { name: 'Google', create: (m) => m.gemini25flash() }
115
+ ];
116
+
117
+ for (const provider of providers) {
118
+ const model = ModelMix.new();
119
+ provider.create(model).addText('Hi');
120
+
121
+ const result = await model.raw();
122
+
123
+ // Verify consistent structure
124
+ expect(result.tokens, `${provider.name} should have tokens object`).to.exist;
125
+ expect(result.tokens.input, `${provider.name} should have input`).to.be.a('number');
126
+ expect(result.tokens.output, `${provider.name} should have output`).to.be.a('number');
127
+ expect(result.tokens.total, `${provider.name} should have total`).to.be.a('number');
128
+
129
+ // Verify values are positive
130
+ expect(result.tokens.input, `${provider.name} input should be > 0`).to.be.greaterThan(0);
131
+ expect(result.tokens.output, `${provider.name} output should be > 0`).to.be.greaterThan(0);
132
+ expect(result.tokens.total, `${provider.name} total should be > 0`).to.be.greaterThan(0);
133
+ }
134
+ });
135
+ });