lynkr 8.0.1 → 9.0.2
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 +238 -315
- package/bin/cli.js +16 -3
- package/index.js +7 -3
- package/install.sh +3 -3
- package/lynkr-skill.tar.gz +0 -0
- package/native/Cargo.toml +26 -0
- package/native/index.js +29 -0
- package/native/lynkr-native.node +0 -0
- package/native/src/lib.rs +321 -0
- package/package.json +8 -6
- package/src/api/files-multipart.js +30 -0
- package/src/api/files-router.js +81 -0
- package/src/api/openai-router.js +379 -308
- package/src/api/providers-handler.js +171 -3
- package/src/api/router.js +109 -5
- package/src/cache/prompt.js +13 -0
- package/src/clients/circuit-breaker.js +10 -247
- package/src/clients/codex-process.js +342 -0
- package/src/clients/codex-utils.js +143 -0
- package/src/clients/databricks.js +243 -76
- package/src/clients/ollama-utils.js +21 -17
- package/src/clients/openai-format.js +20 -6
- package/src/clients/openrouter-utils.js +42 -37
- package/src/clients/prompt-cache-injection.js +140 -0
- package/src/clients/provider-capabilities.js +41 -0
- package/src/clients/resilience.js +540 -0
- package/src/clients/responses-format.js +8 -7
- package/src/clients/retry.js +22 -167
- package/src/clients/standard-tools.js +1 -1
- package/src/clients/xml-tool-extractor.js +307 -0
- package/src/cluster.js +82 -0
- package/src/config/index.js +66 -0
- package/src/context/compression.js +42 -9
- package/src/context/distill.js +507 -0
- package/src/context/tool-result-compressor.js +563 -0
- package/src/memory/extractor.js +22 -0
- package/src/orchestrator/index.js +147 -205
- package/src/routing/complexity-analyzer.js +258 -5
- package/src/routing/index.js +15 -34
- package/src/routing/latency-tracker.js +148 -0
- package/src/routing/model-tiers.js +2 -0
- package/src/routing/quality-scorer.js +113 -0
- package/src/routing/telemetry.js +502 -0
- package/src/server.js +23 -0
- package/src/stores/file-store.js +69 -0
- package/src/stores/response-store.js +25 -0
- package/src/tools/code-graph.js +538 -0
- package/src/tools/code-mode.js +304 -0
- package/src/tools/index.js +1 -1
- package/src/tools/lazy-loader.js +11 -0
- package/src/tools/mcp-remote.js +7 -0
- package/src/tools/smart-selection.js +11 -0
- package/src/tools/web.js +1 -1
- package/src/utils/payload.js +206 -0
- package/src/utils/perf-timer.js +80 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Mode — Meta-Tools for MCP Token Optimization
|
|
3
|
+
*
|
|
4
|
+
* Replaces 100+ individual MCP tool definitions with 4 meta-tools,
|
|
5
|
+
* reducing tool-catalog token overhead from ~17,500 to ~700 tokens.
|
|
6
|
+
*
|
|
7
|
+
* Inspired by Bifrost's Code Mode. Instead of sending every MCP tool
|
|
8
|
+
* schema in every request, the LLM discovers tools lazily:
|
|
9
|
+
* 1. mcp_list_tools → discover available tools (compact)
|
|
10
|
+
* 2. mcp_tool_info → load full schema for one tool
|
|
11
|
+
* 3. mcp_tool_docs → get usage examples
|
|
12
|
+
* 4. mcp_execute → execute a tool by name
|
|
13
|
+
*
|
|
14
|
+
* Activation: CODE_MODE_ENABLED=true
|
|
15
|
+
*
|
|
16
|
+
* @module tools/code-mode
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const { registerTool } = require('.');
|
|
20
|
+
const { listServers, ensureClient } = require('../mcp');
|
|
21
|
+
const config = require('../config');
|
|
22
|
+
const logger = require('../logger');
|
|
23
|
+
|
|
24
|
+
// ── Tool List Cache ─────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
let toolListCache = null;
|
|
27
|
+
let toolListCacheTs = 0;
|
|
28
|
+
|
|
29
|
+
function getCacheTtl() {
|
|
30
|
+
return config.mcp?.codeMode?.toolListCacheTtl || 60_000;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Fetch tool lists from all MCP servers, with caching.
|
|
35
|
+
* @param {string} [filterServerId] - Optional: only fetch from this server
|
|
36
|
+
* @param {boolean} [forceRefresh] - Bypass cache
|
|
37
|
+
* @returns {Promise<Object>} { serverId: [{ name, description }] }
|
|
38
|
+
*/
|
|
39
|
+
async function fetchToolList(filterServerId, forceRefresh = false) {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
if (!forceRefresh && toolListCache && (now - toolListCacheTs < getCacheTtl())) {
|
|
42
|
+
if (filterServerId) {
|
|
43
|
+
return { [filterServerId]: toolListCache[filterServerId] || [] };
|
|
44
|
+
}
|
|
45
|
+
return toolListCache;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const servers = listServers();
|
|
49
|
+
const result = {};
|
|
50
|
+
|
|
51
|
+
await Promise.all(
|
|
52
|
+
servers.map(async (server) => {
|
|
53
|
+
if (filterServerId && server.id !== filterServerId) return;
|
|
54
|
+
try {
|
|
55
|
+
const client = await ensureClient(server.id);
|
|
56
|
+
if (!client) {
|
|
57
|
+
result[server.id] = { error: 'Server not available' };
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const response = await client.request('tools/list', {});
|
|
61
|
+
const tools = Array.isArray(response?.tools) ? response.tools : [];
|
|
62
|
+
result[server.id] = tools.map(t => ({
|
|
63
|
+
name: t.name ?? t.method ?? 'unknown',
|
|
64
|
+
description: (t.description || '').substring(0, 100),
|
|
65
|
+
}));
|
|
66
|
+
} catch (err) {
|
|
67
|
+
result[server.id] = { error: err.message };
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Update cache if we fetched all servers
|
|
73
|
+
if (!filterServerId) {
|
|
74
|
+
toolListCache = result;
|
|
75
|
+
toolListCacheTs = now;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return filterServerId ? { [filterServerId]: result[filterServerId] || [] } : result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Fetch full tool schema for a specific tool on a specific server.
|
|
83
|
+
* @param {string} serverId
|
|
84
|
+
* @param {string} toolName
|
|
85
|
+
* @returns {Promise<Object|null>}
|
|
86
|
+
*/
|
|
87
|
+
async function fetchToolSchema(serverId, toolName) {
|
|
88
|
+
const client = await ensureClient(serverId);
|
|
89
|
+
if (!client) return null;
|
|
90
|
+
|
|
91
|
+
const response = await client.request('tools/list', {});
|
|
92
|
+
const tools = Array.isArray(response?.tools) ? response.tools : [];
|
|
93
|
+
return tools.find(t => (t.name ?? t.method) === toolName) || null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Generate a usage example from a tool's input schema.
|
|
98
|
+
* @param {Object} tool - Tool definition with inputSchema
|
|
99
|
+
* @returns {string} Example JSON
|
|
100
|
+
*/
|
|
101
|
+
function generateExample(tool) {
|
|
102
|
+
const schema = tool.inputSchema || tool.input_schema || {};
|
|
103
|
+
const props = schema.properties || {};
|
|
104
|
+
const example = {};
|
|
105
|
+
|
|
106
|
+
for (const [key, def] of Object.entries(props)) {
|
|
107
|
+
if (def.type === 'string') example[key] = def.example || `<${key}>`;
|
|
108
|
+
else if (def.type === 'number' || def.type === 'integer') example[key] = def.example || 0;
|
|
109
|
+
else if (def.type === 'boolean') example[key] = def.example ?? true;
|
|
110
|
+
else if (def.type === 'array') example[key] = [];
|
|
111
|
+
else if (def.type === 'object') example[key] = {};
|
|
112
|
+
else example[key] = null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return JSON.stringify(example, null, 2);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Meta-Tool Registration ──────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
function registerCodeModeTools() {
|
|
121
|
+
// 1. mcp_list_tools — discover available tools
|
|
122
|
+
registerTool(
|
|
123
|
+
'mcp_list_tools',
|
|
124
|
+
async ({ args = {} }) => {
|
|
125
|
+
const serverId = args.server_id || null;
|
|
126
|
+
const forceRefresh = args.force_refresh === true;
|
|
127
|
+
const result = await fetchToolList(serverId, forceRefresh);
|
|
128
|
+
|
|
129
|
+
// Add summary stats
|
|
130
|
+
let totalTools = 0;
|
|
131
|
+
for (const tools of Object.values(result)) {
|
|
132
|
+
if (Array.isArray(tools)) totalTools += tools.length;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
ok: true,
|
|
137
|
+
status: 200,
|
|
138
|
+
content: JSON.stringify({ total_tools: totalTools, servers: result }, null, 2),
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
category: 'code-mode',
|
|
143
|
+
description: 'List all available MCP tools across all servers. Returns tool names and brief descriptions. Use this first to discover what tools are available.',
|
|
144
|
+
input_schema: {
|
|
145
|
+
type: 'object',
|
|
146
|
+
properties: {
|
|
147
|
+
server_id: { type: 'string', description: 'Optional: filter to a specific MCP server ID' },
|
|
148
|
+
force_refresh: { type: 'boolean', description: 'Bypass cache and refresh tool list' },
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// 2. mcp_tool_info — load full schema for one tool
|
|
155
|
+
registerTool(
|
|
156
|
+
'mcp_tool_info',
|
|
157
|
+
async ({ args = {} }) => {
|
|
158
|
+
const serverId = args.server_id;
|
|
159
|
+
const toolName = args.tool_name;
|
|
160
|
+
|
|
161
|
+
if (!serverId || !toolName) {
|
|
162
|
+
throw new Error('mcp_tool_info requires server_id and tool_name');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const tool = await fetchToolSchema(serverId, toolName);
|
|
166
|
+
if (!tool) {
|
|
167
|
+
throw new Error(`Tool "${toolName}" not found on server "${serverId}"`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
ok: true,
|
|
172
|
+
status: 200,
|
|
173
|
+
content: JSON.stringify({
|
|
174
|
+
server: serverId,
|
|
175
|
+
name: tool.name ?? tool.method,
|
|
176
|
+
description: tool.description || '',
|
|
177
|
+
inputSchema: tool.inputSchema || tool.input_schema || {},
|
|
178
|
+
}, null, 2),
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
category: 'code-mode',
|
|
183
|
+
description: 'Get the full schema and detailed description for a specific MCP tool. Use after mcp_list_tools to get the exact parameters needed before calling mcp_execute.',
|
|
184
|
+
input_schema: {
|
|
185
|
+
type: 'object',
|
|
186
|
+
properties: {
|
|
187
|
+
server_id: { type: 'string', description: 'MCP server ID' },
|
|
188
|
+
tool_name: { type: 'string', description: 'Tool name from mcp_list_tools' },
|
|
189
|
+
},
|
|
190
|
+
required: ['server_id', 'tool_name'],
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// 3. mcp_tool_docs — usage examples
|
|
196
|
+
registerTool(
|
|
197
|
+
'mcp_tool_docs',
|
|
198
|
+
async ({ args = {} }) => {
|
|
199
|
+
const serverId = args.server_id;
|
|
200
|
+
const toolName = args.tool_name;
|
|
201
|
+
|
|
202
|
+
if (!serverId || !toolName) {
|
|
203
|
+
throw new Error('mcp_tool_docs requires server_id and tool_name');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const tool = await fetchToolSchema(serverId, toolName);
|
|
207
|
+
if (!tool) {
|
|
208
|
+
throw new Error(`Tool "${toolName}" not found on server "${serverId}"`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const schema = tool.inputSchema || tool.input_schema || {};
|
|
212
|
+
const params = Object.entries(schema.properties || {}).map(([name, def]) => ({
|
|
213
|
+
name,
|
|
214
|
+
type: def.type || 'any',
|
|
215
|
+
required: (schema.required || []).includes(name),
|
|
216
|
+
description: def.description || '',
|
|
217
|
+
}));
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
ok: true,
|
|
221
|
+
status: 200,
|
|
222
|
+
content: JSON.stringify({
|
|
223
|
+
server: serverId,
|
|
224
|
+
tool: tool.name ?? tool.method,
|
|
225
|
+
description: tool.description || '',
|
|
226
|
+
parameters: params,
|
|
227
|
+
example_arguments: generateExample(tool),
|
|
228
|
+
usage: `Use mcp_execute with server_id="${serverId}", tool_name="${toolName}", and arguments matching the schema above.`,
|
|
229
|
+
}, null, 2),
|
|
230
|
+
};
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
category: 'code-mode',
|
|
234
|
+
description: 'Get usage documentation, parameter details, and example arguments for an MCP tool.',
|
|
235
|
+
input_schema: {
|
|
236
|
+
type: 'object',
|
|
237
|
+
properties: {
|
|
238
|
+
server_id: { type: 'string', description: 'MCP server ID' },
|
|
239
|
+
tool_name: { type: 'string', description: 'Tool name' },
|
|
240
|
+
},
|
|
241
|
+
required: ['server_id', 'tool_name'],
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// 4. mcp_execute — execute a tool by name
|
|
247
|
+
registerTool(
|
|
248
|
+
'mcp_execute',
|
|
249
|
+
async ({ args = {} }) => {
|
|
250
|
+
const serverId = args.server_id;
|
|
251
|
+
const toolName = args.tool_name;
|
|
252
|
+
const toolArgs = args.arguments ?? {};
|
|
253
|
+
|
|
254
|
+
if (!serverId || !toolName) {
|
|
255
|
+
throw new Error('mcp_execute requires server_id and tool_name');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const client = await ensureClient(serverId.trim());
|
|
259
|
+
if (!client) {
|
|
260
|
+
throw new Error(`MCP server "${serverId}" is not available.`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const result = await client.request(toolName.trim(), toolArgs);
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
ok: true,
|
|
267
|
+
status: 200,
|
|
268
|
+
content: JSON.stringify({
|
|
269
|
+
server: serverId,
|
|
270
|
+
tool: toolName,
|
|
271
|
+
result,
|
|
272
|
+
}, null, 2),
|
|
273
|
+
metadata: { server: serverId, tool: toolName },
|
|
274
|
+
};
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
category: 'code-mode',
|
|
278
|
+
description: 'Execute an MCP tool by name with JSON arguments. First use mcp_list_tools to discover tools, then mcp_tool_info to get the schema, then this tool to execute.',
|
|
279
|
+
input_schema: {
|
|
280
|
+
type: 'object',
|
|
281
|
+
properties: {
|
|
282
|
+
server_id: { type: 'string', description: 'MCP server ID' },
|
|
283
|
+
tool_name: { type: 'string', description: 'Tool method name' },
|
|
284
|
+
arguments: {
|
|
285
|
+
type: 'object',
|
|
286
|
+
description: 'JSON arguments matching the tool input schema',
|
|
287
|
+
additionalProperties: true,
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
required: ['server_id', 'tool_name'],
|
|
291
|
+
},
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
logger.info('[code-mode] Registered 4 meta-tools: mcp_list_tools, mcp_tool_info, mcp_tool_docs, mcp_execute');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
module.exports = {
|
|
299
|
+
registerCodeModeTools,
|
|
300
|
+
// Exported for testing
|
|
301
|
+
fetchToolList,
|
|
302
|
+
fetchToolSchema,
|
|
303
|
+
generateExample,
|
|
304
|
+
};
|
package/src/tools/index.js
CHANGED
package/src/tools/lazy-loader.js
CHANGED
|
@@ -77,6 +77,11 @@ const TOOL_CATEGORIES = {
|
|
|
77
77
|
loader: () => require('./agent-task').registerAgentTaskTool,
|
|
78
78
|
priority: 2,
|
|
79
79
|
},
|
|
80
|
+
'code-mode': {
|
|
81
|
+
keywords: ['mcp', 'execute', 'server', 'tool', 'code mode'],
|
|
82
|
+
loader: () => require('./code-mode').registerCodeModeTools,
|
|
83
|
+
priority: 3,
|
|
84
|
+
},
|
|
80
85
|
};
|
|
81
86
|
|
|
82
87
|
/**
|
|
@@ -281,6 +286,12 @@ function loadCategoryForTool(toolName) {
|
|
|
281
286
|
'workspace_sandbox_sessions': 'mcp',
|
|
282
287
|
'workspace_mcp_servers': 'mcp',
|
|
283
288
|
|
|
289
|
+
// Code Mode meta-tools
|
|
290
|
+
'mcp_list_tools': 'code-mode',
|
|
291
|
+
'mcp_tool_info': 'code-mode',
|
|
292
|
+
'mcp_tool_docs': 'code-mode',
|
|
293
|
+
'mcp_execute': 'code-mode',
|
|
294
|
+
|
|
284
295
|
// Agent task
|
|
285
296
|
// TinyFish (web agent)
|
|
286
297
|
'web_agent': 'tinyfish',
|
package/src/tools/mcp-remote.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { registerTool } = require(".");
|
|
2
2
|
const { listServers, ensureClient } = require("../mcp");
|
|
3
|
+
const config = require("../config");
|
|
3
4
|
const logger = require("../logger");
|
|
4
5
|
|
|
5
6
|
const REMOTE_TOOL_PREFIX = "mcp";
|
|
@@ -12,6 +13,12 @@ function sanitiseName(value) {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
async function registerRemoteTools() {
|
|
16
|
+
// Code Mode: register 4 meta-tools instead of individual remote tools
|
|
17
|
+
if (config.mcp?.codeMode?.enabled) {
|
|
18
|
+
const { registerCodeModeTools } = require("./code-mode");
|
|
19
|
+
registerCodeModeTools();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
15
22
|
const servers = listServers();
|
|
16
23
|
await Promise.all(
|
|
17
24
|
servers.map(async (server) => {
|
|
@@ -316,6 +316,17 @@ function selectToolsSmartly(tools, classification, options = {}) {
|
|
|
316
316
|
selectedTools = selectedTools.filter(t => minimalTools.includes(t.name));
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
+
// Code Mode: always include the 4 meta-tools (only ~700 tokens total)
|
|
320
|
+
const codeConfig = require('../config');
|
|
321
|
+
if (codeConfig.mcp?.codeMode?.enabled) {
|
|
322
|
+
const codeModeNames = new Set(['mcp_list_tools', 'mcp_tool_info', 'mcp_tool_docs', 'mcp_execute']);
|
|
323
|
+
for (const tool of tools) {
|
|
324
|
+
if (codeModeNames.has(tool.name) && !selectedTools.some(t => t.name === tool.name)) {
|
|
325
|
+
selectedTools.push(tool);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
319
330
|
return selectedTools;
|
|
320
331
|
}
|
|
321
332
|
|
package/src/tools/web.js
CHANGED
|
@@ -152,7 +152,7 @@ function summariseResult(item) {
|
|
|
152
152
|
return {
|
|
153
153
|
title: item.title ?? item.name ?? null,
|
|
154
154
|
url: item.url ?? item.link ?? null,
|
|
155
|
-
snippet: item.snippet ?? item.summary ?? item.excerpt ?? null,
|
|
155
|
+
snippet: item.snippet ?? item.content ?? item.summary ?? item.excerpt ?? null,
|
|
156
156
|
score: item.score ?? item.rank ?? null,
|
|
157
157
|
source: item.source ?? null,
|
|
158
158
|
metadata: item.metadata ?? null,
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Payload Cloning & Size Estimation
|
|
3
|
+
*
|
|
4
|
+
* Optimizes deep-cloning of LLM request payloads to avoid
|
|
5
|
+
* wasting memory on large base64 media blocks that will be
|
|
6
|
+
* discarded by flattenBlocks() for most providers.
|
|
7
|
+
*
|
|
8
|
+
* @module utils/payload
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const logger = require('../logger');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Estimate the byte size of message content without full serialization.
|
|
15
|
+
* Scans for base64 image/audio data blocks and text blocks.
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} payload - Request payload
|
|
18
|
+
* @returns {number} Estimated size in bytes
|
|
19
|
+
*/
|
|
20
|
+
function estimateContentSize(payload) {
|
|
21
|
+
if (!payload || !Array.isArray(payload.messages)) return 0;
|
|
22
|
+
|
|
23
|
+
let size = 0;
|
|
24
|
+
for (const msg of payload.messages) {
|
|
25
|
+
if (!msg) continue;
|
|
26
|
+
|
|
27
|
+
if (typeof msg.content === 'string') {
|
|
28
|
+
size += msg.content.length;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!Array.isArray(msg.content)) continue;
|
|
33
|
+
|
|
34
|
+
for (const block of msg.content) {
|
|
35
|
+
if (!block || typeof block !== 'object') continue;
|
|
36
|
+
|
|
37
|
+
if (block.text) {
|
|
38
|
+
size += block.text.length;
|
|
39
|
+
}
|
|
40
|
+
// Anthropic image format
|
|
41
|
+
if (block.source?.data) {
|
|
42
|
+
size += block.source.data.length;
|
|
43
|
+
}
|
|
44
|
+
// OpenAI image_url format (inline base64)
|
|
45
|
+
if (block.image_url?.url && block.image_url.url.startsWith('data:')) {
|
|
46
|
+
size += block.image_url.url.length;
|
|
47
|
+
}
|
|
48
|
+
// tool_result content
|
|
49
|
+
if (block.type === 'tool_result' && typeof block.content === 'string') {
|
|
50
|
+
size += block.content.length;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return size;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if any message content block has base64 media data exceeding threshold.
|
|
60
|
+
*
|
|
61
|
+
* @param {Object} payload - Request payload
|
|
62
|
+
* @param {number} threshold - Size threshold in bytes (default 1MB)
|
|
63
|
+
* @returns {boolean}
|
|
64
|
+
*/
|
|
65
|
+
function hasLargeMedia(payload, threshold = 1_048_576) {
|
|
66
|
+
if (!payload || !Array.isArray(payload.messages)) return false;
|
|
67
|
+
|
|
68
|
+
for (const msg of payload.messages) {
|
|
69
|
+
if (!msg || !Array.isArray(msg.content)) continue;
|
|
70
|
+
|
|
71
|
+
for (const block of msg.content) {
|
|
72
|
+
if (!block || typeof block !== 'object') continue;
|
|
73
|
+
|
|
74
|
+
// Anthropic base64 image
|
|
75
|
+
if (block.source?.data && block.source.data.length > threshold) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
// OpenAI inline base64 image
|
|
79
|
+
if (block.image_url?.url && block.image_url.url.length > threshold) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Block types that flattenBlocks() discards (returns empty string)
|
|
89
|
+
const HEAVY_BLOCK_TYPES = new Set(['image', 'audio', 'image_url', 'video']);
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check if a content block is a heavy media block that flattenBlocks() discards.
|
|
93
|
+
* @param {Object} block
|
|
94
|
+
* @returns {boolean}
|
|
95
|
+
*/
|
|
96
|
+
function isHeavyMediaBlock(block) {
|
|
97
|
+
if (!block || typeof block !== 'object') return false;
|
|
98
|
+
if (HEAVY_BLOCK_TYPES.has(block.type)) return true;
|
|
99
|
+
if (block.source?.type === 'base64') return true;
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Clone payload optimized for providers that will flatten content.
|
|
105
|
+
* Skips cloning heavy media blocks since flattenBlocks() discards them.
|
|
106
|
+
*
|
|
107
|
+
* @param {Object} payload
|
|
108
|
+
* @returns {Object} Cloned payload with media placeholders
|
|
109
|
+
*/
|
|
110
|
+
function cloneWithFlattenAwareness(payload) {
|
|
111
|
+
const clean = { ...payload };
|
|
112
|
+
|
|
113
|
+
// Deep-clone messages array but skip heavy media blocks
|
|
114
|
+
if (Array.isArray(payload.messages)) {
|
|
115
|
+
clean.messages = payload.messages.map(msg => {
|
|
116
|
+
if (!msg) return msg;
|
|
117
|
+
const cloned = { ...msg };
|
|
118
|
+
|
|
119
|
+
if (Array.isArray(msg.content)) {
|
|
120
|
+
cloned.content = msg.content.map(block => {
|
|
121
|
+
if (!block || typeof block !== 'object') return block;
|
|
122
|
+
|
|
123
|
+
// Skip heavy media blocks — flattenBlocks() produces "" for these
|
|
124
|
+
if (isHeavyMediaBlock(block)) {
|
|
125
|
+
return { type: block.type, _skipped: true };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Shallow clone small blocks (text, tool_result, tool_use)
|
|
129
|
+
if (block.type === 'tool_result' && typeof block.content === 'object') {
|
|
130
|
+
return { ...block, content: JSON.parse(JSON.stringify(block.content)) };
|
|
131
|
+
}
|
|
132
|
+
return { ...block };
|
|
133
|
+
});
|
|
134
|
+
} else if (typeof msg.content === 'object' && msg.content !== null) {
|
|
135
|
+
cloned.content = { ...msg.content };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Clone tool_calls if present
|
|
139
|
+
if (Array.isArray(msg.tool_calls)) {
|
|
140
|
+
cloned.tool_calls = JSON.parse(JSON.stringify(msg.tool_calls));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return cloned;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Deep-clone small arrays that get mutated
|
|
148
|
+
if (Array.isArray(payload.tools)) {
|
|
149
|
+
clean.tools = JSON.parse(JSON.stringify(payload.tools));
|
|
150
|
+
}
|
|
151
|
+
if (Array.isArray(payload.system)) {
|
|
152
|
+
clean.system = JSON.parse(JSON.stringify(payload.system));
|
|
153
|
+
} else if (typeof payload.system === 'string') {
|
|
154
|
+
clean.system = payload.system;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return clean;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Smart deep-clone a request payload.
|
|
162
|
+
*
|
|
163
|
+
* - If willFlatten is true: skips cloning heavy media blocks (they'll be discarded)
|
|
164
|
+
* - If willFlatten is false: uses structuredClone (faster than JSON round-trip)
|
|
165
|
+
* - Falls back to JSON.parse(JSON.stringify()) for compatibility
|
|
166
|
+
*
|
|
167
|
+
* @param {Object} payload - Request payload to clone
|
|
168
|
+
* @param {Object} options
|
|
169
|
+
* @param {boolean} options.willFlatten - Whether flattenBlocks() will run (true for most providers)
|
|
170
|
+
* @returns {Object} Cloned payload
|
|
171
|
+
*/
|
|
172
|
+
function clonePayloadSmart(payload, options = {}) {
|
|
173
|
+
if (!payload) return {};
|
|
174
|
+
|
|
175
|
+
const { willFlatten = false } = options;
|
|
176
|
+
|
|
177
|
+
// Fast path: provider will flatten content — skip cloning media blocks
|
|
178
|
+
if (willFlatten && Array.isArray(payload.messages)) {
|
|
179
|
+
const hasMedia = payload.messages.some(msg =>
|
|
180
|
+
Array.isArray(msg?.content) && msg.content.some(isHeavyMediaBlock)
|
|
181
|
+
);
|
|
182
|
+
if (hasMedia) {
|
|
183
|
+
logger.debug('[payload] Using flatten-aware clone (skipping media blocks)');
|
|
184
|
+
return cloneWithFlattenAwareness(payload);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Medium path: structuredClone (faster, no string intermediate)
|
|
189
|
+
if (typeof structuredClone === 'function') {
|
|
190
|
+
try {
|
|
191
|
+
return structuredClone(payload);
|
|
192
|
+
} catch {
|
|
193
|
+
// structuredClone can fail on functions, symbols, etc.
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Slow path: JSON round-trip (original behavior)
|
|
198
|
+
return JSON.parse(JSON.stringify(payload));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = {
|
|
202
|
+
estimateContentSize,
|
|
203
|
+
hasLargeMedia,
|
|
204
|
+
clonePayloadSmart,
|
|
205
|
+
isHeavyMediaBlock,
|
|
206
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request Performance Timer
|
|
3
|
+
*
|
|
4
|
+
* Lightweight timing instrumentation for the request hot path.
|
|
5
|
+
* Enable with LOG_LEVEL=debug or PERF_TIMER=true to see per-request
|
|
6
|
+
* breakdown of where time is spent.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const timer = createTimer('processMessage');
|
|
10
|
+
* timer.mark('sanitizePayload');
|
|
11
|
+
* // ... do work ...
|
|
12
|
+
* timer.mark('cacheCheck');
|
|
13
|
+
* // ... do work ...
|
|
14
|
+
* timer.done(); // logs full breakdown
|
|
15
|
+
*
|
|
16
|
+
* @module utils/perf-timer
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const { performance } = require('perf_hooks');
|
|
20
|
+
const logger = require('../logger');
|
|
21
|
+
|
|
22
|
+
const ENABLED = process.env.PERF_TIMER === 'true';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a performance timer for a named operation.
|
|
26
|
+
* @param {string} name - Timer name (e.g., 'processMessage', 'invokeModel')
|
|
27
|
+
* @returns {{ mark: (label: string) => void, done: () => Object }}
|
|
28
|
+
*/
|
|
29
|
+
function createTimer(name) {
|
|
30
|
+
if (!ENABLED) {
|
|
31
|
+
// No-op when disabled — zero overhead
|
|
32
|
+
return {
|
|
33
|
+
mark() {},
|
|
34
|
+
done() { return null; },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const start = performance.now();
|
|
39
|
+
const marks = [];
|
|
40
|
+
let lastMark = start;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
/**
|
|
44
|
+
* Record a checkpoint.
|
|
45
|
+
* @param {string} label - What just completed
|
|
46
|
+
*/
|
|
47
|
+
mark(label) {
|
|
48
|
+
const now = performance.now();
|
|
49
|
+
marks.push({
|
|
50
|
+
label,
|
|
51
|
+
elapsed: now - lastMark,
|
|
52
|
+
cumulative: now - start,
|
|
53
|
+
});
|
|
54
|
+
lastMark = now;
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Finish timing and log the breakdown.
|
|
59
|
+
* @returns {Object} Timing breakdown
|
|
60
|
+
*/
|
|
61
|
+
done() {
|
|
62
|
+
const total = performance.now() - start;
|
|
63
|
+
const breakdown = {};
|
|
64
|
+
|
|
65
|
+
for (const m of marks) {
|
|
66
|
+
breakdown[m.label] = `${m.elapsed.toFixed(2)}ms`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
logger.info({
|
|
70
|
+
timer: name,
|
|
71
|
+
totalMs: total.toFixed(2),
|
|
72
|
+
breakdown,
|
|
73
|
+
}, `[perf] ${name}: ${total.toFixed(1)}ms`);
|
|
74
|
+
|
|
75
|
+
return { name, totalMs: total, marks, breakdown };
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { createTimer };
|