modelmix 4.4.14 → 4.4.16
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 +2 -1
- package/demo/gemini.js +12 -9
- package/demo/gpt-realtime.js +22 -0
- package/demo/{gpt51.js → gpt54.js} +2 -2
- package/index.js +355 -1
- package/package.json +6 -5
- package/demo/save_the_cat-spanish.md +0 -109
- package/demo/story.md +0 -15
package/README.md
CHANGED
|
@@ -135,9 +135,10 @@ Here's a comprehensive list of available methods:
|
|
|
135
135
|
|
|
136
136
|
| Method | Provider | Model | Price (I/O) per 1 M tokens |
|
|
137
137
|
| ------------------ | ---------- | ------------------------------ | -------------------------- |
|
|
138
|
+
| `gpt54()` | OpenAI | gpt-5.4 | [\$2.50 / \$15.00][1] |
|
|
138
139
|
| `gpt52()` | OpenAI | gpt-5.2 | [\$1.75 / \$14.00][1] |
|
|
139
140
|
| `gpt51()` | OpenAI | gpt-5.1 | [\$1.25 / \$10.00][1] |
|
|
140
|
-
| `
|
|
141
|
+
| `gpt53codex()` | OpenAI | gpt-5.3-codex | [\$1.25 / \$14.00][1] |
|
|
141
142
|
| `gpt5mini()` | OpenAI | gpt-5-mini | [\$0.25 / \$2.00][1] |
|
|
142
143
|
| `gpt5nano()` | OpenAI | gpt-5-nano | [\$0.05 / \$0.40][1] |
|
|
143
144
|
| `gpt41()` | OpenAI | gpt-4.1 | [\$2.00 / \$8.00][1] |
|
package/demo/gemini.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ModelMix, MixGoogle } from '../index.js';
|
|
2
|
-
try { process.loadEnvFile(); } catch {}
|
|
2
|
+
try { process.loadEnvFile(); } catch { }
|
|
3
3
|
|
|
4
4
|
const mmix = new ModelMix({
|
|
5
5
|
options: {
|
|
@@ -12,9 +12,9 @@ const mmix = new ModelMix({
|
|
|
12
12
|
}
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
// Using
|
|
15
|
+
// Using gemini3flash (Gemini 3 Flash) with built-in method
|
|
16
16
|
console.log("\n" + '--------| gemini25flash() |--------');
|
|
17
|
-
const flash = await mmix.
|
|
17
|
+
const flash = await mmix.gemini3flash()
|
|
18
18
|
.addText('Hi there! Do you like cats?')
|
|
19
19
|
.message();
|
|
20
20
|
|
|
@@ -22,20 +22,23 @@ console.log(flash);
|
|
|
22
22
|
|
|
23
23
|
// Using gemini3pro (Gemini 3 Pro) with custom config
|
|
24
24
|
console.log("\n" + '--------| gemini3pro() with JSON response |--------');
|
|
25
|
-
const pro = mmix.new().
|
|
25
|
+
const pro = mmix.new().gemini31pro();
|
|
26
26
|
|
|
27
27
|
pro.addText('Give me a fun fact about cats');
|
|
28
|
-
|
|
28
|
+
|
|
29
|
+
const jsonExampleAndSchema = {
|
|
29
30
|
fact: 'A fun fact about cats',
|
|
30
|
-
category: 'animal behavior'
|
|
31
|
-
}
|
|
31
|
+
category: 'animal behavior'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const jsonResponse = await pro.json(jsonExampleAndSchema, jsonExampleAndSchema);
|
|
32
35
|
|
|
33
36
|
console.log(jsonResponse);
|
|
34
37
|
|
|
35
38
|
// Using attach method with MixGoogle for custom model
|
|
36
39
|
console.log("\n" + '--------| Custom Gemini with attach() |--------');
|
|
37
|
-
mmix.attach('gemini-2.5-flash', new MixGoogle());
|
|
40
|
+
const customModel = mmix.new().attach('gemini-2.5-flash', new MixGoogle());
|
|
38
41
|
|
|
39
|
-
const custom = await
|
|
42
|
+
const custom = await customModel.addText('Tell me a short joke about cats.').message();
|
|
40
43
|
console.log(custom);
|
|
41
44
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ModelMix } from '../index.js';
|
|
2
|
+
try { process.loadEnvFile(); } catch {}
|
|
3
|
+
|
|
4
|
+
const mmix = new ModelMix({
|
|
5
|
+
config: {
|
|
6
|
+
debug: 3
|
|
7
|
+
}
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
console.log('\n--------| gptRealtime() |--------');
|
|
11
|
+
|
|
12
|
+
const realtime = mmix.gptRealtimeMini({
|
|
13
|
+
options: {
|
|
14
|
+
stream: true
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
realtime.addText('Explain quantum entanglement in simple terms.');
|
|
19
|
+
const response = await realtime.stream(({ delta }) => {
|
|
20
|
+
process.stdout.write(delta || '');
|
|
21
|
+
});
|
|
22
|
+
console.log('\n\n[done]\n', response.tokens);
|
|
@@ -8,10 +8,10 @@ const mmix = new ModelMix({
|
|
|
8
8
|
}
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
-
console.log("\n" + '--------|
|
|
11
|
+
console.log("\n" + '--------| gpt54() |--------');
|
|
12
12
|
|
|
13
13
|
const gptArgs = { options: { reasoning_effort: "none", verbosity: "low" } };
|
|
14
|
-
const gpt = mmix.
|
|
14
|
+
const gpt = mmix.gpt54(gptArgs);
|
|
15
15
|
|
|
16
16
|
gpt.addText("Explain quantum entanglement in simple terms.");
|
|
17
17
|
const response = await gpt.message();
|
package/index.js
CHANGED
|
@@ -5,6 +5,7 @@ const { inspect } = require('util');
|
|
|
5
5
|
const log = require('lemonlog')('ModelMix');
|
|
6
6
|
const Bottleneck = require('bottleneck');
|
|
7
7
|
const path = require('path');
|
|
8
|
+
const WebSocket = require('ws');
|
|
8
9
|
const generateJsonSchema = require('./schema');
|
|
9
10
|
const { Client } = require("@modelcontextprotocol/sdk/client/index.js");
|
|
10
11
|
const { StdioClientTransport } = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
@@ -14,6 +15,11 @@ const { MCPToolsManager } = require('./mcp-tools');
|
|
|
14
15
|
// Based on provider pricing pages linked in README
|
|
15
16
|
const MODEL_PRICING = {
|
|
16
17
|
// OpenAI
|
|
18
|
+
'gpt-realtime-mini': [0.60, 2.40],
|
|
19
|
+
'gpt-realtime': [4.00, 16.00],
|
|
20
|
+
'gpt-5.4': [2.50, 15.00],
|
|
21
|
+
'gpt-5.4-pro': [30, 180.00],
|
|
22
|
+
'gpt-5.3-codex': [1.75, 14.00],
|
|
17
23
|
'gpt-5.2': [1.75, 14.00],
|
|
18
24
|
'gpt-5.2-chat-latest': [1.75, 14.00],
|
|
19
25
|
'gpt-5.1': [1.25, 10.00],
|
|
@@ -268,6 +274,21 @@ class ModelMix {
|
|
|
268
274
|
gpt52({ options = {}, config = {} } = {}) {
|
|
269
275
|
return this.attach('gpt-5.2', new MixOpenAI({ options, config }));
|
|
270
276
|
}
|
|
277
|
+
gpt54({ options = {}, config = {} } = {}) {
|
|
278
|
+
return this.attach('gpt-5.4', new MixOpenAIResponses({ options, config }));
|
|
279
|
+
}
|
|
280
|
+
gpt54pro({ options = {}, config = {} } = {}) {
|
|
281
|
+
return this.attach('gpt-5.4-pro', new MixOpenAIResponses({ options, config }));
|
|
282
|
+
}
|
|
283
|
+
gptRealtime({ options = {}, config = {} } = {}) {
|
|
284
|
+
return this.attach('gpt-realtime', new MixOpenAIWebSocket({ options, config }));
|
|
285
|
+
}
|
|
286
|
+
gptRealtimeMini({ options = {}, config = {} } = {}) {
|
|
287
|
+
return this.attach('gpt-realtime-mini', new MixOpenAIWebSocket({ options, config }));
|
|
288
|
+
}
|
|
289
|
+
gpt53codex({ options = {}, config = {} } = {}) {
|
|
290
|
+
return this.attach('gpt-5.3-codex', new MixOpenAIResponses({ options, config }));
|
|
291
|
+
}
|
|
271
292
|
gpt52chat({ options = {}, config = {} } = {}) {
|
|
272
293
|
return this.attach('gpt-5.2-chat-latest', new MixOpenAI({ options, config }));
|
|
273
294
|
}
|
|
@@ -1499,6 +1520,339 @@ class MixOpenAI extends MixCustom {
|
|
|
1499
1520
|
}
|
|
1500
1521
|
}
|
|
1501
1522
|
|
|
1523
|
+
class MixOpenAIResponses extends MixOpenAI {
|
|
1524
|
+
async create({ config = {}, options = {} } = {}) {
|
|
1525
|
+
|
|
1526
|
+
// Keep GPT/o-model option normalization behavior
|
|
1527
|
+
if (options.model?.startsWith('o')) {
|
|
1528
|
+
delete options.max_tokens;
|
|
1529
|
+
delete options.temperature;
|
|
1530
|
+
}
|
|
1531
|
+
if (options.model?.includes('gpt-5')) {
|
|
1532
|
+
if (options.max_tokens) {
|
|
1533
|
+
options.max_completion_tokens = options.max_tokens;
|
|
1534
|
+
delete options.max_tokens;
|
|
1535
|
+
}
|
|
1536
|
+
delete options.temperature;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
const responsesUrl = this.config.url.replace('/chat/completions', '/responses');
|
|
1540
|
+
const request = MixOpenAIResponses.buildResponsesRequest(options);
|
|
1541
|
+
const response = await axios.post(responsesUrl, request, {
|
|
1542
|
+
headers: this.headers
|
|
1543
|
+
});
|
|
1544
|
+
|
|
1545
|
+
return MixOpenAIResponses.processResponsesResponse(response);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
static buildResponsesRequest(options = {}) {
|
|
1549
|
+
const request = {
|
|
1550
|
+
model: options.model,
|
|
1551
|
+
input: MixOpenAIResponses.messagesToResponsesInput(options.messages),
|
|
1552
|
+
stream: false
|
|
1553
|
+
};
|
|
1554
|
+
|
|
1555
|
+
if (options.reasoning_effort) request.reasoning = { effort: options.reasoning_effort };
|
|
1556
|
+
if (options.verbosity) request.text = { verbosity: options.verbosity };
|
|
1557
|
+
|
|
1558
|
+
if (typeof options.max_completion_tokens === 'number') {
|
|
1559
|
+
request.max_output_tokens = options.max_completion_tokens;
|
|
1560
|
+
} else if (typeof options.max_tokens === 'number') {
|
|
1561
|
+
request.max_output_tokens = options.max_tokens;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
if (typeof options.temperature === 'number') request.temperature = options.temperature;
|
|
1565
|
+
if (typeof options.top_p === 'number') request.top_p = options.top_p;
|
|
1566
|
+
if (typeof options.presence_penalty === 'number') request.presence_penalty = options.presence_penalty;
|
|
1567
|
+
if (typeof options.frequency_penalty === 'number') request.frequency_penalty = options.frequency_penalty;
|
|
1568
|
+
if (options.stop !== undefined) request.stop = options.stop;
|
|
1569
|
+
if (typeof options.n === 'number') request.n = options.n;
|
|
1570
|
+
if (options.logit_bias !== undefined) request.logit_bias = options.logit_bias;
|
|
1571
|
+
if (options.user !== undefined) request.user = options.user;
|
|
1572
|
+
|
|
1573
|
+
return request;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
static processResponsesResponse(response) {
|
|
1577
|
+
const message = MixOpenAIResponses.extractResponsesMessage(response.data);
|
|
1578
|
+
return {
|
|
1579
|
+
message,
|
|
1580
|
+
think: null,
|
|
1581
|
+
toolCalls: [],
|
|
1582
|
+
tokens: MixOpenAIResponses.extractResponsesTokens(response.data),
|
|
1583
|
+
response: response.data
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
static extractResponsesTokens(data) {
|
|
1588
|
+
if (data.usage) {
|
|
1589
|
+
return {
|
|
1590
|
+
input: data.usage.input_tokens || 0,
|
|
1591
|
+
output: data.usage.output_tokens || 0,
|
|
1592
|
+
total: data.usage.total_tokens || ((data.usage.input_tokens || 0) + (data.usage.output_tokens || 0))
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
return {
|
|
1596
|
+
input: 0,
|
|
1597
|
+
output: 0,
|
|
1598
|
+
total: 0
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
static extractResponsesMessage(data) {
|
|
1603
|
+
if (!Array.isArray(data.output)) return '';
|
|
1604
|
+
return data.output
|
|
1605
|
+
.filter(item => item.type === 'message')
|
|
1606
|
+
.flatMap(item => Array.isArray(item.content) ? item.content : [])
|
|
1607
|
+
.filter(content => content.type === 'output_text' && typeof content.text === 'string')
|
|
1608
|
+
.map(content => content.text)
|
|
1609
|
+
.join('\n')
|
|
1610
|
+
.trim();
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
static messagesToResponsesInput(messages = []) {
|
|
1614
|
+
const mapped = [];
|
|
1615
|
+
|
|
1616
|
+
for (const message of messages) {
|
|
1617
|
+
if (!message || !message.role) continue;
|
|
1618
|
+
if (message.tool_calls || message.role === 'tool') continue;
|
|
1619
|
+
|
|
1620
|
+
let text = '';
|
|
1621
|
+
if (typeof message.content === 'string') {
|
|
1622
|
+
text = message.content;
|
|
1623
|
+
} else if (Array.isArray(message.content)) {
|
|
1624
|
+
text = message.content
|
|
1625
|
+
.filter(item => item && item.type === 'text' && typeof item.text === 'string')
|
|
1626
|
+
.map(item => item.text)
|
|
1627
|
+
.join('\n');
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
if (!text) continue;
|
|
1631
|
+
mapped.push({
|
|
1632
|
+
role: message.role,
|
|
1633
|
+
content: [{ type: 'input_text', text }]
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
return mapped;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
class MixOpenAIWebSocket extends MixOpenAIResponses {
|
|
1642
|
+
getDefaultConfig(customConfig) {
|
|
1643
|
+
return super.getDefaultConfig({
|
|
1644
|
+
realtimeUrl: 'wss://api.openai.com/v1/realtime',
|
|
1645
|
+
websocketTimeoutMs: 120000,
|
|
1646
|
+
...customConfig
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
async create({ config = {}, options = {} } = {}) {
|
|
1651
|
+
if (options.model?.startsWith('o')) {
|
|
1652
|
+
delete options.max_tokens;
|
|
1653
|
+
delete options.temperature;
|
|
1654
|
+
}
|
|
1655
|
+
if (options.model?.includes('gpt-5')) {
|
|
1656
|
+
if (options.max_tokens) {
|
|
1657
|
+
options.max_completion_tokens = options.max_tokens;
|
|
1658
|
+
delete options.max_tokens;
|
|
1659
|
+
}
|
|
1660
|
+
delete options.temperature;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
const mergedConfig = { ...this.config, ...config };
|
|
1664
|
+
const realtimeUrl = `${mergedConfig.realtimeUrl}?model=${encodeURIComponent(options.model)}`;
|
|
1665
|
+
const timeoutMs = mergedConfig.websocketTimeoutMs || 120000;
|
|
1666
|
+
|
|
1667
|
+
return await new Promise((resolve, reject) => {
|
|
1668
|
+
const ws = new WebSocket(realtimeUrl, {
|
|
1669
|
+
headers: {
|
|
1670
|
+
authorization: `Bearer ${mergedConfig.apiKey}`
|
|
1671
|
+
}
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
const events = [];
|
|
1675
|
+
let message = '';
|
|
1676
|
+
let settled = false;
|
|
1677
|
+
let finalResponse = null;
|
|
1678
|
+
|
|
1679
|
+
const timeout = setTimeout(() => {
|
|
1680
|
+
if (settled) return;
|
|
1681
|
+
settled = true;
|
|
1682
|
+
ws.close();
|
|
1683
|
+
reject({
|
|
1684
|
+
message: `Realtime WebSocket timed out after ${timeoutMs}ms`,
|
|
1685
|
+
statusCode: null,
|
|
1686
|
+
details: null,
|
|
1687
|
+
config: mergedConfig,
|
|
1688
|
+
options
|
|
1689
|
+
});
|
|
1690
|
+
}, timeoutMs);
|
|
1691
|
+
|
|
1692
|
+
const cleanUp = () => clearTimeout(timeout);
|
|
1693
|
+
|
|
1694
|
+
ws.on('open', () => {
|
|
1695
|
+
const session = {
|
|
1696
|
+
type: 'realtime',
|
|
1697
|
+
output_modalities: ['text']
|
|
1698
|
+
};
|
|
1699
|
+
|
|
1700
|
+
if (mergedConfig.system) session.instructions = mergedConfig.system;
|
|
1701
|
+
if (Array.isArray(options.tools) && options.tools.length > 0) {
|
|
1702
|
+
session.tools = options.tools;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
ws.send(JSON.stringify({ type: 'session.update', session }));
|
|
1706
|
+
|
|
1707
|
+
const items = MixOpenAIWebSocket.messagesToConversationItems(options.messages);
|
|
1708
|
+
for (const item of items) {
|
|
1709
|
+
ws.send(JSON.stringify({
|
|
1710
|
+
type: 'conversation.item.create',
|
|
1711
|
+
item
|
|
1712
|
+
}));
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
const responseConfig = { output_modalities: ['text'] };
|
|
1716
|
+
if (typeof options.max_completion_tokens === 'number') {
|
|
1717
|
+
responseConfig.max_output_tokens = Math.min(options.max_completion_tokens, 4096);
|
|
1718
|
+
} else if (typeof options.max_tokens === 'number') {
|
|
1719
|
+
responseConfig.max_output_tokens = Math.min(options.max_tokens, 4096);
|
|
1720
|
+
}
|
|
1721
|
+
if (Array.isArray(options.tools) && options.tools.length > 0) responseConfig.tools = options.tools;
|
|
1722
|
+
|
|
1723
|
+
ws.send(JSON.stringify({
|
|
1724
|
+
type: 'response.create',
|
|
1725
|
+
response: responseConfig
|
|
1726
|
+
}));
|
|
1727
|
+
});
|
|
1728
|
+
|
|
1729
|
+
ws.on('message', raw => {
|
|
1730
|
+
let event;
|
|
1731
|
+
try {
|
|
1732
|
+
event = JSON.parse(raw.toString());
|
|
1733
|
+
} catch {
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
events.push(event);
|
|
1738
|
+
|
|
1739
|
+
const isTextDeltaEvent = event.type === 'response.text.delta' || event.type === 'response.output_text.delta';
|
|
1740
|
+
if (isTextDeltaEvent) {
|
|
1741
|
+
const delta = MixOpenAIWebSocket.extractDelta(event);
|
|
1742
|
+
if (delta) {
|
|
1743
|
+
message += delta;
|
|
1744
|
+
if (this.streamCallback) {
|
|
1745
|
+
this.streamCallback({ response: event, message, delta });
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
if (event.type === 'response.done') {
|
|
1752
|
+
finalResponse = event.response || null;
|
|
1753
|
+
if (!message && finalResponse) {
|
|
1754
|
+
message = MixOpenAIResponses.extractResponsesMessage(finalResponse);
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
if (!settled) {
|
|
1758
|
+
settled = true;
|
|
1759
|
+
cleanUp();
|
|
1760
|
+
ws.close();
|
|
1761
|
+
resolve({
|
|
1762
|
+
message: message.trim(),
|
|
1763
|
+
think: null,
|
|
1764
|
+
toolCalls: [],
|
|
1765
|
+
tokens: MixOpenAIResponses.extractResponsesTokens(finalResponse || {}),
|
|
1766
|
+
response: {
|
|
1767
|
+
response: finalResponse,
|
|
1768
|
+
events
|
|
1769
|
+
}
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
if (event.type === 'error' && !settled) {
|
|
1776
|
+
settled = true;
|
|
1777
|
+
cleanUp();
|
|
1778
|
+
ws.close();
|
|
1779
|
+
reject({
|
|
1780
|
+
message: event.error?.message || 'Realtime WebSocket error',
|
|
1781
|
+
statusCode: null,
|
|
1782
|
+
details: event.error || event,
|
|
1783
|
+
config: mergedConfig,
|
|
1784
|
+
options
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
});
|
|
1788
|
+
|
|
1789
|
+
ws.on('error', error => {
|
|
1790
|
+
if (settled) return;
|
|
1791
|
+
settled = true;
|
|
1792
|
+
cleanUp();
|
|
1793
|
+
reject({
|
|
1794
|
+
message: error.message || 'Realtime WebSocket connection error',
|
|
1795
|
+
statusCode: null,
|
|
1796
|
+
details: null,
|
|
1797
|
+
stack: error.stack,
|
|
1798
|
+
config: mergedConfig,
|
|
1799
|
+
options
|
|
1800
|
+
});
|
|
1801
|
+
});
|
|
1802
|
+
|
|
1803
|
+
ws.on('close', () => {
|
|
1804
|
+
if (settled) return;
|
|
1805
|
+
settled = true;
|
|
1806
|
+
cleanUp();
|
|
1807
|
+
reject({
|
|
1808
|
+
message: 'Realtime WebSocket closed before response.done',
|
|
1809
|
+
statusCode: null,
|
|
1810
|
+
details: null,
|
|
1811
|
+
config: mergedConfig,
|
|
1812
|
+
options
|
|
1813
|
+
});
|
|
1814
|
+
});
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
static messagesToConversationItems(messages = []) {
|
|
1819
|
+
const items = [];
|
|
1820
|
+
|
|
1821
|
+
for (const message of messages) {
|
|
1822
|
+
if (!message || !message.role) continue;
|
|
1823
|
+
if (message.role === 'tool' || message.tool_calls) continue;
|
|
1824
|
+
|
|
1825
|
+
const role = message.role === 'assistant' ? 'assistant' : (message.role === 'system' ? 'system' : 'user');
|
|
1826
|
+
const content = [];
|
|
1827
|
+
|
|
1828
|
+
if (typeof message.content === 'string') {
|
|
1829
|
+
content.push({
|
|
1830
|
+
type: role === 'assistant' ? 'text' : 'input_text',
|
|
1831
|
+
text: message.content
|
|
1832
|
+
});
|
|
1833
|
+
} else if (Array.isArray(message.content)) {
|
|
1834
|
+
for (const item of message.content) {
|
|
1835
|
+
if (!item || item.type !== 'text' || typeof item.text !== 'string') continue;
|
|
1836
|
+
content.push({
|
|
1837
|
+
type: role === 'assistant' ? 'text' : 'input_text',
|
|
1838
|
+
text: item.text
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
if (content.length === 0) continue;
|
|
1844
|
+
items.push({ type: 'message', role, content });
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
return items;
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
static extractDelta(event) {
|
|
1851
|
+
if (typeof event.delta === 'string') return event.delta;
|
|
1852
|
+
return '';
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1502
1856
|
class MixOpenRouter extends MixOpenAI {
|
|
1503
1857
|
getDefaultConfig(customConfig) {
|
|
1504
1858
|
|
|
@@ -2273,4 +2627,4 @@ class MixGoogle extends MixCustom {
|
|
|
2273
2627
|
}
|
|
2274
2628
|
}
|
|
2275
2629
|
|
|
2276
|
-
module.exports = { MixCustom, ModelMix, MixAnthropic, MixMiniMax, MixOpenAI, MixOpenRouter, MixPerplexity, MixOllama, MixLMStudio, MixGroq, MixTogether, MixGrok, MixCerebras, MixGoogle, MixFireworks };
|
|
2630
|
+
module.exports = { MixCustom, ModelMix, MixAnthropic, MixMiniMax, MixOpenAI, MixOpenAIResponses, MixOpenAIWebSocket, MixOpenRouter, MixPerplexity, MixOllama, MixLMStudio, MixGroq, MixTogether, MixGrok, MixCerebras, MixGoogle, MixFireworks };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modelmix",
|
|
3
|
-
"version": "4.4.
|
|
3
|
+
"version": "4.4.16",
|
|
4
4
|
"description": "🧬 Reliable interface with automatic fallback for AI LLMs.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"openai",
|
|
17
17
|
"anthropic",
|
|
18
18
|
"agent",
|
|
19
|
-
"
|
|
19
|
+
"realtime",
|
|
20
20
|
"gpt",
|
|
21
21
|
"claude",
|
|
22
22
|
"llama",
|
|
@@ -47,16 +47,17 @@
|
|
|
47
47
|
},
|
|
48
48
|
"homepage": "https://github.com/clasen/ModelMix#readme",
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
50
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
51
51
|
"axios": "^1.13.5",
|
|
52
52
|
"bottleneck": "^2.19.5",
|
|
53
53
|
"file-type": "^16.5.4",
|
|
54
54
|
"form-data": "^4.0.4",
|
|
55
|
-
"lemonlog": "^1.2.0"
|
|
55
|
+
"lemonlog": "^1.2.0",
|
|
56
|
+
"ws": "^8.19.0"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"chai": "^5.2.1",
|
|
59
|
-
"mocha": "^11.
|
|
60
|
+
"mocha": "^11.7.5",
|
|
60
61
|
"nock": "^14.0.9",
|
|
61
62
|
"sinon": "^21.0.0"
|
|
62
63
|
},
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
## MONSTER IN THE HOUSE
|
|
2
|
-
|
|
3
|
-
¿Qué tienen en común _Jaws_, _The Exorcist_ y _Alien_? Son ejemplos del género que llamo “Monstruo en la casa”. Se basa en dos elementos: un monstruo y una casa. Al meter personas dentro, intentando matar al monstruo, surge una historia **primitiva** y universal: no… te… dejes… devorar.
|
|
4
|
-
|
|
5
|
-
Por eso este género ha generado tantos éxitos y franquicias. _Jurassic Park_; las series _Nightmare On Elm Street_, _Friday the 13th_ y _Scream_; _Tremors_ y sus secuelas; y las historias de casas embrujadas entran aquí. Incluso sin lo sobrenatural, como _Fatal Attraction_ (con Glenn Close como el “monstruo”), funciona igual. Películas como _Arachnophobia_, _Lake Placid_ y _Deep Blue Sea_ muestran que si no entiendes sus reglas, fallas.
|
|
6
|
-
Para mí, las reglas son simples. La “casa” debe ser un espacio confinado: un pueblo costero, una nave espacial, un Disneyland futurista con dinosaurios o una familia. Debe cometerse un pecado —casi siempre la codicia (económica o sexual)— que detona la creación de un monstruo sobrenatural, un ángel vengador que mata a los culpables y perdona a quienes entienden su falta. El resto es “correr y esconderse”. El trabajo del guionista es aportar un giro al monstruo, sus poderes y la forma de asustar (“¡Bú!”).
|
|
7
|
-
|
|
8
|
-
Un mal ejemplo es _Arachnophobia_, con Jeff Daniels y John Goodman. El “monstruo” es una araña pequeña: poco sobrenatural y no tan aterradora; la pisas y muere. Además, no hay “casa”: los personajes pueden irse cuando quieran. Sin encierro, no hay tensión. Al romper las reglas de “Monstruo en la Casa”, la película queda en un híbrido: ¿comedia o drama?, ¿de verdad busca asustar?
|
|
9
|
-
Ningún género está agotado. Siempre se puede crear uno nuevo, pero debe tener un giro fresco y romper el cliché: “Danos lo mismo… pero distinto”. Quien crea que el género *Monstruo en la casa* ya no ofrece nada, piense en el mito del Minotauro: un gran monstruo (mitad hombre/mitad toro) y una gran casa (un laberinto donde envían a morir a los condenados). Aun así, nadie imaginó variaciones modernas como Glenn Close con un mal permanente y un conejo hervido.
|
|
10
|
-
|
|
11
|
-
## EL VELLOCINO DE ORO
|
|
12
|
-
|
|
13
|
-
El mito de la búsqueda sigue siendo de los más efectivos. Si tu guion es una *Road Movie*, aplica las reglas de “El Vellocino de Oro”, inspirado en Jasón y los Argonautas: un héroe sale a la ruta por una cosa y termina descubriendo otra —a sí mismo. Así, _Wizard Of Oz_, _Planes, Trains and Automobiles_, _Star Wars_, _Road Trip_ y _Back to the Future_ son, en esencia, la misma historia.
|
|
14
|
-
|
|
15
|
-
¿Da miedo, no?
|
|
16
|
-
Como en cualquier historia, los hitos de *El vellocino de oro* son las personas y los incidentes que el héroe encuentra en el camino. Aunque sea episódico y parezca desconectado, debe estar unido por un tema: el crecimiento interno. El efecto de cada incidente en el héroe es la trama; el progreso real no es la distancia recorrida, sino cómo cambia. Tu tarea es hacer que esos hitos tengan significado para el héroe.
|
|
17
|
-
Estoy trabajando en una historia de “Vellocino de Oro” con mi socio de escritura, Sheldon Bull, y hemos analizado varias películas del género. Como la nuestra es una comedia, revisamos _Planes, Trains and Automobiles_ y hablamos de las dinámicas de _Rain Man_, _Road Trip_ y _Animal House_ para entender mejor la premisa: un chico vuelve a casa tras ser expulsado injustamente de una escuela militar y descubre que sus padres se mudaron sin avisarle. Es, en esencia, “_Home Alone_ en la carretera”.
|
|
18
|
-
|
|
19
|
-
Los cambios no se enfocan en la aventura, sino en lo que cada incidente significa para el protagonista: las escenas deben marcar hitos de crecimiento. Al final, como en _The Odyssey_ y _Gulliver’s Travels_, lo que hace funcionar la historia no son los hechos, sino lo que el héroe aprende de ellos.
|
|
20
|
-
Este género incluye las películas de atracos. Cualquier búsqueda, misión o “tesoro en un castillo” emprendida por una persona o un grupo entra en la categoría del **Vellocino de Oro** y sigue las mismas reglas. A menudo, la misión pasa a segundo plano frente a descubrimientos personales; los giros importan menos que el sentido que deja el atraco, como muestran _Ocean’s Eleven_, _The Dirty Dozen_ y _The Magnificent Seven_.
|
|
21
|
-
|
|
22
|
-
## FUERA DE LA BOTELLA
|
|
23
|
-
|
|
24
|
-
“¡Ojalá tuviera mi propio dinero!” dice Preston Waters en _Blank Check_, película que Colby Carr y yo escribimos y vendimos a Disney. Pronto tendrá un millón de dólares y lo gastará sin control. Este tipo de cumplimiento de deseos es común porque refleja una parte central de la psicología humana: “Ojalá tuviera ________” es una de las plegarias más repetidas. Las historias “¿qué pasaría si...?” que explotan esas fantasías son primitivas, fáciles de entender, abundan y suelen funcionar.
|
|
25
|
-
_Bruce Almighty_ ejemplifica el género “Out of the Bottle”. La magia no tiene que venir de Dios: puede provenir de un objeto (_The Mask_), un auto (_The Love Bug_), una fórmula (_Love Potion #9_) o una sustancia (_Flubber_).
|
|
26
|
-
|
|
27
|
-
El nombre sugiere a un genio que concede deseos, pero no requiere magia literal. En _Blank Check_ no hay hechizo: por suerte o circunstancias, el deseo se cumple. Como simpatizamos con el protagonista y creemos que lo merece, su vida empieza a cambiar.
|
|
28
|
-
|
|
29
|
-
La otra cara del mismo esquema es la maldición: historias de castigo o lección. _Liar, Liar_ es un ejemplo, con la misma premisa.
|
|
30
|
-
Un niño desea que su padre, un abogado mentiroso, diga solo la verdad, y sucede: de pronto Jim Carrey no puede mentir justo el día de un caso clave. Para salir adelante debe cambiar y madurar, y así obtiene lo que quería: el respeto de su esposa e hijo. Otras historias de “escarmiento” incluyen _Freaky Friday_, _All Of Me_ y _Groundhog Day_.
|
|
31
|
-
|
|
32
|
-
Las reglas de **Out of the Bottle** son: en relatos de cumplimiento de deseos, el héroe debe ser un “Cenicienta” oprimido por su entorno, de modo que el público quiera que al fin sea feliz. Pero tampoco queremos verlo triunfar demasiado tiempo. Al final debe aprender que la magia no lo es todo y que es mejor ser como la audiencia; por eso la historia debe cerrar con una lección moral.
|
|
33
|
-
Si es una versión de **ajuste de cuentas** de *Out of the Bottle*, se invierte la premisa: el protagonista merece una lección, pero tiene algo rescatable. Esto es más difícil y requiere una escena inicial de *Save the Cat* que muestre que, aunque sea un patán, vale la pena salvarlo. A lo largo de la historia recibe el “beneficio” de la magia (aunque sea una maldición) y al final triunfa.
|
|
34
|
-
|
|
35
|
-
## TIPO CON UN PROBLEMA
|
|
36
|
-
|
|
37
|
-
Este género se define así: “Una persona común se ve envuelta en circunstancias extraordinarias”. Nos atrae porque nos identificamos con alguien “normal” desde el inicio. En un comienzo de “día cualquiera”, irrumpe algo fuera de lo común: terroristas toman un edificio (*Die Hard*), llegan nazis (*Schindler’s List*), aparece un robot del futuro que amenaza a la protagonista y a su hijo no nacido (*The Terminator*), o un barco choca con un iceberg y se hunde sin botes suficientes (*Titanic*).
|
|
38
|
-
Estos son problemas grandes y primarios.
|
|
39
|
-
|
|
40
|
-
¿Como los enfrenta una persona común?
|
|
41
|
-
|
|
42
|
-
Como *Monster in the House*, este género tiene dos partes: un tipo cualquiera (hombre o mujer) y un problema que debe vencer sacando fuerzas de sí mismo. Mientras más común sea el protagonista, más grande debe ser el desafío.
|
|
43
|
-
|
|
44
|
-
En *Breakdown*, Kurt Russell no tiene poderes ni entrenamiento; su objetivo es simple y universal: salvar a la esposa que ama. Más que la habilidad del héroe, lo que sostiene la historia es el tamaño del reto: cuanto peor sea el villano, mayor será el heroísmo. El protagonista triunfa al usar su individualidad para superar fuerzas mucho más poderosas.
|
|
45
|
-
|
|
46
|
-
## RITES OF PASSAGE
|
|
47
|
-
Recuerda la pubertad incómoda y a esa chica que te gustaba y ni sabía que existías. O la fiesta de tus 40, cuando tu esposo te pidió el divorcio. Estas transiciones nos tocan porque casi todos las hemos vivido. Las historias de “dolores de crecimiento” se sienten intensas por ser etapas sensibles; nos humanizan y dan pie a relatos conmovedores o incluso graciosos (como la crisis de mediana edad en _10_ con Dudley Moore). Ya sea drama o comedia, las historias de “Ritos de paso” comparten el mismo tipo y las mismas reglas.
|
|
48
|
-
Todas las películas tratan del cambio, pero los **ritos de paso** se enfocan en el dolor causado por una fuerza externa: la vida. El “monstruo” suele ser invisible o innombrable, y el héroe tarda en reconocerlo. Historias sobre adicciones, pubertad, crisis de mediana edad, vejez, rupturas o duelo comparten algo: todos entienden lo que ocurre excepto quien lo vive, y solo la experiencia trae la salida.
|
|
49
|
-
|
|
50
|
-
Sea comedia o drama, el monstruo aparece sin aviso y el relato sigue el descubrimiento gradual de su naturaleza. Al final, la victoria llega al **rendirse** ante fuerzas mayores y aceptar nuestra humanidad. La moraleja es siempre la misma: _¡Así es la vida!_
|
|
51
|
-
Si tu idea puede considerarse una historia de Rito de Paso, estas películas son aptas para proyectarse. Como las etapas de aceptación descritas en _On Death and Dying_ de Elizabeth Kübler-Ross, la estructura se traza en la aceptación a regañadientes del héroe ante fuerzas de la naturaleza que no puede controlar ni comprender, y el triunfo llega cuando finalmente logra sonreír.
|
|
52
|
-
|
|
53
|
-
## AMOR ENTRE AMIGOS
|
|
54
|
-
La historia clásica de “compañeros” es, en gran medida, un producto del cine. Aunque existen antecedentes como *Don Quijote*, el formato despegó con la pantalla: al no poder recurrir al monólogo interior, se creó un segundo personaje para que el protagonista tuviera con quién reaccionar y debatir los temas clave.
|
|
55
|
-
|
|
56
|
-
Así nació el “buddy movie”, que se volvió un básico: dos personajes conversando y enfrentando el mundo juntos, porque las historias de “yo y mi mejor amigo” son universales y fácilmente comprensibles.
|
|
57
|
-
|
|
58
|
-
El secreto es que un buen buddy movie suele ser una historia de amor disfrazada; y, a la inversa, muchas historias de amor funcionan como buddy movies con potencial sexual. Películas como *Bringing Up Baby*, *Pat and Mike*, *Woman of the Year*, *Two Weeks Notice* y *How to Lose a Guy in 10 Days* son, por género, versiones más sofisticadas de Laurel y Har
|
|
59
|
-
Hay películas donde uno de los amigos usa falda, pero las reglas son las mismas: drama o comedia, con sexo o sin él. Al inicio, los “amigos” se odian, pero la aventura revela que se necesitan; son mitades incompletas de un todo. Aceptarlo genera más conflicto: ¿quién soporta necesitar a alguien?
|
|
60
|
-
En el momento de **Todo está perdido** (más en el Capítulo Cuatro), hacia el final de estas historias, parece haber separación, pelea o un “adiós y que te vaya bien”, pero en realidad no es eso. Son dos personas que no soportan vivir tan bien sin la otra y deben rendir el ego para ganar. Cuando cae el telón, lo han logrado.
|
|
61
|
-
|
|
62
|
-
A menudo, como en _Rain Man_, uno es el héroe y cambia casi todo (Tom Cruise), mientras el otro funciona como catalizador y cambia poco o nada (Dustin Hoffman). La discusión suele reducirse a: _¿De quién es la historia?_ En _Lethal Weapon_, en gran medida es la de Danny Glover; Mel Gibson impulsa el cambio. Aunque Mel deja de ser suicida, la transformación que más importa es la de Danny. Estas historias de “catalizador”, donde alguien llega, impacta y se va, son un subgrupo clave del Buddy Love. Muchas historias de “niño y su perro” funcionan así, incluida _E.T._
|
|
63
|
-
Si estás escribiendo una película de colegas o una historia de amor, en drama o comedia, debes conocer la estructura Buddy Love. Al ver varias, notarás que comparten patrones muy similares. No es plagio: es narrativa efectiva, y esos momentos se repiten porque funcionan.
|
|
64
|
-
|
|
65
|
-
## WHYDUNIT
|
|
66
|
-
|
|
67
|
-
Sabemos que existen la codicia y el crimen, pero el “quién” rara vez importa tanto como el “por qué”. Un buen Whydunit no trata de que el héroe cambie, sino de que el público descubra algo inesperado y a menudo oscuro sobre la naturaleza humana, respondiendo la pregunta central: ¿por qué?
|
|
68
|
-
_Chinatown_ quizá sea el mejor *Whydunit* y un referente de gran guion: cada revisión revela capas nuevas. Como en _China Syndrome_, _All the President’s Men_, _JFK_ o _Mystic River_, estas historias exploran el lado oscuro. Las reglas son simples: el público es el detective. Aunque haya un sustituto en pantalla que investigue, somos nosotros quienes ordenamos la información y quedamos impactados por lo que descubrimos.
|
|
69
|
-
|
|
70
|
-
Si tu película trata de este tipo de revelación, estudia los grandes *Whydunits*: cómo un personaje nos representa y cómo la pesquisa del lado oscuro de la humanidad termina siendo una pesquisa sobre nosotros mismos. Eso hace un buen *Whydunit*: vuelve la radiografía hacia el espectador y pregunta: “¿Somos *nosotros* así de malvados?”
|
|
71
|
-
|
|
72
|
-
## EL TONTO TRIUNFANTE
|
|
73
|
-
El “Tonto” ha sido un personaje clave en mitos y leyendas. Por fuera parece el Idiota del Pueblo, pero al mirarlo mejor suele ser el más sabio. Su condición de desvalido le da anonimato y hace que otros lo subestimen, permitiéndole destacar al final.
|
|
74
|
-
|
|
75
|
-
En el cine, esta figura viene de Chaplin, Keaton y Lloyd: hombres pequeños y pasados por alto que triunfan por suerte, valentía y por no rendirse. En el cine moderno, ejemplos como _Dave_, _Being There_, _Amadeus_ y _Forrest Gump_ muestran cómo la tradición evoluciona.
|
|
76
|
-
|
|
77
|
-
El principio de “El Tonto Triunfante” enfrenta al Tonto con un villano más poderoso, a menudo del “establishment”. Ver cómo un supuesto “idiota” vence a quienes la sociedad considera ganadores da esperanza y ridiculiza las estructuras que tomamos demasiado en serio; ningún poder es intocable.
|
|
78
|
-
Un filme de “Fool Triumphant” se basa en dos elementos: un perdedor subestimado, visto como inútil en la introducción, y una institución contra la que choca. A menudo lo acompaña un “insider” que entiende el engaño y no puede creer que funcione; suele llevarse la peor parte del slapstick por intentar intervenir.
|
|
79
|
-
|
|
80
|
-
Los “Fools” especiales, en comedias o dramas, muestran la vida del marginado. Como todos nos sentimos así a veces, estas historias ofrecen el placer vicario de ver al outsider triunfar.
|
|
81
|
-
|
|
82
|
-
## INSTITUCIONALIZADO
|
|
83
|
-
¿Dónde estaríamos sin los demás? Cuando nos unimos por una causa común, aparecen las tensiones entre sacrificar los objetivos de unos pocos por los de la mayoría. El género que llamo “Institucionalizado” cuenta historias sobre grupos, instituciones y “familias”. Estas narrativas honran a la institución, pero también revelan el costo de perder la identidad dentro de ella.
|
|
84
|
-
|
|
85
|
-
_One Flew Over the Cuckoo’s Nest_ trata sobre pacientes psiquiátricos; _American Beauty_, sobre suburbios modernos; _M*A*S*H_, sobre el ejército estadounidense; y _The Godfather_, sobre una familia mafiosa. En cada caso, un personaje destacado expone como engañoso el objetivo del grupo (Jack Nicholson, Kevin Spacey, Donald Sutherland y Al Pacino).
|
|
86
|
-
Llamo a estas historias **Institucionalizadas** porque la dinámica del grupo suele ser irracional e incluso autodestructiva. “Suicide Is Painless”, tema de _M*A*S*H_, trata menos de la locura de la guerra que de la mentalidad de rebaño. Al ponernos un uniforme —militar o simbólico— cedemos parte de nuestra identidad. Estas películas exploran los pros y contras de anteponer el grupo al individuo: una lealtad “primitiva” que a veces contradice el sentido común o la supervivencia, pero que repetimos desde siempre. Ver a otros librar ese conflicto explica por qué el género es tan popular y tan visceral.
|
|
87
|
-
|
|
88
|
-
A menudo se narra desde la perspectiva de un recién llegado: es el espectador, alguien nuevo en el grupo, guiado por otro más experimentado. Jane Fonda en _9 to 5_ y Tom Hulce en _Animal House_ son ejemplos. En mundos con tecnología, jerga o reglas poco familiares, estos personajes sirven para hacer preguntas (“¿Cómo funciona eso?”) y transmitir la información necesaria, mostrando ese entorno “loco” al público.
|
|
89
|
-
En el fondo, estas historias se reducen a una pregunta: ¿quién está más loco, yo o ellos? Para entender lo insensato que puede ser sacrificarse por el grupo basta con ver el rostro de Al Pacino al final de _El padrino 2_: se destruye por “la familia” y la “tradición”, y el resultado es devastador. Impacta como el giro final de _American Beauty_ y refleja la expresión vacía de Jack Nicholson en _Atrapado sin salida_. Es, en esencia, el mismo mensaje contado de formas distintas.
|
|
90
|
-
|
|
91
|
-
Funcionan porque siguen las reglas y nos dan lo mismo… pero diferente.
|
|
92
|
-
|
|
93
|
-
## SUPERHÉROE
|
|
94
|
-
|
|
95
|
-
El género “Superhéroe” es lo opuesto a “Tipo con un problema”: una persona extraordinaria cae en un mundo ordinario. Como Gulliver atado por los liliputienses, la historia nos pide humanizar a un ser superior, sentir empatía y entender lo que significa lidiar con “gente pequeña” como nosotros. Por eso tantos geeks y adolescentes se identifican: saben lo que es sentirse incomprendidos.
|
|
96
|
-
Este género va más allá de hombres con capa y mallas; no se limita a Marvel o DC. _Gladiator_ y _A Beautiful Mind_ muestran “superhéroes” humanos enfrentados a la mediocridad: el verdadero obstáculo son las mentes pequeñas que los rodean, incapaces de entenderlos. _Frankenstein_, _Dracula_ y _X-Men_ comparten esa idea. En el fondo, los relatos de superhéroes tratan de ser “diferente”: alguien con una visión única que provoca celos y rechazo, una sensación que cualquiera puede reconocer al ser desestimado por pensar distinto.
|
|
97
|
-
La dificultad de sentir simpatía por millonarios como Bruce Wayne o genios como Russell Crowe se resuelve al subrayar el dolor que acompaña esas ventajas. No es fácil ser Bruce Wayne: vive torturado. Y aunque la terapia sería más barata, es admirable porque renuncia a su comodidad para ayudar a la comunidad.
|
|
98
|
-
|
|
99
|
-
Esto explica por qué suele funcionar la primera película de una saga de superhéroes y las siguientes no (como _Robocop 2_): el mito de origen enfatiza la empatía por su conflicto, pero después se olvida reconstruirla y mostrarnos de nuevo su lado humano. (_Spider-Man 2_ evita ese error y fue un éxito.)
|
|
100
|
-
|
|
101
|
-
En realidad, nunca entenderemos del todo al superhéroe; nuestra conexión nace de la simpatía por ser _mal_ entendido. Por eso este tipo de historias perdura: impulsa nuestras fantasías sobre el potencial, pero las equilibra con realidad.
|
|
102
|
-
|
|
103
|
-
## EL PEQUEÑO Y SUCIO SECRETO DE HOLLYWOOD
|
|
104
|
-
Tras revisar estos géneros, es fácil notar por qué tantas películas se parecen estructuralmente y pensar que hay “plagio”. Y no estás tan equivocado.
|
|
105
|
-
|
|
106
|
-
Mira _Point Break_ y luego _Fast and Furious_: casi la misma historia, pero con surf vs. autos. Compara _The Matrix_ con _Monsters, Inc._: también comparten estructura. Hay muchos casos así.
|
|
107
|
-
|
|
108
|
-
A veces la copia es consciente; otras, coincidencia. Pero con frecuencia ocurre porque las plantillas narrativas funcionan y se repiten: son ejemplos de narración eficaz y, a menudo, éxitos. ¿De verdad alguien se queja de que _Fast and Furious_ tome los mismos beats de _Point Break_? Probablemente casi nadie lo nota.
|
|
109
|
-
Mi punto es: funciona, y por una razón. Las leyes del storytelling se aplican siempre. Tu trabajo es aprender por qué funciona y cómo encajan sus piezas. Si parece que estás copiando, no lo hagas; si suena a cliché, dale un giro; si es familiar, busca una forma nueva. Entiende por qué te atraen el cliché y lo conocido: las reglas existen por algo. Cuando dejes de sentirte limitado, verás lo liberadoras que son. La verdadera originalidad empieza cuando sabes de qué te estás alejando.
|
package/demo/story.md
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
Clara Velásquez: Todo apunta a que usaste esa vieja computadora para dominar la red, ¿verdad?
|
|
2
|
-
Ramón "El Gnomo" Herrera: ¿Dominar? Si apenas y sé enchufarla bien, Clara, pero no subestimes lo que una mente rápida puede hacer.
|
|
3
|
-
Diana Salazar: Rápido o lento, lo que importa es el control. Y alguien ha estado jugando sucio en esta partida.
|
|
4
|
-
Clara Velásquez: Ramón, encontré archivos con tu firma y experimentos de código que podrían hundir a cualquier rival.
|
|
5
|
-
Ramón "El Gnomo" Herrera: Tú quieres que crea que yo soy el hacker estrella detrás de todo este lío... ¿y si es alguien más haciéndolo parecer así?
|
|
6
|
-
Diana Salazar: Eso es justamente lo que quiero saber. ¿Quién tiene acceso a esta red desde fuera?
|
|
7
|
-
Clara Velásquez: Solo un ingeniero con mi nivel podía infiltrarse sin dejar rastro. Tengo evidencia de una copia de mi proyecto en el sistema.
|
|
8
|
-
Ramón "El Gnomo" Herrera: ¿Y si el verdadero troll es la propia Diana? Alguien con interés en desbaratar ambos bandos...
|
|
9
|
-
Diana Salazar: ¿Y qué ganaría yo haciendo eso? Solo busco proteger mi causa, no hundir la red por curiosidad.
|
|
10
|
-
Clara Velásquez: Entonces dime, Diana, ¿por qué tus movimientos siempre terminan beneficiando a la corporación contaminante para la que espías?
|
|
11
|
-
Ramón "El Gnomo" Herrera: Esperen, eso no cuadra con lo que he visto. Claramente hay una mano invisible manejando esto...
|
|
12
|
-
Diana Salazar: Justo cuando pensé que estaba perdiendo el control, revelo que todos tenemos un fragmento de culpa, pero no el control total.
|
|
13
|
-
Clara Velásquez: Entonces, ¿quién queda? Alguien aquí ha estado falsificando pruebas desde el principio, y esa persona está en esta habitación.
|
|
14
|
-
Ramón "El Gnomo" Herrera: Y si no somos nosotros, ¿qué significa eso para el juego que creíamos entender?
|
|
15
|
-
Diana Salazar: Significa que la verdadera partida apenas comienza, y esta vez, nadie puede confiar en nadie.
|