modelmix 4.7.4 → 5.0.1
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/AGENTS.md +43 -0
- package/README.md +283 -92
- package/demo/demo.js +4 -4
- package/demo/prompt.md +2 -2
- package/http-client.js +15 -2
- package/index.d.ts +102 -12
- package/index.js +965 -275
- package/package.json +4 -1
- package/skills/modelmix/SKILL.md +109 -19
- package/test/README.md +10 -9
- package/test/anthropic.test.js +146 -9
- package/test/effort.test.js +2 -2
- package/test/fallback.test.js +192 -2
- package/test/fixtures/account-details.txt +4 -0
- package/test/fixtures/choice-options.txt +6 -0
- package/test/fixtures/choice-template.txt +2 -0
- package/test/fixtures/dynamic-include.txt +2 -0
- package/test/fixtures/system-rules.txt +1 -0
- package/test/fixtures/system-template.txt +2 -0
- package/test/fixtures/template.txt +4 -11
- package/test/fixtures/tree.ejs +6 -0
- package/test/grok.test.js +28 -1
- package/test/history.test.js +2 -2
- package/test/live.mcp.js +20 -20
- package/test/live.test.js +11 -11
- package/test/templates.test.js +621 -280
- package/test/tokens.test.js +409 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modelmix",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.1",
|
|
4
4
|
"description": "🧬 Reliable interface with automatic fallback for AI LLMs.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
"kimi",
|
|
26
26
|
"mythos",
|
|
27
27
|
"gpt5",
|
|
28
|
+
"opus50",
|
|
28
29
|
"opus5",
|
|
30
|
+
"fable50",
|
|
29
31
|
"fable",
|
|
30
32
|
"sonnet",
|
|
31
33
|
"openrouter",
|
|
@@ -51,6 +53,7 @@
|
|
|
51
53
|
"dependencies": {
|
|
52
54
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
53
55
|
"bottleneck": "^2.19.5",
|
|
56
|
+
"ejs": "6.0.1",
|
|
54
57
|
"file-type": "^21.3.4",
|
|
55
58
|
"lemonlog": "^1.2.2",
|
|
56
59
|
"ws": "^8.21.1"
|
package/skills/modelmix/SKILL.md
CHANGED
|
@@ -37,7 +37,7 @@ Do NOT use for:
|
|
|
37
37
|
- [Get raw response (tokens, thinking, tool calls)](#get-raw-response)
|
|
38
38
|
- [Access full response with lastRaw](#access-full-response-with-lastraw)
|
|
39
39
|
- [Add images](#add-images)
|
|
40
|
-
- [
|
|
40
|
+
- [EJS templates](#ejs-templates)
|
|
41
41
|
- [Round-robin load balancing](#round-robin-load-balancing)
|
|
42
42
|
- [MCP integration](#mcp-integration)
|
|
43
43
|
- [Custom local tools](#custom-local-tools)
|
|
@@ -122,9 +122,9 @@ ModelMix.new({ config: { effort: 80 } })
|
|
|
122
122
|
`gpt52()` `gpt52chat()` `gpt51()` `gpt5()` `gpt5mini()` `gpt5nano()` `gpt45()` `gpt41()` `gpt41mini()` `gpt41nano()` `o3()` `o4mini()`
|
|
123
123
|
|
|
124
124
|
### Anthropic
|
|
125
|
-
`
|
|
125
|
+
`fable50()` `opus50()` `opus48()` `opus47()` `opus46()` `sonnet5()` `sonnet46()` `sonnet45()` `haiku45()`
|
|
126
126
|
|
|
127
|
-
Use `.effort(n)` (or `config.effort`) to enable Anthropic thinking — e.g. `.effort(100).opus5()
|
|
127
|
+
Use `.effort(n)` (or `config.effort`) to enable Anthropic thinking — e.g. `.effort(100).opus50()`. `fable5()` and `opus5()` remain available as compatibility aliases.
|
|
128
128
|
|
|
129
129
|
### Google
|
|
130
130
|
`gemini3pro()` `gemini3flash()` `gemini36flash()` `gemini35flash()` `gemini35flashLite()` `gemini31flashLite()` `gemini25pro()` `gemini25flash()`
|
|
@@ -301,11 +301,51 @@ After calling `message()`, `json()`, `block()`, or `stream()`, use `lastRaw` to
|
|
|
301
301
|
const model = ModelMix.new().gpt5mini().addText("Hello!");
|
|
302
302
|
const text = await model.message();
|
|
303
303
|
console.log(model.lastRaw.tokens);
|
|
304
|
-
// {
|
|
304
|
+
// {
|
|
305
|
+
// input: 1200, output: 50, total: 1250,
|
|
306
|
+
// cached: 1024, cacheWrite: 0, uncachedInput: 176,
|
|
307
|
+
// cacheWrite5m: 0, cacheWrite1h: 0,
|
|
308
|
+
// cacheHitRate: 0.8533, cacheSavings: 0.00018432,
|
|
309
|
+
// cacheWritePremium: 0, breakEvenHits: 0,
|
|
310
|
+
// cost: 0.00011568,
|
|
311
|
+
// costBreakdown: {
|
|
312
|
+
// uncachedInput: 0.0000352, cachedInput: 0.00002048,
|
|
313
|
+
// cacheWrite: 0, cacheWrite5m: 0, cacheWrite1h: 0,
|
|
314
|
+
// output: 0.00006, total: 0.00011568
|
|
315
|
+
// },
|
|
316
|
+
// speed: 38
|
|
317
|
+
// }
|
|
305
318
|
console.log(model.lastRaw.think); // reasoning content (if available)
|
|
306
319
|
console.log(model.lastRaw.response); // raw API response
|
|
307
320
|
```
|
|
308
321
|
|
|
322
|
+
### GPT-5.6 explicit prompt caching
|
|
323
|
+
|
|
324
|
+
```javascript
|
|
325
|
+
const model = ModelMix.new()
|
|
326
|
+
.gpt56luna({
|
|
327
|
+
options: {
|
|
328
|
+
prompt_cache_key: 'stable-prefix-v1',
|
|
329
|
+
prompt_cache_options: { mode: 'explicit', ttl: '30m' }
|
|
330
|
+
}
|
|
331
|
+
})
|
|
332
|
+
.haiku45({
|
|
333
|
+
options: {
|
|
334
|
+
cache_control: { type: 'ephemeral', ttl: '1h' }
|
|
335
|
+
}
|
|
336
|
+
})
|
|
337
|
+
.addText(longStableInstructions, {
|
|
338
|
+
cache: { breakpoint: true }
|
|
339
|
+
})
|
|
340
|
+
.addText('Handle this variable request.');
|
|
341
|
+
|
|
342
|
+
const result = await model.raw();
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
`cache: { breakpoint: true }` is provider-neutral: GPT-5.6 receives `prompt_cache_breakpoint`, Anthropic receives `cache_control`, and unsupported providers omit it. Keep native request policies inside each model shorthand so they do not leak across fallbacks. Anthropic usage separates `cacheWrite5m` and `cacheWrite1h`; `cacheWrite` stays as their compatible aggregate.
|
|
346
|
+
|
|
347
|
+
GPT-5.6 replaces `prompt_cache_retention` with `prompt_cache_options.ttl`. Explicit breakpoints also work on image methods and Responses-native `input_text`, `input_image`, and `input_file` blocks. Prompts need at least 1,024 tokens to be cached. Requests over 272K input tokens use 2× input and 1.5× output prices for the complete request; ModelMix applies these multipliers to `cost`, `costBreakdown`, and cache economics.
|
|
348
|
+
|
|
309
349
|
### Add images
|
|
310
350
|
|
|
311
351
|
```javascript
|
|
@@ -319,20 +359,69 @@ const description = await model.message();
|
|
|
319
359
|
|
|
320
360
|
All image methods accept an optional second argument `{ role }` (default `"user"`).
|
|
321
361
|
|
|
322
|
-
###
|
|
362
|
+
### EJS templates
|
|
323
363
|
|
|
324
364
|
```javascript
|
|
325
365
|
const model = ModelMix.new().gpt5mini();
|
|
326
366
|
model.setSystemFromFile('./prompts/system.md');
|
|
327
367
|
model.addTextFromFile('./prompts/task.md');
|
|
328
|
-
model.
|
|
329
|
-
|
|
330
|
-
|
|
368
|
+
model.assign({
|
|
369
|
+
role: 'data analyst',
|
|
370
|
+
language: 'Spanish'
|
|
331
371
|
});
|
|
332
|
-
model.replaceKeyFromFile('{code}', './src/utils.js');
|
|
333
372
|
console.log(await model.message());
|
|
334
373
|
```
|
|
335
374
|
|
|
375
|
+
Templates use standard EJS syntax. Use `<%- value %>` for raw prompt content and `<%= value %>` only when XML escaping is intentional. Missing variables and files throw. Templates may contain JavaScript, so the template source must be developer-controlled; untrusted content belongs only in `assign()` data.
|
|
376
|
+
|
|
377
|
+
Use `assignKey(key, value)` for one value and `assign({ ... })` for several values.
|
|
378
|
+
|
|
379
|
+
Start with a static include. Paths are resolved relative to the containing template:
|
|
380
|
+
|
|
381
|
+
```ejs
|
|
382
|
+
<%- include('shared/rules.md') %>
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Use a variable when the included file must be selected dynamically:
|
|
386
|
+
|
|
387
|
+
```ejs
|
|
388
|
+
Analyze the following source:
|
|
389
|
+
|
|
390
|
+
<%- include(sourceFile) %>
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
```javascript
|
|
394
|
+
model.assign({ sourceFile: '../src/utils.js' });
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Included files are EJS source, so the path and file must be developer-controlled. Untrusted runtime content belongs in ordinary `assign()` values, not include paths. To expose a rendered file as a data key, call `assignKeyFromFile(key, filePath)`; it uses EJS `include`, supports includes relative to that file, and renders once per request. For recursive data, a template may include itself with an explicit stopping condition:
|
|
398
|
+
|
|
399
|
+
```ejs
|
|
400
|
+
<%- node.text %>
|
|
401
|
+
<% if (node.children?.length && depth < maxDepth) { %>
|
|
402
|
+
<% for (const child of node.children) { %>
|
|
403
|
+
<%- include('tree.ejs', { node: child, depth: depth + 1, maxDepth }) %>
|
|
404
|
+
<% } %>
|
|
405
|
+
<% } %>
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Initialize it with `assign({ node, depth: 0, maxDepth: 10 })`. Values supplied through `assign()` remain data and are never interpreted recursively as EJS.
|
|
409
|
+
|
|
410
|
+
Use ModelMix choice directives for random prompt variants:
|
|
411
|
+
|
|
412
|
+
```ejs
|
|
413
|
+
<% choice %>
|
|
414
|
+
<% option 20 %>
|
|
415
|
+
Use emojis.
|
|
416
|
+
<% option 40 %>
|
|
417
|
+
Use few emojis.
|
|
418
|
+
<% option 40 %>
|
|
419
|
+
Do not use emojis.
|
|
420
|
+
<% /choice %>
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Omit all weights for equal probabilities. Otherwise every option needs a positive relative weight; weights do not need to total 100. Keep directives on their own lines. Nested choices and choices inside includes are supported. A request keeps its selections through retries, provider fallbacks, and tool continuations.
|
|
424
|
+
|
|
336
425
|
### Round-robin load balancing
|
|
337
426
|
|
|
338
427
|
```javascript
|
|
@@ -464,27 +553,28 @@ const model = ModelMix.new({
|
|
|
464
553
|
- When using MCP tools or `addTool()`, set `max_history` to at least 3 — tool call/response pairs consume history slots.
|
|
465
554
|
- Use `.json()` for structured output instead of parsing text manually. Use descriptor objects `{ description, required, enum, default, nullable }` for richer schema control.
|
|
466
555
|
- Use `.message()` for simple text, `.raw()` when you need tokens/thinking/toolCalls.
|
|
467
|
-
- For Anthropic thinking, use unified `effort` (`-1` or `0`–`100`) via `config.effort` or `.effort(n)` — e.g. `.effort(100).
|
|
468
|
-
-
|
|
556
|
+
- For Anthropic thinking, use unified `effort` (`-1` or `0`–`100`) via `config.effort` or `.effort(n)` — e.g. `.effort(100).opus50()`. Never put `effort` in `options`. Native fields win if already set.
|
|
557
|
+
- Templates use EJS syntax in both system prompts and user messages; prefer `<%- key %>` for raw prompt data.
|
|
469
558
|
- The library uses CommonJS internally but supports ESM import via `{ ModelMix }`.
|
|
470
559
|
- GPT-5+ models automatically use `max_completion_tokens` instead of `max_tokens`.
|
|
471
560
|
- o-series models (o3, o4mini) automatically strip `max_tokens` and `temperature` since those APIs don't support them.
|
|
472
561
|
- Anthropic Opus 4.7+ / Claude 5 family automatically strip `temperature`, `top_p`, and `top_k` (API rejects them).
|
|
473
|
-
- `addText()`, `addImage()`, `addImageFromUrl()`, and `addImageFromBuffer()`
|
|
562
|
+
- `addText()`, `addImage()`, `addImageFromUrl()`, and `addImageFromBuffer()` accept `{ role, cache?: { breakpoint: true } }` as the second argument (default role: `"user"`). Adapters translate or omit the neutral marker by provider.
|
|
474
563
|
|
|
475
564
|
## API Quick Reference
|
|
476
565
|
|
|
477
566
|
| Method | Returns | Description |
|
|
478
567
|
| --- | --- | --- |
|
|
479
|
-
| `.addText(text, {role?})` | `this` | Add user message |
|
|
480
|
-
| `.addTextFromFile(path, {role?})` | `this` | Add user message from file |
|
|
568
|
+
| `.addText(text, {role?, cache?})` | `this` | Add user message |
|
|
569
|
+
| `.addTextFromFile(path, {role?, cache?})` | `this` | Add user message from file |
|
|
481
570
|
| `.setSystem(text)` | `this` | Set system prompt |
|
|
482
571
|
| `.setSystemFromFile(path)` | `this` | Set system prompt from file |
|
|
483
|
-
| `.addImage(path, {role?})` | `this` | Add image from file |
|
|
484
|
-
| `.addImageFromUrl(url, {role?})` | `this` | Add image from URL or data URI |
|
|
485
|
-
| `.addImageFromBuffer(buffer, {role?})` | `this` | Add image from Buffer |
|
|
486
|
-
| `.
|
|
487
|
-
| `.
|
|
572
|
+
| `.addImage(path, {role?, cache?})` | `this` | Add image from file |
|
|
573
|
+
| `.addImageFromUrl(url, {role?, cache?})` | `this` | Add image from URL or data URI |
|
|
574
|
+
| `.addImageFromBuffer(buffer, {role?, cache?})` | `this` | Add image from Buffer |
|
|
575
|
+
| `.assign({})` | `this` | Assign EJS template data |
|
|
576
|
+
| `.assignKey(key, value)` | `this` | Assign one EJS template-data value |
|
|
577
|
+
| `.assignKeyFromFile(key, path)` | `this` | Assign the rendered output of an EJS file to one key |
|
|
488
578
|
| `.message()` | `Promise<string>` | Get text response |
|
|
489
579
|
| `.json(example, desc?, opts?)` | `Promise<object\|array>` | Get structured JSON |
|
|
490
580
|
| `.raw()` | `Promise<{message, think, toolCalls, tokens, response}>` | Full response |
|
package/test/README.md
CHANGED
|
@@ -9,7 +9,7 @@ This comprehensive test suite provides complete coverage for the ModelMix librar
|
|
|
9
9
|
These tests require actual API keys and test the complete integration:
|
|
10
10
|
- Real image processing with multiple providers
|
|
11
11
|
- Actual JSON structured output
|
|
12
|
-
-
|
|
12
|
+
- EJS template rendering with real models
|
|
13
13
|
- Multi-modal combinations
|
|
14
14
|
- Performance testing with real APIs
|
|
15
15
|
|
|
@@ -45,13 +45,14 @@ npm test -- --grep "Live Integration"
|
|
|
45
45
|
- ✅ Context preservation through fallbacks
|
|
46
46
|
- ✅ Provider-specific configurations
|
|
47
47
|
|
|
48
|
-
### 3. File Operations and Templates (`templates.test.js`)
|
|
49
|
-
- ✅
|
|
50
|
-
- ✅
|
|
51
|
-
- ✅
|
|
52
|
-
- ✅
|
|
53
|
-
- ✅
|
|
54
|
-
- ✅
|
|
48
|
+
### 3. File Operations and EJS Templates (`templates.test.js`)
|
|
49
|
+
- ✅ Raw and XML-escaped EJS output
|
|
50
|
+
- ✅ Nested data, conditionals, and loops
|
|
51
|
+
- ✅ Uniform, weighted, nested, and included choice blocks
|
|
52
|
+
- ✅ File templates and relative includes
|
|
53
|
+
- ✅ Raw file and JSON data injection
|
|
54
|
+
- ✅ Missing-variable and missing-file errors
|
|
55
|
+
- ✅ JSON, block, system, and multi-turn integration
|
|
55
56
|
|
|
56
57
|
### 4. Image Processing and Multimodal (`images.test.js`)
|
|
57
58
|
- ✅ Base64 data handling
|
|
@@ -155,4 +156,4 @@ DEBUG_TESTS=true npm test
|
|
|
155
156
|
- All tests use API mocking to avoid real calls
|
|
156
157
|
- Test environment variables are configured in `setup.js`
|
|
157
158
|
- Tests are independent and can run in any order
|
|
158
|
-
- Automatic cleanup prevents interference between tests
|
|
159
|
+
- Automatic cleanup prevents interference between tests
|
package/test/anthropic.test.js
CHANGED
|
@@ -5,15 +5,29 @@ const { ModelMix, MixAnthropic } = require('../index.js');
|
|
|
5
5
|
describe('Anthropic Model Registration Tests', () => {
|
|
6
6
|
it('should register Claude Fable 5', () => {
|
|
7
7
|
const model = ModelMix.new();
|
|
8
|
-
model.
|
|
8
|
+
model.fable50();
|
|
9
9
|
|
|
10
10
|
expect(model.models).to.have.length(1);
|
|
11
11
|
expect(model.models[0].key).to.equal('claude-fable-5');
|
|
12
12
|
expect(model.models[0].provider).to.be.instanceOf(MixAnthropic);
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
it('should
|
|
16
|
-
const model = ModelMix.new()
|
|
15
|
+
it('should keep fable5() as an alias for fable50()', () => {
|
|
16
|
+
const model = ModelMix.new();
|
|
17
|
+
|
|
18
|
+
expect(model.fable5({
|
|
19
|
+
options: { max_tokens: 123 },
|
|
20
|
+
config: { url: 'https://anthropic.example.test' }
|
|
21
|
+
})).to.equal(model);
|
|
22
|
+
expect(model.models).to.have.length(1);
|
|
23
|
+
expect(model.models[0].key).to.equal('claude-fable-5');
|
|
24
|
+
expect(model.models[0].provider).to.be.instanceOf(MixAnthropic);
|
|
25
|
+
expect(model.models[0].provider.options.max_tokens).to.equal(123);
|
|
26
|
+
expect(model.models[0].provider.config.url).to.equal('https://anthropic.example.test');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should apply max effort thinking via .effort(100).fable50()', () => {
|
|
30
|
+
const model = ModelMix.new().effort(100).fable50();
|
|
17
31
|
const { applyUnifiedEffort } = require('../effort.js');
|
|
18
32
|
|
|
19
33
|
expect(model.config.effort).to.equal(100);
|
|
@@ -25,15 +39,29 @@ describe('Anthropic Model Registration Tests', () => {
|
|
|
25
39
|
|
|
26
40
|
it('should register Claude Opus 5', () => {
|
|
27
41
|
const model = ModelMix.new();
|
|
28
|
-
model.
|
|
42
|
+
model.opus50();
|
|
29
43
|
|
|
30
44
|
expect(model.models).to.have.length(1);
|
|
31
45
|
expect(model.models[0].key).to.equal('claude-opus-5');
|
|
32
46
|
expect(model.models[0].provider).to.be.instanceOf(MixAnthropic);
|
|
33
47
|
});
|
|
34
48
|
|
|
35
|
-
it('should
|
|
36
|
-
const model = ModelMix.new()
|
|
49
|
+
it('should keep opus5() as an alias for opus50()', () => {
|
|
50
|
+
const model = ModelMix.new();
|
|
51
|
+
|
|
52
|
+
expect(model.opus5({
|
|
53
|
+
options: { max_tokens: 123 },
|
|
54
|
+
config: { url: 'https://anthropic.example.test' }
|
|
55
|
+
})).to.equal(model);
|
|
56
|
+
expect(model.models).to.have.length(1);
|
|
57
|
+
expect(model.models[0].key).to.equal('claude-opus-5');
|
|
58
|
+
expect(model.models[0].provider).to.be.instanceOf(MixAnthropic);
|
|
59
|
+
expect(model.models[0].provider.options.max_tokens).to.equal(123);
|
|
60
|
+
expect(model.models[0].provider.config.url).to.equal('https://anthropic.example.test');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should apply max effort thinking via .effort(100).opus50()', () => {
|
|
64
|
+
const model = ModelMix.new().effort(100).opus50();
|
|
37
65
|
const { applyUnifiedEffort } = require('../effort.js');
|
|
38
66
|
|
|
39
67
|
expect(model.config.effort).to.equal(100);
|
|
@@ -133,6 +161,101 @@ describe('Anthropic Model Registration Tests', () => {
|
|
|
133
161
|
});
|
|
134
162
|
});
|
|
135
163
|
|
|
164
|
+
describe('Provider-neutral prompt caching', () => {
|
|
165
|
+
it('should translate neutral breakpoints and remove foreign OpenAI controls', async () => {
|
|
166
|
+
const originalApiKey = process.env.ANTHROPIC_API_KEY;
|
|
167
|
+
process.env.ANTHROPIC_API_KEY = 'test-anthropic-key';
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const provider = new MixAnthropic();
|
|
171
|
+
let requestBody;
|
|
172
|
+
nock('https://api.anthropic.com')
|
|
173
|
+
.post('/v1/messages', body => {
|
|
174
|
+
requestBody = body;
|
|
175
|
+
return true;
|
|
176
|
+
})
|
|
177
|
+
.reply(200, {
|
|
178
|
+
content: [{ type: 'text', text: 'Done' }],
|
|
179
|
+
usage: { input_tokens: 1, output_tokens: 1 }
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await provider.create({
|
|
183
|
+
config: { system: 'You are an assistant.' },
|
|
184
|
+
options: {
|
|
185
|
+
model: 'claude-haiku-4-5-20251001',
|
|
186
|
+
max_tokens: 16,
|
|
187
|
+
cache_control: { type: 'ephemeral', ttl: '1h' },
|
|
188
|
+
prompt_cache_key: 'openai-only',
|
|
189
|
+
prompt_cache_options: { mode: 'explicit', ttl: '30m' },
|
|
190
|
+
messages: [{
|
|
191
|
+
role: 'user',
|
|
192
|
+
content: [
|
|
193
|
+
{ type: 'text', text: 'Stable', cache: { breakpoint: true } },
|
|
194
|
+
{
|
|
195
|
+
type: 'text',
|
|
196
|
+
text: 'Variable',
|
|
197
|
+
prompt_cache_breakpoint: { mode: 'explicit' }
|
|
198
|
+
}
|
|
199
|
+
]
|
|
200
|
+
}]
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(requestBody).to.not.have.property('prompt_cache_key');
|
|
205
|
+
expect(requestBody).to.not.have.property('prompt_cache_options');
|
|
206
|
+
expect(requestBody).to.not.have.property('cache_control');
|
|
207
|
+
expect(requestBody.messages[0].content[0]).to.deep.equal({
|
|
208
|
+
type: 'text',
|
|
209
|
+
text: 'Stable',
|
|
210
|
+
cache_control: { type: 'ephemeral', ttl: '1h' }
|
|
211
|
+
});
|
|
212
|
+
expect(requestBody.messages[0].content[1]).to.deep.equal({
|
|
213
|
+
type: 'text',
|
|
214
|
+
text: 'Variable'
|
|
215
|
+
});
|
|
216
|
+
} finally {
|
|
217
|
+
if (originalApiKey === undefined) delete process.env.ANTHROPIC_API_KEY;
|
|
218
|
+
else process.env.ANTHROPIC_API_KEY = originalApiKey;
|
|
219
|
+
nock.cleanAll();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should preserve top-level automatic caching when there is no explicit breakpoint', async () => {
|
|
224
|
+
const originalApiKey = process.env.ANTHROPIC_API_KEY;
|
|
225
|
+
process.env.ANTHROPIC_API_KEY = 'test-anthropic-key';
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const provider = new MixAnthropic();
|
|
229
|
+
let requestBody;
|
|
230
|
+
nock('https://api.anthropic.com')
|
|
231
|
+
.post('/v1/messages', body => {
|
|
232
|
+
requestBody = body;
|
|
233
|
+
return true;
|
|
234
|
+
})
|
|
235
|
+
.reply(200, {
|
|
236
|
+
content: [{ type: 'text', text: 'Done' }],
|
|
237
|
+
usage: { input_tokens: 1, output_tokens: 1 }
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
await provider.create({
|
|
241
|
+
config: { system: 'You are an assistant.' },
|
|
242
|
+
options: {
|
|
243
|
+
model: 'claude-haiku-4-5-20251001',
|
|
244
|
+
max_tokens: 16,
|
|
245
|
+
cache_control: { type: 'ephemeral' },
|
|
246
|
+
messages: [{ role: 'user', content: 'Hello' }]
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
expect(requestBody.cache_control).to.deep.equal({ type: 'ephemeral' });
|
|
251
|
+
} finally {
|
|
252
|
+
if (originalApiKey === undefined) delete process.env.ANTHROPIC_API_KEY;
|
|
253
|
+
else process.env.ANTHROPIC_API_KEY = originalApiKey;
|
|
254
|
+
nock.cleanAll();
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
136
259
|
it('should register Claude Opus 4.8', () => {
|
|
137
260
|
const model = ModelMix.new();
|
|
138
261
|
model.opus48();
|
|
@@ -154,15 +277,29 @@ describe('Anthropic Model Registration Tests', () => {
|
|
|
154
277
|
|
|
155
278
|
it('should register Claude Sonnet 5', () => {
|
|
156
279
|
const model = ModelMix.new();
|
|
157
|
-
model.
|
|
280
|
+
model.sonnet50();
|
|
281
|
+
|
|
282
|
+
expect(model.models).to.have.length(1);
|
|
283
|
+
expect(model.models[0].key).to.equal('claude-sonnet-5');
|
|
284
|
+
expect(model.models[0].provider).to.be.instanceOf(MixAnthropic);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should keep sonnet5() as an alias for sonnet50()', () => {
|
|
288
|
+
const model = ModelMix.new();
|
|
158
289
|
|
|
290
|
+
expect(model.sonnet5({
|
|
291
|
+
options: { max_tokens: 123 },
|
|
292
|
+
config: { url: 'https://anthropic.example.test' }
|
|
293
|
+
})).to.equal(model);
|
|
159
294
|
expect(model.models).to.have.length(1);
|
|
160
295
|
expect(model.models[0].key).to.equal('claude-sonnet-5');
|
|
161
296
|
expect(model.models[0].provider).to.be.instanceOf(MixAnthropic);
|
|
297
|
+
expect(model.models[0].provider.options.max_tokens).to.equal(123);
|
|
298
|
+
expect(model.models[0].provider.config.url).to.equal('https://anthropic.example.test');
|
|
162
299
|
});
|
|
163
300
|
|
|
164
|
-
it('should apply adaptive thinking via .effort(100).
|
|
165
|
-
const model = ModelMix.new().effort(100).
|
|
301
|
+
it('should apply adaptive thinking via .effort(100).sonnet50()', () => {
|
|
302
|
+
const model = ModelMix.new().effort(100).sonnet50();
|
|
166
303
|
const { applyUnifiedEffort } = require('../effort.js');
|
|
167
304
|
|
|
168
305
|
const options = { model: 'claude-sonnet-5' };
|
package/test/effort.test.js
CHANGED
|
@@ -336,8 +336,8 @@ describe('Unified effort scale', () => {
|
|
|
336
336
|
expect(request.reasoning).to.deep.equal({ effort: 'none' });
|
|
337
337
|
});
|
|
338
338
|
|
|
339
|
-
it('Anthropic config.effort maps through .effort().
|
|
340
|
-
const model = ModelMix.new().effort(100).
|
|
339
|
+
it('Anthropic config.effort maps through .effort().opus50()', () => {
|
|
340
|
+
const model = ModelMix.new().effort(100).opus50();
|
|
341
341
|
expect(model.config.effort).to.equal(100);
|
|
342
342
|
|
|
343
343
|
const options = {
|