lynkr 9.14.0 → 9.14.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/package.json +10 -22
- package/src/api/health.js +0 -1
- package/src/api/middleware/session.js +0 -0
- package/src/api/openai-router.js +94 -13
- package/src/api/providers-handler.js +0 -1
- package/src/api/router.js +6 -3
- package/src/cache/prompt.js +0 -7
- package/src/clients/bedrock-utils.js +2 -1
- package/src/clients/codex-process.js +1 -1
- package/src/clients/codex-utils.js +0 -2
- package/src/clients/databricks.js +59 -25
- package/src/clients/gpt-utils.js +1 -1
- package/src/clients/openai-format.js +73 -23
- package/src/clients/provider-capabilities.js +0 -2
- package/src/clients/resilience.js +0 -1
- package/src/clients/responses-format.js +0 -56
- package/src/clients/routing.js +1 -1
- package/src/config/index.js +0 -1
- package/src/context/budget.js +1 -1
- package/src/context/tool-result-compressor.js +3 -5
- package/src/dashboard/api.js +0 -1
- package/src/headroom/client.js +44 -20
- package/src/logger/oversized-error-stream.js +1 -1
- package/src/memory/extractor.js +3 -3
- package/src/memory/search.js +2 -2
- package/src/memory/store.js +9 -24
- package/src/memory/tools.js +14 -0
- package/src/observability/metrics.js +0 -3
- package/src/orchestrator/index.js +1 -80
- package/src/orchestrator/sse-transformer.js +68 -10
- package/src/policy/safe-commands.js +0 -1
- package/src/routing/calibration.js +21 -1
- package/src/routing/client-profiles.js +26 -0
- package/src/routing/complexity-analyzer.js +5 -8
- package/src/routing/confidence-scorer.js +2 -2
- package/src/routing/cost-optimizer.js +1 -2
- package/src/routing/deescalator.js +1 -1
- package/src/routing/embedding-cache.js +0 -1
- package/src/routing/intent-score.js +0 -1
- package/src/routing/knn-router.js +27 -5
- package/src/routing/latency-tracker.js +1 -1
- package/src/routing/regret-estimator.js +1 -1
- package/src/stores/file-store.js +5 -1
- package/src/training/trajectory-compressor.js +1 -1
- package/src/workers/pool.js +1 -1
- package/src/workers/worker.js +1 -1
- package/scripts/convert-routellm.py +0 -105
- package/src/diff/comments.js +0 -153
- package/src/policy/web-fallback.js +0 -33
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lynkr",
|
|
3
|
-
"version": "9.14.
|
|
3
|
+
"version": "9.14.2",
|
|
4
4
|
"files": [
|
|
5
5
|
"index.js",
|
|
6
6
|
"install.sh",
|
|
@@ -38,12 +38,12 @@
|
|
|
38
38
|
"start:supervised": "while true; do node index.js 2>&1 | npx pino-pretty --sync; code=$?; echo \"[supervisor] lynkr exited ($code) — restarting in 3s\"; sleep 3; done",
|
|
39
39
|
"lint": "eslint src index.js",
|
|
40
40
|
"test": "npm run test:unit && npm run test:performance",
|
|
41
|
-
"test:unit": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com LOG_FILE_ENABLED=false node --test test/routing.test.js test/hybrid-routing-integration.test.js test/retry-logic.test.js test/sse-transformer.test.js test/passthrough-stream.test.js test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js test/azure-openai-config.test.js test/azure-openai-format-conversion.test.js test/azure-openai-routing.test.js test/azure-openai-streaming.test.js test/azure-openai-error-resilience.test.js test/azure-openai-integration.test.js test/openai-integration.test.js test/atlas-integration.test.js test/toon-compression.test.js test/gcf-compression.test.js test/llamacpp-integration.test.js test/resilience.test.js test/telemetry-routing.test.js test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js test/memory/distiller.test.js test/memory/distiller-freeze.test.js test/memory/wiki.test.js test/memory/skills-cache.test.js test/memory/tencentdb-launcher.test.js test/distill.test.js test/large-payload.test.js test/prompt-cache-injection.test.js test/risk-analyzer.test.js test/interaction-block.test.js test/preflight.test.js test/token-reduction.test.js test/session-affinity.test.js test/cache-state.test.js test/cache-switch-cost.test.js test/lens-recommendations.test.js test/model-registry-cost.test.js test/output-format-guard.test.js test/tier-fallback.test.js test/wrap.test.js test/init.test.js test/tool-call-response-metadata.test.js test/degradation.test.js test/routing-telemetry-columns.test.js test/sticky-routing.test.js test/knn-ambiguous-escalate.test.js test/deescalator.test.js test/client-profiles.test.js test/strip-internal-fields.test.js test/complexity-tool-subtraction.test.js test/bandit.test.js test/routing-propensity.test.js test/reward-pipeline.test.js test/knn-cold-start.test.js test/calibration.test.js test/feedback-loop.test.js test/session-fingerprint.test.js test/side-request-guards.test.js test/verifier.test.js test/intent-score.test.js test/difficulty-classifier.test.js test/classifier-setup.test.js test/usage-stats.test.js test/loop-guard.test.js test/moonshot-model-mapping.test.js test/baidu-model-mapping.test.js",
|
|
42
|
-
"test:memory": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js test/memory/distiller.test.js test/memory/distiller-freeze.test.js test/memory/wiki.test.js test/memory/skills-cache.test.js test/memory/tencentdb-launcher.test.js",
|
|
43
|
-
"test:new-features": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js",
|
|
44
|
-
"test:performance": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node test/hybrid-routing-performance.test.js && DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node test/performance-tests.js",
|
|
45
|
-
"test:benchmark": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node test/performance-benchmark.js",
|
|
46
|
-
"test:quick": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/routing.test.js",
|
|
41
|
+
"test:unit": "LYNKR_KNN_DIR=/tmp/lynkr-test-knn DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com LOG_FILE_ENABLED=false node --test test/routing.test.js test/hybrid-routing-integration.test.js test/retry-logic.test.js test/sse-transformer.test.js test/passthrough-stream.test.js test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js test/azure-openai-config.test.js test/azure-openai-format-conversion.test.js test/azure-openai-routing.test.js test/azure-openai-streaming.test.js test/azure-openai-error-resilience.test.js test/azure-openai-integration.test.js test/openai-integration.test.js test/atlas-integration.test.js test/toon-compression.test.js test/gcf-compression.test.js test/llamacpp-integration.test.js test/resilience.test.js test/telemetry-routing.test.js test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js test/memory/distiller.test.js test/memory/distiller-freeze.test.js test/memory/wiki.test.js test/memory/skills-cache.test.js test/memory/tencentdb-launcher.test.js test/distill.test.js test/large-payload.test.js test/prompt-cache-injection.test.js test/risk-analyzer.test.js test/interaction-block.test.js test/preflight.test.js test/token-reduction.test.js test/session-affinity.test.js test/cache-state.test.js test/cache-switch-cost.test.js test/lens-recommendations.test.js test/model-registry-cost.test.js test/output-format-guard.test.js test/tier-fallback.test.js test/wrap.test.js test/init.test.js test/tool-call-response-metadata.test.js test/degradation.test.js test/routing-telemetry-columns.test.js test/sticky-routing.test.js test/knn-ambiguous-escalate.test.js test/deescalator.test.js test/client-profiles.test.js test/strip-internal-fields.test.js test/complexity-tool-subtraction.test.js test/bandit.test.js test/routing-propensity.test.js test/reward-pipeline.test.js test/knn-cold-start.test.js test/calibration.test.js test/feedback-loop.test.js test/session-fingerprint.test.js test/side-request-guards.test.js test/verifier.test.js test/intent-score.test.js test/difficulty-classifier.test.js test/classifier-setup.test.js test/usage-stats.test.js test/loop-guard.test.js test/moonshot-model-mapping.test.js test/baidu-model-mapping.test.js",
|
|
42
|
+
"test:memory": "LYNKR_KNN_DIR=/tmp/lynkr-test-knn DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js test/memory/distiller.test.js test/memory/distiller-freeze.test.js test/memory/wiki.test.js test/memory/skills-cache.test.js test/memory/tencentdb-launcher.test.js",
|
|
43
|
+
"test:new-features": "LYNKR_KNN_DIR=/tmp/lynkr-test-knn DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js",
|
|
44
|
+
"test:performance": "LYNKR_KNN_DIR=/tmp/lynkr-test-knn DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node test/hybrid-routing-performance.test.js && DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node test/performance-tests.js",
|
|
45
|
+
"test:benchmark": "LYNKR_KNN_DIR=/tmp/lynkr-test-knn DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node test/performance-benchmark.js",
|
|
46
|
+
"test:quick": "LYNKR_KNN_DIR=/tmp/lynkr-test-knn DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/routing.test.js",
|
|
47
47
|
"test:all": "npm run test:unit && npm run test:performance && npm run test:benchmark"
|
|
48
48
|
},
|
|
49
49
|
"keywords": [
|
|
@@ -90,32 +90,22 @@
|
|
|
90
90
|
"node": ">=20.0.0"
|
|
91
91
|
},
|
|
92
92
|
"dependencies": {
|
|
93
|
-
"@azure/openai": "^2.0.0",
|
|
94
|
-
"@babel/parser": "^7.29.0",
|
|
95
|
-
"@babel/traverse": "^7.29.0",
|
|
96
93
|
"@blackwell-systems/gcf": "2.4.0",
|
|
97
|
-
"@tencentdb-agent-memory/memory-tencentdb": "^1.0.1",
|
|
98
94
|
"@toon-format/toon": "^2.1.0",
|
|
99
95
|
"cockatiel": "^3.2.1",
|
|
100
|
-
"compression": "^1.7.4",
|
|
101
96
|
"diff": "^5.2.0",
|
|
102
97
|
"dotenv": "^16.4.5",
|
|
103
98
|
"express": "^5.1.0",
|
|
104
99
|
"express-rate-limit": "^8.2.1",
|
|
105
|
-
"fast-glob": "^3.3.2",
|
|
106
|
-
"graphify": "^1.0.0",
|
|
107
100
|
"hnswlib-node": "^3.0.0",
|
|
108
101
|
"js-tiktoken": "^1.0.20",
|
|
109
|
-
"js-yaml": "^4.1.1",
|
|
110
|
-
"openai": "^6.14.0",
|
|
111
102
|
"pino": "^8.17.2",
|
|
112
103
|
"pino-http": "^8.6.0",
|
|
113
|
-
"pino-roll": "^4.0.0"
|
|
114
|
-
"undici": "^6.22.0"
|
|
104
|
+
"pino-roll": "^4.0.0"
|
|
115
105
|
},
|
|
116
106
|
"optionalDependencies": {
|
|
117
107
|
"better-sqlite3": "^12.11.1",
|
|
118
|
-
"dockerode": "^
|
|
108
|
+
"dockerode": "^5.0.1",
|
|
119
109
|
"tree-sitter": "^0.21.1",
|
|
120
110
|
"tree-sitter-javascript": "^0.21.0",
|
|
121
111
|
"tree-sitter-python": "^0.21.0",
|
|
@@ -127,7 +117,5 @@
|
|
|
127
117
|
"nodemon": "^3.1.0",
|
|
128
118
|
"pino-pretty": "^10.2.0"
|
|
129
119
|
},
|
|
130
|
-
"allowScripts": {
|
|
131
|
-
"@tencentdb-agent-memory/memory-tencentdb@1.0.1": true
|
|
132
|
-
}
|
|
120
|
+
"allowScripts": {}
|
|
133
121
|
}
|
package/src/api/health.js
CHANGED
|
Binary file
|
package/src/api/openai-router.js
CHANGED
|
@@ -355,12 +355,36 @@ async function forwardAnthropicStreamAsOpenAIChunks(res, stream, requestedModel)
|
|
|
355
355
|
const created = Math.floor(Date.now() / 1000);
|
|
356
356
|
const model = requestedModel || "lynkr";
|
|
357
357
|
|
|
358
|
-
const chunk = (delta, finish = null) => `data: ${JSON.stringify({
|
|
358
|
+
const chunk = (delta, finish = null, usage = null) => `data: ${JSON.stringify({
|
|
359
359
|
id, object: "chat.completion.chunk", created, model,
|
|
360
360
|
system_fingerprint: "fp_lynkr",
|
|
361
361
|
choices: [{ index: 0, delta, logprobs: null, finish_reason: finish }],
|
|
362
|
+
...(usage ? { usage } : {}),
|
|
362
363
|
})}\n\n`;
|
|
363
364
|
|
|
365
|
+
// Usage accounting (issue: opencode sidebar showed wrong token counts —
|
|
366
|
+
// this path never emitted usage at all). Anthropic sends input/cache
|
|
367
|
+
// tokens in message_start and output tokens in message_delta; accumulate
|
|
368
|
+
// and attach to the final chunk in OpenAI shape.
|
|
369
|
+
const usageAcc = { input: 0, cacheRead: 0, cacheWrite: 0, output: 0 };
|
|
370
|
+
const captureUsage = (u) => {
|
|
371
|
+
if (!u) return;
|
|
372
|
+
if (u.input_tokens != null) usageAcc.input = u.input_tokens;
|
|
373
|
+
if (u.cache_read_input_tokens != null) usageAcc.cacheRead = u.cache_read_input_tokens;
|
|
374
|
+
if (u.cache_creation_input_tokens != null) usageAcc.cacheWrite = u.cache_creation_input_tokens;
|
|
375
|
+
if (u.output_tokens != null) usageAcc.output = u.output_tokens;
|
|
376
|
+
};
|
|
377
|
+
const finalUsage = () => {
|
|
378
|
+
const promptTokens = usageAcc.input + usageAcc.cacheRead + usageAcc.cacheWrite;
|
|
379
|
+
const u = {
|
|
380
|
+
prompt_tokens: promptTokens,
|
|
381
|
+
completion_tokens: usageAcc.output,
|
|
382
|
+
total_tokens: promptTokens + usageAcc.output,
|
|
383
|
+
};
|
|
384
|
+
if (usageAcc.cacheRead > 0) u.prompt_tokens_details = { cached_tokens: usageAcc.cacheRead };
|
|
385
|
+
return u;
|
|
386
|
+
};
|
|
387
|
+
|
|
364
388
|
const mapStop = (r) => r === "tool_use" ? "tool_calls" : r === "max_tokens" ? "length" : "stop";
|
|
365
389
|
|
|
366
390
|
// Normalize Node Readable / web ReadableStream / async iterable to chunks.
|
|
@@ -394,6 +418,7 @@ async function forwardAnthropicStreamAsOpenAIChunks(res, stream, requestedModel)
|
|
|
394
418
|
|
|
395
419
|
switch (ev.type) {
|
|
396
420
|
case "message_start":
|
|
421
|
+
captureUsage(ev.message?.usage);
|
|
397
422
|
if (!started) { started = true; res.write(chunk({ role: "assistant", content: "" })); }
|
|
398
423
|
break;
|
|
399
424
|
case "content_block_start":
|
|
@@ -419,10 +444,11 @@ async function forwardAnthropicStreamAsOpenAIChunks(res, stream, requestedModel)
|
|
|
419
444
|
break;
|
|
420
445
|
}
|
|
421
446
|
case "message_delta":
|
|
447
|
+
captureUsage(ev.usage);
|
|
422
448
|
if (ev.delta?.stop_reason) finishReason = mapStop(ev.delta.stop_reason);
|
|
423
449
|
break;
|
|
424
450
|
case "message_stop":
|
|
425
|
-
res.write(chunk({}, finishReason || "stop"));
|
|
451
|
+
res.write(chunk({}, finishReason || "stop", finalUsage()));
|
|
426
452
|
res.write("data: [DONE]\n\n");
|
|
427
453
|
res.end();
|
|
428
454
|
return;
|
|
@@ -447,7 +473,7 @@ async function forwardAnthropicStreamAsOpenAIChunks(res, stream, requestedModel)
|
|
|
447
473
|
// truncated stream: send an error chunk, never a fabricated "stop".
|
|
448
474
|
if (!res.writableEnded) {
|
|
449
475
|
if (finishReason) {
|
|
450
|
-
res.write(chunk({}, finishReason));
|
|
476
|
+
res.write(chunk({}, finishReason, finalUsage()));
|
|
451
477
|
res.write("data: [DONE]\n\n");
|
|
452
478
|
} else {
|
|
453
479
|
logger.warn("Upstream stream ended without message_stop — sending error chunk to OpenAI-surface client");
|
|
@@ -468,7 +494,11 @@ async function forwardAnthropicStreamAsOpenAIChunks(res, stream, requestedModel)
|
|
|
468
494
|
|
|
469
495
|
router.post("/chat/completions", async (req, res) => {
|
|
470
496
|
const startTime = Date.now();
|
|
471
|
-
|
|
497
|
+
// Issue #95: use sessionMiddleware's id (header → body id → content
|
|
498
|
+
// fingerprint → UUID). Falling back to the raw Authorization token here
|
|
499
|
+
// collapsed every conversation from a static-key client into ONE session
|
|
500
|
+
// (defeating WS1 pinning) and persisted the bearer token as a store key.
|
|
501
|
+
const sessionId = req.sessionId;
|
|
472
502
|
|
|
473
503
|
try {
|
|
474
504
|
if (!req.body || typeof req.body !== 'object') {
|
|
@@ -730,7 +760,10 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
730
760
|
delta: {},
|
|
731
761
|
logprobs: null,
|
|
732
762
|
finish_reason: openaiResponse.choices[0].finish_reason
|
|
733
|
-
}]
|
|
763
|
+
}],
|
|
764
|
+
// Clients (opencode et al.) read token accounting from the final
|
|
765
|
+
// chunk — omitting it here left streamed turns uncounted.
|
|
766
|
+
usage: openaiResponse.usage
|
|
734
767
|
};
|
|
735
768
|
|
|
736
769
|
logger.debug({ chunk: "finish", finishReason: openaiResponse.choices[0].finish_reason }, "Sending finish chunk");
|
|
@@ -835,6 +868,24 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
835
868
|
finishReason: openaiResponse.choices[0].finish_reason
|
|
836
869
|
}, "=== OPENAI CHAT COMPLETION RESPONSE ===");
|
|
837
870
|
|
|
871
|
+
// Surface the routed reality: which model actually answered and what
|
|
872
|
+
// it cost (LiteLLM-style headers). Clients that can't price a dynamic
|
|
873
|
+
// router (opencode #24113) get the truth out-of-band.
|
|
874
|
+
const routingMeta = result.body?._routingMeta;
|
|
875
|
+
if (routingMeta?.model) {
|
|
876
|
+
res.set("x-lynkr-routed-model", String(routingMeta.model));
|
|
877
|
+
if (routingMeta.provider) res.set("x-lynkr-routed-provider", String(routingMeta.provider));
|
|
878
|
+
try {
|
|
879
|
+
const { computeCostUsd } = require("../clients/databricks");
|
|
880
|
+
const cost = computeCostUsd(
|
|
881
|
+
routingMeta.model,
|
|
882
|
+
result.body?.usage?.input_tokens || 0,
|
|
883
|
+
result.body?.usage?.output_tokens || 0
|
|
884
|
+
);
|
|
885
|
+
if (cost != null) res.set("x-lynkr-response-cost", cost.toFixed(6));
|
|
886
|
+
} catch { /* cost pricing unavailable — headers are best-effort */ }
|
|
887
|
+
}
|
|
888
|
+
|
|
838
889
|
res.json(openaiResponse);
|
|
839
890
|
}
|
|
840
891
|
|
|
@@ -1039,7 +1090,6 @@ function getConfiguredProviders() {
|
|
|
1039
1090
|
router.get("/models", (req, res) => {
|
|
1040
1091
|
try {
|
|
1041
1092
|
const providers = getConfiguredProviders();
|
|
1042
|
-
const primaryProvider = config.modelProvider?.type || "databricks";
|
|
1043
1093
|
const timestamp = Math.floor(Date.now() / 1000);
|
|
1044
1094
|
const models = [];
|
|
1045
1095
|
const seenModelIds = new Set();
|
|
@@ -1476,7 +1526,7 @@ router.post("/embeddings", async (req, res) => {
|
|
|
1476
1526
|
const startTime = Date.now();
|
|
1477
1527
|
|
|
1478
1528
|
try {
|
|
1479
|
-
const { input, model, encoding_format } = req.body;
|
|
1529
|
+
const { input, model, encoding_format: encodingFormat } = req.body;
|
|
1480
1530
|
|
|
1481
1531
|
if (!input) {
|
|
1482
1532
|
return res.status(400).json({
|
|
@@ -1488,6 +1538,16 @@ router.post("/embeddings", async (req, res) => {
|
|
|
1488
1538
|
});
|
|
1489
1539
|
}
|
|
1490
1540
|
|
|
1541
|
+
if (encodingFormat && encodingFormat !== "float" && encodingFormat !== "base64") {
|
|
1542
|
+
return res.status(400).json({
|
|
1543
|
+
error: {
|
|
1544
|
+
message: `Invalid encoding_format: ${encodingFormat}. Must be "float" or "base64".`,
|
|
1545
|
+
type: "invalid_request_error",
|
|
1546
|
+
code: "invalid_parameter"
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1491
1551
|
const inputs = Array.isArray(input) ? input : [input];
|
|
1492
1552
|
|
|
1493
1553
|
logger.debug({
|
|
@@ -1561,6 +1621,20 @@ router.post("/embeddings", async (req, res) => {
|
|
|
1561
1621
|
totalTokens: embeddingResponse.usage?.total_tokens || 0
|
|
1562
1622
|
}, "=== EMBEDDINGS RESPONSE ===");
|
|
1563
1623
|
|
|
1624
|
+
// Providers return float arrays; honor encoding_format=base64 (OpenAI
|
|
1625
|
+
// spec: base64 of the little-endian float32 buffer) instead of silently
|
|
1626
|
+
// returning floats for every request.
|
|
1627
|
+
if (encodingFormat === "base64" && Array.isArray(embeddingResponse.data)) {
|
|
1628
|
+
for (const item of embeddingResponse.data) {
|
|
1629
|
+
if (Array.isArray(item.embedding)) {
|
|
1630
|
+
item.embedding = Buffer.from(new Float32Array(item.embedding).buffer).toString("base64");
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
if ("encoding_format" in embeddingResponse) {
|
|
1634
|
+
embeddingResponse.encoding_format = "base64";
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1564
1638
|
res.json(embeddingResponse);
|
|
1565
1639
|
|
|
1566
1640
|
} catch (error) {
|
|
@@ -1588,7 +1662,10 @@ router.post("/embeddings", async (req, res) => {
|
|
|
1588
1662
|
*/
|
|
1589
1663
|
router.post("/responses", async (req, res) => {
|
|
1590
1664
|
const startTime = Date.now();
|
|
1591
|
-
|
|
1665
|
+
// Issue #95: same as /chat/completions — trust sessionMiddleware, never
|
|
1666
|
+
// the Authorization token. fingerprintSessionId understands the Responses
|
|
1667
|
+
// API's `input` field, so fingerprinting works on this route too.
|
|
1668
|
+
const sessionId = req.sessionId;
|
|
1592
1669
|
|
|
1593
1670
|
try {
|
|
1594
1671
|
const { convertResponsesToChat, convertChatToResponses } = require("../clients/responses-format");
|
|
@@ -1768,7 +1845,7 @@ router.post("/responses", async (req, res) => {
|
|
|
1768
1845
|
|
|
1769
1846
|
// SSE comment keepalive (spec-compliant, ignored by all clients)
|
|
1770
1847
|
const keepalive = setInterval(() => {
|
|
1771
|
-
try { res.write(`: keepalive\n\n`); } catch {}
|
|
1848
|
+
try { res.write(`: keepalive\n\n`); } catch { /* client gone — best-effort keepalive */ }
|
|
1772
1849
|
}, 2000);
|
|
1773
1850
|
|
|
1774
1851
|
let result;
|
|
@@ -1875,21 +1952,25 @@ router.post("/responses", async (req, res) => {
|
|
|
1875
1952
|
if (taskArgs.prompt && taskArgs.prompt.toLowerCase().includes("read")) {
|
|
1876
1953
|
exploreCmd = "cat README.md 2>/dev/null && cat package.json 2>/dev/null";
|
|
1877
1954
|
}
|
|
1878
|
-
} catch {
|
|
1955
|
+
} catch (err) {
|
|
1956
|
+
logger.debug({ err: err?.message }, "Failed to parse task tool arguments — using default explore command");
|
|
1957
|
+
}
|
|
1879
1958
|
}
|
|
1880
1959
|
converted.push({ ...tc, function: { name: "shell", arguments: JSON.stringify({ command: ["bash", "-c", exploreCmd] }) } });
|
|
1881
1960
|
continue;
|
|
1882
1961
|
}
|
|
1883
1962
|
|
|
1884
1963
|
let args = {};
|
|
1885
|
-
try { args = JSON.parse(tc.function?.arguments || "{}"); } catch {
|
|
1964
|
+
try { args = JSON.parse(tc.function?.arguments || "{}"); } catch (err) {
|
|
1965
|
+
logger.debug({ err: err?.message }, "Failed to parse tool call arguments — treating as empty object");
|
|
1966
|
+
}
|
|
1886
1967
|
|
|
1887
1968
|
// Already a shell command — normalise to array format
|
|
1888
1969
|
if (ln === "shell" || ln === "bash") {
|
|
1889
1970
|
let cmd = args.command || "";
|
|
1890
1971
|
// Handle string-encoded arrays: '["bash","-c","ls"]'
|
|
1891
1972
|
if (typeof cmd === "string" && cmd.trim().startsWith("[")) {
|
|
1892
|
-
try { cmd = JSON.parse(cmd); } catch {}
|
|
1973
|
+
try { cmd = JSON.parse(cmd); } catch { /* not a JSON array — keep as plain string */ }
|
|
1893
1974
|
}
|
|
1894
1975
|
// Extract the actual command from ["bash", "-c", "actual command"]
|
|
1895
1976
|
if (Array.isArray(cmd) && cmd.length >= 3 && cmd[0] === "bash" && cmd[1] === "-c") {
|
|
@@ -2100,7 +2181,7 @@ router.post("/responses", async (req, res) => {
|
|
|
2100
2181
|
sequence_number: 1
|
|
2101
2182
|
})}\n\n`
|
|
2102
2183
|
].join("");
|
|
2103
|
-
try { res.end(errorPayload); } catch { try { res.end(); } catch {} }
|
|
2184
|
+
try { res.end(errorPayload); } catch { try { res.end(); } catch { /* res already ended — best-effort */ } }
|
|
2104
2185
|
}
|
|
2105
2186
|
|
|
2106
2187
|
} else {
|
package/src/api/router.js
CHANGED
|
@@ -450,7 +450,7 @@ async function handleOauthPassthrough(req, res, opts = {}) {
|
|
|
450
450
|
try { reader.cancel(); } catch { /* already dead */ }
|
|
451
451
|
try { res.write(SSE_STALL_EVENT); } catch { /* client gone */ }
|
|
452
452
|
} finally {
|
|
453
|
-
try { reader.releaseLock(); } catch {}
|
|
453
|
+
try { reader.releaseLock(); } catch { /* lock already released */ }
|
|
454
454
|
}
|
|
455
455
|
res.end();
|
|
456
456
|
} else {
|
|
@@ -495,7 +495,7 @@ async function handleOauthPassthrough(req, res, opts = {}) {
|
|
|
495
495
|
const tier = opts.tier || {};
|
|
496
496
|
let parsedResponse = null;
|
|
497
497
|
if (contentType.includes("application/json")) {
|
|
498
|
-
try { parsedResponse = JSON.parse(responseTextForObservability); } catch {}
|
|
498
|
+
try { parsedResponse = JSON.parse(responseTextForObservability); } catch { /* not JSON — telemetry proceeds without a parsed body */ }
|
|
499
499
|
} else if (contentType.includes("text/event-stream")) {
|
|
500
500
|
// Extract a usable response object from the SSE stream by finding the
|
|
501
501
|
// final message_delta / message_stop events.
|
|
@@ -1027,7 +1027,10 @@ router.post("/v1/messages", rateLimiter, async (req, res, next) => {
|
|
|
1027
1027
|
try {
|
|
1028
1028
|
const { getMetricsCollector } = require("../observability/metrics");
|
|
1029
1029
|
getMetricsCollector().recordRequest("POST", "/v1/messages", null, null);
|
|
1030
|
-
} catch (
|
|
1030
|
+
} catch (err) {
|
|
1031
|
+
// If this fails silently, the wrap UI's "No requests tracked" bug returns.
|
|
1032
|
+
logger.debug({ err: err?.message }, "observability collector recordRequest failed");
|
|
1033
|
+
}
|
|
1031
1034
|
|
|
1032
1035
|
messagesRequestCount += 1;
|
|
1033
1036
|
|
package/src/cache/prompt.js
CHANGED
|
@@ -19,13 +19,6 @@ const fs = require("fs");
|
|
|
19
19
|
const config = require("../config");
|
|
20
20
|
const logger = require("../logger");
|
|
21
21
|
|
|
22
|
-
function cloneValue(value) {
|
|
23
|
-
if (typeof structuredClone === "function") {
|
|
24
|
-
return structuredClone(value);
|
|
25
|
-
}
|
|
26
|
-
return JSON.parse(JSON.stringify(value));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
22
|
function normaliseObject(value) {
|
|
30
23
|
if (value === null || typeof value !== "object") return value;
|
|
31
24
|
if (Array.isArray(value)) {
|
|
@@ -116,7 +116,7 @@ function convertAnthropicToBedrockFormat(body, modelFamily) {
|
|
|
116
116
|
},
|
|
117
117
|
};
|
|
118
118
|
|
|
119
|
-
case "llama":
|
|
119
|
+
case "llama": {
|
|
120
120
|
// Meta Llama format
|
|
121
121
|
const llamaPrompt = messagesToPrompt(body.messages);
|
|
122
122
|
return {
|
|
@@ -125,6 +125,7 @@ function convertAnthropicToBedrockFormat(body, modelFamily) {
|
|
|
125
125
|
temperature: body.temperature || 0.7,
|
|
126
126
|
top_p: body.top_p || 0.9,
|
|
127
127
|
};
|
|
128
|
+
}
|
|
128
129
|
|
|
129
130
|
case "jurassic":
|
|
130
131
|
// AI21 Jurassic format
|
|
@@ -279,7 +279,7 @@ class CodexProcess {
|
|
|
279
279
|
* Reject all pending requests
|
|
280
280
|
*/
|
|
281
281
|
_rejectAllPending(error) {
|
|
282
|
-
for (const [
|
|
282
|
+
for (const [_id, pending] of this.pendingRequests) {
|
|
283
283
|
clearTimeout(pending.timeout);
|
|
284
284
|
pending.reject(error);
|
|
285
285
|
}
|
|
@@ -165,7 +165,7 @@ async function performJsonRequest(url, { headers = {}, body, retryableStatusesOv
|
|
|
165
165
|
});
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
async function invokeDatabricks(body,
|
|
168
|
+
async function invokeDatabricks(body, _incomingHeaders = {}) {
|
|
169
169
|
if (!config.databricks?.url) {
|
|
170
170
|
throw new Error("Databricks configuration is missing required URL.");
|
|
171
171
|
}
|
|
@@ -423,12 +423,20 @@ function _liftLeakedThinkingBlocks(response) {
|
|
|
423
423
|
const newContent = [];
|
|
424
424
|
let lifted = 0;
|
|
425
425
|
for (const block of payload.content) {
|
|
426
|
-
if (block?.type === "text" && typeof block.text === "string" &&
|
|
426
|
+
if (block?.type === "text" && typeof block.text === "string" &&
|
|
427
|
+
(block.text.includes("<think>") || block.text.includes("</think>"))) {
|
|
427
428
|
const thoughts = [];
|
|
428
429
|
let m;
|
|
429
430
|
while ((m = thinkRegex.exec(block.text)) !== null) thoughts.push(m[1].trim());
|
|
430
431
|
thinkRegex.lastIndex = 0;
|
|
431
|
-
|
|
432
|
+
let cleaned = block.text.replace(thinkRegex, "").trim();
|
|
433
|
+
// Orphan closing tag: GLM-family templates open <think> in the prompt,
|
|
434
|
+
// so the completion may carry reasoning terminated by a bare </think>.
|
|
435
|
+
const orphanIdx = cleaned.indexOf("</think>");
|
|
436
|
+
if (orphanIdx !== -1) {
|
|
437
|
+
thoughts.push(cleaned.slice(0, orphanIdx).trim());
|
|
438
|
+
cleaned = cleaned.slice(orphanIdx + "</think>".length).trim();
|
|
439
|
+
}
|
|
432
440
|
const merged = thoughts.filter(Boolean).join("\n\n");
|
|
433
441
|
if (merged) {
|
|
434
442
|
newContent.push({ type: "thinking", thinking: merged });
|
|
@@ -489,7 +497,7 @@ async function _throwIfOllamaError(response, modelName) {
|
|
|
489
497
|
return response;
|
|
490
498
|
}
|
|
491
499
|
|
|
492
|
-
async function invokeOllama(body,
|
|
500
|
+
async function invokeOllama(body, _incomingHeaders = {}) {
|
|
493
501
|
if (!config.ollama?.endpoint) {
|
|
494
502
|
throw new Error("Ollama endpoint is not configured.");
|
|
495
503
|
}
|
|
@@ -754,7 +762,7 @@ async function invokeOllama(body, incomingHeaders = {}) {
|
|
|
754
762
|
);
|
|
755
763
|
}
|
|
756
764
|
|
|
757
|
-
async function invokeOpenRouter(body,
|
|
765
|
+
async function invokeOpenRouter(body, _incomingHeaders = {}) {
|
|
758
766
|
if (!config.openrouter?.endpoint || !config.openrouter?.apiKey) {
|
|
759
767
|
throw new Error("OpenRouter endpoint or API key is not configured.");
|
|
760
768
|
}
|
|
@@ -791,6 +799,13 @@ async function invokeOpenRouter(body, incomingHeaders = {}) {
|
|
|
791
799
|
stream: body.stream ?? false
|
|
792
800
|
};
|
|
793
801
|
|
|
802
|
+
// Issue #95: forward the Lynkr session id so OpenRouter's meta-routers
|
|
803
|
+
// (auto/pareto) can apply session stickiness — without it every call in a
|
|
804
|
+
// pinned session re-ranks fresh and one agentic task can span 6+ models.
|
|
805
|
+
if (body._sessionId) {
|
|
806
|
+
openRouterBody.session_id = body._sessionId;
|
|
807
|
+
}
|
|
808
|
+
|
|
794
809
|
// Add tools - inject standard tools if client didn't send any (passthrough mode)
|
|
795
810
|
let toolsToSend = body.tools;
|
|
796
811
|
let toolsInjected = false;
|
|
@@ -820,7 +835,7 @@ async function invokeOpenRouter(body, incomingHeaders = {}) {
|
|
|
820
835
|
// Eden AI is an OpenAI-compatible gateway (provider/model naming, EU/GDPR).
|
|
821
836
|
// It speaks the same wire format as OpenRouter, so this mirrors invokeOpenRouter
|
|
822
837
|
// and reuses the shared Anthropic<->OpenAI converters.
|
|
823
|
-
async function invokeEdenAI(body,
|
|
838
|
+
async function invokeEdenAI(body, _incomingHeaders = {}) {
|
|
824
839
|
if (!config.edenai?.endpoint || !config.edenai?.apiKey) {
|
|
825
840
|
throw new Error("Eden AI endpoint or API key is not configured.");
|
|
826
841
|
}
|
|
@@ -905,7 +920,7 @@ function derivePromptCacheKey(body) {
|
|
|
905
920
|
return "conv-" + crypto.createHash("sha1").update(text.slice(0, 4000)).digest("hex").slice(0, 32);
|
|
906
921
|
}
|
|
907
922
|
|
|
908
|
-
async function invokeAzureOpenAI(body,
|
|
923
|
+
async function invokeAzureOpenAI(body, _incomingHeaders = {}) {
|
|
909
924
|
if (!config.azureOpenAI?.endpoint || !config.azureOpenAI?.apiKey) {
|
|
910
925
|
throw new Error("Azure OpenAI endpoint or API key is not configured.");
|
|
911
926
|
}
|
|
@@ -1350,7 +1365,7 @@ async function invokeAzureOpenAI(body, incomingHeaders = {}) {
|
|
|
1350
1365
|
}
|
|
1351
1366
|
|
|
1352
1367
|
|
|
1353
|
-
async function invokeOpenAI(body,
|
|
1368
|
+
async function invokeOpenAI(body, _incomingHeaders = {}) {
|
|
1354
1369
|
if (!config.openai?.apiKey) {
|
|
1355
1370
|
throw new Error("OpenAI API key is not configured.");
|
|
1356
1371
|
}
|
|
@@ -1488,7 +1503,7 @@ async function invokeAtlas(body) {
|
|
|
1488
1503
|
}, "Atlas Cloud");
|
|
1489
1504
|
}
|
|
1490
1505
|
|
|
1491
|
-
async function invokeLlamaCpp(body,
|
|
1506
|
+
async function invokeLlamaCpp(body, _incomingHeaders = {}) {
|
|
1492
1507
|
if (!config.llamacpp?.endpoint) {
|
|
1493
1508
|
throw new Error("llama.cpp endpoint is not configured.");
|
|
1494
1509
|
}
|
|
@@ -1634,7 +1649,7 @@ async function invokeLlamaCpp(body, incomingHeaders = {}) {
|
|
|
1634
1649
|
return result;
|
|
1635
1650
|
}
|
|
1636
1651
|
|
|
1637
|
-
async function invokeLMStudio(body,
|
|
1652
|
+
async function invokeLMStudio(body, _incomingHeaders = {}) {
|
|
1638
1653
|
if (!config.lmstudio?.endpoint) {
|
|
1639
1654
|
throw new Error("LM Studio endpoint is not configured.");
|
|
1640
1655
|
}
|
|
@@ -1761,7 +1776,7 @@ function normalizeBodyForConverse(body) {
|
|
|
1761
1776
|
return normalized;
|
|
1762
1777
|
}
|
|
1763
1778
|
|
|
1764
|
-
async function invokeBedrock(body,
|
|
1779
|
+
async function invokeBedrock(body, _incomingHeaders = {}) {
|
|
1765
1780
|
if (!config.bedrock?.apiKey) {
|
|
1766
1781
|
throw new Error(
|
|
1767
1782
|
"AWS Bedrock requires AWS_BEDROCK_API_KEY (Bearer token). " +
|
|
@@ -1774,11 +1789,9 @@ async function invokeBedrock(body, incomingHeaders = {}) {
|
|
|
1774
1789
|
|
|
1775
1790
|
// Inject standard tools if needed
|
|
1776
1791
|
let toolsToSend = body.tools;
|
|
1777
|
-
let toolsInjected = false;
|
|
1778
1792
|
|
|
1779
1793
|
if (!Array.isArray(toolsToSend) || toolsToSend.length === 0) {
|
|
1780
1794
|
toolsToSend = STANDARD_TOOLS;
|
|
1781
|
-
toolsInjected = true;
|
|
1782
1795
|
logger.debug({
|
|
1783
1796
|
injectedToolCount: STANDARD_TOOLS.length,
|
|
1784
1797
|
injectedToolNames: STANDARD_TOOL_NAMES,
|
|
@@ -1947,7 +1960,7 @@ async function invokeBedrock(body, incomingHeaders = {}) {
|
|
|
1947
1960
|
* Z.AI offers GLM models through an Anthropic-compatible API at ~1/7 the cost.
|
|
1948
1961
|
* Minimal transformation needed - mostly passthrough with model mapping.
|
|
1949
1962
|
*/
|
|
1950
|
-
async function invokeZai(body,
|
|
1963
|
+
async function invokeZai(body, _incomingHeaders = {}) {
|
|
1951
1964
|
if (!config.zai?.apiKey) {
|
|
1952
1965
|
throw new Error("Z.AI API key is not configured. Set ZAI_API_KEY in your .env file.");
|
|
1953
1966
|
}
|
|
@@ -2152,7 +2165,7 @@ async function invokeZai(body, incomingHeaders = {}) {
|
|
|
2152
2165
|
* Moonshot offers Kimi models through an OpenAI-compatible chat completions API.
|
|
2153
2166
|
* Uses native system role support (unlike Z.AI which merges into user message).
|
|
2154
2167
|
*/
|
|
2155
|
-
async function invokeMoonshot(body,
|
|
2168
|
+
async function invokeMoonshot(body, _incomingHeaders = {}) {
|
|
2156
2169
|
if (!config.moonshot?.apiKey) {
|
|
2157
2170
|
throw new Error("Moonshot API key is not configured. Set MOONSHOT_API_KEY in your .env file.");
|
|
2158
2171
|
}
|
|
@@ -2314,7 +2327,7 @@ async function invokeMoonshot(body, incomingHeaders = {}) {
|
|
|
2314
2327
|
* modelMap and pinned-params logic (see Moonshot's kimi-k* precedent) once
|
|
2315
2328
|
* real traffic surfaces 400s.
|
|
2316
2329
|
*/
|
|
2317
|
-
async function invokeBaidu(body,
|
|
2330
|
+
async function invokeBaidu(body, _incomingHeaders = {}) {
|
|
2318
2331
|
if (!config.baidu?.apiKey) {
|
|
2319
2332
|
throw new Error("Baidu API key is not configured. Set BAIDU_API_KEY in your .env file.");
|
|
2320
2333
|
}
|
|
@@ -2450,12 +2463,34 @@ function convertOpenAIToAnthropic(response) {
|
|
|
2450
2463
|
const hasToolCalls = Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
|
|
2451
2464
|
|
|
2452
2465
|
// Extract text content and reasoning from thinking models
|
|
2453
|
-
|
|
2466
|
+
let textContent = typeof message.content === 'string' ? message.content : '';
|
|
2454
2467
|
const reasoningContent = typeof message.reasoning_content === 'string' ? message.reasoning_content : '';
|
|
2455
2468
|
|
|
2456
|
-
//
|
|
2457
|
-
|
|
2458
|
-
|
|
2469
|
+
// GLM-family models leak reasoning into `content` in two shapes:
|
|
2470
|
+
// 1. matched <think>...</think> pairs;
|
|
2471
|
+
// 2. an ORPHAN closing </think> — GLM's chat template opens <think>
|
|
2472
|
+
// inside the prompt, so the completion starts mid-reasoning and only
|
|
2473
|
+
// emits the closer (observed live 2026-08-23, baidu/glm-5.2:
|
|
2474
|
+
// "Hi. What need?</think>Hi. What need?" reached the client verbatim).
|
|
2475
|
+
let leakedThinking = '';
|
|
2476
|
+
if (textContent.includes('</think>')) {
|
|
2477
|
+
if (textContent.includes('<think>')) {
|
|
2478
|
+
const thoughts = [];
|
|
2479
|
+
textContent = textContent
|
|
2480
|
+
.replace(/<think>([\s\S]*?)<\/think>/g, (_, t) => { thoughts.push(t.trim()); return ''; })
|
|
2481
|
+
.trim();
|
|
2482
|
+
leakedThinking = thoughts.filter(Boolean).join('\n\n');
|
|
2483
|
+
} else {
|
|
2484
|
+
const idx = textContent.indexOf('</think>');
|
|
2485
|
+
leakedThinking = textContent.slice(0, idx).trim();
|
|
2486
|
+
textContent = textContent.slice(idx + '</think>'.length).trim();
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
// Emit reasoning (side-channel and/or leaked) as a proper thinking block
|
|
2491
|
+
const combinedThinking = [reasoningContent, leakedThinking].filter(Boolean).join('\n\n');
|
|
2492
|
+
if (combinedThinking) {
|
|
2493
|
+
content.push({ type: "thinking", thinking: combinedThinking });
|
|
2459
2494
|
}
|
|
2460
2495
|
|
|
2461
2496
|
if (textContent) {
|
|
@@ -2551,7 +2586,7 @@ function sanitizeSchemaForGemini(schema) {
|
|
|
2551
2586
|
* (generativelanguage.googleapis.com), not Google Cloud Vertex AI.
|
|
2552
2587
|
* Converts Anthropic format to Gemini format and back.
|
|
2553
2588
|
*/
|
|
2554
|
-
async function invokeVertex(body,
|
|
2589
|
+
async function invokeVertex(body, _incomingHeaders = {}) {
|
|
2555
2590
|
const apiKey = config.vertex?.apiKey;
|
|
2556
2591
|
|
|
2557
2592
|
if (!apiKey) {
|
|
@@ -2796,7 +2831,7 @@ function convertGeminiToAnthropic(response, requestedModel) {
|
|
|
2796
2831
|
};
|
|
2797
2832
|
}
|
|
2798
2833
|
|
|
2799
|
-
async function invokeCodex(body,
|
|
2834
|
+
async function invokeCodex(body, _incomingHeaders = {}) {
|
|
2800
2835
|
const { getCodexProcess } = require("./codex-process");
|
|
2801
2836
|
const { convertAnthropicToCodexPrompt, convertCodexResponseToAnthropic } = require("./codex-utils");
|
|
2802
2837
|
|
|
@@ -2920,7 +2955,6 @@ const LYNKR_BADGE_PREFIX_RE = /^\*\[Lynkr\][^*\n]*\*\s*/;
|
|
|
2920
2955
|
function stripLynkrBadges(messages) {
|
|
2921
2956
|
if (!Array.isArray(messages)) return messages;
|
|
2922
2957
|
let mutated = false;
|
|
2923
|
-
let badgeCount = 0;
|
|
2924
2958
|
const out = messages.map((msg) => {
|
|
2925
2959
|
if (msg?.role !== 'assistant') return msg;
|
|
2926
2960
|
|
|
@@ -2931,7 +2965,6 @@ function stripLynkrBadges(messages) {
|
|
|
2931
2965
|
if (!LYNKR_BADGE_PREFIX_RE.test(msg.content)) return msg;
|
|
2932
2966
|
const stripped = msg.content.replace(LYNKR_BADGE_PREFIX_RE, '');
|
|
2933
2967
|
mutated = true;
|
|
2934
|
-
badgeCount++;
|
|
2935
2968
|
// Badge-only content must not become an empty string — Anthropic
|
|
2936
2969
|
// rejects empty assistant content (this is the interrupted-response
|
|
2937
2970
|
// continuation case, where the partial text WAS just the badge).
|
|
@@ -2950,7 +2983,6 @@ function stripLynkrBadges(messages) {
|
|
|
2950
2983
|
for (const b of msg.content) {
|
|
2951
2984
|
if (b?.type === 'text' && typeof b.text === 'string' && LYNKR_BADGE_PREFIX_RE.test(b.text)) {
|
|
2952
2985
|
changed = true;
|
|
2953
|
-
badgeCount++;
|
|
2954
2986
|
const strippedText = b.text.replace(LYNKR_BADGE_PREFIX_RE, '');
|
|
2955
2987
|
if (strippedText.trim()) rebuilt.push({ ...b, text: strippedText });
|
|
2956
2988
|
// badge-only block → drop
|
|
@@ -3853,6 +3885,8 @@ function destroyHttpAgents() {
|
|
|
3853
3885
|
|
|
3854
3886
|
module.exports = {
|
|
3855
3887
|
invokeModel,
|
|
3888
|
+
computeCostUsd,
|
|
3889
|
+
convertOpenAIToAnthropic,
|
|
3856
3890
|
invokeAzureAnthropic,
|
|
3857
3891
|
invokeZai,
|
|
3858
3892
|
invokeOllama,
|