modelmix 5.1.20 → 5.2.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/.gitignore ADDED
@@ -0,0 +1,138 @@
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ lerna-debug.log*
8
+ .pnpm-debug.log*
9
+
10
+ # Diagnostic reports (https://nodejs.org/api/report.html)
11
+ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12
+
13
+ # Runtime data
14
+ pids
15
+ *.pid
16
+ *.seed
17
+ *.pid.lock
18
+
19
+ # Directory for instrumented libs generated by jscoverage/JSCover
20
+ lib-cov
21
+
22
+ # Coverage directory used by tools like istanbul
23
+ coverage
24
+ *.lcov
25
+
26
+ # nyc test coverage
27
+ .nyc_output
28
+
29
+ # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30
+ .grunt
31
+
32
+ # Bower dependency directory (https://bower.io/)
33
+ bower_components
34
+
35
+ # node-waf configuration
36
+ .lock-wscript
37
+
38
+ # Compiled binary addons (https://nodejs.org/api/addons.html)
39
+ build/Release
40
+
41
+ # Dependency directories
42
+ node_modules/
43
+ jspm_packages/
44
+
45
+ # Snowpack dependency directory (https://snowpack.dev/)
46
+ web_modules/
47
+
48
+ # TypeScript cache
49
+ *.tsbuildinfo
50
+
51
+ # Optional npm cache directory
52
+ .npm
53
+
54
+ # Optional eslint cache
55
+ .eslintcache
56
+
57
+ # Optional stylelint cache
58
+ .stylelintcache
59
+
60
+ # Microbundle cache
61
+ .rpt2_cache/
62
+ .rts2_cache_cjs/
63
+ .rts2_cache_es/
64
+ .rts2_cache_umd/
65
+
66
+ # Optional REPL history
67
+ .node_repl_history
68
+
69
+ # Output of 'npm pack'
70
+ *.tgz
71
+
72
+ # Yarn Integrity file
73
+ .yarn-integrity
74
+
75
+ # dotenv environment variable files
76
+ .env
77
+ .env.development.local
78
+ .env.test.local
79
+ .env.production.local
80
+ .env.local
81
+
82
+ # parcel-bundler cache (https://parceljs.org/)
83
+ .cache
84
+ .parcel-cache
85
+
86
+ # Next.js build output
87
+ .next
88
+ out
89
+
90
+ # Nuxt.js build / generate output
91
+ .nuxt
92
+ dist
93
+
94
+ # Gatsby files
95
+ .cache/
96
+ # Comment in the public line in if your project uses Gatsby and not Next.js
97
+ # https://nextjs.org/blog/next-9-1#public-directory-support
98
+ # public
99
+
100
+ # vuepress build output
101
+ .vuepress/dist
102
+
103
+ # vuepress v2.x temp and cache directory
104
+ .temp
105
+ .cache
106
+
107
+ # Docusaurus cache and generated files
108
+ .docusaurus
109
+
110
+ # Serverless directories
111
+ .serverless/
112
+
113
+ # FuseBox cache
114
+ .fusebox/
115
+
116
+ # DynamoDB Local files
117
+ .dynamodb/
118
+
119
+ # TernJS port file
120
+ .tern-port
121
+
122
+ # Stores VSCode versions used for testing VSCode extensions
123
+ .vscode-test
124
+
125
+ # yarn v2
126
+ .yarn/cache
127
+ .yarn/unplugged
128
+ .yarn/build-state.yml
129
+ .yarn/install-state.gz
130
+ .pnp.*
131
+
132
+ .DS_Store
133
+ demo/jailbreak.mjs
134
+ CLAUDE.md
135
+ demo/jailbreak.js
136
+ /demo/lab
137
+ /demo/results
138
+ /.pnpm-store/
package/README.md CHANGED
@@ -53,6 +53,7 @@ OPENAI_API_KEY="sk-proj-..."
53
53
  OPENROUTER_API_KEY="sk-or-..."
54
54
  MOONSHOT_API_KEY="your-moonshot-key..."
55
55
  MINIMAX_API_KEY="your-minimax-key..."
56
+ DEEPSEEK_API_KEY="your-deepseek-key..."
56
57
  NVIDIA_API_KEY="nvapi-..."
57
58
  ...
58
59
  GEMINI_API_KEY="AIza..."
@@ -209,7 +210,9 @@ ModelMix provides convenient shorthand methods for quickly accessing different A
209
210
  | `qwen3827b()` | OpenRouter | qwen/qwen3.8-27b | [\$0.45][15] | [\$3.20][15] |
210
211
  | `qwen38flash()` | OpenRouter | qwen/qwen3.8-flash | [\$0.16][19] | [\$0.47][19] |
