lynkr 9.14.16 → 9.14.18

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.
@@ -8,12 +8,33 @@ const { getMetricsCollector } = require("../observability/metrics");
8
8
  const { getHealthTracker } = require("../observability/health-tracker");
9
9
  const { createBulkhead } = require("./resilience");
10
10
  const logger = require("../logger");
11
- const { STANDARD_TOOLS, STANDARD_TOOL_NAMES } = require("./standard-tools");
11
+
12
+ function clientStructuredOutput(body) {
13
+ if (process.env.LYNKR_FORWARD_RESPONSE_FORMAT === "false") return null;
14
+ const of = body?.output_format;
15
+ if (of && typeof of === "object") {
16
+ if (of.type === "json_schema" && of.schema) return { kind: "json_schema", schema: of.schema, name: of.name || of.schema?.title || "response" };
17
+ if (of.type === "json_object") return { kind: "json_object" };
18
+ }
19
+ const rf = body?.response_format;
20
+ if (rf && typeof rf === "object") {
21
+ if (rf.type === "json_schema" && rf.json_schema?.schema) return { kind: "json_schema", schema: rf.json_schema.schema, name: rf.json_schema.name || "response" };
22
+ if (rf.type === "json_object") return { kind: "json_object" };
23
+ }
24
+ return null;
25
+ }
26
+ /** OpenAI-style response_format for a provider; schemaSupported=false → json_object. */
27
+ function openaiResponseFormat(body, schemaSupported) {
28
+ const so = clientStructuredOutput(body);
29
+ if (!so) return null;
30
+ if (so.kind === "json_schema" && schemaSupported) return { type: "json_schema", json_schema: { name: String(so.name).slice(0, 64), schema: so.schema } };
31
+ return { type: "json_object" };
32
+ }
12
33
  const { convertAnthropicToolsToOpenRouter } = require("./openrouter-utils");
13
34
 
