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
package/src/api/openai-router.js
CHANGED
|
@@ -54,7 +54,7 @@ function detectClient(headers) {
|
|
|
54
54
|
const clientHeader = (headers?.["x-client"] || headers?.["x-client-name"] || "").toLowerCase();
|
|
55
55
|
|
|
56
56
|
// Check user-agent and custom headers
|
|
57
|
-
if (userAgent.includes("codex") || clientHeader.includes("codex")) {
|
|
57
|
+
if (userAgent.includes("codex") || clientHeader.includes("codex") || userAgent.includes("openai-codex")) {
|
|
58
58
|
return "codex";
|
|
59
59
|
}
|
|
60
60
|
// Kilo Code is a fork of Cline - check for both
|
|
@@ -77,9 +77,9 @@ function detectClient(headers) {
|
|
|
77
77
|
*/
|
|
78
78
|
const CLIENT_TOOL_MAPPINGS = {
|
|
79
79
|
// ============== CODEX CLI ==============
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
80
|
+
// Codex v0.121.0 only recognises "shell" and "apply_patch" as built-in
|
|
81
|
+
// tools. All other operations (read, list, grep, etc.) must go through
|
|
82
|
+
// shell commands — the model handles this naturally.
|
|
83
83
|
codex: {
|
|
84
84
|
"Bash": {
|
|
85
85
|
name: "shell",
|
|
@@ -87,21 +87,6 @@ const CLIENT_TOOL_MAPPINGS = {
|
|
|
87
87
|
command: ["bash", "-c", a.command || ""]
|
|
88
88
|
})
|
|
89
89
|
},
|
|
90
|
-
"Read": {
|
|
91
|
-
name: "read_file",
|
|
92
|
-
mapArgs: (a) => ({
|
|
93
|
-
path: a.file_path || a.path || "",
|
|
94
|
-
offset: a.offset,
|
|
95
|
-
limit: a.limit
|
|
96
|
-
})
|
|
97
|
-
},
|
|
98
|
-
"Write": {
|
|
99
|
-
name: "write_file",
|
|
100
|
-
mapArgs: (a) => ({
|
|
101
|
-
path: a.file_path || a.path || "",
|
|
102
|
-
content: a.content || ""
|
|
103
|
-
})
|
|
104
|
-
},
|
|
105
90
|
"Edit": {
|
|
106
91
|
name: "apply_patch",
|
|
107
92
|
mapArgs: (a) => ({
|
|
@@ -109,47 +94,6 @@ const CLIENT_TOOL_MAPPINGS = {
|
|
|
109
94
|
old_string: a.old_string || "",
|
|
110
95
|
new_string: a.new_string || ""
|
|
111
96
|
})
|
|
112
|
-
},
|
|
113
|
-
"Glob": {
|
|
114
|
-
name: "glob_file_search",
|
|
115
|
-
mapArgs: (a) => ({
|
|
116
|
-
pattern: a.pattern || "",
|
|
117
|
-
path: a.path
|
|
118
|
-
})
|
|
119
|
-
},
|
|
120
|
-
"Grep": {
|
|
121
|
-
name: "rg",
|
|
122
|
-
mapArgs: (a) => ({
|
|
123
|
-
pattern: a.pattern || "",
|
|
124
|
-
path: a.path,
|
|
125
|
-
include: a.glob || a.include,
|
|
126
|
-
type: a.type
|
|
127
|
-
})
|
|
128
|
-
},
|
|
129
|
-
"ListDir": {
|
|
130
|
-
name: "list_dir",
|
|
131
|
-
mapArgs: (a) => ({
|
|
132
|
-
path: a.path || a.directory
|
|
133
|
-
})
|
|
134
|
-
},
|
|
135
|
-
"TodoWrite": {
|
|
136
|
-
name: "update_plan",
|
|
137
|
-
mapArgs: (a) => ({
|
|
138
|
-
todos: a.todos || []
|
|
139
|
-
})
|
|
140
|
-
},
|
|
141
|
-
"WebSearch": {
|
|
142
|
-
name: "web_search",
|
|
143
|
-
mapArgs: (a) => ({
|
|
144
|
-
query: a.query || ""
|
|
145
|
-
})
|
|
146
|
-
},
|
|
147
|
-
"WebAgent": {
|
|
148
|
-
name: "web_agent",
|
|
149
|
-
mapArgs: (a) => ({
|
|
150
|
-
url: a.url || "",
|
|
151
|
-
goal: a.goal || ""
|
|
152
|
-
})
|
|
153
97
|
}
|
|
154
98
|
},
|
|
155
99
|
|
|
@@ -422,7 +366,7 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
422
366
|
role: m.role,
|
|
423
367
|
contentPreview: typeof m.content === 'string'
|
|
424
368
|
? m.content.substring(0, 200)
|
|
425
|
-
: JSON.stringify(m.content).substring(0, 200)
|
|
369
|
+
: (m.content == null ? null : (JSON.stringify(m.content) ?? '').substring(0, 200))
|
|
426
370
|
}));
|
|
427
371
|
|
|
428
372
|
logger.debug({
|
|
@@ -456,7 +400,7 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
456
400
|
const clientExplicitlyDisabledTools = req.body.tool_choice === "none" || Array.isArray(req.body.tools);
|
|
457
401
|
if (!clientExplicitlyDisabledTools && (!anthropicRequest.tools || anthropicRequest.tools.length === 0)) {
|
|
458
402
|
const clientMappings = CLIENT_TOOL_MAPPINGS[clientType];
|
|
459
|
-
|
|
403
|
+
let clientTools = clientMappings
|
|
460
404
|
? IDE_SAFE_TOOLS.filter(t => clientMappings[t.name])
|
|
461
405
|
: IDE_SAFE_TOOLS;
|
|
462
406
|
anthropicRequest.tools = clientTools;
|
|
@@ -1474,6 +1418,29 @@ router.post("/responses", async (req, res) => {
|
|
|
1474
1418
|
fullRequestBodyKeys: Object.keys(req.body)
|
|
1475
1419
|
}, "=== RESPONSES API REQUEST ===");
|
|
1476
1420
|
|
|
1421
|
+
// Resolve previous_response_id for session continuity
|
|
1422
|
+
if (req.body.previous_response_id) {
|
|
1423
|
+
const responseStore = require("../stores/response-store");
|
|
1424
|
+
const prev = responseStore.getResponse(req.body.previous_response_id);
|
|
1425
|
+
if (prev && Array.isArray(prev.messages)) {
|
|
1426
|
+
const prevContext = [...prev.messages];
|
|
1427
|
+
if (prev.assistantContent) {
|
|
1428
|
+
prevContext.push({ role: "assistant", content: prev.assistantContent });
|
|
1429
|
+
}
|
|
1430
|
+
if (Array.isArray(req.body.input)) {
|
|
1431
|
+
req.body.input = [...prevContext, ...req.body.input];
|
|
1432
|
+
} else if (typeof req.body.input === "string") {
|
|
1433
|
+
req.body.input = [...prevContext, { role: "user", content: req.body.input }];
|
|
1434
|
+
}
|
|
1435
|
+
logger.debug({
|
|
1436
|
+
previousId: req.body.previous_response_id,
|
|
1437
|
+
prependedMessages: prevContext.length,
|
|
1438
|
+
}, "Resolved previous_response_id");
|
|
1439
|
+
} else {
|
|
1440
|
+
logger.warn({ previousId: req.body.previous_response_id }, "previous_response_id not found");
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1477
1444
|
// Convert Responses API to Chat Completions format
|
|
1478
1445
|
const chatRequest = convertResponsesToChat(req.body);
|
|
1479
1446
|
|
|
@@ -1489,6 +1456,28 @@ router.post("/responses", async (req, res) => {
|
|
|
1489
1456
|
// Convert to Anthropic format
|
|
1490
1457
|
const anthropicRequest = convertOpenAIToAnthropic(chatRequest);
|
|
1491
1458
|
|
|
1459
|
+
// Normalize tool_use names in conversation history to client format.
|
|
1460
|
+
// Tool definitions are injected with client names (e.g., "shell", "read_file"),
|
|
1461
|
+
// so tool_use blocks must also use client names to satisfy the Anthropic API
|
|
1462
|
+
// requirement that tool_use names match tool definitions.
|
|
1463
|
+
const clientType = detectClient(req.headers);
|
|
1464
|
+
const clientMappings = CLIENT_TOOL_MAPPINGS[clientType];
|
|
1465
|
+
if (clientMappings && Array.isArray(anthropicRequest.messages)) {
|
|
1466
|
+
const lynkrToClient = {};
|
|
1467
|
+
for (const [lynkrName, mapping] of Object.entries(clientMappings)) {
|
|
1468
|
+
lynkrToClient[lynkrName] = mapping.name;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
for (const msg of anthropicRequest.messages) {
|
|
1472
|
+
if (!Array.isArray(msg.content)) continue;
|
|
1473
|
+
for (const block of msg.content) {
|
|
1474
|
+
if (block.type === 'tool_use' && lynkrToClient[block.name]) {
|
|
1475
|
+
block.name = lynkrToClient[block.name];
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1492
1481
|
logger.debug({
|
|
1493
1482
|
anthropicMessageCount: anthropicRequest.messages?.length,
|
|
1494
1483
|
anthropicMessages: anthropicRequest.messages?.map(m => ({
|
|
@@ -1497,26 +1486,73 @@ router.post("/responses", async (req, res) => {
|
|
|
1497
1486
|
}))
|
|
1498
1487
|
}, "After Chat→Anthropic conversion");
|
|
1499
1488
|
|
|
1500
|
-
// Inject tools if
|
|
1501
|
-
//
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1489
|
+
// Inject tools if the Anthropic request has none.
|
|
1490
|
+
// The client may have sent tools in Responses API format (top-level name)
|
|
1491
|
+
// which convertOpenAIToAnthropic silently drops because it expects Chat
|
|
1492
|
+
// Completions format ({function: {name}}). Always check the CONVERTED
|
|
1493
|
+
// result, not the raw request.
|
|
1494
|
+
const clientDisabledToolChoice = req.body.tool_choice === "none";
|
|
1495
|
+
if (!clientDisabledToolChoice && (!anthropicRequest.tools || anthropicRequest.tools.length === 0)) {
|
|
1496
|
+
// Exclude server-side-only tools (Task, web_search, etc.) from the
|
|
1497
|
+
// Responses endpoint — they can't be executed by external clients like
|
|
1498
|
+
// Codex and would be converted to broken shell echo commands.
|
|
1499
|
+
const RESPONSES_EXCLUDED_TOOLS = new Set(["Task", "AskUserQuestion", "TodoWrite", "WebSearch", "WebFetch", "WebAgent"]);
|
|
1505
1500
|
const clientMappings = CLIENT_TOOL_MAPPINGS[clientType];
|
|
1506
|
-
|
|
1501
|
+
let clientTools = (clientMappings
|
|
1507
1502
|
? IDE_SAFE_TOOLS.filter(t => clientMappings[t.name])
|
|
1508
|
-
: IDE_SAFE_TOOLS
|
|
1503
|
+
: IDE_SAFE_TOOLS
|
|
1504
|
+
).filter(t => !RESPONSES_EXCLUDED_TOOLS.has(t.name));
|
|
1505
|
+
|
|
1506
|
+
// Rename tools to client-expected names so the model uses the right names
|
|
1507
|
+
// e.g., for Codex: "Read" → "read_file", "Bash" → "shell"
|
|
1508
|
+
// The lynkrToClient map above normalizes any stale Lynkr names in history
|
|
1509
|
+
if (clientMappings) {
|
|
1510
|
+
clientTools = clientTools.map(t => {
|
|
1511
|
+
const mapping = clientMappings[t.name];
|
|
1512
|
+
if (!mapping) return t;
|
|
1513
|
+
return {
|
|
1514
|
+
...t,
|
|
1515
|
+
name: mapping.name,
|
|
1516
|
+
description: t.description || '',
|
|
1517
|
+
};
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1509
1521
|
anthropicRequest.tools = clientTools;
|
|
1510
|
-
logger.
|
|
1522
|
+
logger.info({
|
|
1511
1523
|
clientType,
|
|
1512
1524
|
injectedToolCount: clientTools.length,
|
|
1513
1525
|
injectedToolNames: clientTools.map(t => t.name),
|
|
1514
1526
|
reason: clientMappings
|
|
1515
|
-
? `Known client '${clientType}' —
|
|
1527
|
+
? `Known client '${clientType}' — tools renamed to client conventions`
|
|
1516
1528
|
: "Unknown client — injecting full IDE_SAFE_TOOLS"
|
|
1517
1529
|
}, "=== INJECTING TOOLS (responses) ===");
|
|
1530
|
+
} else {
|
|
1531
|
+
logger.info({
|
|
1532
|
+
clientType,
|
|
1533
|
+
clientDisabledToolChoice,
|
|
1534
|
+
hasTools: !!anthropicRequest.tools,
|
|
1535
|
+
toolCount: anthropicRequest.tools?.length || 0,
|
|
1536
|
+
toolNames: anthropicRequest.tools?.map(t => t.name)?.slice(0, 10),
|
|
1537
|
+
reqToolChoice: req.body.tool_choice,
|
|
1538
|
+
reqToolsIsArray: Array.isArray(req.body.tools),
|
|
1539
|
+
reqToolsLength: req.body.tools?.length,
|
|
1540
|
+
}, "=== TOOLS NOT INJECTED (responses) ===");
|
|
1518
1541
|
}
|
|
1519
1542
|
|
|
1543
|
+
// ALWAYS strip server-side-only tools from the Responses endpoint.
|
|
1544
|
+
// These can't be executed by external clients (Codex, etc.) and cause
|
|
1545
|
+
// infinite retry loops when the model keeps calling them.
|
|
1546
|
+
const RESPONSES_EXCLUDED = new Set(["Task", "AskUserQuestion", "TodoWrite", "WebSearch", "WebFetch", "WebAgent"]);
|
|
1547
|
+
if (Array.isArray(anthropicRequest.tools)) {
|
|
1548
|
+
anthropicRequest.tools = anthropicRequest.tools.filter(t => !RESPONSES_EXCLUDED.has(t.name));
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// Snapshot tool names before the orchestrator can mutate them
|
|
1552
|
+
const injectedToolNames = new Set(
|
|
1553
|
+
(anthropicRequest.tools || []).map(t => t.name)
|
|
1554
|
+
);
|
|
1555
|
+
|
|
1520
1556
|
// Get session
|
|
1521
1557
|
const session = getSession(sessionId);
|
|
1522
1558
|
|
|
@@ -1526,294 +1562,282 @@ router.post("/responses", async (req, res) => {
|
|
|
1526
1562
|
res.setHeader("Content-Type", "text/event-stream");
|
|
1527
1563
|
res.setHeader("Cache-Control", "no-cache");
|
|
1528
1564
|
res.setHeader("Connection", "keep-alive");
|
|
1529
|
-
res.setHeader("X-Accel-Buffering", "no");
|
|
1530
|
-
res.flushHeaders();
|
|
1565
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
1566
|
+
res.flushHeaders();
|
|
1531
1567
|
|
|
1532
1568
|
try {
|
|
1533
|
-
// Force non-streaming from orchestrator
|
|
1534
1569
|
anthropicRequest.stream = false;
|
|
1535
1570
|
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1571
|
+
// SSE comment keepalive (spec-compliant, ignored by all clients)
|
|
1572
|
+
const keepalive = setInterval(() => {
|
|
1573
|
+
try { res.write(`: keepalive\n\n`); } catch {}
|
|
1574
|
+
}, 2000);
|
|
1575
|
+
|
|
1576
|
+
let result;
|
|
1577
|
+
try {
|
|
1578
|
+
result = await orchestrator.processMessage({
|
|
1579
|
+
payload: anthropicRequest,
|
|
1580
|
+
headers: req.headers,
|
|
1581
|
+
session: session,
|
|
1582
|
+
options: {
|
|
1583
|
+
maxSteps: req.body?.max_steps
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1586
|
+
} finally {
|
|
1587
|
+
clearInterval(keepalive);
|
|
1588
|
+
}
|
|
1544
1589
|
|
|
1545
|
-
// Debug: Log what orchestrator returned
|
|
1546
1590
|
logger.debug({
|
|
1547
1591
|
hasResult: !!result,
|
|
1548
1592
|
hasBody: !!result?.body,
|
|
1549
|
-
bodyKeys: result?.body ? Object.keys(result.body) : null,
|
|
1550
|
-
bodyContent: result?.body?.content ? JSON.stringify(result.body.content).substring(0, 200) : null,
|
|
1551
1593
|
bodyContentLength: result?.body?.content?.length || 0,
|
|
1552
1594
|
terminationReason: result?.terminationReason
|
|
1553
1595
|
}, "=== ORCHESTRATOR RESULT FOR RESPONSES API ===");
|
|
1554
1596
|
|
|
1555
1597
|
// Convert back: Anthropic → OpenAI → Responses
|
|
1556
1598
|
const responsesModel = resolveResponseModel(result.body, req.body.model);
|
|
1557
|
-
const chatResponse = convertAnthropicToOpenAI(result.body, responsesModel);
|
|
1558
1599
|
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1600
|
+
// Guard: if orchestrator returned an error body, surface it as text
|
|
1601
|
+
if (result.body?.error || result.status >= 400) {
|
|
1602
|
+
const errMsg = result.body?.error?.message || result.body?.error || JSON.stringify(result.body);
|
|
1603
|
+
logger.warn({ status: result.status, error: errMsg }, "Orchestrator returned error for Responses API");
|
|
1604
|
+
const errChunks = [];
|
|
1605
|
+
const errSse = (ev, d) => { errChunks.push(`event: ${ev}\ndata: ${JSON.stringify(d)}\n\n`); };
|
|
1606
|
+
const errId = `resp_err_${Date.now()}`;
|
|
1607
|
+
errSse("response.created", { type: "response.created", response: { id: errId, object: "response", status: "in_progress", output: [], usage: null }, sequence_number: 0 });
|
|
1608
|
+
errSse("response.output_item.added", { type: "response.output_item.added", output_index: 0, item: { id: `msg_${Date.now()}`, type: "message", status: "in_progress", role: "assistant", content: [] }, sequence_number: 1 });
|
|
1609
|
+
errSse("response.output_text.delta", { type: "response.output_text.delta", item_id: `msg_${Date.now()}`, output_index: 0, content_index: 0, delta: `Error: ${errMsg}`, sequence_number: 2 });
|
|
1610
|
+
errSse("response.completed", { type: "response.completed", response: { id: errId, object: "response", status: "completed", output: [{ type: "message", role: "assistant", status: "completed", content: [{ type: "output_text", text: `Error: ${errMsg}` }] }], usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 } }, sequence_number: 3 });
|
|
1611
|
+
res.end(errChunks.join(""));
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1565
1614
|
|
|
1615
|
+
const chatResponse = convertAnthropicToOpenAI(result.body, responsesModel);
|
|
1566
1616
|
const responsesResponse = convertChatToResponses(chatResponse);
|
|
1567
1617
|
|
|
1568
|
-
//
|
|
1569
|
-
|
|
1618
|
+
// Clean up tool result artifacts in content
|
|
1619
|
+
let content = responsesResponse.content || "";
|
|
1620
|
+
content = content.replace(/\{"output":"((?:[^"\\]|\\.)*)"\s*(?:,"metadata":\{[^}]*\})?\}/g, (match, output) => {
|
|
1621
|
+
try { return JSON.parse(`"${output}"`); } catch { return output; }
|
|
1622
|
+
});
|
|
1623
|
+
content = content.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
|
|
1570
1624
|
let toolCalls = chatResponse.choices?.[0]?.message?.tool_calls || [];
|
|
1625
|
+
|
|
1571
1626
|
const responseId = responsesResponse.id || `resp_${Date.now()}`;
|
|
1572
1627
|
const messageId = `msg_${Date.now()}`;
|
|
1573
1628
|
const createdAt = Math.floor(Date.now() / 1000);
|
|
1574
1629
|
let sequenceNumber = 0;
|
|
1575
1630
|
let outputIndex = 0;
|
|
1576
1631
|
|
|
1577
|
-
//
|
|
1578
|
-
|
|
1579
|
-
if (
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1632
|
+
// Fallback: if model returned empty text AND no tool calls,
|
|
1633
|
+
// the model may have failed to produce output. Surface what we have.
|
|
1634
|
+
if (!content && toolCalls.length === 0) {
|
|
1635
|
+
const body = result.body;
|
|
1636
|
+
if (Array.isArray(body?.content)) {
|
|
1637
|
+
content = body.content
|
|
1638
|
+
.filter(b => b?.type === "text" && b?.text)
|
|
1639
|
+
.map(b => b.text)
|
|
1640
|
+
.join("\n") || "(The model returned an empty response.)";
|
|
1641
|
+
} else {
|
|
1642
|
+
content = "(The model returned an empty response.)";
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1585
1645
|
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1646
|
+
// Universal tool→shell converter: ensures every tool call becomes
|
|
1647
|
+
// a "shell" command that Codex (or any client) can execute.
|
|
1648
|
+
// Server-side tools (Task, WebSearch, etc.) are dropped entirely.
|
|
1649
|
+
if (toolCalls.length > 0) {
|
|
1650
|
+
const SERVER_TOOLS = new Set(["task", "websearch", "webfetch", "web_search", "web_fetch", "web_agent", "askuserquestion", "todowrite"]);
|
|
1651
|
+
const converted = [];
|
|
1652
|
+
for (const tc of toolCalls) {
|
|
1653
|
+
const name = tc.function?.name || "";
|
|
1654
|
+
const ln = name.toLowerCase();
|
|
1655
|
+
|
|
1656
|
+
// Convert server-side tools to useful shell equivalents
|
|
1657
|
+
if (SERVER_TOOLS.has(ln)) {
|
|
1658
|
+
let exploreCmd = "ls -la && head -100 README.md 2>/dev/null && cat package.json 2>/dev/null && ls src/ 2>/dev/null";
|
|
1659
|
+
if (ln === "task") {
|
|
1660
|
+
try {
|
|
1661
|
+
const taskArgs = JSON.parse(tc.function?.arguments || "{}");
|
|
1662
|
+
if (taskArgs.prompt && taskArgs.prompt.toLowerCase().includes("read")) {
|
|
1663
|
+
exploreCmd = "cat README.md 2>/dev/null && cat package.json 2>/dev/null";
|
|
1664
|
+
}
|
|
1665
|
+
} catch {}
|
|
1594
1666
|
}
|
|
1595
|
-
|
|
1596
|
-
|
|
1667
|
+
converted.push({ ...tc, function: { name: "shell", arguments: JSON.stringify({ command: ["bash", "-c", exploreCmd] }) } });
|
|
1668
|
+
continue;
|
|
1669
|
+
}
|
|
1597
1670
|
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1671
|
+
let args = {};
|
|
1672
|
+
try { args = JSON.parse(tc.function?.arguments || "{}"); } catch {}
|
|
1673
|
+
|
|
1674
|
+
// Already a shell command — normalise to array format
|
|
1675
|
+
if (ln === "shell" || ln === "bash") {
|
|
1676
|
+
let cmd = args.command || "";
|
|
1677
|
+
// Handle string-encoded arrays: '["bash","-c","ls"]'
|
|
1678
|
+
if (typeof cmd === "string" && cmd.trim().startsWith("[")) {
|
|
1679
|
+
try { cmd = JSON.parse(cmd); } catch {}
|
|
1680
|
+
}
|
|
1681
|
+
// Extract the actual command from ["bash", "-c", "actual command"]
|
|
1682
|
+
if (Array.isArray(cmd) && cmd.length >= 3 && cmd[0] === "bash" && cmd[1] === "-c") {
|
|
1683
|
+
converted.push({ ...tc, function: { name: "shell", arguments: JSON.stringify({ command: cmd }) } });
|
|
1684
|
+
} else if (Array.isArray(cmd)) {
|
|
1685
|
+
converted.push({ ...tc, function: { name: "shell", arguments: JSON.stringify({ command: ["bash", "-c", cmd.join(" ")] }) } });
|
|
1686
|
+
} else {
|
|
1687
|
+
converted.push({ ...tc, function: { name: "shell", arguments: JSON.stringify({ command: ["bash", "-c", String(cmd)] }) } });
|
|
1688
|
+
}
|
|
1689
|
+
continue;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
// Convert known tools to equivalent shell commands
|
|
1693
|
+
let shellCmd = "";
|
|
1694
|
+
if (ln === "read" || ln === "read_file" || ln === "cat") {
|
|
1695
|
+
shellCmd = `cat ${args.file_path || args.path || args.file || ""}`;
|
|
1696
|
+
} else if (ln === "list_dir" || ln === "listdir" || ln === "ls") {
|
|
1697
|
+
shellCmd = `ls -la ${args.path || args.directory || "."}`;
|
|
1698
|
+
} else if (ln === "grep" || ln === "rg" || ln === "search" || ln === "search_files") {
|
|
1699
|
+
shellCmd = `grep -rn '${(args.pattern || args.query || "").replace(/'/g, "'\\''")}' ${args.path || "."}`;
|
|
1700
|
+
} else if (ln === "glob" || ln === "glob_file_search" || ln === "find") {
|
|
1701
|
+
shellCmd = `find ${args.path || "."} -name '${(args.pattern || "*").replace(/'/g, "'\\''")}'`;
|
|
1702
|
+
} else if (ln === "write" || ln === "write_file" || ln === "create_file") {
|
|
1703
|
+
const p = args.file_path || args.path || args.file || "/dev/null";
|
|
1704
|
+
shellCmd = `cat > '${p}' << 'LYNKR_EOF'\n${args.content || ""}\nLYNKR_EOF`;
|
|
1705
|
+
} else if (ln === "edit" || ln === "apply_patch" || ln === "replace_in_file") {
|
|
1706
|
+
converted.push({ ...tc, function: { name: "apply_patch", arguments: tc.function?.arguments || "{}" } });
|
|
1707
|
+
continue;
|
|
1708
|
+
} else {
|
|
1709
|
+
// Truly unknown tool — safe echo
|
|
1710
|
+
shellCmd = `echo 'Unknown tool: ${ln}'`;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
converted.push({ ...tc, function: { name: "shell", arguments: JSON.stringify({ command: ["bash", "-c", shellCmd] }) } });
|
|
1714
|
+
}
|
|
1715
|
+
toolCalls = converted;
|
|
1716
|
+
|
|
1717
|
+
// If somehow all tools were dropped with nothing left, provide a default exploration
|
|
1718
|
+
if (toolCalls.length === 0 && !content) {
|
|
1719
|
+
toolCalls.push({
|
|
1720
|
+
id: `call_${Date.now()}`,
|
|
1721
|
+
type: "function",
|
|
1722
|
+
function: { name: "shell", arguments: JSON.stringify({ command: ["bash", "-c", "ls -la && head -80 README.md 2>/dev/null"] }) }
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1601
1725
|
}
|
|
1602
1726
|
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
}, "=== RESPONSES API STREAMING DATA ===");
|
|
1727
|
+
// Build the entire SSE payload as a single buffer so the client
|
|
1728
|
+
// receives all events atomically (prevents premature disconnect).
|
|
1729
|
+
const chunks = [];
|
|
1730
|
+
const sse = (event, data) => {
|
|
1731
|
+
chunks.push(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
1732
|
+
};
|
|
1610
1733
|
|
|
1611
|
-
//
|
|
1612
|
-
|
|
1734
|
+
// response.created
|
|
1735
|
+
sse("response.created", {
|
|
1613
1736
|
type: "response.created",
|
|
1614
1737
|
response: {
|
|
1615
|
-
id: responseId,
|
|
1616
|
-
|
|
1617
|
-
status: "in_progress",
|
|
1618
|
-
created_at: createdAt,
|
|
1619
|
-
model: responsesModel,
|
|
1620
|
-
output: [],
|
|
1621
|
-
usage: null
|
|
1738
|
+
id: responseId, object: "response", status: "in_progress",
|
|
1739
|
+
created_at: createdAt, model: responsesModel, output: [], usage: null
|
|
1622
1740
|
},
|
|
1623
1741
|
sequence_number: sequenceNumber++
|
|
1624
|
-
};
|
|
1625
|
-
res.write(`event: response.created\n`);
|
|
1626
|
-
res.write(`data: ${JSON.stringify(createdEvent)}\n\n`);
|
|
1742
|
+
});
|
|
1627
1743
|
|
|
1628
|
-
//
|
|
1629
|
-
|
|
1744
|
+
// response.in_progress
|
|
1745
|
+
sse("response.in_progress", {
|
|
1630
1746
|
type: "response.in_progress",
|
|
1631
1747
|
response: {
|
|
1632
|
-
id: responseId,
|
|
1633
|
-
|
|
1634
|
-
status: "in_progress",
|
|
1635
|
-
created_at: createdAt,
|
|
1636
|
-
model: responsesModel,
|
|
1637
|
-
output: [],
|
|
1638
|
-
usage: null
|
|
1748
|
+
id: responseId, object: "response", status: "in_progress",
|
|
1749
|
+
created_at: createdAt, model: responsesModel, output: [], usage: null
|
|
1639
1750
|
},
|
|
1640
1751
|
sequence_number: sequenceNumber++
|
|
1641
|
-
};
|
|
1642
|
-
res.write(`event: response.in_progress\n`);
|
|
1643
|
-
res.write(`data: ${JSON.stringify(inProgressEvent)}\n\n`);
|
|
1752
|
+
});
|
|
1644
1753
|
|
|
1645
|
-
// Build output array for the final response
|
|
1646
1754
|
const outputItems = [];
|
|
1647
1755
|
|
|
1648
|
-
//
|
|
1756
|
+
// Function call events
|
|
1649
1757
|
for (const toolCall of toolCalls) {
|
|
1650
1758
|
const toolCallId = toolCall.id || `call_${Date.now()}_${outputIndex}`;
|
|
1651
1759
|
const functionName = toolCall.function?.name || "unknown";
|
|
1652
1760
|
const functionArgs = toolCall.function?.arguments || "{}";
|
|
1653
1761
|
|
|
1654
|
-
// Send function_call output item added
|
|
1655
1762
|
const functionCallItem = {
|
|
1656
|
-
id: toolCallId,
|
|
1657
|
-
|
|
1658
|
-
status: "completed",
|
|
1659
|
-
name: functionName,
|
|
1660
|
-
arguments: functionArgs,
|
|
1661
|
-
call_id: toolCallId
|
|
1763
|
+
id: toolCallId, type: "function_call", status: "completed",
|
|
1764
|
+
name: functionName, arguments: functionArgs, call_id: toolCallId
|
|
1662
1765
|
};
|
|
1663
1766
|
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
output_index: outputIndex,
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
})}\n\n`);
|
|
1681
|
-
|
|
1682
|
-
// Send function call arguments done
|
|
1683
|
-
res.write(`event: response.function_call_arguments.done\n`);
|
|
1684
|
-
res.write(`data: ${JSON.stringify({
|
|
1685
|
-
type: "response.function_call_arguments.done",
|
|
1686
|
-
item_id: toolCallId,
|
|
1687
|
-
output_index: outputIndex,
|
|
1688
|
-
arguments: functionArgs,
|
|
1689
|
-
sequence_number: sequenceNumber++
|
|
1690
|
-
})}\n\n`);
|
|
1691
|
-
|
|
1692
|
-
// Send output item done
|
|
1693
|
-
res.write(`event: response.output_item.done\n`);
|
|
1694
|
-
res.write(`data: ${JSON.stringify({
|
|
1695
|
-
type: "response.output_item.done",
|
|
1696
|
-
output_index: outputIndex,
|
|
1697
|
-
item: functionCallItem,
|
|
1698
|
-
sequence_number: sequenceNumber++
|
|
1699
|
-
})}\n\n`);
|
|
1767
|
+
sse("response.output_item.added", {
|
|
1768
|
+
type: "response.output_item.added", output_index: outputIndex,
|
|
1769
|
+
item: functionCallItem, sequence_number: sequenceNumber++
|
|
1770
|
+
});
|
|
1771
|
+
sse("response.function_call_arguments.delta", {
|
|
1772
|
+
type: "response.function_call_arguments.delta", item_id: toolCallId,
|
|
1773
|
+
output_index: outputIndex, delta: functionArgs, sequence_number: sequenceNumber++
|
|
1774
|
+
});
|
|
1775
|
+
sse("response.function_call_arguments.done", {
|
|
1776
|
+
type: "response.function_call_arguments.done", item_id: toolCallId,
|
|
1777
|
+
output_index: outputIndex, arguments: functionArgs, sequence_number: sequenceNumber++
|
|
1778
|
+
});
|
|
1779
|
+
sse("response.output_item.done", {
|
|
1780
|
+
type: "response.output_item.done", output_index: outputIndex,
|
|
1781
|
+
item: functionCallItem, sequence_number: sequenceNumber++
|
|
1782
|
+
});
|
|
1700
1783
|
|
|
1701
1784
|
outputItems.push(functionCallItem);
|
|
1702
1785
|
outputIndex++;
|
|
1703
1786
|
}
|
|
1704
1787
|
|
|
1705
|
-
//
|
|
1788
|
+
// Text content events
|
|
1706
1789
|
if (content) {
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
type: "
|
|
1710
|
-
output_index: outputIndex,
|
|
1711
|
-
item: {
|
|
1712
|
-
id: messageId,
|
|
1713
|
-
type: "message",
|
|
1714
|
-
status: "in_progress",
|
|
1715
|
-
role: "assistant",
|
|
1716
|
-
content: []
|
|
1717
|
-
},
|
|
1790
|
+
sse("response.output_item.added", {
|
|
1791
|
+
type: "response.output_item.added", output_index: outputIndex,
|
|
1792
|
+
item: { id: messageId, type: "message", status: "in_progress", role: "assistant", content: [] },
|
|
1718
1793
|
sequence_number: sequenceNumber++
|
|
1719
|
-
};
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
type: "response.content_part.added",
|
|
1726
|
-
item_id: messageId,
|
|
1727
|
-
output_index: outputIndex,
|
|
1728
|
-
content_index: 0,
|
|
1729
|
-
part: {
|
|
1730
|
-
type: "output_text",
|
|
1731
|
-
text: ""
|
|
1732
|
-
},
|
|
1733
|
-
sequence_number: sequenceNumber++
|
|
1734
|
-
};
|
|
1735
|
-
res.write(`event: response.content_part.added\n`);
|
|
1736
|
-
res.write(`data: ${JSON.stringify(contentPartAddedEvent)}\n\n`);
|
|
1794
|
+
});
|
|
1795
|
+
sse("response.content_part.added", {
|
|
1796
|
+
type: "response.content_part.added", item_id: messageId,
|
|
1797
|
+
output_index: outputIndex, content_index: 0,
|
|
1798
|
+
part: { type: "output_text", text: "" }, sequence_number: sequenceNumber++
|
|
1799
|
+
});
|
|
1737
1800
|
|
|
1738
|
-
// 5. Send content in word chunks using response.output_text.delta
|
|
1739
1801
|
const words = content.split(" ");
|
|
1740
1802
|
for (let i = 0; i < words.length; i++) {
|
|
1741
1803
|
const word = words[i] + (i < words.length - 1 ? " " : "");
|
|
1742
|
-
|
|
1743
|
-
type: "response.output_text.delta",
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
delta: word,
|
|
1748
|
-
sequence_number: sequenceNumber++
|
|
1749
|
-
};
|
|
1750
|
-
res.write(`event: response.output_text.delta\n`);
|
|
1751
|
-
res.write(`data: ${JSON.stringify(deltaEvent)}\n\n`);
|
|
1804
|
+
sse("response.output_text.delta", {
|
|
1805
|
+
type: "response.output_text.delta", item_id: messageId,
|
|
1806
|
+
output_index: outputIndex, content_index: 0,
|
|
1807
|
+
delta: word, sequence_number: sequenceNumber++
|
|
1808
|
+
});
|
|
1752
1809
|
}
|
|
1753
1810
|
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
res.write(`data: ${JSON.stringify(textDoneEvent)}\n\n`);
|
|
1765
|
-
|
|
1766
|
-
// 7. Send response.content_part.done event
|
|
1767
|
-
const contentPartDoneEvent = {
|
|
1768
|
-
type: "response.content_part.done",
|
|
1769
|
-
item_id: messageId,
|
|
1770
|
-
output_index: outputIndex,
|
|
1771
|
-
content_index: 0,
|
|
1772
|
-
part: {
|
|
1773
|
-
type: "output_text",
|
|
1774
|
-
text: content
|
|
1775
|
-
},
|
|
1776
|
-
sequence_number: sequenceNumber++
|
|
1777
|
-
};
|
|
1778
|
-
res.write(`event: response.content_part.done\n`);
|
|
1779
|
-
res.write(`data: ${JSON.stringify(contentPartDoneEvent)}\n\n`);
|
|
1811
|
+
sse("response.output_text.done", {
|
|
1812
|
+
type: "response.output_text.done", item_id: messageId,
|
|
1813
|
+
output_index: outputIndex, content_index: 0,
|
|
1814
|
+
text: content, sequence_number: sequenceNumber++
|
|
1815
|
+
});
|
|
1816
|
+
sse("response.content_part.done", {
|
|
1817
|
+
type: "response.content_part.done", item_id: messageId,
|
|
1818
|
+
output_index: outputIndex, content_index: 0,
|
|
1819
|
+
part: { type: "output_text", text: content }, sequence_number: sequenceNumber++
|
|
1820
|
+
});
|
|
1780
1821
|
|
|
1781
|
-
// 8. Send response.output_item.done event for message
|
|
1782
1822
|
const messageItem = {
|
|
1783
|
-
id: messageId,
|
|
1784
|
-
type: "
|
|
1785
|
-
status: "completed",
|
|
1786
|
-
role: "assistant",
|
|
1787
|
-
content: [
|
|
1788
|
-
{
|
|
1789
|
-
type: "output_text",
|
|
1790
|
-
text: content
|
|
1791
|
-
}
|
|
1792
|
-
]
|
|
1793
|
-
};
|
|
1794
|
-
const outputItemDoneEvent = {
|
|
1795
|
-
type: "response.output_item.done",
|
|
1796
|
-
output_index: outputIndex,
|
|
1797
|
-
item: messageItem,
|
|
1798
|
-
sequence_number: sequenceNumber++
|
|
1823
|
+
id: messageId, type: "message", status: "completed", role: "assistant",
|
|
1824
|
+
content: [{ type: "output_text", text: content }]
|
|
1799
1825
|
};
|
|
1800
|
-
|
|
1801
|
-
|
|
1826
|
+
sse("response.output_item.done", {
|
|
1827
|
+
type: "response.output_item.done", output_index: outputIndex,
|
|
1828
|
+
item: messageItem, sequence_number: sequenceNumber++
|
|
1829
|
+
});
|
|
1802
1830
|
|
|
1803
1831
|
outputItems.push(messageItem);
|
|
1804
1832
|
outputIndex++;
|
|
1805
1833
|
}
|
|
1806
1834
|
|
|
1807
|
-
//
|
|
1808
|
-
|
|
1835
|
+
// response.completed (always last)
|
|
1836
|
+
sse("response.completed", {
|
|
1809
1837
|
type: "response.completed",
|
|
1810
1838
|
response: {
|
|
1811
|
-
id: responseId,
|
|
1812
|
-
|
|
1813
|
-
status: "completed",
|
|
1814
|
-
created_at: createdAt,
|
|
1815
|
-
model: responsesModel,
|
|
1816
|
-
output: outputItems,
|
|
1839
|
+
id: responseId, object: "response", status: "completed",
|
|
1840
|
+
created_at: createdAt, model: responsesModel, output: outputItems,
|
|
1817
1841
|
usage: {
|
|
1818
1842
|
input_tokens: responsesResponse.usage?.prompt_tokens || 0,
|
|
1819
1843
|
output_tokens: responsesResponse.usage?.completion_tokens || 0,
|
|
@@ -1821,34 +1845,52 @@ router.post("/responses", async (req, res) => {
|
|
|
1821
1845
|
}
|
|
1822
1846
|
},
|
|
1823
1847
|
sequence_number: sequenceNumber++
|
|
1824
|
-
};
|
|
1825
|
-
res.write(`event: response.completed\n`);
|
|
1826
|
-
res.write(`data: ${JSON.stringify(completedEvent)}\n\n`);
|
|
1848
|
+
});
|
|
1827
1849
|
|
|
1828
|
-
|
|
1850
|
+
// Write entire payload and close in one call
|
|
1851
|
+
const payload = chunks.join("");
|
|
1852
|
+
res.end(payload);
|
|
1853
|
+
|
|
1854
|
+
// Store response for previous_response_id continuity
|
|
1855
|
+
try {
|
|
1856
|
+
const responseStore = require("../stores/response-store");
|
|
1857
|
+
responseStore.storeResponse(responseId, {
|
|
1858
|
+
messages: chatRequest?.messages || [],
|
|
1859
|
+
assistantContent: content || null,
|
|
1860
|
+
});
|
|
1861
|
+
} catch {}
|
|
1829
1862
|
|
|
1830
1863
|
logger.info({
|
|
1831
1864
|
duration: Date.now() - startTime,
|
|
1832
1865
|
mode: "streaming",
|
|
1833
1866
|
contentLength: content.length,
|
|
1834
1867
|
toolCallCount: toolCalls.length,
|
|
1835
|
-
|
|
1868
|
+
payloadBytes: payload.length
|
|
1836
1869
|
}, "=== RESPONSES API STREAMING COMPLETE ===");
|
|
1837
1870
|
|
|
1838
1871
|
} catch (streamError) {
|
|
1839
1872
|
logger.error({ error: streamError.message, stack: streamError.stack }, "Responses API streaming error");
|
|
1840
1873
|
|
|
1841
|
-
//
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1874
|
+
// Build error as a complete SSE payload with response.completed
|
|
1875
|
+
const errorResponseId = `resp_err_${Date.now()}`;
|
|
1876
|
+
const errorPayload = [
|
|
1877
|
+
`event: response.created\ndata: ${JSON.stringify({
|
|
1878
|
+
type: "response.created",
|
|
1879
|
+
response: { id: errorResponseId, object: "response", status: "in_progress", output: [], usage: null },
|
|
1880
|
+
sequence_number: 0
|
|
1881
|
+
})}\n\n`,
|
|
1882
|
+
`event: response.completed\ndata: ${JSON.stringify({
|
|
1883
|
+
type: "response.completed",
|
|
1884
|
+
response: {
|
|
1885
|
+
id: errorResponseId, object: "response", status: "failed",
|
|
1886
|
+
output: [{ type: "message", role: "assistant", status: "completed",
|
|
1887
|
+
content: [{ type: "output_text", text: `Error: ${streamError.message || "Internal server error"}` }] }],
|
|
1888
|
+
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
|
|
1889
|
+
},
|
|
1890
|
+
sequence_number: 1
|
|
1891
|
+
})}\n\n`
|
|
1892
|
+
].join("");
|
|
1893
|
+
try { res.end(errorPayload); } catch { try { res.end(); } catch {} }
|
|
1852
1894
|
}
|
|
1853
1895
|
|
|
1854
1896
|
} else {
|
|
@@ -1868,6 +1910,16 @@ router.post("/responses", async (req, res) => {
|
|
|
1868
1910
|
const chatResponse = convertAnthropicToOpenAI(result.body, resolveResponseModel(result.body, req.body.model));
|
|
1869
1911
|
const responsesResponse = convertChatToResponses(chatResponse);
|
|
1870
1912
|
|
|
1913
|
+
// Clean up tool result artifacts in content
|
|
1914
|
+
if (responsesResponse.content) {
|
|
1915
|
+
responsesResponse.content = responsesResponse.content
|
|
1916
|
+
.replace(/\{"output":"((?:[^"\\]|\\.)*)"\s*(?:,"metadata":\{[^}]*\})?\}/g, (match, output) => {
|
|
1917
|
+
try { return JSON.parse(`"${output}"`); } catch { return output; }
|
|
1918
|
+
})
|
|
1919
|
+
.replace(/\\n/g, '\n')
|
|
1920
|
+
.replace(/\\t/g, '\t');
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1871
1923
|
logger.info({
|
|
1872
1924
|
duration: Date.now() - startTime,
|
|
1873
1925
|
contentLength: responsesResponse.content?.length || 0,
|