211
212
  | `deepseekV4Flash()` | Fireworks | models/deepseek-v4-flash | [\$0.14][10] | [\$0.28][10] |
213
+ | `deepseekV41Flash()` | OpenRouter | deepseek/deepseek-v4.1-flash | [\$0.15][27] | [\$0.60][27] |
212
214
  | `deepseekV4Pro()` | Fireworks | models/deepseek-v4-pro-0813 | [\$1.32][12] | [\$3.96][12] |
215
+ | `deepseekPro()` | OpenRouter | deepseek/deepseek-v4-pro-0813 | [\$0.5808][28] | [\$1.7424][28] |
213
216
  | `GLM53()` | OpenRouter | z-ai/glm-5.3 | [\$1.40][16] | [\$4.40][16] |
214
217
  | `GLM53Flash()` | OpenRouter | z-ai/glm-5.3-flash | [\$0.075][20] | [\$0.25][20] |
215
218
  | `GLM52()` | Together | zai-org/GLM-5.2 | [\$1.40][7] | [\$4.40][7] |
@@ -262,6 +265,16 @@ OpenRouter fallbacks are disabled globally by default and are appended only with
262
265
 
263
266
  [25]: https://openrouter.ai/meta/muse-spark-1.2 "Muse Spark 1.2 on OpenRouter"
264
267
  [26]: https://openrouter.ai/meta/muse-spark-1.3-contributor "Muse Spark 1.3 Contributor on OpenRouter"