14
35
  /**
15
36
  * Capability-boundary guard (issue #114): a caller that sets
16
- * tool_choice "none" grants no tools. Substituting STANDARD_TOOLS widens
37
+ * tool_choice "none" grants no tools. Substituting server-side tools would widen
17
38
  * the grant to shell execution with zero caller-visible signal, so every
18
39
  * injection site below must consult this first. Matches the convention
19
40
  * already used by the OpenRouter ingress path.
@@ -214,17 +235,7 @@ async function invokeDatabricks(body, _incomingHeaders = {}) {
214
235
  // Create a copy of body to avoid mutating the original
215
236
  const databricksBody = { ...body };
216
237
 
217
- // Inject standard tools if client didn't send any (passthrough mode).
218
- // Never when the caller declined tools (issue #114).
219
- if (!toolsDeclined(body) && (!Array.isArray(databricksBody.tools) || databricksBody.tools.length === 0)) {
220
- databricksBody.tools = STANDARD_TOOLS;
221
- logger.debug({
222
- injectedToolCount: STANDARD_TOOLS.length,
223
- injectedToolNames: STANDARD_TOOL_NAMES,
224
- reason: "Client did not send tools (passthrough mode)"
225
- }, "=== INJECTING STANDARD TOOLS (Databricks) ===");
226
- }
227
-
238
+
228
239
  // Convert Anthropic format tools to OpenAI format (Databricks uses OpenAI format)
229
240
  if (Array.isArray(databricksBody.tools) && databricksBody.tools.length > 0) {
230
241
  // Check if tools are already in OpenAI format (have type: "function")
@@ -560,9 +571,6 @@ async function invokeOllama(body, _incomingHeaders = {}) {
560
571
 
561
572
  if (!supportsTools) {
562
573
  toolsToSend = null;
563
- } else if (!toolsDeclined(body) && injectToolsOllama && (!Array.isArray(toolsToSend) || toolsToSend.length === 0)) {
564
- toolsToSend = STANDARD_TOOLS;
565
- toolsInjected = true;
566
574
  }
567
575
 
568
576
  // Consolidated tool injection log
@@ -841,6 +849,63 @@ async function invokeOpenRouter(body, _incomingHeaders = {}) {
841
849
  stream: body.stream ?? false
842
850
  };
843
851
 
852
+ {
853
+ // Per-model pin: OPENROUTER_PROVIDER_ORDER_MAP="deepseek-v4.1-flash=DeepSeek;glm-5.3-flash=Novita,DeepInfra"
854
+ // (';' between models, ',' between providers; substring match on model id). Global knob is the default.
855
+ let _orderSrc = process.env.OPENROUTER_PROVIDER_ORDER || "";
856
+ for (const entry of (process.env.OPENROUTER_PROVIDER_ORDER_MAP || "").split(";")) {
857
+ const eq = entry.indexOf("=");
858
+ if (eq < 0) continue;
859
+ const k = entry.slice(0, eq).trim(), v = entry.slice(eq + 1).trim();
860
+ if (k && v && String(openRouterBody.model).includes(k)) { _orderSrc = v; break; }
861
+ }
862
+ const _order = _orderSrc.split(",").map((x) => x.trim()).filter(Boolean);
863
+ if (_order.length) {
864
+ openRouterBody.provider = { order: _order, allow_fallbacks: process.env.OPENROUTER_ALLOW_FALLBACKS === "true" };
865
+ }
866
+ openRouterBody._pinnedProviders = _order;
867
+ let _effort = body._effort || process.env.OPENROUTER_REASONING_EFFORT || null;
868
+ const _map = body._effort ? "" : (process.env.OPENROUTER_REASONING_EFFORT_MAP || "");
869
+ for (const pair of _map.split(",")) {
870
+ const [k, v] = pair.split("=").map((x) => (x || "").trim());
871
+ if (k && v && String(openRouterBody.model).includes(k)) { _effort = v; break; }
872
+ }
873
+ if (body.thinking?.type === "enabled" && body.thinking.budget_tokens) {
874
+ openRouterBody.reasoning = { max_tokens: body.thinking.budget_tokens };
875
+ } else if (_effort && _effort !== "none") {
876
+ openRouterBody.reasoning = { effort: _effort };
877
+ } else if (_effort === "none") {
878
+ openRouterBody.reasoning = { enabled: false };
879
+ }
880
+ // Per-model map lookup helper: "k1=v1,k2=v2" (substring match on model id).
881
+ const _mapLookup = (envName) => {
882
+ for (const pair of (process.env[envName] || "").split(",")) {
883
+ const [k, v] = pair.split("=").map((x) => (x || "").trim());
884
+ if (k && v && String(openRouterBody.model).includes(k)) return v;
885
+ }
886
+ return null;
887
+ };
888
+ const _cap = parseInt(_mapLookup("OPENROUTER_MAX_TOKENS_MAP") || "", 10);
889
+ if (_cap > 0 && openRouterBody.max_tokens > _cap) openRouterBody.max_tokens = _cap;
890
+ // Schema enforcement only where the host supports it (Novita / DeepSeek
891
+ // official reject json_schema; Friendli, Parasail, Together, Xiaomi honour it).
892
+ // OPENROUTER_SCHEMA_PROVIDERS="Friendli,Parasail,Together,Xiaomi"
893
+ const _schemaProviders = (process.env.OPENROUTER_SCHEMA_PROVIDERS || "").split(",").map((x) => x.trim().toLowerCase()).filter(Boolean);
894
+ const _schemaOk = (prov) => !!prov && _schemaProviders.includes(String(prov).toLowerCase());
895
+ const _globalSchema = process.env.OPENROUTER_RESPONSE_FORMAT_SCHEMA === "true";
896
+ const _rf = openaiResponseFormat(body, _globalSchema || _schemaOk(_order[0]));
897
+ if (_rf) {
898
+ if (_rf.type === "json_schema") { _rf.json_schema.strict = true; if (openRouterBody.provider) openRouterBody.provider.require_parameters = true; }
899
+ openRouterBody.response_format = _rf;
900
+ }
901
+ openRouterBody._schemaOk = _schemaOk;
902
+ // Upstream timeout (Lynkr had none → waited 846s on a stalled host).
903
+ // OPENROUTER_TIMEOUT_MS=150000 OPENROUTER_TIMEOUT_MS_MAP="glm-5.3-flash=90000"
904
+ openRouterBody._timeoutMs = parseInt(_mapLookup("OPENROUTER_TIMEOUT_MS_MAP") || process.env.OPENROUTER_TIMEOUT_MS || "150000", 10);
905
+ // Ask for provider-side usage (cached prompt tokens, reasoning tokens, cost).
906
+ openRouterBody.usage = { include: true };
907
+ }
908
+
844
909
  // Issue #95: forward the Lynkr session id so OpenRouter's meta-routers
845
910
  // (auto/pareto) can apply session stickiness — without it every call in a
846
911
  // pinned session re-ranks fresh and one agentic task can span 6+ models.
@@ -852,16 +917,7 @@ async function invokeOpenRouter(body, _incomingHeaders = {}) {
852
917
  let toolsToSend = body.tools;
853
918
  let toolsInjected = false;
854
919
 
855
- if (!toolsDeclined(body) && (!Array.isArray(toolsToSend) || toolsToSend.length === 0)) {
856
- toolsToSend = STANDARD_TOOLS;
857
- toolsInjected = true;
858
- logger.debug({
859
- injectedToolCount: STANDARD_TOOLS.length,
860
- injectedToolNames: STANDARD_TOOL_NAMES,
861
- reason: "Client did not send tools (passthrough mode)"
862
- }, "=== INJECTING STANDARD TOOLS (OpenRouter) ===");
863
- }
864
-
920
+
865
921
  if (Array.isArray(toolsToSend) && toolsToSend.length > 0) {
866
922
  openRouterBody.tools = convertAnthropicToolsToOpenRouter(toolsToSend);
867
923
  logger.debug({
@@ -877,11 +933,50 @@ async function invokeOpenRouter(body, _incomingHeaders = {}) {
877
933
 
878
934
  // Same pre-return 429 check as invokeMoonshot/invokeBaidu — see the comment
879
935
  // on invokeOpenAI's equivalent check for why this matters for streaming.
880
- const response = await performJsonRequest(endpoint, {
881
- headers,
882
- body: openRouterBody,
883
- retryableStatusesOverride: [500, 502, 503, 504],
884
- }, "OpenRouter");
936
+ const _pinnedProviders = openRouterBody._pinnedProviders; delete openRouterBody._pinnedProviders;
937
+ const _schemaOkFn = openRouterBody._schemaOk; delete openRouterBody._schemaOk;
938
+ const _timeoutMs = openRouterBody._timeoutMs; delete openRouterBody._timeoutMs;
939
+ const _isTimeout = (e) => e && (e.name === "TimeoutError" || e.name === "AbortError" || e.cause?.name === "TimeoutError" || e.code === "UND_ERR_HEADERS_TIMEOUT");
940
+ let response, _lastErr = null;
941
+ const _orders = _pinnedProviders && _pinnedProviders.length > 1
942
+ ? _pinnedProviders.map((_, i) => _pinnedProviders.slice(i))
943
+ : [_pinnedProviders || []];
944
+ for (let ai = 0; ai < _orders.length; ai++) {
945
+ const ord = _orders[ai];
946
+ if (ord.length) {
947
+ openRouterBody.provider = { ...(openRouterBody.provider || {}), order: ord, allow_fallbacks: process.env.OPENROUTER_ALLOW_FALLBACKS === "true" };
948
+ const wantSchema = !!(_schemaOkFn && _schemaOkFn(ord[0])) || process.env.OPENROUTER_RESPONSE_FORMAT_SCHEMA === "true";
949
+ const rf = openaiResponseFormat(body, wantSchema);
950
+ if (rf) { if (rf.type === "json_schema") rf.json_schema.strict = true; openRouterBody.response_format = rf; }
951
+ openRouterBody.provider.require_parameters = !!(rf && rf.type === "json_schema");
952
+ }
953
+ const t0 = Date.now();
954
+ try {
955
+ response = await performJsonRequest(endpoint, {
956
+ headers,
957
+ body: openRouterBody,
958
+ retryableStatusesOverride: [500, 502, 503, 504],
959
+ timeoutMs: _timeoutMs,
960
+ }, "OpenRouter");
961
+ _lastErr = null;
962
+ } catch (e) {
963
+ _lastErr = e; response = null;
964
+ }
965
+ const failedSoft = response && !response.ok && (response.status >= 500 || response.status === 404 || response.status === 429);
966
+ if ((_lastErr || failedSoft) && ai < _orders.length - 1) {
967
+ logger.warn({
968
+ model: openRouterBody.model, failedProvider: ord[0], nextProviders: _orders[ai + 1],
969
+ reason: _lastErr ? (_isTimeout(_lastErr) ? `timeout after ${Date.now() - t0}ms (limit ${_timeoutMs})` : _lastErr.message) : `HTTP ${response.status} ${String(response.json?.error?.message || "").slice(0, 100)}`,
970
+ }, "[Failover] OpenRouter provider failed — trying next pinned provider");
971
+ continue;
972
+ }
973
+ break;
974
+ }
975
+ if (_lastErr) {
976
+ if (_isTimeout(_lastErr)) { const err = new Error(`OpenRouter upstream timeout (${_timeoutMs}ms) on ${openRouterBody.model} via ${(_pinnedProviders || []).join(",") || "auto"}`); err.status = 504; err.code = "UPSTREAM_TIMEOUT"; throw err; }
977
+ throw _lastErr;
978
+ }
979
+ openRouterBody._pinnedProviders = _pinnedProviders;
885
980
 
886
981
  if (!response.ok && response.status === 429) {
887
982
  const err = new Error(`OpenRouter rate-limited: ${String(response.json?.error?.message || '').slice(0, 120)}`);
@@ -889,6 +984,36 @@ async function invokeOpenRouter(body, _incomingHeaders = {}) {
889
984
  throw err;
890
985
  }
891
986
 
987
+ // Streaming: hand the raw SSE stream to the orchestrator's stream branch.
988
+ if (response?.stream) {
989
+ return response;
990
+ }
991
+
992
+ if (response?.ok && response?.json?.choices) {
993
+ const raw = response.json;
994
+ const anthropicJson = convertOpenAIToAnthropic(raw);
995
+ if (raw.model) anthropicJson._providerModel = raw.model;
996
+ if (raw.provider) anthropicJson._providerName = raw.provider;
997
+ const _pinned = (openRouterBody._pinnedProviders || []).map((x) => String(x).toLowerCase());
998
+ if (_pinned.length && raw.provider && !_pinned.includes(String(raw.provider).toLowerCase())) {
999
+ logger.warn({ pinned: _pinned, served: raw.provider, model: raw.model }, "[ProviderCheck] OpenRouter served a provider outside OPENROUTER_PROVIDER_ORDER");
1000
+ }
1001
+ logger.info({
1002
+ model: raw.model, provider: raw.provider,
1003
+ prompt_tokens: raw.usage?.prompt_tokens, cached_tokens: raw.usage?.prompt_tokens_details?.cached_tokens,
1004
+ completion_tokens: raw.usage?.completion_tokens, reasoning_tokens: raw.usage?.completion_tokens_details?.reasoning_tokens,
1005
+ cost_usd: raw.usage?.cost,
1006
+ }, "=== OpenRouter USAGE ===");
1007
+ return {
1008
+ ok: response.ok,
1009
+ status: response.status,
1010
+ json: anthropicJson,
1011
+ text: JSON.stringify(anthropicJson),
1012
+ contentType: "application/json",
1013
+ headers: response.headers,
1014
+ };
1015
+ }
1016
+
892
1017
  return response;
893
1018
  }
894
1019
 
@@ -933,16 +1058,7 @@ async function invokeEdenAI(body, _incomingHeaders = {}) {
933
1058
  let toolsToSend = body.tools;
934
1059
  let toolsInjected = false;
935
1060
 
936
- if (!toolsDeclined(body) && (!Array.isArray(toolsToSend) || toolsToSend.length === 0)) {
937
- toolsToSend = STANDARD_TOOLS;
938
- toolsInjected = true;
939
- logger.debug({
940
- injectedToolCount: STANDARD_TOOLS.length,
941
- injectedToolNames: STANDARD_TOOL_NAMES,
942
- reason: "Client did not send tools (passthrough mode)"
943
- }, "=== INJECTING STANDARD TOOLS (Eden AI) ===");
944
- }
945
-
1061
+
946
1062
  if (Array.isArray(toolsToSend) && toolsToSend.length > 0) {
947
1063
  edenAIBody.tools = convertAnthropicToolsToOpenRouter(toolsToSend);
948
1064
  logger.debug({
@@ -1042,16 +1158,7 @@ async function invokeAzureOpenAI(body, _incomingHeaders = {}) {
1042
1158
  let toolsToSend = body.tools;
1043
1159
  let toolsInjected = false;
1044
1160
 
1045
- if (!toolsDeclined(body) && (!Array.isArray(toolsToSend) || toolsToSend.length === 0)) {
1046
- toolsToSend = STANDARD_TOOLS;
1047
- toolsInjected = true;
1048
- logger.debug({
1049
- injectedToolCount: STANDARD_TOOLS.length,
1050
- injectedToolNames: STANDARD_TOOL_NAMES,
1051
- reason: "Client did not send tools (passthrough mode)"
1052
- }, "=== INJECTING STANDARD TOOLS ===");
1053
- }
1054
-
1161
+
1055
1162
  if (Array.isArray(toolsToSend) && toolsToSend.length > 0) {
1056
1163
  azureBody.tools = convertAnthropicToolsToOpenRouter(toolsToSend);
1057
1164
  azureBody.parallel_tool_calls = true;
@@ -1485,21 +1592,19 @@ async function invokeOpenAI(body, _incomingHeaders = {}) {
1485
1592
  top_p: body.top_p ?? 1.0,
1486
1593
  stream: body.stream ?? false
1487
1594
  };
1595
+ if (process.env.OPENAI_REASONING_EFFORT && (!body.thinking || body.thinking?.type === "disabled")) {
1596
+ openAIBody.reasoning_effort = process.env.OPENAI_REASONING_EFFORT;
1597
+ }
1598
+ {
1599
+ const _rf = openaiResponseFormat(body, process.env.OPENAI_RESPONSE_FORMAT_SCHEMA !== "false");
1600
+ if (_rf) openAIBody.response_format = _rf;
1601
+ }
1488
1602
 
1489
1603
  // Add tools - inject standard tools if client didn't send any (passthrough mode)
1490
1604
  let toolsToSend = body.tools;
1491
1605
  let toolsInjected = false;
1492
1606
 
1493
- if (!toolsDeclined(body) && (!Array.isArray(toolsToSend) || toolsToSend.length === 0)) {
1494
- toolsToSend = STANDARD_TOOLS;
1495
- toolsInjected = true;
1496
- logger.debug({
1497
- injectedToolCount: STANDARD_TOOLS.length,
1498
- injectedToolNames: STANDARD_TOOL_NAMES,
1499
- reason: "Client did not send tools (passthrough mode)"
1500
- }, "=== INJECTING STANDARD TOOLS (OpenAI) ===");
1501
- }
1502
-
1607
+
1503
1608
  if (Array.isArray(toolsToSend) && toolsToSend.length > 0) {
1504
1609
  openAIBody.tools = convertAnthropicToolsToOpenRouter(toolsToSend);
1505
1610
  openAIBody.parallel_tool_calls = false; // Disable parallel tool calls - GPT often makes duplicate calls
@@ -1571,11 +1676,7 @@ async function invokeAtlas(body) {
1571
1676
 
1572
1677
  let toolsToSend = body.tools;
1573
1678
  let toolsInjected = false;
1574
- if (!toolsDeclined(body) && (!Array.isArray(toolsToSend) || toolsToSend.length === 0)) {
1575
- toolsToSend = STANDARD_TOOLS;
1576
- toolsInjected = true;
1577
- }
1578
-
1679
+
1579
1680
  if (toolsToSend.length > 0) {
1580
1681
  atlasBody.tools = convertAnthropicToolsToOpenRouter(toolsToSend);
1581
1682
  atlasBody.parallel_tool_calls = false;
@@ -1709,22 +1810,9 @@ async function invokeLlamaCpp(body, _incomingHeaders = {}) {
1709
1810
  stream: body.stream ?? false
1710
1811
  };
1711
1812
 
1712
- // Inject standard tools if client didn't send any
1713
1813
  let toolsToSend = body.tools;
1714
- let toolsInjected = false;
1814
+ const toolsInjected = false;
1715
1815
 
1716
- const injectToolsLlamacpp = process.env.INJECT_TOOLS_LLAMACPP !== "false";
1717
- if (!toolsDeclined(body) && injectToolsLlamacpp && (!Array.isArray(toolsToSend) || toolsToSend.length === 0)) {
1718
- toolsToSend = STANDARD_TOOLS;
1719
- toolsInjected = true;
1720
- logger.debug({
1721
- injectedToolCount: STANDARD_TOOLS.length,
1722
- injectedToolNames: STANDARD_TOOL_NAMES,
1723
- reason: "Client did not send tools (passthrough mode)"
1724
- }, "=== INJECTING STANDARD TOOLS (llama.cpp) ===");
1725
- } else if (!injectToolsLlamacpp) {
1726
- logger.debug({}, "Tool injection disabled for llama.cpp (INJECT_TOOLS_LLAMACPP=false)");
1727
- }
1728
1816
 
1729
1817
  if (Array.isArray(toolsToSend) && toolsToSend.length > 0) {
1730
1818
  llamacppBody.tools = convertAnthropicToolsToOpenRouter(toolsToSend);
@@ -1808,20 +1896,10 @@ async function invokeLMStudio(body, _incomingHeaders = {}) {
1808
1896
  stream: body.stream ?? false
1809
1897
  };
1810
1898
 
1811
- // Inject standard tools if client didn't send any
1812
1899
  let toolsToSend = body.tools;
1813
- let toolsInjected = false;
1814
-
1815
- if (!toolsDeclined(body) && (!Array.isArray(toolsToSend) || toolsToSend.length === 0)) {
1816
- toolsToSend = STANDARD_TOOLS;
1817
- toolsInjected = true;
1818
- logger.debug({
1819
- injectedToolCount: STANDARD_TOOLS.length,
1820
- injectedToolNames: STANDARD_TOOL_NAMES,
1821
- reason: "Client did not send tools (passthrough mode)"
1822
- }, "=== INJECTING STANDARD TOOLS (LM Studio) ===");
1823
- }
1900
+ const toolsInjected = false;
1824
1901
 
1902
+
1825
1903
  if (Array.isArray(toolsToSend) && toolsToSend.length > 0) {
1826
1904
  lmstudioBody.tools = convertAnthropicToolsToOpenRouter(toolsToSend);
1827
1905
  lmstudioBody.tool_choice = forwardToolChoice(body);
@@ -1971,15 +2049,7 @@ async function invokeBedrock(body, _incomingHeaders = {}) {
1971
2049
  // Inject standard tools if needed
1972
2050
  let toolsToSend = body.tools;
1973
2051
 
1974
- if (!toolsDeclined(body) && (!Array.isArray(toolsToSend) || toolsToSend.length === 0)) {
1975
- toolsToSend = STANDARD_TOOLS;
1976
- logger.debug({
1977
- injectedToolCount: STANDARD_TOOLS.length,
1978
- injectedToolNames: STANDARD_TOOL_NAMES,
1979
- reason: "Client did not send tools (passthrough mode)"
1980
- }, "=== INJECTING STANDARD TOOLS (Bedrock) ===");
1981
- }
1982
-
2052
+
1983
2053
  // Normalize away cache_control / array shapes that prompt-cache injection
1984
2054
  // may have applied: the Converse API expects plain-string system and
1985
2055
  // message content, not Anthropic cache_control blocks.
@@ -2162,7 +2232,7 @@ async function invokeZai(body, _incomingHeaders = {}) {
2162
2232
  };
2163
2233
 
2164
2234
  const requestedModel = body._tierModel || body.model || config.zai.model;
2165
- let mappedModel = modelMap[requestedModel] || config.zai.model || "glm-4.7";
2235
+ let mappedModel = modelMap[requestedModel] || (body._tierModel ? requestedModel : null) || config.zai.model || "glm-4.7";
2166
2236
  mappedModel = mappedModel.toLowerCase();
2167
2237
 
2168
2238
  let zaiBody;
@@ -2227,6 +2297,11 @@ async function invokeZai(body, _incomingHeaders = {}) {
2227
2297
  temperature: body.temperature ?? 0.7,
2228
2298
  stream: body.stream,
2229
2299
  };
2300
+ {
2301
+ // OpenAI-compat branch → response_format (Z.AI accepts json_schema here).
2302
+ const _rf = openaiResponseFormat(body, true);
2303
+ if (_rf) zaiBody.response_format = _rf;
2304
+ }
2230
2305
 
2231
2306
  if (tools && tools.length > 0) {
2232
2307
  zaiBody.tools = tools;
@@ -2263,17 +2338,7 @@ async function invokeZai(body, _incomingHeaders = {}) {
2263
2338
  // that's a different bug — add zai to DEFAULT_OPENAI_SSE_PROVIDERS then,
2264
2339
  // don't reintroduce this override.
2265
2340
 
2266
- // Inject standard tools if client didn't send any (passthrough mode).
2267
- // Never when the caller declined tools (issue #114).
2268
- if (!toolsDeclined(body) && (!Array.isArray(zaiBody.tools) || zaiBody.tools.length === 0)) {
2269
- zaiBody.tools = STANDARD_TOOLS;
2270
- logger.debug({
2271
- injectedToolCount: STANDARD_TOOLS.length,
2272
- injectedToolNames: STANDARD_TOOL_NAMES,
2273
- reason: "Client did not send tools (passthrough mode)"
2274
- }, "=== INJECTING STANDARD TOOLS (Z.AI Anthropic) ===");
2275
- }
2276
-
2341
+
2277
2342
  // Normalize to Anthropic tool shape — the /anthropic/v1/messages endpoint
2278
2343
  // rejects OpenAI-nested tools ({type:"function", function:{name,...}}) with
2279
2344
  // 422 missing body.tools[0].name. Callers that stayed OpenAI-format upstream
@@ -2576,7 +2641,7 @@ async function invokeBaidu(body, _incomingHeaders = {}) {
2576
2641
  const baiduBody = {
2577
2642
  model: mappedModel,
2578
2643
  messages,
2579
- max_tokens: body.max_tokens || 16384,
2644
+ max_tokens: Math.min(body.max_tokens || 12288, 12288),
2580
2645
  temperature: body.temperature ?? 0.7,
2581
2646
  top_p: body.top_p ?? 1.0,
2582
2647
  // glm-5.2 emits verbose reasoning_content by default, sharing the same
@@ -2727,6 +2792,26 @@ async function invokeFireworks(body, _incomingHeaders = {}) {
2727
2792
  // path below regardless.
2728
2793
  stream: body.stream ?? false,
2729
2794
  };
2795
+ const _keepThinkingRe = new RegExp(process.env.FIREWORKS_THINKING_MODELS || "glm-5p3|glm-5\\.3|deepseek-v4p1-flash", "i");
2796
+ if (_keepThinkingRe.test(String(fireworksBody.model)) && fireworksBody.thinking?.type === "disabled") {
2797
+ delete fireworksBody.thinking;
2798
+ }
2799
+ {
2800
+ let _effort = body._effort || process.env.FIREWORKS_REASONING_EFFORT || null;
2801
+ const _map = body._effort ? "" : (process.env.FIREWORKS_REASONING_EFFORT_MAP || "");
2802
+ for (const pair of _map.split(",")) {
2803
+ const [k, v] = pair.split("=").map((x) => (x || "").trim());
2804
+ if (k && v && String(fireworksBody.model).includes(k)) { _effort = v; break; }
2805
+ }
2806
+ if (_effort && (!body.thinking || body.thinking?.type === "disabled") && !fireworksBody.reasoning_effort) {
2807
+ fireworksBody.reasoning_effort = _effort;
2808
+ }
2809
+ }
2810
+ {
2811
+ // Fireworks 400s on json_schema with $ref; json_object is universally accepted.
2812
+ const _rf = openaiResponseFormat(body, process.env.FIREWORKS_RESPONSE_FORMAT_SCHEMA === "true");
2813
+ if (_rf) fireworksBody.response_format = _rf;
2814
+ }
2730
2815
 
2731
2816
  if (Array.isArray(body.tools) && body.tools.length > 0) {
2732
2817
  fireworksBody.tools = convertAnthropicToolsToOpenRouter(body.tools);
@@ -2771,6 +2856,7 @@ async function invokeFireworks(body, _incomingHeaders = {}) {
2771
2856
 
2772
2857
  if (response?.ok && response?.json) {
2773
2858
  const anthropicJson = convertOpenAIToAnthropic(response.json);
2859
+ if (response.json?.model) anthropicJson._providerModel = response.json.model;
2774
2860
  return {
2775
2861
  ok: response.ok,
2776
2862
  status: response.status,
@@ -2797,7 +2883,9 @@ function convertOpenAIToAnthropic(response) {
2797
2883
  const content = [];
2798
2884
 
2799
2885
  // Extract tool calls embedded as XML/text in content (Minimax, Qwen, GLM, etc.)
2800
- if (!message.tool_calls?.length && typeof message.content === "string" && message.content.trim()) {
2886
+ const _mc = typeof message.content === "string" ? message.content.trim() : "";
2887
+ const _isJsonObject = _mc.startsWith("{") && _mc.endsWith("}");
2888
+ if (!_isJsonObject && !message.tool_calls?.length && typeof message.content === "string" && message.content.trim()) {
2801
2889
  const { extractToolCallsFromText } = require("./xml-tool-extractor");
2802
2890
  const extracted = extractToolCallsFromText(message.content);
2803
2891
  if (extracted.toolCalls.length > 0) {
@@ -2879,14 +2967,22 @@ function convertOpenAIToAnthropic(response) {
2879
2967
  model: response.model,
2880
2968
  stop_reason: stopReason,
2881
2969
  stop_sequence: null,
2882
- usage: {
2883
- input_tokens: response.usage?.prompt_tokens || 0,
2884
- output_tokens: response.usage?.completion_tokens || 0,
2885
- // Provider-side prompt-cache hits — telemetry reads this field name.
2886
- cache_read_input_tokens: response.usage?.cache_read_input_tokens
2970
+ usage: (() => {
2971
+ // Anthropic semantics: input_tokens EXCLUDES cache reads. OpenAI's
2972
+ // prompt_tokens INCLUDES them, so subtract here — downstream rebuilders
2973
+ // (openai-format.js finalUsage/convertUsage) add cache_read back when
2974
+ // emitting OpenAI shape. Copying prompt_tokens verbatim double-counted
2975
+ // cached tokens (2× reported prompt, cache % halved — exp2 2026-09-29).
2976
+ const cacheRead = response.usage?.cache_read_input_tokens
2887
2977
  ?? response.usage?.prompt_tokens_details?.cached_tokens
2888
- ?? null,
2889
- }
2978
+ ?? null;
2979
+ return {
2980
+ input_tokens: Math.max(0, (response.usage?.prompt_tokens || 0) - (cacheRead || 0)),
2981
+ output_tokens: response.usage?.completion_tokens || 0,
2982
+ // Provider-side prompt-cache hits — telemetry reads this field name.
2983
+ cache_read_input_tokens: cacheRead,
2984
+ };
2985
+ })()
2890
2986
  };
2891
2987
  }
2892
2988
 
@@ -3661,7 +3757,7 @@ const PROVIDER_INVOKERS = {
3661
3757
  orcarouter: invokeOrcaRouter,
3662
3758
  };
3663
3759
 
3664
- function invokeProvider(provider, body, incomingHeaders) {
3760
+ async function invokeProvider(provider, body, incomingHeaders) {
3665
3761
  const invoke = PROVIDER_INVOKERS[provider];
3666
3762
  if (!invoke) {
3667
3763
  // databricks itself, or an unknown name. Loud, not silent: under tier
@@ -3671,9 +3767,19 @@ function invokeProvider(provider, body, incomingHeaders) {
3671
3767
  }
3672
3768
  return invokeDatabricks(body, incomingHeaders);
3673
3769
  }
3674
- return invoke(body, incomingHeaders);
3770
+ const res = await invoke(body, incomingHeaders);
3771
+ try {
3772
+ const wanted = body?._tierModel ? String(body._tierModel).split("/").pop().toLowerCase() : null;
3773
+ const _rawServed = res?.json?._providerModel || res?.json?.model;
3774
+ const served = _rawServed ? String(_rawServed).split("/").pop().toLowerCase() : null;
3775
+ if (wanted && served && served !== wanted && !served.startsWith(wanted) && !wanted.startsWith(served)) {
3776
+ logger.warn({ provider, requested: body._tierModel, served: _rawServed }, "[ModelCheck] provider served a DIFFERENT model than requested");
3777
+ }
3778
+ } catch { /* never block */ }
3779
+ return res;
3675
3780
  }
