lynkr 9.14.1 → 9.14.3
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 +5 -1
- package/bin/cli.js +14 -6
- package/bin/lynkr-desktop-token.js +108 -0
- package/bin/lynkr-init.js +13 -1
- package/bin/lynkr-restart.js +125 -0
- package/bin/opencode-setup.js +175 -0
- package/bin/run.js +92 -0
- package/config/model-capabilities.json +20 -0
- package/config/model-capability-seeds.json +75 -0
- package/config/model-tiers.json +13 -1
- package/package.json +16 -22
- package/scripts/claude-desktop.js +212 -0
- package/scripts/ope-report.js +78 -0
- package/scripts/seed-capabilities.js +230 -0
- package/src/api/claude-desktop-gateway.js +123 -0
- package/src/api/health.js +0 -1
- package/src/api/mcp-broker.js +149 -0
- package/src/api/middleware/budget-enforcer.js +77 -7
- package/src/api/middleware/budget.js +36 -2
- package/src/api/middleware/session.js +0 -0
- package/src/api/openai-router.js +210 -15
- package/src/api/providers-handler.js +16 -1
- package/src/api/router.js +73 -106
- package/src/auth-mode.js +40 -4
- package/src/budget/index.js +86 -0
- package/src/cache/embeddings.js +125 -35
- package/src/cache/onnx-embedder.js +92 -0
- 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 +337 -46
- package/src/clients/gpt-utils.js +84 -8
- package/src/clients/health-probe.js +157 -0
- package/src/clients/openai-format.js +73 -23
- package/src/clients/provider-capabilities.js +27 -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 +13 -2
- package/src/context/budget.js +1 -1
- package/src/context/compression.js +127 -19
- package/src/context/tool-result-compressor.js +3 -5
- package/src/dashboard/api.js +1 -1
- 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 +52 -3
- package/src/observability/otel.js +180 -0
- package/src/orchestrator/azure-responses-sse.js +223 -0
- package/src/orchestrator/index.js +216 -212
- package/src/orchestrator/passthrough-stream.js +7 -1
- package/src/orchestrator/sse-transformer.js +130 -13
- package/src/policy/safe-commands.js +0 -1
- package/src/routing/bandit.js +39 -0
- package/src/routing/calibration.js +21 -1
- package/src/routing/capabilities.js +88 -0
- package/src/routing/capability-seeds/benchmarks-dir.js +58 -0
- package/src/routing/capability-seeds/family-heuristics.js +77 -0
- package/src/routing/capability-seeds/family.js +105 -0
- package/src/routing/capability-seeds/models-dev.js +64 -0
- package/src/routing/capability-seeds/normalize.js +160 -0
- package/src/routing/capability-seeds/registry.js +125 -0
- package/src/routing/capability-seeds/sources.js +69 -0
- package/src/routing/capability-seeds/swebench.js +66 -0
- 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/decide.js +149 -0
- package/src/routing/deescalator.js +38 -1
- package/src/routing/embedding-cache.js +0 -1
- package/src/routing/index.js +202 -86
- 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/model-registry.js +25 -0
- package/src/routing/model-slots.js +65 -0
- package/src/routing/model-tiers.js +2 -0
- package/src/routing/ope.js +255 -0
- package/src/routing/openai-model-slots.js +82 -0
- package/src/routing/regret-estimator.js +1 -1
- package/src/routing/shortfall.js +290 -0
- package/src/routing/stuck-detector.js +127 -0
- package/src/routing/telemetry.js +11 -2
- package/src/server.js +41 -3
- package/src/stores/file-store.js +5 -1
- package/src/tools/web-search-exec.js +182 -0
- 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
|
@@ -77,11 +77,26 @@ function _stripInternalFields(body) {
|
|
|
77
77
|
return cleaned || body;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
async function performJsonRequest(url, { headers = {}, body, retryableStatusesOverride, maxRetriesOverride }, providerLabel) {
|
|
80
|
+
async function performJsonRequest(url, { headers = {}, body, retryableStatusesOverride, maxRetriesOverride, timeoutMs }, providerLabel) {
|
|
81
81
|
const agent = url.startsWith('https:') ? httpsAgent : httpAgent;
|
|
82
82
|
body = _stripInternalFields(body);
|
|
83
83
|
const isStreaming = body.stream === true;
|
|
84
84
|
|
|
85
|
+
// Optional per-call abort timeout (opt-in via timeoutMs) — most callers
|
|
86
|
+
// omit it and keep the historical unbounded-fetch behavior. Added for
|
|
87
|
+
// llama.cpp (see invokeLlamaCpp): a dead upstream that accepts the TCP
|
|
88
|
+
// connection but never answers at the application layer hangs fetch()
|
|
89
|
+
// indefinitely — live-observed at ~75s, riding an incidental OS/tunnel
|
|
90
|
+
// socket timeout rather than any deliberate bound. This makes the
|
|
91
|
+
// already-configured (but previously unused) LLAMACPP_TIMEOUT_MS real.
|
|
92
|
+
// A fresh signal is created per attempt below (not hoisted here) —
|
|
93
|
+
// AbortSignal.timeout()'s clock starts at construction, so one signal
|
|
94
|
+
// shared across withRetry's attempts would leave later retries with a
|
|
95
|
+
// shrinking (or already-expired) budget instead of a full timeout each.
|
|
96
|
+
const makeSignal = () => (typeof timeoutMs === "number" && timeoutMs > 0
|
|
97
|
+
? AbortSignal.timeout(timeoutMs)
|
|
98
|
+
: undefined);
|
|
99
|
+
|
|
85
100
|
// Streaming requests can't be retried, so handle them directly
|
|
86
101
|
if (isStreaming) {
|
|
87
102
|
const response = await fetch(url, {
|
|
@@ -89,6 +104,7 @@ async function performJsonRequest(url, { headers = {}, body, retryableStatusesOv
|
|
|
89
104
|
headers,
|
|
90
105
|
body: JSON.stringify(body),
|
|
91
106
|
agent,
|
|
107
|
+
signal: makeSignal(),
|
|
92
108
|
});
|
|
93
109
|
|
|
94
110
|
logger.debug({
|
|
@@ -122,6 +138,7 @@ async function performJsonRequest(url, { headers = {}, body, retryableStatusesOv
|
|
|
122
138
|
headers,
|
|
123
139
|
body: JSON.stringify(body),
|
|
124
140
|
agent,
|
|
141
|
+
signal: makeSignal(),
|
|
125
142
|
});
|
|
126
143
|
const text = await response.text();
|
|
127
144
|
|
|
@@ -165,7 +182,7 @@ async function performJsonRequest(url, { headers = {}, body, retryableStatusesOv
|
|
|
165
182
|
});
|
|
166
183
|
}
|
|
167
184
|
|
|
168
|
-
async function invokeDatabricks(body,
|
|
185
|
+
async function invokeDatabricks(body, _incomingHeaders = {}) {
|
|
169
186
|
if (!config.databricks?.url) {
|
|
170
187
|
throw new Error("Databricks configuration is missing required URL.");
|
|
171
188
|
}
|
|
@@ -423,12 +440,20 @@ function _liftLeakedThinkingBlocks(response) {
|
|
|
423
440
|
const newContent = [];
|
|
424
441
|
let lifted = 0;
|
|
425
442
|
for (const block of payload.content) {
|
|
426
|
-
if (block?.type === "text" && typeof block.text === "string" &&
|
|
443
|
+
if (block?.type === "text" && typeof block.text === "string" &&
|
|
444
|
+
(block.text.includes("<think>") || block.text.includes("</think>"))) {
|
|
427
445
|
const thoughts = [];
|
|
428
446
|
let m;
|
|
429
447
|
while ((m = thinkRegex.exec(block.text)) !== null) thoughts.push(m[1].trim());
|
|
430
448
|
thinkRegex.lastIndex = 0;
|
|
431
|
-
|
|
449
|
+
let cleaned = block.text.replace(thinkRegex, "").trim();
|
|
450
|
+
// Orphan closing tag: GLM-family templates open <think> in the prompt,
|
|
451
|
+
// so the completion may carry reasoning terminated by a bare </think>.
|
|
452
|
+
const orphanIdx = cleaned.indexOf("</think>");
|
|
453
|
+
if (orphanIdx !== -1) {
|
|
454
|
+
thoughts.push(cleaned.slice(0, orphanIdx).trim());
|
|
455
|
+
cleaned = cleaned.slice(orphanIdx + "</think>".length).trim();
|
|
456
|
+
}
|
|
432
457
|
const merged = thoughts.filter(Boolean).join("\n\n");
|
|
433
458
|
if (merged) {
|
|
434
459
|
newContent.push({ type: "thinking", thinking: merged });
|
|
@@ -489,7 +514,7 @@ async function _throwIfOllamaError(response, modelName) {
|
|
|
489
514
|
return response;
|
|
490
515
|
}
|
|
491
516
|
|
|
492
|
-
async function invokeOllama(body,
|
|
517
|
+
async function invokeOllama(body, _incomingHeaders = {}) {
|
|
493
518
|
if (!config.ollama?.endpoint) {
|
|
494
519
|
throw new Error("Ollama endpoint is not configured.");
|
|
495
520
|
}
|
|
@@ -754,7 +779,7 @@ async function invokeOllama(body, incomingHeaders = {}) {
|
|
|
754
779
|
);
|
|
755
780
|
}
|
|
756
781
|
|
|
757
|
-
async function invokeOpenRouter(body,
|
|
782
|
+
async function invokeOpenRouter(body, _incomingHeaders = {}) {
|
|
758
783
|
if (!config.openrouter?.endpoint || !config.openrouter?.apiKey) {
|
|
759
784
|
throw new Error("OpenRouter endpoint or API key is not configured.");
|
|
760
785
|
}
|
|
@@ -821,13 +846,27 @@ async function invokeOpenRouter(body, incomingHeaders = {}) {
|
|
|
821
846
|
}, "Sending tools to OpenRouter");
|
|
822
847
|
}
|
|
823
848
|
|
|
824
|
-
return
|
|
849
|
+
// Same pre-return 429 check as invokeMoonshot/invokeBaidu — see the comment
|
|
850
|
+
// on invokeOpenAI's equivalent check for why this matters for streaming.
|
|
851
|
+
const response = await performJsonRequest(endpoint, {
|
|
852
|
+
headers,
|
|
853
|
+
body: openRouterBody,
|
|
854
|
+
retryableStatusesOverride: [500, 502, 503, 504],
|
|
855
|
+
}, "OpenRouter");
|
|
856
|
+
|
|
857
|
+
if (!response.ok && response.status === 429) {
|
|
858
|
+
const err = new Error(`OpenRouter rate-limited: ${String(response.json?.error?.message || '').slice(0, 120)}`);
|
|
859
|
+
err.status = 429;
|
|
860
|
+
throw err;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
return response;
|
|
825
864
|
}
|
|
826
865
|
|
|
827
866
|
// Eden AI is an OpenAI-compatible gateway (provider/model naming, EU/GDPR).
|
|
828
867
|
// It speaks the same wire format as OpenRouter, so this mirrors invokeOpenRouter
|
|
829
868
|
// and reuses the shared Anthropic<->OpenAI converters.
|
|
830
|
-
async function invokeEdenAI(body,
|
|
869
|
+
async function invokeEdenAI(body, _incomingHeaders = {}) {
|
|
831
870
|
if (!config.edenai?.endpoint || !config.edenai?.apiKey) {
|
|
832
871
|
throw new Error("Eden AI endpoint or API key is not configured.");
|
|
833
872
|
}
|
|
@@ -912,7 +951,7 @@ function derivePromptCacheKey(body) {
|
|
|
912
951
|
return "conv-" + crypto.createHash("sha1").update(text.slice(0, 4000)).digest("hex").slice(0, 32);
|
|
913
952
|
}
|
|
914
953
|
|
|
915
|
-
async function invokeAzureOpenAI(body,
|
|
954
|
+
async function invokeAzureOpenAI(body, _incomingHeaders = {}) {
|
|
916
955
|
if (!config.azureOpenAI?.endpoint || !config.azureOpenAI?.apiKey) {
|
|
917
956
|
throw new Error("Azure OpenAI endpoint or API key is not configured.");
|
|
918
957
|
}
|
|
@@ -1077,7 +1116,7 @@ async function invokeAzureOpenAI(body, incomingHeaders = {}) {
|
|
|
1077
1116
|
originalMessageCount: (body.messages || []).length,
|
|
1078
1117
|
convertedMessageCount: azureBody.messages.length,
|
|
1079
1118
|
messageRoles: (body.messages || []).map(m => m.role),
|
|
1080
|
-
}, "
|
|
1119
|
+
}, "Continuation request — system prompt PRESERVED, system-reminder blocks stripped uniformly (the filter trigger was the reminders, not the prompt; see system-message handling below)");
|
|
1081
1120
|
} else {
|
|
1082
1121
|
logger.debug({
|
|
1083
1122
|
hasToolResults: false,
|
|
@@ -1221,17 +1260,34 @@ async function invokeAzureOpenAI(body, incomingHeaders = {}) {
|
|
|
1221
1260
|
// the conversation's first user message — stable across every turn of
|
|
1222
1261
|
// the same task. Keep per-key traffic under ~15 req/min.
|
|
1223
1262
|
...(isGpt5 ? { prompt_cache_key: derivePromptCacheKey(body) } : {}),
|
|
1224
|
-
stream: false
|
|
1263
|
+
stream: body.stream ?? false
|
|
1225
1264
|
};
|
|
1226
1265
|
logger.debug({
|
|
1227
1266
|
format: "responses",
|
|
1228
1267
|
inputCount: responsesBody.input?.length,
|
|
1229
1268
|
model: responsesBody.model,
|
|
1230
|
-
hasTools: !!responsesBody.tools
|
|
1269
|
+
hasTools: !!responsesBody.tools,
|
|
1270
|
+
streaming: responsesBody.stream,
|
|
1231
1271
|
}, "Using Responses API format");
|
|
1232
1272
|
|
|
1233
1273
|
const result = await performJsonRequest(endpoint, { headers, body: responsesBody }, "Azure OpenAI Responses");
|
|
1234
1274
|
|
|
1275
|
+
// Streaming: hand back a synthetic OpenAI-Chat-Completions-shaped SSE
|
|
1276
|
+
// stream (see src/orchestrator/azure-responses-sse.js for why that shape
|
|
1277
|
+
// rather than Anthropic SSE directly) so the orchestrator's existing
|
|
1278
|
+
// sseTransform.openaiToAnthropicSSE call site — already wired for
|
|
1279
|
+
// moonshot/baidu/openai's raw streams — handles this one identically,
|
|
1280
|
+
// with zero changes needed there. Skips the whole buffered
|
|
1281
|
+
// output-array/dedup/usage-remap conversion below entirely; that logic
|
|
1282
|
+
// only makes sense for a complete, already-materialized response.
|
|
1283
|
+
if (result.stream) {
|
|
1284
|
+
const { azureResponsesToOpenAIChunks } = require("../orchestrator/azure-responses-sse");
|
|
1285
|
+
return {
|
|
1286
|
+
...result,
|
|
1287
|
+
stream: azureResponsesToOpenAIChunks(result.stream, { model: responsesBody.model }),
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1235
1291
|
// Convert Responses API response to Chat Completions format
|
|
1236
1292
|
if (result.ok && result.json?.output) {
|
|
1237
1293
|
const outputArray = result.json.output || [];
|
|
@@ -1298,6 +1354,8 @@ async function invokeAzureOpenAI(body, incomingHeaders = {}) {
|
|
|
1298
1354
|
toolCallNames: toolCalls.map(tc => tc.function.name)
|
|
1299
1355
|
}, "Parsing Responses API output");
|
|
1300
1356
|
|
|
1357
|
+
const _rawUsage = result.json.usage;
|
|
1358
|
+
|
|
1301
1359
|
result.json = {
|
|
1302
1360
|
id: result.json.id,
|
|
1303
1361
|
object: "chat.completion",
|
|
@@ -1314,15 +1372,15 @@ async function invokeAzureOpenAI(body, incomingHeaders = {}) {
|
|
|
1314
1372
|
}],
|
|
1315
1373
|
// Responses API usage is {input,output}_tokens; downstream reads
|
|
1316
1374
|
// Chat Completions' {prompt,completion}_tokens.
|
|
1317
|
-
usage:
|
|
1318
|
-
prompt_tokens:
|
|
1319
|
-
completion_tokens:
|
|
1320
|
-
total_tokens:
|
|
1321
|
-
?? ((
|
|
1375
|
+
usage: _rawUsage ? {
|
|
1376
|
+
prompt_tokens: _rawUsage.prompt_tokens ?? _rawUsage.input_tokens ?? 0,
|
|
1377
|
+
completion_tokens: _rawUsage.completion_tokens ?? _rawUsage.output_tokens ?? 0,
|
|
1378
|
+
total_tokens: _rawUsage.total_tokens
|
|
1379
|
+
?? ((_rawUsage.input_tokens ?? 0) + (_rawUsage.output_tokens ?? 0)),
|
|
1322
1380
|
// Provider-side prompt-cache hits (Responses API: input_tokens_details,
|
|
1323
1381
|
// Chat Completions: prompt_tokens_details) — telemetry reads this name.
|
|
1324
|
-
cache_read_input_tokens:
|
|
1325
|
-
??
|
|
1382
|
+
cache_read_input_tokens: _rawUsage.input_tokens_details?.cached_tokens
|
|
1383
|
+
?? _rawUsage.prompt_tokens_details?.cached_tokens
|
|
1326
1384
|
?? null,
|
|
1327
1385
|
} : undefined
|
|
1328
1386
|
};
|
|
@@ -1357,7 +1415,7 @@ async function invokeAzureOpenAI(body, incomingHeaders = {}) {
|
|
|
1357
1415
|
}
|
|
1358
1416
|
|
|
1359
1417
|
|
|
1360
|
-
async function invokeOpenAI(body,
|
|
1418
|
+
async function invokeOpenAI(body, _incomingHeaders = {}) {
|
|
1361
1419
|
if (!config.openai?.apiKey) {
|
|
1362
1420
|
throw new Error("OpenAI API key is not configured.");
|
|
1363
1421
|
}
|
|
@@ -1429,7 +1487,23 @@ async function invokeOpenAI(body, incomingHeaders = {}) {
|
|
|
1429
1487
|
max_tokens: openAIBody.max_tokens,
|
|
1430
1488
|
}, "=== OPENAI REQUEST ===");
|
|
1431
1489
|
|
|
1432
|
-
return
|
|
1490
|
+
// Same pre-return 429 check as invokeMoonshot/invokeBaidu: performJsonRequest's
|
|
1491
|
+
// streaming branch never throws on a bad status (retry is skipped entirely for
|
|
1492
|
+
// stream:true), so without this a 429 was silently handed back as a "successful"
|
|
1493
|
+
// stream/response and tier-fallback never got a chance to climb.
|
|
1494
|
+
const response = await performJsonRequest(endpoint, {
|
|
1495
|
+
headers,
|
|
1496
|
+
body: openAIBody,
|
|
1497
|
+
retryableStatusesOverride: [500, 502, 503, 504],
|
|
1498
|
+
}, "OpenAI");
|
|
1499
|
+
|
|
1500
|
+
if (!response.ok && response.status === 429) {
|
|
1501
|
+
const err = new Error(`OpenAI rate-limited: ${String(response.json?.error?.message || '').slice(0, 120)}`);
|
|
1502
|
+
err.status = 429;
|
|
1503
|
+
throw err;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
return response;
|
|
1433
1507
|
}
|
|
1434
1508
|
|
|
1435
1509
|
async function invokeAtlas(body) {
|
|
@@ -1487,15 +1561,29 @@ async function invokeAtlas(body) {
|
|
|
1487
1561
|
|
|
1488
1562
|
// Chat completions are billable POSTs, so Atlas requests are never replayed
|
|
1489
1563
|
// automatically. Callers can retry explicitly with their own idempotency policy.
|
|
1490
|
-
|
|
1564
|
+
// maxRetriesOverride/retryableStatusesOverride stay at 0/[] — this does NOT
|
|
1565
|
+
// retry the Atlas request itself, it only decides whether invokeModel's
|
|
1566
|
+
// caller escalates to a DIFFERENT provider/tier on 429, same as
|
|
1567
|
+
// invokeMoonshot/invokeBaidu/invokeOpenAI/invokeOpenRouter already do.
|
|
1568
|
+
// Without this, a 429 here was silently handed back as a "successful"
|
|
1569
|
+
// response and tier-fallback never got a chance to climb.
|
|
1570
|
+
const response = await performJsonRequest(endpoint, {
|
|
1491
1571
|
headers,
|
|
1492
1572
|
body: atlasBody,
|
|
1493
1573
|
maxRetriesOverride: 0,
|
|
1494
1574
|
retryableStatusesOverride: [],
|
|
1495
1575
|
}, "Atlas Cloud");
|
|
1576
|
+
|
|
1577
|
+
if (!response.ok && response.status === 429) {
|
|
1578
|
+
const err = new Error(`Atlas Cloud rate-limited: ${String(response.json?.error?.message || '').slice(0, 120)}`);
|
|
1579
|
+
err.status = 429;
|
|
1580
|
+
throw err;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
return response;
|
|
1496
1584
|
}
|
|
1497
1585
|
|
|
1498
|
-
async function invokeLlamaCpp(body,
|
|
1586
|
+
async function invokeLlamaCpp(body, _incomingHeaders = {}) {
|
|
1499
1587
|
if (!config.llamacpp?.endpoint) {
|
|
1500
1588
|
throw new Error("llama.cpp endpoint is not configured.");
|
|
1501
1589
|
}
|
|
@@ -1572,10 +1660,18 @@ async function invokeLlamaCpp(body, incomingHeaders = {}) {
|
|
|
1572
1660
|
}, 'llama.cpp: Removed consecutive duplicate roles from message sequence');
|
|
1573
1661
|
}
|
|
1574
1662
|
|
|
1663
|
+
// max_tokens floor: reasoning-capable local models (live-confirmed on this
|
|
1664
|
+
// deployment's GPT-OSS build) can spend their entire budget on
|
|
1665
|
+
// `reasoning_content` before writing any visible answer — a caller-
|
|
1666
|
+
// supplied tiny budget (e.g. the caveman probe's max_tokens:1 seen in
|
|
1667
|
+
// production logs) then returns finish_reason:"length" with zero visible
|
|
1668
|
+
// text. Same fix already shipped for Ollama as LYNKR_OLLAMA_MIN_MAX_TOKENS;
|
|
1669
|
+
// mirrored here rather than inventing a different mechanism.
|
|
1670
|
+
const llamacppMinMaxTokens = Number(process.env.LYNKR_LLAMACPP_MIN_MAX_TOKENS) || 1536;
|
|
1575
1671
|
const llamacppBody = {
|
|
1576
1672
|
messages: deduplicated,
|
|
1577
1673
|
temperature: body.temperature ?? 0.7,
|
|
1578
|
-
max_tokens: body.max_tokens ?? 16384,
|
|
1674
|
+
max_tokens: Math.max(body.max_tokens ?? 16384, llamacppMinMaxTokens),
|
|
1579
1675
|
top_p: body.top_p ?? 1.0,
|
|
1580
1676
|
stream: body.stream ?? false
|
|
1581
1677
|
};
|
|
@@ -1625,7 +1721,11 @@ async function invokeLlamaCpp(body, incomingHeaders = {}) {
|
|
|
1625
1721
|
}))
|
|
1626
1722
|
}, "=== LLAMA.CPP REQUEST ===");
|
|
1627
1723
|
|
|
1628
|
-
const result = await performJsonRequest(
|
|
1724
|
+
const result = await performJsonRequest(
|
|
1725
|
+
endpoint,
|
|
1726
|
+
{ headers, body: llamacppBody, timeoutMs: config.llamacpp.timeout },
|
|
1727
|
+
"llama.cpp",
|
|
1728
|
+
);
|
|
1629
1729
|
|
|
1630
1730
|
// Context overflow is a CAPACITY error, not a request error: the local
|
|
1631
1731
|
// model's slot is simply too small for this conversation (llama-server
|
|
@@ -1641,7 +1741,7 @@ async function invokeLlamaCpp(body, incomingHeaders = {}) {
|
|
|
1641
1741
|
return result;
|
|
1642
1742
|
}
|
|
1643
1743
|
|
|
1644
|
-
async function invokeLMStudio(body,
|
|
1744
|
+
async function invokeLMStudio(body, _incomingHeaders = {}) {
|
|
1645
1745
|
if (!config.lmstudio?.endpoint) {
|
|
1646
1746
|
throw new Error("LM Studio endpoint is not configured.");
|
|
1647
1747
|
}
|
|
@@ -1768,7 +1868,7 @@ function normalizeBodyForConverse(body) {
|
|
|
1768
1868
|
return normalized;
|
|
1769
1869
|
}
|
|
1770
1870
|
|
|
1771
|
-
async function invokeBedrock(body,
|
|
1871
|
+
async function invokeBedrock(body, _incomingHeaders = {}) {
|
|
1772
1872
|
if (!config.bedrock?.apiKey) {
|
|
1773
1873
|
throw new Error(
|
|
1774
1874
|
"AWS Bedrock requires AWS_BEDROCK_API_KEY (Bearer token). " +
|
|
@@ -1781,11 +1881,9 @@ async function invokeBedrock(body, incomingHeaders = {}) {
|
|
|
1781
1881
|
|
|
1782
1882
|
// Inject standard tools if needed
|
|
1783
1883
|
let toolsToSend = body.tools;
|
|
1784
|
-
let toolsInjected = false;
|
|
1785
1884
|
|
|
1786
1885
|
if (!Array.isArray(toolsToSend) || toolsToSend.length === 0) {
|
|
1787
1886
|
toolsToSend = STANDARD_TOOLS;
|
|
1788
|
-
toolsInjected = true;
|
|
1789
1887
|
logger.debug({
|
|
1790
1888
|
injectedToolCount: STANDARD_TOOLS.length,
|
|
1791
1889
|
injectedToolNames: STANDARD_TOOL_NAMES,
|
|
@@ -1954,7 +2052,7 @@ async function invokeBedrock(body, incomingHeaders = {}) {
|
|
|
1954
2052
|
* Z.AI offers GLM models through an Anthropic-compatible API at ~1/7 the cost.
|
|
1955
2053
|
* Minimal transformation needed - mostly passthrough with model mapping.
|
|
1956
2054
|
*/
|
|
1957
|
-
async function invokeZai(body,
|
|
2055
|
+
async function invokeZai(body, _incomingHeaders = {}) {
|
|
1958
2056
|
if (!config.zai?.apiKey) {
|
|
1959
2057
|
throw new Error("Z.AI API key is not configured. Set ZAI_API_KEY in your .env file.");
|
|
1960
2058
|
}
|
|
@@ -2057,11 +2155,23 @@ async function invokeZai(body, incomingHeaders = {}) {
|
|
|
2057
2155
|
zaiBody = { ...body };
|
|
2058
2156
|
zaiBody.model = mappedModel;
|
|
2059
2157
|
|
|
2060
|
-
//
|
|
2061
|
-
//
|
|
2062
|
-
//
|
|
2063
|
-
//
|
|
2064
|
-
|
|
2158
|
+
// Honor body.stream as-is here. The ONLY caller that ever sets it true
|
|
2159
|
+
// for this branch is passthrough-stream.js's handleNativeStream — zai
|
|
2160
|
+
// isn't in sse-transformer.js's DEFAULT_OPENAI_SSE_PROVIDERS, so the
|
|
2161
|
+
// normal buffered orchestrator (orchestrator/index.js) always sets
|
|
2162
|
+
// cleanPayload.stream=false before invoking any provider not on that
|
|
2163
|
+
// allowlist, and never reaches this branch with stream:true itself.
|
|
2164
|
+
// Forcing false unconditionally (as this used to do) broke exactly that
|
|
2165
|
+
// caller: _anthropicNative() advertises this endpoint as native-passthrough
|
|
2166
|
+
// eligible, handleNativeStream sets stream:true and expects real
|
|
2167
|
+
// Anthropic SSE bytes back, but got a buffered JSON body instead —
|
|
2168
|
+
// handleNativeStream then saw no `.stream` field, silently gave up, and
|
|
2169
|
+
// fell back to the buffered orchestrator path, which called zai AGAIN.
|
|
2170
|
+
// Every native-passthrough request to this endpoint was double-invoking
|
|
2171
|
+
// the real API. If a future caller other than handleNativeStream ever
|
|
2172
|
+
// needs stream:true routed through the OpenAI-SSE transformer instead,
|
|
2173
|
+
// that's a different bug — add zai to DEFAULT_OPENAI_SSE_PROVIDERS then,
|
|
2174
|
+
// don't reintroduce this override.
|
|
2065
2175
|
|
|
2066
2176
|
// Inject standard tools if client didn't send any (passthrough mode)
|
|
2067
2177
|
if (!Array.isArray(zaiBody.tools) || zaiBody.tools.length === 0) {
|
|
@@ -2159,7 +2269,7 @@ async function invokeZai(body, incomingHeaders = {}) {
|
|
|
2159
2269
|
* Moonshot offers Kimi models through an OpenAI-compatible chat completions API.
|
|
2160
2270
|
* Uses native system role support (unlike Z.AI which merges into user message).
|
|
2161
2271
|
*/
|
|
2162
|
-
async function invokeMoonshot(body,
|
|
2272
|
+
async function invokeMoonshot(body, _incomingHeaders = {}) {
|
|
2163
2273
|
if (!config.moonshot?.apiKey) {
|
|
2164
2274
|
throw new Error("Moonshot API key is not configured. Set MOONSHOT_API_KEY in your .env file.");
|
|
2165
2275
|
}
|
|
@@ -2216,12 +2326,18 @@ async function invokeMoonshot(body, incomingHeaders = {}) {
|
|
|
2216
2326
|
// Only the moonshot-v1-* models accept caller-supplied values.
|
|
2217
2327
|
const isKimiPinned = /^kimi-k/i.test(mappedModel);
|
|
2218
2328
|
|
|
2329
|
+
const { resolveThinkingParam } = require("./provider-capabilities");
|
|
2219
2330
|
const moonshotBody = {
|
|
2220
2331
|
model: mappedModel,
|
|
2221
2332
|
messages,
|
|
2222
2333
|
max_tokens: body.max_tokens || 16384,
|
|
2223
2334
|
temperature: isKimiPinned ? 1 : (body.temperature ?? 0.7),
|
|
2224
2335
|
top_p: isKimiPinned ? 0.95 : (body.top_p ?? 1.0),
|
|
2336
|
+
// kimi-k3 emits verbose reasoning_content by default, sharing the same
|
|
2337
|
+
// token budget as the answer — without this, reasoning alone can (and
|
|
2338
|
+
// did, live-confirmed) consume the whole max_tokens before any answer
|
|
2339
|
+
// is written. See resolveThinkingParam's doc comment for the full story.
|
|
2340
|
+
thinking: resolveThinkingParam(body),
|
|
2225
2341
|
// Streaming honored since the Phase-2b sse-transformer landed: the raw
|
|
2226
2342
|
// OpenAI SSE stream is returned below and reshaped in flight by the
|
|
2227
2343
|
// orchestrator (moonshot is in DEFAULT_OPENAI_SSE_PROVIDERS). Buffered
|
|
@@ -2321,7 +2437,7 @@ async function invokeMoonshot(body, incomingHeaders = {}) {
|
|
|
2321
2437
|
* modelMap and pinned-params logic (see Moonshot's kimi-k* precedent) once
|
|
2322
2438
|
* real traffic surfaces 400s.
|
|
2323
2439
|
*/
|
|
2324
|
-
async function invokeBaidu(body,
|
|
2440
|
+
async function invokeBaidu(body, _incomingHeaders = {}) {
|
|
2325
2441
|
if (!config.baidu?.apiKey) {
|
|
2326
2442
|
throw new Error("Baidu API key is not configured. Set BAIDU_API_KEY in your .env file.");
|
|
2327
2443
|
}
|
|
@@ -2365,12 +2481,18 @@ async function invokeBaidu(body, incomingHeaders = {}) {
|
|
|
2365
2481
|
messages.unshift({ role: "system", content: systemContent });
|
|
2366
2482
|
}
|
|
2367
2483
|
|
|
2484
|
+
const { resolveThinkingParam } = require("./provider-capabilities");
|
|
2368
2485
|
const baiduBody = {
|
|
2369
2486
|
model: mappedModel,
|
|
2370
2487
|
messages,
|
|
2371
2488
|
max_tokens: body.max_tokens || 16384,
|
|
2372
2489
|
temperature: body.temperature ?? 0.7,
|
|
2373
2490
|
top_p: body.top_p ?? 1.0,
|
|
2491
|
+
// glm-5.2 emits verbose reasoning_content by default, sharing the same
|
|
2492
|
+
// token budget as the answer — without this, reasoning alone can (and
|
|
2493
|
+
// did, live-confirmed) consume the whole max_tokens before any answer
|
|
2494
|
+
// is written. See resolveThinkingParam's doc comment for the full story.
|
|
2495
|
+
thinking: resolveThinkingParam(body),
|
|
2374
2496
|
// Streaming honored once "baidu" is added to DEFAULT_OPENAI_SSE_PROVIDERS
|
|
2375
2497
|
// (sse-transformer.js) and confirmed to match OpenAI SSE shape. Buffered
|
|
2376
2498
|
// requests use the Anthropic conversion path below regardless.
|
|
@@ -2430,6 +2552,147 @@ async function invokeBaidu(body, incomingHeaders = {}) {
|
|
|
2430
2552
|
return response;
|
|
2431
2553
|
}
|
|
2432
2554
|
|
|
2555
|
+
/**
|
|
2556
|
+
* Fireworks AI Provider (serverless inference)
|
|
2557
|
+
*
|
|
2558
|
+
* Fireworks exposes an OpenAI-compatible Chat Completions API
|
|
2559
|
+
* (https://api.fireworks.ai/inference/v1/chat/completions, bearer-token
|
|
2560
|
+
* auth). Modeled on invokeBaidu: request side reuses the shared
|
|
2561
|
+
* openrouter-utils converters, response is converted to Anthropic shape
|
|
2562
|
+
* locally so the orchestrator branch is a plain passthrough.
|
|
2563
|
+
*
|
|
2564
|
+
* Model ids are long-form serverless paths
|
|
2565
|
+
* (accounts/fireworks/models/<slug>, accounts/fireworks/routers/<slug>).
|
|
2566
|
+
* The modelMap below covers bare Anthropic names; tier-selected Fireworks
|
|
2567
|
+
* ids (e.g. TIER_COMPLEX=fireworks:accounts/fireworks/models/glm-5p2)
|
|
2568
|
+
* reach the wire unchanged.
|
|
2569
|
+
*
|
|
2570
|
+
* NOTE (E2E-unverified as of this addition, same caveat Baidu shipped
|
|
2571
|
+
* with): sampling quirks, reasoning_content emission on reasoning models,
|
|
2572
|
+
* finish_reason shape on tool calls, and 429 retryability are best-effort
|
|
2573
|
+
* from public docs, not yet probed against a live key. Revisit once real
|
|
2574
|
+
* traffic surfaces 400s — see Moonshot's kimi-k* precedent for the shape
|
|
2575
|
+
* such fixes take.
|
|
2576
|
+
*/
|
|
2577
|
+
async function invokeFireworks(body, _incomingHeaders = {}) {
|
|
2578
|
+
if (!config.fireworks?.apiKey) {
|
|
2579
|
+
throw new Error("Fireworks API key is not configured. Set FIREWORKS_API_KEY in your .env file.");
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
const {
|
|
2583
|
+
convertAnthropicToolsToOpenRouter,
|
|
2584
|
+
convertAnthropicMessagesToOpenRouter
|
|
2585
|
+
} = require("./openrouter-utils");
|
|
2586
|
+
|
|
2587
|
+
const endpoint = config.fireworks.endpoint || "https://api.fireworks.ai/inference/v1/chat/completions";
|
|
2588
|
+
|
|
2589
|
+
// Model mapping: Anthropic names → Fireworks serverless ids.
|
|
2590
|
+
const modelMap = {
|
|
2591
|
+
"claude-sonnet-4-5-20250929": "accounts/fireworks/models/kimi-k2-instruct-0905",
|
|
2592
|
+
"claude-sonnet-4-5": "accounts/fireworks/models/kimi-k2-instruct-0905",
|
|
2593
|
+
"claude-sonnet-4.5": "accounts/fireworks/models/kimi-k2-instruct-0905",
|
|
2594
|
+
"claude-3-5-sonnet": "accounts/fireworks/models/kimi-k2-instruct-0905",
|
|
2595
|
+
"claude-opus-4-5": "accounts/fireworks/models/glm-5p2",
|
|
2596
|
+
"claude-opus-4.5": "accounts/fireworks/models/glm-5p2",
|
|
2597
|
+
"claude-3-opus": "accounts/fireworks/models/glm-5p2",
|
|
2598
|
+
"claude-haiku-4-5-20251001": "accounts/fireworks/models/llama-3.1-8b-instruct",
|
|
2599
|
+
"claude-haiku-4-5": "accounts/fireworks/models/llama-3.1-8b-instruct",
|
|
2600
|
+
"claude-3-haiku": "accounts/fireworks/models/llama-3.1-8b-instruct",
|
|
2601
|
+
};
|
|
2602
|
+
|
|
2603
|
+
const requestedModel = body._tierModel || body.model || config.fireworks.model;
|
|
2604
|
+
|
|
2605
|
+
// Honor tier-selected Fireworks ids instead of silently swapping in the
|
|
2606
|
+
// .env default model. Accepts full serverless paths as well as well-known
|
|
2607
|
+
// open-weight family slugs hosted on Fireworks.
|
|
2608
|
+
const FIREWORKS_ID_RE = /^(accounts\/|kimi-|glm-|deepseek-|qwen|llama-|mistral-|mixtral-|phi-|gemma-|grok-|solar-|yi-|fireworks-)/i;
|
|
2609
|
+
const mappedModel = modelMap[requestedModel]
|
|
2610
|
+
|| (FIREWORKS_ID_RE.test(requestedModel || "") ? requestedModel : null)
|
|
2611
|
+
|| config.fireworks.model
|
|
2612
|
+
|| "accounts/fireworks/models/kimi-k2-instruct-0905";
|
|
2613
|
+
|
|
2614
|
+
const messages = convertAnthropicMessagesToOpenRouter(body.messages || []);
|
|
2615
|
+
|
|
2616
|
+
// Fireworks supports the system role natively.
|
|
2617
|
+
if (body.system) {
|
|
2618
|
+
const systemContent = Array.isArray(body.system)
|
|
2619
|
+
? body.system.map(s => s.text || s).join("\n")
|
|
2620
|
+
: body.system;
|
|
2621
|
+
messages.unshift({ role: "system", content: systemContent });
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
const { resolveThinkingParam } = require("./provider-capabilities");
|
|
2625
|
+
const fireworksBody = {
|
|
2626
|
+
model: mappedModel,
|
|
2627
|
+
messages,
|
|
2628
|
+
max_tokens: body.max_tokens || 16384,
|
|
2629
|
+
temperature: body.temperature ?? 0.7,
|
|
2630
|
+
top_p: body.top_p ?? 1.0,
|
|
2631
|
+
// Reasoning models (R1/GLM) share the answer budget with thinking output;
|
|
2632
|
+
// the buffered path lifts reasoning_content into thinking blocks.
|
|
2633
|
+
thinking: resolveThinkingParam(body),
|
|
2634
|
+
// Streaming honored once "fireworks" joins DEFAULT_OPENAI_SSE_PROVIDERS
|
|
2635
|
+
// (sse-transformer.js). Buffered requests use the Anthropic conversion
|
|
2636
|
+
// path below regardless.
|
|
2637
|
+
stream: body.stream ?? false,
|
|
2638
|
+
};
|
|
2639
|
+
|
|
2640
|
+
if (Array.isArray(body.tools) && body.tools.length > 0) {
|
|
2641
|
+
fireworksBody.tools = convertAnthropicToolsToOpenRouter(body.tools);
|
|
2642
|
+
fireworksBody.tool_choice = "auto";
|
|
2643
|
+
fireworksBody.parallel_tool_calls = false;
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
const headers = {
|
|
2647
|
+
"Content-Type": "application/json",
|
|
2648
|
+
"Authorization": `Bearer ${config.fireworks.apiKey}`,
|
|
2649
|
+
};
|
|
2650
|
+
|
|
2651
|
+
logger.debug({
|
|
2652
|
+
endpoint,
|
|
2653
|
+
model: fireworksBody.model,
|
|
2654
|
+
originalModel: requestedModel,
|
|
2655
|
+
messageCount: fireworksBody.messages?.length || 0,
|
|
2656
|
+
hasTools: !!fireworksBody.tools,
|
|
2657
|
+
toolCount: fireworksBody.tools?.length || 0,
|
|
2658
|
+
}, "=== Fireworks REQUEST ===");
|
|
2659
|
+
|
|
2660
|
+
// No retryableStatusesOverride: Fireworks serverless 429s are standard
|
|
2661
|
+
// per-minute rate limits (unlike Moonshot's persistent org quotas), so the
|
|
2662
|
+
// default retry-with-backoff applies. A 429 that survives retries throws
|
|
2663
|
+
// below with status set so tier-fallback climbs instead of hanging.
|
|
2664
|
+
const response = await performJsonRequest(endpoint, {
|
|
2665
|
+
headers,
|
|
2666
|
+
body: fireworksBody,
|
|
2667
|
+
}, "Fireworks");
|
|
2668
|
+
|
|
2669
|
+
if (!response.ok && response.status === 429) {
|
|
2670
|
+
const err = new Error(`Fireworks rate-limited: ${String(response.json?.error?.message || '').slice(0, 120)}`);
|
|
2671
|
+
err.status = 429;
|
|
2672
|
+
throw err;
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
// Streaming request: hand the raw stream to the orchestrator's stream
|
|
2676
|
+
// branch. The Anthropic conversion below is buffered-only.
|
|
2677
|
+
if (response?.stream) {
|
|
2678
|
+
return response;
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
if (response?.ok && response?.json) {
|
|
2682
|
+
const anthropicJson = convertOpenAIToAnthropic(response.json);
|
|
2683
|
+
return {
|
|
2684
|
+
ok: response.ok,
|
|
2685
|
+
status: response.status,
|
|
2686
|
+
json: anthropicJson,
|
|
2687
|
+
text: JSON.stringify(anthropicJson),
|
|
2688
|
+
contentType: "application/json",
|
|
2689
|
+
headers: response.headers,
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
return response;
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2433
2696
|
/**
|
|
2434
2697
|
* Convert OpenAI response to Anthropic format
|
|
2435
2698
|
*/
|
|
@@ -2457,12 +2720,34 @@ function convertOpenAIToAnthropic(response) {
|
|
|
2457
2720
|
const hasToolCalls = Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
|
|
2458
2721
|
|
|
2459
2722
|
// Extract text content and reasoning from thinking models
|
|
2460
|
-
|
|
2723
|
+
let textContent = typeof message.content === 'string' ? message.content : '';
|
|
2461
2724
|
const reasoningContent = typeof message.reasoning_content === 'string' ? message.reasoning_content : '';
|
|
2462
2725
|
|
|
2463
|
-
//
|
|
2464
|
-
|
|
2465
|
-
|
|
2726
|
+
// GLM-family models leak reasoning into `content` in two shapes:
|
|
2727
|
+
// 1. matched <think>...</think> pairs;
|
|
2728
|
+
// 2. an ORPHAN closing </think> — GLM's chat template opens <think>
|
|
2729
|
+
// inside the prompt, so the completion starts mid-reasoning and only
|
|
2730
|
+
// emits the closer (observed live 2026-08-23, baidu/glm-5.2:
|
|
2731
|
+
// "Hi. What need?</think>Hi. What need?" reached the client verbatim).
|
|
2732
|
+
let leakedThinking = '';
|
|
2733
|
+
if (textContent.includes('</think>')) {
|
|
2734
|
+
if (textContent.includes('<think>')) {
|
|
2735
|
+
const thoughts = [];
|
|
2736
|
+
textContent = textContent
|
|
2737
|
+
.replace(/<think>([\s\S]*?)<\/think>/g, (_, t) => { thoughts.push(t.trim()); return ''; })
|
|
2738
|
+
.trim();
|
|
2739
|
+
leakedThinking = thoughts.filter(Boolean).join('\n\n');
|
|
2740
|
+
} else {
|
|
2741
|
+
const idx = textContent.indexOf('</think>');
|
|
2742
|
+
leakedThinking = textContent.slice(0, idx).trim();
|
|
2743
|
+
textContent = textContent.slice(idx + '</think>'.length).trim();
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
// Emit reasoning (side-channel and/or leaked) as a proper thinking block
|
|
2748
|
+
const combinedThinking = [reasoningContent, leakedThinking].filter(Boolean).join('\n\n');
|
|
2749
|
+
if (combinedThinking) {
|
|
2750
|
+
content.push({ type: "thinking", thinking: combinedThinking });
|
|
2466
2751
|
}
|
|
2467
2752
|
|
|
2468
2753
|
if (textContent) {
|
|
@@ -2558,7 +2843,7 @@ function sanitizeSchemaForGemini(schema) {
|
|
|
2558
2843
|
* (generativelanguage.googleapis.com), not Google Cloud Vertex AI.
|
|
2559
2844
|
* Converts Anthropic format to Gemini format and back.
|
|
2560
2845
|
*/
|
|
2561
|
-
async function invokeVertex(body,
|
|
2846
|
+
async function invokeVertex(body, _incomingHeaders = {}) {
|
|
2562
2847
|
const apiKey = config.vertex?.apiKey;
|
|
2563
2848
|
|
|
2564
2849
|
if (!apiKey) {
|
|
@@ -2803,7 +3088,7 @@ function convertGeminiToAnthropic(response, requestedModel) {
|
|
|
2803
3088
|
};
|
|
2804
3089
|
}
|
|
2805
3090
|
|
|
2806
|
-
async function invokeCodex(body,
|
|
3091
|
+
async function invokeCodex(body, _incomingHeaders = {}) {
|
|
2807
3092
|
const { getCodexProcess } = require("./codex-process");
|
|
2808
3093
|
const { convertAnthropicToCodexPrompt, convertCodexResponseToAnthropic } = require("./codex-utils");
|
|
2809
3094
|
|
|
@@ -2927,7 +3212,6 @@ const LYNKR_BADGE_PREFIX_RE = /^\*\[Lynkr\][^*\n]*\*\s*/;
|
|
|
2927
3212
|
function stripLynkrBadges(messages) {
|
|
2928
3213
|
if (!Array.isArray(messages)) return messages;
|
|
2929
3214
|
let mutated = false;
|
|
2930
|
-
let badgeCount = 0;
|
|
2931
3215
|
const out = messages.map((msg) => {
|
|
2932
3216
|
if (msg?.role !== 'assistant') return msg;
|
|
2933
3217
|
|
|
@@ -2938,7 +3222,6 @@ function stripLynkrBadges(messages) {
|
|
|
2938
3222
|
if (!LYNKR_BADGE_PREFIX_RE.test(msg.content)) return msg;
|
|
2939
3223
|
const stripped = msg.content.replace(LYNKR_BADGE_PREFIX_RE, '');
|
|
2940
3224
|
mutated = true;
|
|
2941
|
-
badgeCount++;
|
|
2942
3225
|
// Badge-only content must not become an empty string — Anthropic
|
|
2943
3226
|
// rejects empty assistant content (this is the interrupted-response
|
|
2944
3227
|
// continuation case, where the partial text WAS just the badge).
|
|
@@ -2957,7 +3240,6 @@ function stripLynkrBadges(messages) {
|
|
|
2957
3240
|
for (const b of msg.content) {
|
|
2958
3241
|
if (b?.type === 'text' && typeof b.text === 'string' && LYNKR_BADGE_PREFIX_RE.test(b.text)) {
|
|
2959
3242
|
changed = true;
|
|
2960
|
-
badgeCount++;
|
|
2961
3243
|
const strippedText = b.text.replace(LYNKR_BADGE_PREFIX_RE, '');
|
|
2962
3244
|
if (strippedText.trim()) rebuilt.push({ ...b, text: strippedText });
|
|
2963
3245
|
// badge-only block → drop
|
|
@@ -3003,6 +3285,7 @@ const PROVIDER_INVOKERS = {
|
|
|
3003
3285
|
moonshot: invokeMoonshot,
|
|
3004
3286
|
codex: invokeCodex,
|
|
3005
3287
|
baidu: invokeBaidu,
|
|
3288
|
+
fireworks: invokeFireworks,
|
|
3006
3289
|
};
|
|
3007
3290
|
|
|
3008
3291
|
function invokeProvider(provider, body, incomingHeaders) {
|
|
@@ -3264,6 +3547,7 @@ async function invokeModel(body, options = {}) {
|
|
|
3264
3547
|
base_tier: routingResult.base_tier ?? null,
|
|
3265
3548
|
escalation_source: routingResult.escalation_source ?? null,
|
|
3266
3549
|
propensity: routingResult.propensity ?? null,
|
|
3550
|
+
context: routingResult._banditContext ?? null,
|
|
3267
3551
|
candidates: routingResult.candidates ?? null,
|
|
3268
3552
|
pinned: routingResult.pinned ? 1 : 0,
|
|
3269
3553
|
switch_reason: routingResult.switch_reason ?? null,
|
|
@@ -3498,6 +3782,7 @@ async function invokeModel(body, options = {}) {
|
|
|
3498
3782
|
base_tier: routingResult.base_tier ?? null,
|
|
3499
3783
|
escalation_source: routingResult.escalation_source ?? null,
|
|
3500
3784
|
propensity: routingResult.propensity ?? null,
|
|
3785
|
+
context: routingResult._banditContext ?? null,
|
|
3501
3786
|
candidates: routingResult.candidates ?? null,
|
|
3502
3787
|
pinned: routingResult.pinned ? 1 : 0,
|
|
3503
3788
|
switch_reason: routingResult.switch_reason ?? null,
|
|
@@ -3610,6 +3895,7 @@ async function invokeModel(body, options = {}) {
|
|
|
3610
3895
|
base_tier: routingResult.base_tier ?? null,
|
|
3611
3896
|
escalation_source: routingResult.escalation_source ?? null,
|
|
3612
3897
|
propensity: routingResult.propensity ?? null,
|
|
3898
|
+
context: routingResult._banditContext ?? null,
|
|
3613
3899
|
candidates: routingResult.candidates ?? null,
|
|
3614
3900
|
pinned: routingResult.pinned ? 1 : 0,
|
|
3615
3901
|
switch_reason: routingResult.switch_reason ?? null,
|
|
@@ -3706,6 +3992,7 @@ async function invokeModel(body, options = {}) {
|
|
|
3706
3992
|
base_tier: routingResult.base_tier ?? null,
|
|
3707
3993
|
escalation_source: routingResult.escalation_source ?? null,
|
|
3708
3994
|
propensity: routingResult.propensity ?? null,
|
|
3995
|
+
context: routingResult._banditContext ?? null,
|
|
3709
3996
|
candidates: routingResult.candidates ?? null,
|
|
3710
3997
|
pinned: routingResult.pinned ? 1 : 0,
|
|
3711
3998
|
switch_reason: routingResult.switch_reason ?? null,
|
|
@@ -3766,6 +4053,7 @@ async function invokeModel(body, options = {}) {
|
|
|
3766
4053
|
base_tier: routingResult.base_tier ?? null,
|
|
3767
4054
|
escalation_source: routingResult.escalation_source ?? null,
|
|
3768
4055
|
propensity: routingResult.propensity ?? null,
|
|
4056
|
+
context: routingResult._banditContext ?? null,
|
|
3769
4057
|
candidates: routingResult.candidates ?? null,
|
|
3770
4058
|
pinned: routingResult.pinned ? 1 : 0,
|
|
3771
4059
|
switch_reason: routingResult.switch_reason ?? null,
|
|
@@ -3860,11 +4148,14 @@ function destroyHttpAgents() {
|
|
|
3860
4148
|
|
|
3861
4149
|
module.exports = {
|
|
3862
4150
|
invokeModel,
|
|
4151
|
+
computeCostUsd,
|
|
4152
|
+
convertOpenAIToAnthropic,
|
|
3863
4153
|
invokeAzureAnthropic,
|
|
3864
4154
|
invokeZai,
|
|
3865
4155
|
invokeOllama,
|
|
3866
4156
|
invokeMoonshot,
|
|
3867
4157
|
invokeBaidu,
|
|
4158
|
+
invokeFireworks,
|
|
3868
4159
|
invokeAtlas,
|
|
3869
4160
|
PROVIDER_INVOKERS,
|
|
3870
4161
|
stripLynkrBadges,
|