268
+ [27]: https://openrouter.ai/deepseek/deepseek-v4.1-flash "DeepSeek V4.1 Flash on OpenRouter"
269
+ [28]: https://openrouter.ai/deepseek/deepseek-v4-pro-0813 "DeepSeek V4 Pro 0813 on OpenRouter"
270
+
271
+ `deepseekPro()` uses the pinned OpenRouter model `deepseek/deepseek-v4-pro-0813` and requires `OPENROUTER_API_KEY`. Use `chain('deepseekPro@100')` for maximum reasoning effort. Cost estimates use the listed base rates and $0.05808/M cached input tokens; actual rates may change, including provider and time-based pricing.
272
+
273
+ `deepseekV41Flash()` supports text and image input through OpenRouter and requires `OPENROUTER_API_KEY`. Use `chain('deepseekV41Flash@100')` for maximum reasoning effort. Cost estimates use the listed base rates and $0.015/M cached input tokens; actual OpenRouter pricing varies by provider and time.
274
+
275
+ For direct Fireworks access, use `deepseekV41Flash({ mix: { fireworks: true, openrouter: false } })` with `FIREWORKS_API_KEY`. This selects `accounts/fireworks/models/deepseek-v4p1-flash`, priced at [$0.22 input / $0.007 cached input / $0.66 output per 1M tokens](https://fireworks.ai/models/deepseek-ai/deepseek-v4p1-flash). Set both providers to `true` to try Fireworks first and fall back to OpenRouter.
276
+
277
+ For the native DeepSeek API, use `deepseekV41Flash({ mix: { deepseek: true, openrouter: false } })` with `DEEPSEEK_API_KEY`. It calls `https://api.deepseek.com/chat/completions` with `deepseek-flash`, currently DeepSeek V4.1 Flash. `MixDeepSeek` is also available for explicit `.attach()` calls. Native cost estimates use [peak rates](https://api-docs.deepseek.com/quick_start/pricing/): $0.30 input / $0.006 cached input / $1.20 output per 1M tokens; actual off-peak charges are half. When all three providers are enabled, the order is DeepSeek → Fireworks → OpenRouter.
265
278
 
266
279
  Muse Spark methods ending in `c` select Contributor: prompts and outputs may be used to improve Meta products. Methods without `c` select the standard tier. `museSpark12()` now selects standard; use `museSpark12c()` for the previous Contributor behavior.
267
280
 
@@ -755,7 +768,7 @@ Every response from `raw()` now includes a `tokens` object with the following st
755
768
  cacheSavings: 0.00018432, // USD saved by cache reads
756
769
  cacheWritePremium: 0, // Extra USD paid to write this cache entry
757
770
  breakEvenHits: 0, // Full future hits needed to recover that premium
758
- cost: 0.00011568, // Total estimated cost in USD
771
+ cost: 0.00011568, // OpenRouter charge when reported; otherwise estimated USD
759
772
  costBreakdown: {
760
773
  uncachedInput: 0.0000352,
761
774
  cachedInput: 0.00002048,
@@ -780,7 +793,7 @@ console.log(model.lastRaw.tokens);
780
793
  // Same normalized token and cost structure returned by raw()
781
794
  ```
782
795
 
783
- `thinking` contains internal reasoning tokens when a provider reports them separately; cost calculation bills them at the output rate. `cached` aggregates cache reads reported by the provider, while `cacheWrite` aggregates cache writes. Anthropic additionally exposes `cacheWrite5m` and `cacheWrite1h` because those writes cost 1.25× and 2× the normal input rate, respectively. `cacheSavings` compares cache reads with the normal input rate, `cacheWritePremium` compares writes with that rate, and `breakEvenHits` estimates how many complete future hits recover the current write premium. For Anthropic, `input` is normalized to include uncached input, cache reads, and cache writes. Missing usage or pricing categories return `0`. The `speed` field is the generation speed measured in output tokens per second (integer).
796
+ `thinking` contains internal reasoning tokens when a provider reports them separately; `output` excludes those tokens. Cost calculation bills `output + thinking` once at the output rate. OpenAI Chat/Responses and Anthropic include reasoning in their native output totals; native Grok reports it separately, while Gemini uses `thoughtsTokenCount`. `cost` uses OpenRouter's reported charge when available (including zero); otherwise it uses catalog pricing. `costBreakdown` and cache savings remain catalog estimates and can differ from the actual charge. `cached` aggregates cache reads reported by the provider, while `cacheWrite` aggregates cache writes. Anthropic additionally exposes `cacheWrite5m` and `cacheWrite1h` because those writes cost 1.25× and 2× the normal input rate, respectively. `cacheSavings` compares cache reads with the normal input rate, `cacheWritePremium` compares writes with that rate, and `breakEvenHits` estimates how many complete future hits recover the current write premium. For Anthropic, `input` is normalized to include uncached input, cache reads, and cache writes. Missing usage or pricing categories return `0`. The `speed` field is the generation speed measured in output tokens per second (integer).
784
797
 
785
798
  ## 🧠 Prompt Caching
786
799
 
@@ -987,6 +1000,33 @@ Supported policies are `'inherit'`, `'none'`, `{ include: [...] }`, and `{ exclu
987
1000
 
988
1001
  Child `systemFile` templates use the same EJS engine, `assign()` data contract, and relative Markdown includes as ordinary ModelMix templates. Use either `system` or `systemFile`, not both.
989
1002
 
1003
+ ### Benchmark plugin
1004
+
1005
+ The included benchmark plugin derives task-specific criteria, runs each configured model independently, and uses the other distinct models as anonymous judges. Model specifications use the same `shortcut@effort` syntax as `chain()`:
1006
+
1007
+ ```javascript
1008
+ const { ModelMix } = require('modelmix');
1009
+ const { benchmark } = require('modelmix/plugins/benchmark');
1010
+
1011
+ const report = await ModelMix.new()
1012
+ .use(benchmark({
1013
+ criteriaModel: 'gpt56luna@20',
1014
+ models: ['gpt56luna@20', 'sonnet5@20', 'gemini38flash@20']
1015
+ }))
1016
+ .addText('Your benchmark task')
1017
+ .json();
1018
+ ```
1019
+
1020
+ Each criterion is scored from 0 to 10 with equal weight. The report contains the original task, criteria, responses, individual evaluations, averages, errors, elapsed time, token usage, and available cost estimates. Failed responses or evaluations are recorded while the remaining models continue; criteria-generation failures and cancellation stop the run. The first version accepts text tasks, runs sequentially, and does not support streaming.
1021
+
1022
+ Set `config.debug: 1` to print progress and failure reasons. Failed model outputs retain their text and provider finish reason in `errors[].error.details`; outputs marked as truncated are excluded from scoring. Adjust `options.max_tokens` to allow enough room for reasoning and output.
1023
+
1024
+ Pass `mix` to `benchmark()` to select providers for the criteria model, participants, and judges using each shortcut's provider flags. For example, `mix: { deepseek: true, openrouter: false }` routes `deepseekV41Flash` directly to the official DeepSeek API using `DEEPSEEK_API_KEY`. Omitting `mix` preserves each shortcut's default providers.
1025
+
1026
+ JSON parsing accepts a Markdown code block or a lone closing triple-backtick delimiter after valid JSON. Other extra content, malformed JSON, and invalid evaluation scores remain errors.
1027
+
1028
+ Run `node demo/benchmark.js` for a complete five-model comparison. It allows up to 32,768 output tokens per call, prints intermediate progress, a final ranking and error details, then saves each response as Markdown together with the full JSON report under `demo/results/`.
1029
+
990
1030
  ### Recursive Language Model plugin
991
1031
 
992
1032
  The separately publishable `@modelmix/rlm` workspace package keeps document parsing, planner prompts, and `isolated-vm` out of the core `modelmix` dependency tree. It requires Node.js 22 or newer.
@@ -0,0 +1,83 @@
1
+ import { ModelMix } from '../index.js';
2
+ import benchmarkPlugin from '../plugins/benchmark/index.js';
3
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ try { process.loadEnvFile(); } catch {}
8
+
9
+ const { benchmark } = benchmarkPlugin;
10
+
11
+ const task = await readFile(process.argv[2] || new URL('./prompts/story.txt', import.meta.url), 'utf8');
12
+
13
+ const models = [
14
+ // 'opus50@20',
15
+ 'gpt6astra@0',
16
+ 'gpt6astra@20',
17
+ 'gpt56sol@40',
18
+ // 'gemini38flash@20',
19
+ // 'grok46@20',
20
+ 'deepseekV41Flash@60'
21
+ ];
22
+
23
+ console.log(`Running benchmark: ${models.length} models, 26 sequential model calls.`);
24
+
25
+ const report = await ModelMix.new({
26
+ config: { debug: 1 },
27
+ options: { max_tokens: 65536 }
28
+ })
29
+ .use(benchmark({
30
+ criteriaModel: 'opus50@20',
31
+ models,
32
+ mix: { deepseek: true, openrouter: false }
33
+ }))
34
+ .assign({ task })
35
+ .addText('<%- task %>')
36
+ .json();
37
+
38
+ const runId = new Date().toISOString().replace(/[:.]/g, '-');
39
+ const demoDirectory = path.dirname(fileURLToPath(import.meta.url));
40
+ const resultsDirectory = path.join(demoDirectory, 'results', runId);
41
+ await mkdir(resultsDirectory, { recursive: true });
42
+
43
+ for (const result of report.results) {
44
+ if (result.response === null) continue;
45
+ const filename = result.id.replace(/[^A-Za-z0-9_-]/g, '_');
46
+ const contents = `# ${result.id}\n\nScore: ${result.score === null ? 'N/A' : Number(result.score.toFixed(2))}\n\nEstimated generation cost (USD): ${result.responseMetrics?.cost ?? 'N/A'}\n\n---\n\n${result.response}\n`;
47
+ await writeFile(path.join(resultsDirectory, `${filename}.md`), contents, 'utf8');
48
+ }
49
+
50
+ await writeFile(
51
+ path.join(resultsDirectory, 'report.json'),
52
+ `${JSON.stringify(report, null, 2)}\n`,
53
+ 'utf8'
54
+ );
55
+
56
+ const ranking = report.results
57
+ .map(result => ({
58
+ model: result.id,
59
+ score: result.score,
60
+ cost: result.responseMetrics?.cost?.toFixed(2) ?? 'N/A',
61
+ timeSeconds: Number.isFinite(result.responseMetrics?.elapsedMs)
62
+ ? (result.responseMetrics.elapsedMs / 1000).toFixed(2)
63
+ : 'N/A',
64
+ evaluations: `${result.evaluationCount.valid}/${result.evaluationCount.expected}`
65
+ }))
66
+ .sort((left, right) => {
67
+ if (left.score === right.score) return 0;
68
+ if (left.score === null) return 1;
69
+ if (right.score === null) return -1;
70
+ return right.score - left.score;
71
+ })
72
+ .map(row => ({ ...row, score: row.score === null ? null : Number(row.score.toFixed(2)) }));
73
+
74
+ console.table(ranking);
75
+ if (report.errors.length > 0) {
76
+ console.table(report.errors.map(({ stage, participant, judge, error }) => ({
77
+ stage,
78
+ participant,
79
+ judge,
80
+ error: error.details?.error?.message ?? error.message
81
+ })));
82
+ }
83
+ console.log(`Full responses and report saved to ${resultsDirectory}`);
package/demo/package.json CHANGED
@@ -6,6 +6,8 @@
6
6
  "main": "index.js",
7
7
  "scripts": {
8
8
  "test": "echo \"Error: no test specified\" && exit 1",
9
+ "benchmark": "node benchmark.js",
10
+ "benchmark:template-engine": "node benchmark.js prompts/template-engine.txt",
9
11
  "rlm-basic": "node rlm-basic.js",
10
12
  "rlm-simple": "node rlm-simple.js",
11
13
  "repl-powers": "node repl-powers.js"
@@ -0,0 +1,35 @@
1
+ Escribí un cuento breve (600–900 palabras), de cualquier género y ambientación, que cumpla estas restricciones de construcción:
2
+
3
+ ESTRUCTURA
4
+
5
+ - Circular: el protagonista termina en el mismo lugar físico donde empieza, pero al final ese lugar significa lo contrario (refugio → trampa, punto de partida → castigo, etc.).
6
+ - El protagonista enfrenta una prueba o rito con una regla explícita. El criterio real de evaluación es otro, nunca se enuncia, y solo se revela en la última línea de diálogo.
7
+ - El protagonista cumple la tarea visible, pagando un costo moral o físico real, y aun así fracasa en la prueba verdadera.
8
+ - El final muestra la inminencia de la consecuencia, no la consecuencia. Cortar justo antes.
9
+ - La consecuencia final es simétrica: lo que el protagonista dañó durante la prueba vuelve contra él.
10
+
11
+ SIEMBRA
12
+
13
+ - Un personaje marginal y sin peso aparente (un loco, un borracho, un niño, una voz anónima) anuncia el final con una frase críptica. El protagonista lo ignora.
14
+ - Al menos dos detalles que parecen ambientales o caprichosos deben resultar funcionales al final (por ejemplo, algo que impide al protagonista orientarse, o una marca física que lo delata).
15
+ - El momento en que el protagonista falla debe estar a la vista del lector, sin ser señalado como decisivo. Debe resignificarse en la relectura.
16
+ - Una frase o instrucción repetida al principio y al final que enmarque el recorrido.
17
+
18
+ PERSONAJES
19
+
20
+ - Protagonista sin nombre, designado por su función o rol.
21
+ - Antagonista/evaluador con apodo, lacónico, no explica nada y tiene un rasgo físico que oculta (algo que nunca se saca o nunca muestra).
22
+ - La crueldad o arbitrariedad del evaluador se muestra con acciones, sin juicio del narrador.
23
+ - El protagonista enfrenta una elección concreta dentro de la prueba (no si hacerlo, sino cómo o a quién), que lo hace más responsable.
24
+ - Hacia el final, el protagonista hace preguntas que asumen su éxito y no reciben respuesta.
25
+
26
+ VOZ Y TONO
27
+
28
+ - Narrador en primera persona, en pasado, con tono de anécdota oral o leyenda urbana; registro coloquial.
29
+ - El primer párrafo resume la premisa sin rodeos. El suspenso no depende de ocultar de qué trata.
30
+ - Contraste entre un marco cotidiano o burocrático y un contenido brutal o extraño, contado con naturalidad.
31
+ - Al menos una secuencia de diálogo con repetición y escalada (la misma orden dicha varias veces, subiendo de intensidad).
32
+ - Remate: una línea de diálogo muy breve (dos o tres palabras) que explica el fracaso, seguida de la revelación espacial y la amenaza.
33
+ - Economía: descripción mínima, acceso limitado a la mente del protagonista, sin moraleja explícita.
34
+
35
+ No imites la trama de ningún cuento existente. Inventá la prueba, el mundo y la consecuencia desde cero.
@@ -0,0 +1,41 @@
1
+ Develop a minimalist template engine as a Node.js library using ES modules and no external dependencies.
2
+
3
+ The complete implementation must fit within 200 physical lines, including comments and blank lines. Do not minify the code or artificially pack statements onto single lines. Write code and identifiers in English, and explanations in Spanish.
4
+
5
+ Implement these three features:
6
+
7
+ 1. Compilation to JavaScript
8
+ - `compile(template, options)` returns a reusable function that accepts data and produces a string.
9
+ - Support `<% code %>` for executing JavaScript, `<%= expression %>` for HTML-escaped output, and `<%- expression %>` for unescaped output.
10
+ - Allow conditionals and loops to open and close across separate tags, with text between them.
11
+ - Correctly preserve quotes, backslashes, and line breaks in literal text.
12
+ 2. Errors linked to the original template
13
+ - Runtime errors must report the filename, original line number, and a short template excerpt.
14
+ - Preserve the original error as `cause`.
15
+ - For errors inside includes, identify the file where the failure actually occurred.
16
+ - For syntax errors, report at least the filename. Do not invent a line number if it cannot be determined accurately.
17
+ 3. Nested includes
18
+ - Provide an `include(path, data)` function inside templates.
19
+ - Resolve each relative path from the file containing the include call.
20
+ - Includes inherit parent data and may override values locally without mutating the parent data.
21
+ - Support multiple nesting levels.
22
+ - Detect inclusion cycles and report the chain of files involved.
23
+ - Provide clear errors for missing files.
24
+
25
+ Public API:
26
+
27
+ - `compile(template, options)`
28
+ - `render(template, data, options)`
29
+ - `renderFile(filename, data)`
30
+
31
+ Keep everything synchronous. Require explicit property access through the `data` object inside templates, without `with`. Do not add caching, layouts, plugins, configurable delimiters, or browser support.
32
+
33
+ Templates are trusted code. Explicitly document that the engine provides no sandbox and must not execute templates supplied by untrusted users.
34
+
35
+ Deliver only:
36
+
37
+ - The complete implementation in a single file.
38
+ - The actual line count of the implementation file.
39
+ - A brief explanation of limitations.
40
+
41
+ Do not include tests, examples, sample templates, or demonstration code. Prioritize clarity and correctness over additional features.
package/demo/short.js CHANGED
@@ -28,6 +28,8 @@ const mmix = await ModelMix.new(setup)
28
28
  .museSpark12() // (fallback 17) OpenRouter meta/muse-spark-1.2
29
29
  .museSpark13c() // (fallback 18) OpenRouter meta/muse-spark-1.3-contributor
30
30
  .gpt6astra() // (fallback 19) OpenAI gpt-6-astra
31
+ .deepseekV41Flash({ mix: { deepseek: true, fireworks: true, openrouter: true } }) // (fallback 20 + provider fallback) DeepSeek/Fireworks/OpenRouter V4.1 Flash
32
+ .deepseekPro() // (fallback 21) OpenRouter deepseek/deepseek-v4-pro-0813
31
33
  .addText("What's your name?");
32
34
 
33
35
  console.log(await mmix.message());
package/effort.js CHANGED
@@ -125,6 +125,7 @@ const PROVIDER_FAMILY_BY_CLASS = {
125
125
  MixKimi: 'openai',
126
126
  MixMiniMax: 'openai',
127
127
  MixMiMo: 'openai',
128
+ MixDeepSeek: 'openai',
128
129
  MixGrok: 'openai',
129
130
  MixGroq: 'openai',
130
131
  MixTogether: 'openai',
@@ -232,7 +233,8 @@ function isGemini25(modelKey) {
232
233
  function isDeepSeekV4(modelKey) {
233
234
  if (typeof modelKey !== 'string') return false;
234
235
  const key = modelKey.toLowerCase();
235
- return key.includes('deepseek-v4') || key.includes('deepseek_v4');
236
+ return key === 'deepseek-flash' || key === '~deepseek/deepseek-pro-latest'
237
+ || key.includes('deepseek-v4') || key.includes('deepseek_v4');
236
238
  }
237
239
 
238
240
  function isMiniMax(modelKey) {
package/index.d.ts CHANGED
@@ -87,6 +87,7 @@ export interface ModelMixMixFlags {
87
87
  moonshot?: boolean;
88
88
  minimax?: boolean;
89
89
  mimo?: boolean;
90
+ deepseek?: boolean;
90
91
  [key: string]: boolean | undefined;
91
92
  }
92
93
 
@@ -161,6 +162,7 @@ export interface TokenCostBreakdown {
161
162
 
162
163
  export interface TokenUsage {
163
164
  input: number;
165
+ /** Generated tokens excluding separately reported reasoning. */
164
166
  output: number;
165
167
  /** Internal reasoning tokens billed at the output rate when reported separately. */
166
168
  thinking: number;
@@ -177,7 +179,9 @@ export interface TokenUsage {
177
179
  cacheWritePremium: number;
178
180
  /** Full future cache hits needed to recover the current write premium. */
179
181
  breakEvenHits: number;
182
+ /** USD charged by OpenRouter when available, otherwise the catalog estimate. */
180
183
  cost: number;
184
+ /** Catalog-based estimate; its total can differ from the reported charge in cost. */
181
185
  costBreakdown: TokenCostBreakdown;
182
186
  speed?: number;
183
187
  }
@@ -527,7 +531,10 @@ export declare class ModelMix {
527
531
  mimo25(args?: ModelAttachArgs): this;
528
532
  mimo25pro(args?: ModelAttachArgs): this;
529
533
  deepseekV4Pro(args?: ModelAttachArgs): this;
534
+ /** Uses deepseek/deepseek-v4-pro-0813 through OpenRouter. */
535
+ deepseekPro(args?: ModelAttachArgs): this;
530
536
  deepseekV4Flash(args?: ModelAttachArgs): this;
537
+ deepseekV41Flash(args?: ModelAttachArgs): this;
531
538
  GLM52(args?: ModelAttachArgs): this;
532
539
  GLM53(args?: ModelAttachArgs): this;
533
540
  GLM53Flash(args?: ModelAttachArgs): this;
@@ -624,6 +631,7 @@ export declare class MixKimi extends MixOpenAI {}
624
631
  export declare class MixAnthropic extends MixCustom {}
625
632
  export declare class MixMiniMax extends MixOpenAI {}
626
633
  export declare class MixMiMo extends MixOpenAI {}
634
+ export declare class MixDeepSeek extends MixOpenAI {}
627
635
  export declare class MixPerplexity extends MixCustom {}
628
636
  export declare class MixOllama extends MixCustom {}
629
637
  export declare class MixGrok extends MixOpenAI {}
package/index.js CHANGED
@@ -8,6 +8,7 @@ const log = require('lemonlog')('ModelMix');
8
8
  const Bottleneck = require('bottleneck');
9
9
  const path = require('path');
10
10
  const generateJsonSchema = require('./schema');
11
+ const parseJsonResponse = require('./lib/parse-json-response');
11
12
  const { Client } = require("@modelcontextprotocol/sdk/client/index.js");
12
13
  const { StdioClientTransport } = require("@modelcontextprotocol/sdk/client/stdio.js");
13
14
  const { MCPToolsManager } = require('./mcp-tools');
@@ -47,6 +48,7 @@ let MixKimi;
47
48
  let MixAnthropic;
48
49
  let MixMiniMax;
49
50
  let MixMiMo;
51
+ let MixDeepSeek;
50
52
  let MixPerplexity;
51
53
  let MixOllama;
52
54
  let MixGrok;
@@ -764,6 +766,10 @@ class ModelMix {
764
766
  return this;
765
767
  }
766
768
 
769
+ deepseekPro({ options = {}, config = {} } = {}) {
770
+ return this.attach('deepseek/deepseek-v4-pro-0813', new MixOpenRouter({ options, config }));
771
+ }
772
+
767
773
  deepseekV4Pro({ options = {}, config = {}, mix = { fireworks: true } } = {}) {
768
774
  mix = { ...this.mix, ...mix };
769
775
  if (mix.nvidia) this.attach('deepseek-ai/deepseek-v4-pro', new MixNVIDIA({ options, config }));
@@ -782,6 +788,14 @@ class ModelMix {
782
788
  return this;
783
789
  }
784
790
 
791
+ deepseekV41Flash({ options = {}, config = {}, mix = { deepseek: true } } = {}) {
792
+ mix = { ...this.mix, ...mix };
793
+ if (mix.deepseek) this.attach('deepseek-flash', new MixDeepSeek({ options, config }));
794
+ if (mix.fireworks) this.attach('accounts/fireworks/models/deepseek-v4p1-flash', new MixFireworks({ options, config }));
795
+ if (mix.openrouter) this.attach('deepseek/deepseek-v4.1-flash', new MixOpenRouter({ options, config }));
796
+ return this;
797
+ }
798
+
785
799
  GLM52({ options = {}, config = {}, mix = { together: true } } = {}) {
786
800
  mix = { ...this.mix, ...mix };
787
801
  if (mix.together) this.attach('zai-org/GLM-5.2', new MixTogether({ options, config }));
@@ -1000,7 +1014,13 @@ class ModelMix {
1000
1014
  }
1001
1015
  }
1002
1016
  const { message } = await this.execute({ options, config, systemSuffix, outputMode: 'json', signal });
1003
- const parsed = JSON.parse(this._extractBlock(message));
1017
+ let parsed;
1018
+ try {
1019
+ parsed = parseJsonResponse(message);
1020
+ } catch (error) {
1021
+ if (!(error instanceof SyntaxError)) throw error;
1022
+ parsed = JSON.parse(this._extractBlock(message));
1023
+ }
1004
1024
  return isArrayWrap ? parsed.out : parsed;
1005
1025
  }
1006
1026
 
@@ -1471,17 +1491,21 @@ class ModelMix {
1471
1491
  }
1472
1492
  }
1473
1493
 
1474
- _enrichResultTokens(result, resolvedModelKey, elapsedMs) {
1494
+ _enrichResultTokens(result, resolvedModelKey, elapsedMs, provider) {
1475
1495
  if (!result.tokens) return;
1476
1496
 
1477
1497
  const normalizedTokens = ModelMix.normalizeTokenUsage(result.tokens);
1478
1498
  const costBreakdown = ModelMix.calculateCostBreakdown(resolvedModelKey, normalizedTokens);
1479
1499
  const cacheMetrics = ModelMix.calculateCacheMetrics(resolvedModelKey, normalizedTokens);
1500
+ const response = Array.isArray(result.response)
1501
+ ? result.response.findLast(chunk => chunk.usage)
1502
+ : result.response;
1503
+ const reportedCost = provider instanceof MixOpenRouter ? response?.usage?.cost : undefined;
1480
1504
  result.tokens = {
1481
1505
  ...result.tokens,
1482
1506
  ...normalizedTokens,
1483
1507
  ...cacheMetrics,
1484
- cost: tokenUsage.hasModelPricing(resolvedModelKey) ? costBreakdown.total : 0,
1508
+ cost: Number.isFinite(reportedCost) && reportedCost >= 0 ? reportedCost : costBreakdown.total,
1485
1509
  costBreakdown
1486
1510
  };
1487
1511
  const elapsedSec = elapsedMs / 1000;
@@ -1659,7 +1683,7 @@ class ModelMix {
1659
1683
  providerAttempt.resolvedModelKey,
1660
1684
  signal
1661
1685
  );
1662
- this._enrichResultTokens(result, providerAttempt.resolvedModelKey, elapsedMs);
1686
+ this._enrichResultTokens(result, providerAttempt.resolvedModelKey, elapsedMs, providerAttempt.provider);
1663
1687
 
1664
1688
  if (result.toolCalls && result.toolCalls.length > 0) {
1665
1689
  return this._continueToolCalls(result, pluginRequest, {
@@ -1926,6 +1950,7 @@ class ModelMix {
1926
1950
  MixAnthropic,
1927
1951
  MixMiniMax,
1928
1952
  MixMiMo,
1953
+ MixDeepSeek,
1929
1954
  MixPerplexity,
1930
1955
  MixOllama,
1931
1956
  MixGrok,
@@ -1943,4 +1968,4 @@ class ModelMix {
1943
1968
  log
1944
1969
  }));
1945
1970
 
1946
- module.exports = { MixCustom, ModelMix, ModerationMix, MixModeration, MixAnthropic, MixKimi, MixMiniMax, MixMiMo, MixOpenAI, MixOpenAIResponses, MixOpenAIModeration, MixOpenAIWebSocket, MixOpenRouter, MixPerplexity, MixOllama, MixLambda, MixLMStudio, MixGroq, MixTogether, MixGrok, MixCerebras, MixGoogle, MixFireworks, MixNVIDIA, normalizeEffort, applyUnifiedEffort, resolveProviderFamily };
1971
+ module.exports = { MixCustom, ModelMix, ModerationMix, MixModeration, MixAnthropic, MixKimi, MixMiniMax, MixMiMo, MixDeepSeek, MixOpenAI, MixOpenAIResponses, MixOpenAIModeration, MixOpenAIWebSocket, MixOpenRouter, MixPerplexity, MixOllama, MixLambda, MixLMStudio, MixGroq, MixTogether, MixGrok, MixCerebras, MixGoogle, MixFireworks, MixNVIDIA, normalizeEffort, applyUnifiedEffort, resolveProviderFamily };
@@ -15,7 +15,7 @@ const CHAIN_MODEL_SHORTCUTS = new Set([
15
15
  'hermes470b', 'hermes4405b', 'hermes3',
16
16
  'kimiK26', 'kimiK27Code', 'kimiK3', 'kimiK25',
17
17
  'minimaxM27', 'minimaxM3', 'mimo25', 'mimo25pro',
18
- 'deepseekV4Pro', 'deepseekV4Flash', 'GLM52', 'GLM53', 'GLM53Flash'
18
+ 'deepseekV4Pro', 'deepseekPro', 'deepseekV4Flash', 'deepseekV41Flash', 'GLM52', 'GLM53', 'GLM53Flash'
19
19
  ]);
20
20
 
21
21
  function parseChainModels(modelSpecs) {
@@ -0,0 +1,14 @@
1
+ function parseJsonResponse(message) {
2
+ try {
3
+ return JSON.parse(message);
4
+ } catch (error) {
5
+ if (!(error instanceof SyntaxError) || typeof message !== 'string') throw error;
6
+ const text = message.trim();
7
+ const fenced = text.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
8
+ if (fenced) return JSON.parse(fenced[1]);
9
+ if (text.endsWith('```')) return JSON.parse(text.slice(0, -3));
10
+ throw error;
11
+ }
12
+ }
13
+
14
+ module.exports = parseJsonResponse;
@@ -278,9 +278,11 @@ function createAnthropicProviders({ ModelMix, MixCustom, log }) {
278
278
  );
279
279
  const input = (data.usage.input_tokens || 0) + cached + cacheWrite;
280
280
  const output = data.usage.output_tokens || 0;
281
+ const thinking = data.usage.output_tokens_details?.thinking_tokens || 0;
281
282
  return ModelMix.normalizeTokenUsage({
282
283
  input,
283
- output,
284
+ output: output - thinking,
285
+ thinking,
284
286
  total: input + output,
285
287
  cached,
286
288
  cacheWrite,