3676
3781
 
3782
+
3677
3783
  async function invokeModel(body, options = {}) {
3678
3784
  const { determineProviderSmart, isFallbackEnabled, getFallbackProvider } = require("./routing");
3679
3785
  const metricsCollector = getMetricsCollector();
@@ -3711,6 +3817,11 @@ async function invokeModel(body, options = {}) {
3711
3817
  // TaskBand stamp for the continuation telemetry columns
3712
3818
  // (telemetry.taskbandFields reads routingResult.taskband).
3713
3819
  taskband: body._taskband ?? null,
3820
+ // Jev verdict (telemetry.jevFields reads routingResult.jev).
3821
+ jev: body._jev ?? null,
3822
+ engine: body._engine ?? null,
3823
+ prev_outcome: body._prevOutcome ?? null,
3824
+ gate: body._gate ?? null,
3714
3825
  // WS4 — off-policy evaluation from telemetry alone requires
3715
3826
  // propensity + candidates on every row. Deterministic default is
3716
3827
  // 1.0 with a single-entry candidate list matching the served pair.
@@ -3836,6 +3947,24 @@ async function invokeModel(body, options = {}) {
3836
3947
  return await invokeProvider(initialProvider, body, incomingHeaders);
3837
3948
  });
3838
3949
 
3950
+ // Grounding check on completion claims (observe mode: telemetry + log).
3951
+ // Only runs when the reply claims the task is done; ~25 ms/claim on CPU.
3952
+ if (process.env.LYNKR_GROUNDING && process.env.LYNKR_GROUNDING !== 'off' && result?.ok !== false && Array.isArray(result?.json?.content)) {
3953
+ try {
3954
+ const grounding = require('../routing/grounding');
3955
+ const replyText = result.json.content.filter((b) => b && b.type === 'text' && typeof b.text === 'string').map((b) => b.text).join('\n');
3956
+ if (grounding.extractClaims(replyText).claimsDone) {
3957
+ const g = await grounding.check({ replyText, messages: body.messages });
3958
+ routingResult.grounding = { verdict: g.verdict, maxContradiction: g.maxContradiction ?? null, reason: g.reason ?? null };
3959
+ if (g.verdict === 'contradicted' || g.verdict === 'unverified') {
3960
+ logger.info({ provider: initialProvider, model: routingResult.model, verdict: g.verdict, contradiction: g.maxContradiction, pairs: (g.pairs || []).length }, '[Grounding] completion claim not supported by evidence');
3961
+ }
3962
+ }
3963
+ } catch (err) {
3964
+ logger.debug({ err: err.message }, '[Grounding] check failed (ignored)');
3965
+ }
3966
+ }
3967
+
3839
3968
  const latency = Date.now() - startTime;
3840
3969
  metricsCollector.recordProviderSuccess(initialProvider, latency);
3841
3970
  metricsCollector.recordDatabricksRequest(true, retries);
@@ -3928,6 +4057,7 @@ async function invokeModel(body, options = {}) {
3928
4057
  pinned: routingResult.pinned ? 1 : 0,
3929
4058
  switch_reason: routingResult.switch_reason ?? null,
3930
4059
  ...telemetry.jevFields(routingResult),
4060
+ ...telemetry.engineFields(routingResult),
3931
4061
  ...telemetry.taskbandFields(routingResult),
3932
4062
  cache_decision: routingResult._cacheDecision ?? null,
3933
4063
  cache_read_tokens: result.json?.usage?.cache_read_input_tokens ?? null,
@@ -4165,6 +4295,7 @@ async function invokeModel(body, options = {}) {
4165
4295
  pinned: routingResult.pinned ? 1 : 0,
4166
4296
  switch_reason: routingResult.switch_reason ?? null,
4167
4297
  ...telemetry.jevFields(routingResult),
4298
+ ...telemetry.engineFields(routingResult),
4168
4299
  ...telemetry.taskbandFields(routingResult),
4169
4300
  cache_decision: routingResult._cacheDecision ?? null,
4170
4301
  });
@@ -4280,6 +4411,7 @@ async function invokeModel(body, options = {}) {
4280
4411
  pinned: routingResult.pinned ? 1 : 0,
4281
4412
  switch_reason: routingResult.switch_reason ?? null,
4282
4413
  ...telemetry.jevFields(routingResult),
4414
+ ...telemetry.engineFields(routingResult),
4283
4415
  ...telemetry.taskbandFields(routingResult),
4284
4416
  cache_decision: routingResult._cacheDecision ?? null,
4285
4417
  });
