lynkr 9.0.1 → 9.1.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 +70 -21
- package/bin/cli.js +34 -4
- package/bin/lynkr-trajectory.js +136 -0
- package/bin/lynkr-usage.js +219 -0
- package/funding.json +110 -0
- 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 +6 -5
- package/public/dashboard.html +665 -0
- package/src/api/files-multipart.js +30 -0
- package/src/api/files-router.js +81 -0
- package/src/api/middleware/budget.js +19 -1
- package/src/api/middleware/load-shedding.js +17 -0
- package/src/api/openai-router.js +353 -301
- package/src/api/router.js +275 -40
- package/src/cache/prompt.js +13 -0
- package/src/clients/databricks.js +42 -18
- package/src/clients/ollama-utils.js +21 -17
- package/src/clients/openai-format.js +50 -10
- 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/responses-format.js +8 -7
- 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 +16 -0
- package/src/context/distill.js +15 -0
- package/src/context/tool-result-compressor.js +563 -0
- package/src/dashboard/api.js +170 -0
- package/src/dashboard/router.js +13 -0
- package/src/headroom/client.js +3 -109
- package/src/headroom/index.js +0 -14
- package/src/memory/extractor.js +22 -0
- package/src/memory/search.js +0 -50
- package/src/orchestrator/index.js +163 -204
- package/src/orchestrator/preflight.js +188 -0
- package/src/routing/index.js +64 -32
- package/src/routing/interaction.js +183 -0
- package/src/routing/risk-analyzer.js +194 -0
- package/src/routing/telemetry.js +47 -2
- package/src/server.js +15 -0
- package/src/stores/file-store.js +104 -0
- package/src/stores/response-store.js +25 -0
- package/src/tools/index.js +1 -1
- package/src/tools/smart-selection.js +11 -2
- package/src/tools/web.js +1 -1
- package/src/training/trajectory-compressor.js +266 -0
- package/src/usage/aggregator.js +206 -0
- package/src/utils/markdown-ansi.js +146 -0
- package/.lynkr/telemetry.db +0 -0
- package/.lynkr/telemetry.db-shm +0 -0
- package/.lynkr/telemetry.db-wal +0 -0
|
@@ -60,13 +60,16 @@ function convertOpenAIToAnthropic(openaiRequest) {
|
|
|
60
60
|
if (part.type === "text") {
|
|
61
61
|
return { type: "text", text: part.text };
|
|
62
62
|
} else if (part.type === "image_url") {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
const url = part.image_url?.url || "";
|
|
64
|
+
if (url.startsWith("data:")) {
|
|
65
|
+
const match = url.match(/^data:(image\/[^;]+);base64,(.+)$/);
|
|
66
|
+
if (match) {
|
|
67
|
+
return { type: "image", source: { type: "base64", media_type: match[1], data: match[2] } };
|
|
68
68
|
}
|
|
69
|
-
}
|
|
69
|
+
}
|
|
70
|
+
return { type: "image", source: { type: "url", url } };
|
|
71
|
+
} else if (part.type === "document" || part.type === "image") {
|
|
72
|
+
return part;
|
|
70
73
|
}
|
|
71
74
|
return part;
|
|
72
75
|
});
|
|
@@ -200,18 +203,37 @@ function convertAnthropicToOpenAI(anthropicResponse, model = "claude-3-5-sonnet-
|
|
|
200
203
|
|
|
201
204
|
const { id, content, stop_reason, usage } = anthropicResponse;
|
|
202
205
|
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
+
// Tolerant fallback: providers sometimes return reasoning-only responses
|
|
207
|
+
// (Minimax/DeepSeek), error envelopes, or empty bodies. Treat missing/invalid
|
|
208
|
+
// content as an empty turn so jcode/Pi/Codex don't crash on the response.
|
|
209
|
+
const safeContent = Array.isArray(content) ? content : [];
|
|
210
|
+
if (safeContent.length === 0) {
|
|
211
|
+
logger.warn({
|
|
212
|
+
hasContent: content !== undefined,
|
|
213
|
+
contentType: typeof content,
|
|
214
|
+
stop_reason,
|
|
215
|
+
responseKeys: Object.keys(anthropicResponse),
|
|
216
|
+
hasError: !!anthropicResponse.error,
|
|
217
|
+
errorMessage: anthropicResponse.error?.message,
|
|
218
|
+
}, "convertAnthropicToOpenAI: empty/missing content, returning empty assistant message");
|
|
206
219
|
}
|
|
207
220
|
|
|
208
221
|
// Convert content blocks to OpenAI format
|
|
209
222
|
let messageContent = "";
|
|
223
|
+
let reasoningContent = "";
|
|
210
224
|
const toolCalls = [];
|
|
225
|
+
let citations = [];
|
|
211
226
|
|
|
212
|
-
for (const block of
|
|
227
|
+
for (const block of safeContent) {
|
|
213
228
|
if (block.type === "text") {
|
|
214
229
|
messageContent += block.text;
|
|
230
|
+
if (Array.isArray(block.citations)) {
|
|
231
|
+
citations.push(...block.citations);
|
|
232
|
+
}
|
|
233
|
+
} else if (block.type === "thinking") {
|
|
234
|
+
// Preserve reasoning text so reasoning-only models (Minimax, DeepSeek-R1)
|
|
235
|
+
// surface visible output to OpenAI clients that don't render thinking blocks
|
|
236
|
+
reasoningContent += (block.thinking || "");
|
|
215
237
|
} else if (block.type === "tool_use") {
|
|
216
238
|
toolCalls.push({
|
|
217
239
|
id: block.id,
|
|
@@ -224,6 +246,12 @@ function convertAnthropicToOpenAI(anthropicResponse, model = "claude-3-5-sonnet-
|
|
|
224
246
|
}
|
|
225
247
|
}
|
|
226
248
|
|
|
249
|
+
// Fallback: if the model returned only reasoning (no visible text and no tools),
|
|
250
|
+
// promote reasoning into the visible content so jcode/Pi/Codex see something
|
|
251
|
+
if (!messageContent && !toolCalls.length && reasoningContent) {
|
|
252
|
+
messageContent = reasoningContent;
|
|
253
|
+
}
|
|
254
|
+
|
|
227
255
|
// Build OpenAI response
|
|
228
256
|
// Ensure ID has the chatcmpl- prefix that OpenAI clients expect
|
|
229
257
|
const responseId = id && id.startsWith("chatcmpl-") ? id : `chatcmpl-${Date.now()}`;
|
|
@@ -249,6 +277,18 @@ function convertAnthropicToOpenAI(anthropicResponse, model = "claude-3-5-sonnet-
|
|
|
249
277
|
}
|
|
250
278
|
};
|
|
251
279
|
|
|
280
|
+
// Add citations if present
|
|
281
|
+
if (citations.length > 0) {
|
|
282
|
+
openaiResponse.citations = citations;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Add reasoning_content as a side-channel field so clients that render
|
|
286
|
+
// thinking (e.g. some jcode / OpenRouter setups) can show it without losing
|
|
287
|
+
// it from the visible content fallback above
|
|
288
|
+
if (reasoningContent && reasoningContent !== messageContent) {
|
|
289
|
+
openaiResponse.choices[0].message.reasoning_content = reasoningContent;
|
|
290
|
+
}
|
|
291
|
+
|
|
252
292
|
// Add tool_calls if present
|
|
253
293
|
if (toolCalls.length > 0) {
|
|
254
294
|
openaiResponse.choices[0].message.tool_calls = toolCalls;
|
|
@@ -89,12 +89,12 @@ function convertAnthropicMessagesToOpenRouter(anthropicMessages) {
|
|
|
89
89
|
tool_calls
|
|
90
90
|
};
|
|
91
91
|
|
|
92
|
-
//
|
|
93
|
-
//
|
|
92
|
+
// Moonshot/Kimi and some OpenAI-compatible APIs require content to
|
|
93
|
+
// be null (not empty string) when tool_calls are present.
|
|
94
94
|
if (textContent && textContent.trim()) {
|
|
95
95
|
message.content = textContent;
|
|
96
96
|
} else {
|
|
97
|
-
message.content =
|
|
97
|
+
message.content = null;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
converted.push(message);
|
|
@@ -146,37 +146,32 @@ function convertAnthropicMessagesToOpenRouter(anthropicMessages) {
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
//
|
|
149
|
+
// Fix tool_call_id mismatches: ensure every tool message's tool_call_id
|
|
150
|
+
// matches the id in the preceding assistant's tool_calls array.
|
|
151
|
+
// IDs can drift when multiple conversion layers (Anthropic↔OpenAI) each
|
|
152
|
+
// generate their own IDs.
|
|
150
153
|
for (let i = 0; i < converted.length; i++) {
|
|
151
154
|
const msg = converted[i];
|
|
152
|
-
if (msg.role
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
155
|
+
if (msg.role !== 'tool') continue;
|
|
156
|
+
|
|
157
|
+
// Find the nearest preceding assistant with tool_calls
|
|
158
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
159
|
+
const prev = converted[j];
|
|
160
|
+
if (prev.role === 'user') break;
|
|
161
|
+
if (prev.role === 'assistant' && Array.isArray(prev.tool_calls) && prev.tool_calls.length > 0) {
|
|
162
|
+
if (!prev.tool_calls.some(tc => tc.id === msg.tool_call_id)) {
|
|
163
|
+
// Mismatch — pick the first unmatched tool_call id
|
|
164
|
+
const usedIds = new Set();
|
|
165
|
+
for (let k = j + 1; k < converted.length; k++) {
|
|
166
|
+
if (converted[k].role === 'tool' && k !== i) usedIds.add(converted[k].tool_call_id);
|
|
167
|
+
}
|
|
168
|
+
const available = prev.tool_calls.find(tc => !usedIds.has(tc.id));
|
|
169
|
+
if (available) {
|
|
170
|
+
logger.info({ from: msg.tool_call_id, to: available.id }, "Fixed tool_call_id mismatch");
|
|
171
|
+
msg.tool_call_id = available.id;
|
|
162
172
|
}
|
|
163
173
|
}
|
|
164
|
-
|
|
165
|
-
if (prevMsg.role === 'user') break;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (!foundMatchingToolCall) {
|
|
169
|
-
// Log but DON'T remove - the tool result may be valid but IDs mismatched due to format conversion
|
|
170
|
-
logger.debug({
|
|
171
|
-
messageIndex: i,
|
|
172
|
-
toolCallId: msg.tool_call_id,
|
|
173
|
-
precedingMessages: converted.slice(Math.max(0, i - 3), i).map(m => ({
|
|
174
|
-
role: m.role,
|
|
175
|
-
hasToolCalls: !!m.tool_calls,
|
|
176
|
-
toolCallIds: m.tool_calls?.map(tc => tc.id)
|
|
177
|
-
}))
|
|
178
|
-
}, "Tool message without matching tool_call - keeping for API to validate");
|
|
179
|
-
// Don't remove - let the API handle validation
|
|
174
|
+
break;
|
|
180
175
|
}
|
|
181
176
|
}
|
|
182
177
|
}
|
|
@@ -242,6 +237,17 @@ function convertOpenRouterResponseToAnthropic(openRouterResponse, requestedModel
|
|
|
242
237
|
const message = choice.message || {};
|
|
243
238
|
const contentBlocks = [];
|
|
244
239
|
|
|
240
|
+
// Extract tool calls embedded as XML/text in content (Minimax, Qwen, GLM, etc.)
|
|
241
|
+
if (!message.tool_calls?.length && typeof message.content === "string" && message.content.trim()) {
|
|
242
|
+
const { extractToolCallsFromText } = require("./xml-tool-extractor");
|
|
243
|
+
const extracted = extractToolCallsFromText(message.content);
|
|
244
|
+
if (extracted.toolCalls.length > 0) {
|
|
245
|
+
message.tool_calls = extracted.toolCalls;
|
|
246
|
+
message.content = extracted.cleanedText;
|
|
247
|
+
choice.finish_reason = "tool_calls";
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
245
251
|
// Check if there are tool calls present
|
|
246
252
|
const hasToolCalls = Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
|
|
247
253
|
|
|
@@ -262,14 +268,13 @@ function convertOpenRouterResponseToAnthropic(openRouterResponse, requestedModel
|
|
|
262
268
|
trimmed.includes('"arguments"'));
|
|
263
269
|
};
|
|
264
270
|
|
|
265
|
-
//
|
|
271
|
+
// Emit reasoning_content as a thinking block (not as fallback text)
|
|
266
272
|
let textContent = message.content || "";
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
textContent = message.reasoning_content;
|
|
273
|
+
if (message.reasoning_content && typeof message.reasoning_content === "string") {
|
|
274
|
+
contentBlocks.push({ type: "thinking", thinking: message.reasoning_content });
|
|
275
|
+
}
|
|
276
|
+
if (!textContent.trim() && !message.reasoning_content) {
|
|
277
|
+
// No content at all — will be handled below
|
|
273
278
|
}
|
|
274
279
|
|
|
275
280
|
// Add text content if present, but skip if it's a duplicate/malformed tool call JSON
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-Side Prompt Cache Injection
|
|
3
|
+
*
|
|
4
|
+
* Injects `cache_control` breakpoints into requests for providers
|
|
5
|
+
* that support explicit prompt caching (Anthropic, Bedrock, Vertex/Gemini).
|
|
6
|
+
*
|
|
7
|
+
* Strategy: "system_and_3" — places up to 4 breakpoints:
|
|
8
|
+
* 1. System prompt (stable across turns — highest cache hit rate)
|
|
9
|
+
* 2-4. Last 3 non-system messages (rolling window)
|
|
10
|
+
*
|
|
11
|
+
* Providers with automatic caching (OpenAI, DeepSeek) need no injection.
|
|
12
|
+
*
|
|
13
|
+
* @module clients/prompt-cache-injection
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const logger = require('../logger');
|
|
17
|
+
|
|
18
|
+
const CACHE_MARKER = { type: 'ephemeral' };
|
|
19
|
+
const MAX_BREAKPOINTS = 4;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Inject cache_control breakpoints into an Anthropic-format request body.
|
|
23
|
+
* Mutates the body in-place for zero-copy performance.
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} body - Request body with system and messages
|
|
26
|
+
* @returns {number} Number of breakpoints injected
|
|
27
|
+
*/
|
|
28
|
+
function injectAnthropicCacheBreakpoints(body) {
|
|
29
|
+
if (!body) return 0;
|
|
30
|
+
|
|
31
|
+
let injected = 0;
|
|
32
|
+
|
|
33
|
+
// Breakpoint 1: System prompt
|
|
34
|
+
if (body.system) {
|
|
35
|
+
if (typeof body.system === 'string') {
|
|
36
|
+
// Convert string system to array format for cache_control support
|
|
37
|
+
body.system = [{
|
|
38
|
+
type: 'text',
|
|
39
|
+
text: body.system,
|
|
40
|
+
cache_control: CACHE_MARKER,
|
|
41
|
+
}];
|
|
42
|
+
injected++;
|
|
43
|
+
} else if (Array.isArray(body.system) && body.system.length > 0) {
|
|
44
|
+
// Mark the last system block
|
|
45
|
+
const lastBlock = body.system[body.system.length - 1];
|
|
46
|
+
if (lastBlock && typeof lastBlock === 'object' && !lastBlock.cache_control) {
|
|
47
|
+
lastBlock.cache_control = CACHE_MARKER;
|
|
48
|
+
injected++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Breakpoints 2-4: Last 3 non-system messages
|
|
54
|
+
if (Array.isArray(body.messages) && body.messages.length > 0) {
|
|
55
|
+
const remaining = MAX_BREAKPOINTS - injected;
|
|
56
|
+
const messagesToMark = Math.min(remaining, 3, body.messages.length);
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < messagesToMark; i++) {
|
|
59
|
+
const msgIdx = body.messages.length - 1 - i;
|
|
60
|
+
const msg = body.messages[msgIdx];
|
|
61
|
+
if (!msg) continue;
|
|
62
|
+
|
|
63
|
+
if (typeof msg.content === 'string') {
|
|
64
|
+
// Convert string content to array for cache_control
|
|
65
|
+
msg.content = [{
|
|
66
|
+
type: 'text',
|
|
67
|
+
text: msg.content,
|
|
68
|
+
cache_control: CACHE_MARKER,
|
|
69
|
+
}];
|
|
70
|
+
injected++;
|
|
71
|
+
} else if (Array.isArray(msg.content) && msg.content.length > 0) {
|
|
72
|
+
// Mark the last content block in this message
|
|
73
|
+
const lastBlock = msg.content[msg.content.length - 1];
|
|
74
|
+
if (lastBlock && typeof lastBlock === 'object' && !lastBlock.cache_control) {
|
|
75
|
+
lastBlock.cache_control = CACHE_MARKER;
|
|
76
|
+
injected++;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (injected > 0) {
|
|
83
|
+
logger.debug({ breakpoints: injected }, '[prompt-cache] Injected cache_control breakpoints');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return injected;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Inject cache_control for Gemini/Vertex explicit caching.
|
|
91
|
+
* Uses the same cache_control format — Gemini accepts it via LiteLLM/OpenRouter.
|
|
92
|
+
*
|
|
93
|
+
* @param {Object} body - Request body with system and messages (Anthropic format, pre-conversion)
|
|
94
|
+
* @returns {number} Number of breakpoints injected
|
|
95
|
+
*/
|
|
96
|
+
function injectGeminiCacheBreakpoints(body) {
|
|
97
|
+
// Gemini uses the same cache_control format when going through
|
|
98
|
+
// OpenRouter or LiteLLM. For direct Gemini API, implicit caching
|
|
99
|
+
// is automatic — no injection needed.
|
|
100
|
+
// We inject anyway for OpenRouter/proxy paths that forward cache_control.
|
|
101
|
+
return injectAnthropicCacheBreakpoints(body);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Determine if a provider benefits from cache_control injection.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} provider - Provider name
|
|
108
|
+
* @returns {boolean}
|
|
109
|
+
*/
|
|
110
|
+
function needsCacheInjection(provider) {
|
|
111
|
+
// These providers support explicit cache_control breakpoints
|
|
112
|
+
const EXPLICIT_CACHE_PROVIDERS = new Set([
|
|
113
|
+
'azure-anthropic',
|
|
114
|
+
'bedrock',
|
|
115
|
+
'databricks', // Databricks routes to Claude which supports caching
|
|
116
|
+
'openrouter', // OpenRouter forwards cache_control to underlying provider
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
return EXPLICIT_CACHE_PROVIDERS.has(provider);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Inject provider-side prompt caching into the request body.
|
|
124
|
+
* Call this before sending to the provider.
|
|
125
|
+
*
|
|
126
|
+
* @param {Object} body - Request body (Anthropic format)
|
|
127
|
+
* @param {string} provider - Provider name
|
|
128
|
+
* @returns {number} Number of breakpoints injected
|
|
129
|
+
*/
|
|
130
|
+
function injectPromptCaching(body, provider) {
|
|
131
|
+
if (!needsCacheInjection(provider)) return 0;
|
|
132
|
+
return injectAnthropicCacheBreakpoints(body);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = {
|
|
136
|
+
injectPromptCaching,
|
|
137
|
+
injectAnthropicCacheBreakpoints,
|
|
138
|
+
injectGeminiCacheBreakpoints,
|
|
139
|
+
needsCacheInjection,
|
|
140
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const config = require("../config");
|
|
2
|
+
|
|
3
|
+
const NATIVE_THINKING_PROVIDERS = new Set(["azure-anthropic", "databricks"]);
|
|
4
|
+
|
|
5
|
+
const NATIVE_THINKING_BEDROCK_MODELS = [
|
|
6
|
+
"anthropic.claude",
|
|
7
|
+
"claude-3",
|
|
8
|
+
"claude-4",
|
|
9
|
+
"claude-sonnet",
|
|
10
|
+
"claude-opus",
|
|
11
|
+
"claude-haiku",
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const REASONING_CONTENT_PROVIDERS = new Set(["moonshot", "openrouter", "openai", "azure-openai"]);
|
|
15
|
+
|
|
16
|
+
function supportsNativeThinking(providerType, model) {
|
|
17
|
+
if (NATIVE_THINKING_PROVIDERS.has(providerType)) return true;
|
|
18
|
+
if (providerType === "bedrock" && model) {
|
|
19
|
+
return NATIVE_THINKING_BEDROCK_MODELS.some((prefix) => model.toLowerCase().includes(prefix));
|
|
20
|
+
}
|
|
21
|
+
if (providerType === "vertex" && model) {
|
|
22
|
+
return model.toLowerCase().includes("claude");
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function supportsReasoningContent(providerType) {
|
|
28
|
+
return REASONING_CONTENT_PROVIDERS.has(providerType);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getThinkingBehavior(providerType, model) {
|
|
32
|
+
if (supportsNativeThinking(providerType, model)) return "native";
|
|
33
|
+
if (supportsReasoningContent(providerType)) return "reasoning_content";
|
|
34
|
+
return "none";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
supportsNativeThinking,
|
|
39
|
+
supportsReasoningContent,
|
|
40
|
+
getThinkingBehavior,
|
|
41
|
+
};
|
|
@@ -19,6 +19,7 @@ const logger = require("../logger");
|
|
|
19
19
|
function mapClientToolToLynkr(clientToolName) {
|
|
20
20
|
const reverseMapping = {
|
|
21
21
|
// ============== CODEX CLI ==============
|
|
22
|
+
"shell": "Bash",
|
|
22
23
|
"shell_command": "Bash",
|
|
23
24
|
"read_file": "Read",
|
|
24
25
|
"write_file": "Write",
|
|
@@ -140,13 +141,13 @@ function convertResponsesToChat(responsesRequest) {
|
|
|
140
141
|
|
|
141
142
|
// Handle function_call (tool calls - convert to assistant with tool_calls)
|
|
142
143
|
if (msg.type === 'function_call') {
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
|
|
144
|
+
// Keep the client's original tool name (e.g., "shell", "read_file")
|
|
145
|
+
// so it matches the tool definitions injected in the Responses endpoint.
|
|
146
|
+
// Mapping to Lynkr names here would cause a mismatch with
|
|
147
|
+
// client-named tool definitions sent to the model.
|
|
146
148
|
logger.debug({
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}, "Mapping client tool name to Lynkr");
|
|
149
|
+
toolName: msg.name
|
|
150
|
+
}, "Preserving client tool name in function_call");
|
|
150
151
|
|
|
151
152
|
return {
|
|
152
153
|
role: 'assistant',
|
|
@@ -155,7 +156,7 @@ function convertResponsesToChat(responsesRequest) {
|
|
|
155
156
|
id: msg.call_id || msg.id,
|
|
156
157
|
type: 'function',
|
|
157
158
|
function: {
|
|
158
|
-
name:
|
|
159
|
+
name: msg.name,
|
|
159
160
|
arguments: typeof msg.arguments === 'string' ? msg.arguments : JSON.stringify(msg.arguments || {})
|
|
160
161
|
}
|
|
161
162
|
}]
|
|
@@ -275,7 +275,7 @@ EXAMPLE: User says "explore this project" → Call Task with subagent_type="Expl
|
|
|
275
275
|
description: "Optional model override. Default is appropriate for each agent type."
|
|
276
276
|
}
|
|
277
277
|
},
|
|
278
|
-
required: ["
|
|
278
|
+
required: ["prompt"]
|
|
279
279
|
}
|
|
280
280
|
},
|
|
281
281
|
{
|