@@ -4379,6 +4511,7 @@ async function invokeModel(body, options = {}) {
4379
4511
  pinned: routingResult.pinned ? 1 : 0,
4380
4512
  switch_reason: routingResult.switch_reason ?? null,
4381
4513
  ...telemetry.jevFields(routingResult),
4514
+ ...telemetry.engineFields(routingResult),
4382
4515
  ...telemetry.taskbandFields(routingResult),
4383
4516
  cache_decision: routingResult._cacheDecision ?? null,
4384
4517
  cache_read_tokens: fallbackResult.json?.usage?.cache_read_input_tokens ?? null,
@@ -4442,6 +4575,7 @@ async function invokeModel(body, options = {}) {
4442
4575
  pinned: routingResult.pinned ? 1 : 0,
4443
4576
  switch_reason: routingResult.switch_reason ?? null,
4444
4577
  ...telemetry.jevFields(routingResult),
4578
+ ...telemetry.engineFields(routingResult),
4445
4579
  ...telemetry.taskbandFields(routingResult),
4446
4580
  cache_decision: routingResult._cacheDecision ?? null,
4447
4581
  });
@@ -82,6 +82,7 @@ function appendToSystem(system, text) {
82
82
  */
83
83
  function injectFormatGuard(body, opts = {}) {
84
84
  if (!body) return body;
85
+ if (process.env.FMT_GUARD_ENABLED === "false") return body;
85
86
  const { provider, model } = opts;
86
87
  if (producesCleanMarkdown(provider, model)) return body;
87
88
 
@@ -2312,6 +2312,8 @@ IMPORTANT TOOL USAGE RULES:
2312
2312
  const { extractToolCallsFromText } = require("../clients/xml-tool-extractor");
2313
2313
  for (const block of contentArray) {
2314
2314
  if (block?.type === "text" && block?.text) {
2315
+ const _bt = block.text.trim();
2316
+ if (_bt.startsWith("{") && _bt.endsWith("}")) continue;
2315
2317
  const extracted = extractToolCallsFromText(block.text);
2316
2318
  if (extracted.toolCalls.length > 0) {
2317
2319
  toolCalls = extracted.toolCalls;
@@ -2338,7 +2340,8 @@ IMPORTANT TOOL USAGE RULES:
2338
2340
  toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
2339
2341
 
2340
2342
  // Extract tool calls embedded as XML/text in content (Minimax, Qwen, GLM, Llama, etc.)
2341
- if (toolCalls.length === 0 && typeof message.content === "string" && message.content.trim()) {
2343
+ if (toolCalls.length === 0 && typeof message.content === "string" && message.content.trim()
2344
+ && !(message.content.trim().startsWith("{") && message.content.trim().endsWith("}"))) {
2342
2345
  const { extractToolCallsFromText } = require("../clients/xml-tool-extractor");
2343
2346
  const extracted = extractToolCallsFromText(message.content);
2344
2347
  if (extracted.toolCalls.length > 0) {
@@ -2539,6 +2542,10 @@ IMPORTANT TOOL USAGE RULES:
2539
2542
  if (Array.isArray(anthropicPayload?.content)) {
2540
2543
  anthropicPayload.content = policy.sanitiseContent(anthropicPayload.content);
2541
2544
  }
2545
+ } else if ((actualProvider === "openrouter" || actualProvider === "edenai")
2546
+ && databricksResponse.json?.type === "message" && Array.isArray(databricksResponse.json?.content)) {
2547
+ anthropicPayload = databricksResponse.json;
2548
+ anthropicPayload.content = policy.sanitiseContent(anthropicPayload.content);
2542
2549
  } else if (actualProvider === "openrouter" || actualProvider === "edenai") {
2543
2550
  const { convertOpenRouterResponseToAnthropic } = require("../clients/openrouter-utils");
2544
2551
 
@@ -3126,7 +3133,9 @@ async function processMessage({ payload, headers, session, cwd, options = {} })
3126
3133
 
3127
3134
  let cacheKey = null;
3128
3135
  let cachedResponse = null;
3129
- if (promptCache.isEnabled()) {
3136
+ const _structuredAgentRequest = !!(cleanPayload?.output_format || cleanPayload?.response_format)
3137
+ && process.env.LYNKR_CACHE_BYPASS_STRUCTURED !== "false";
3138
+ if (promptCache.isEnabled() && !_structuredAgentRequest) {
3130
3139
  // cleanPayload is already a deep clone from sanitizePayload, no need to clone again
3131
3140
  const { key, entry } = promptCache.lookup(cleanPayload);
3132
3141
  pTimer.mark("cacheCheck");