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.
Files changed (96) hide show
  1. package/README.md +5 -1
  2. package/bin/cli.js +14 -6
  3. package/bin/lynkr-desktop-token.js +108 -0
  4. package/bin/lynkr-init.js +13 -1
  5. package/bin/lynkr-restart.js +125 -0
  6. package/bin/opencode-setup.js +175 -0
  7. package/bin/run.js +92 -0
  8. package/config/model-capabilities.json +20 -0
  9. package/config/model-capability-seeds.json +75 -0
  10. package/config/model-tiers.json +13 -1
  11. package/package.json +16 -22
  12. package/scripts/claude-desktop.js +212 -0
  13. package/scripts/ope-report.js +78 -0
  14. package/scripts/seed-capabilities.js +230 -0
  15. package/src/api/claude-desktop-gateway.js +123 -0
  16. package/src/api/health.js +0 -1
  17. package/src/api/mcp-broker.js +149 -0
  18. package/src/api/middleware/budget-enforcer.js +77 -7
  19. package/src/api/middleware/budget.js +36 -2
  20. package/src/api/middleware/session.js +0 -0
  21. package/src/api/openai-router.js +210 -15
  22. package/src/api/providers-handler.js +16 -1
  23. package/src/api/router.js +73 -106
  24. package/src/auth-mode.js +40 -4
  25. package/src/budget/index.js +86 -0
  26. package/src/cache/embeddings.js +125 -35
  27. package/src/cache/onnx-embedder.js +92 -0
  28. package/src/cache/prompt.js +0 -7
  29. package/src/clients/bedrock-utils.js +2 -1
  30. package/src/clients/codex-process.js +1 -1
  31. package/src/clients/codex-utils.js +0 -2
  32. package/src/clients/databricks.js +337 -46
  33. package/src/clients/gpt-utils.js +84 -8
  34. package/src/clients/health-probe.js +157 -0
  35. package/src/clients/openai-format.js +73 -23
  36. package/src/clients/provider-capabilities.js +27 -2
  37. package/src/clients/resilience.js +0 -1
  38. package/src/clients/responses-format.js +0 -56
  39. package/src/clients/routing.js +1 -1
  40. package/src/config/index.js +13 -2
  41. package/src/context/budget.js +1 -1
  42. package/src/context/compression.js +127 -19
  43. package/src/context/tool-result-compressor.js +3 -5
  44. package/src/dashboard/api.js +1 -1
  45. package/src/logger/oversized-error-stream.js +1 -1
  46. package/src/memory/extractor.js +3 -3
  47. package/src/memory/search.js +2 -2
  48. package/src/memory/store.js +9 -24
  49. package/src/memory/tools.js +14 -0
  50. package/src/observability/metrics.js +52 -3
  51. package/src/observability/otel.js +180 -0
  52. package/src/orchestrator/azure-responses-sse.js +223 -0
  53. package/src/orchestrator/index.js +216 -212
  54. package/src/orchestrator/passthrough-stream.js +7 -1
  55. package/src/orchestrator/sse-transformer.js +130 -13
  56. package/src/policy/safe-commands.js +0 -1
  57. package/src/routing/bandit.js +39 -0
  58. package/src/routing/calibration.js +21 -1
  59. package/src/routing/capabilities.js +88 -0
  60. package/src/routing/capability-seeds/benchmarks-dir.js +58 -0
  61. package/src/routing/capability-seeds/family-heuristics.js +77 -0
  62. package/src/routing/capability-seeds/family.js +105 -0
  63. package/src/routing/capability-seeds/models-dev.js +64 -0
  64. package/src/routing/capability-seeds/normalize.js +160 -0
  65. package/src/routing/capability-seeds/registry.js +125 -0
  66. package/src/routing/capability-seeds/sources.js +69 -0
  67. package/src/routing/capability-seeds/swebench.js +66 -0
  68. package/src/routing/client-profiles.js +26 -0
  69. package/src/routing/complexity-analyzer.js +5 -8
  70. package/src/routing/confidence-scorer.js +2 -2
  71. package/src/routing/cost-optimizer.js +1 -2
  72. package/src/routing/decide.js +149 -0
  73. package/src/routing/deescalator.js +38 -1
  74. package/src/routing/embedding-cache.js +0 -1
  75. package/src/routing/index.js +202 -86
  76. package/src/routing/intent-score.js +0 -1
  77. package/src/routing/knn-router.js +27 -5
  78. package/src/routing/latency-tracker.js +1 -1
  79. package/src/routing/model-registry.js +25 -0
  80. package/src/routing/model-slots.js +65 -0
  81. package/src/routing/model-tiers.js +2 -0
  82. package/src/routing/ope.js +255 -0
  83. package/src/routing/openai-model-slots.js +82 -0
  84. package/src/routing/regret-estimator.js +1 -1
  85. package/src/routing/shortfall.js +290 -0
  86. package/src/routing/stuck-detector.js +127 -0
  87. package/src/routing/telemetry.js +11 -2
  88. package/src/server.js +41 -3
  89. package/src/stores/file-store.js +5 -1
  90. package/src/tools/web-search-exec.js +182 -0
  91. package/src/training/trajectory-compressor.js +1 -1
  92. package/src/workers/pool.js +1 -1
  93. package/src/workers/worker.js +1 -1
  94. package/scripts/convert-routellm.py +0 -105
  95. package/src/diff/comments.js +0 -153
  96. package/src/policy/web-fallback.js +0 -33
@@ -28,6 +28,32 @@ const clientProfiles = require("../routing/client-profiles");
28
28
 
29
29
  const router = express.Router();
30
30
 
31
+ /**
32
+ * Served-model decision headers for the OpenAI-compat ingress.
33
+ *
34
+ * The Anthropic ingress has carried X-Lynkr-* decision headers for a long
35
+ * time; this path never did — the same single-ingress gap as issue #100.
36
+ * X-Lynkr-Context-Window is the served model's REAL context window (the
37
+ * workweave pattern): behind a virtual model name, tier routing serves
38
+ * models with different windows per request, and this header is the
39
+ * client's authoritative per-turn compaction budget. Omitted (never
40
+ * guessed) for models the registry doesn't know.
41
+ *
42
+ * Safe on streaming paths: no-op once headers have flushed.
43
+ *
44
+ * @param {object} res - Express response
45
+ * @param {string|null} servedModel - model from the orchestrator result body
46
+ */
47
+ function setServedModelHeaders(res, servedModel) {
48
+ if (!servedModel || res.headersSent) return;
49
+ try {
50
+ res.set("X-Lynkr-Model", servedModel);
51
+ const { contextWindowFor } = require("../routing/model-registry");
52
+ const contextWindow = contextWindowFor(servedModel);
53
+ if (contextWindow) res.set("X-Lynkr-Context-Window", String(contextWindow));
54
+ } catch { /* headers are best-effort — never fail a response over them */ }
55
+ }
56
+
31
57
  /**
32
58
  * Resolve the model name for OpenAI responses.
33
59
  * In OpenClaw mode, returns the actual provider/model from routing metadata.
@@ -355,12 +381,36 @@ async function forwardAnthropicStreamAsOpenAIChunks(res, stream, requestedModel)
355
381
  const created = Math.floor(Date.now() / 1000);
356
382
  const model = requestedModel || "lynkr";
357
383
 
358
- const chunk = (delta, finish = null) => `data: ${JSON.stringify({
384
+ const chunk = (delta, finish = null, usage = null) => `data: ${JSON.stringify({
359
385
  id, object: "chat.completion.chunk", created, model,
360
386
  system_fingerprint: "fp_lynkr",
361
387
  choices: [{ index: 0, delta, logprobs: null, finish_reason: finish }],
388
+ ...(usage ? { usage } : {}),
362
389
  })}\n\n`;
363
390
 
391
+ // Usage accounting (issue: opencode sidebar showed wrong token counts —
392
+ // this path never emitted usage at all). Anthropic sends input/cache
393
+ // tokens in message_start and output tokens in message_delta; accumulate
394
+ // and attach to the final chunk in OpenAI shape.
395
+ const usageAcc = { input: 0, cacheRead: 0, cacheWrite: 0, output: 0 };
396
+ const captureUsage = (u) => {
397
+ if (!u) return;
398
+ if (u.input_tokens != null) usageAcc.input = u.input_tokens;
399
+ if (u.cache_read_input_tokens != null) usageAcc.cacheRead = u.cache_read_input_tokens;
400
+ if (u.cache_creation_input_tokens != null) usageAcc.cacheWrite = u.cache_creation_input_tokens;
401
+ if (u.output_tokens != null) usageAcc.output = u.output_tokens;
402
+ };
403
+ const finalUsage = () => {
404
+ const promptTokens = usageAcc.input + usageAcc.cacheRead + usageAcc.cacheWrite;
405
+ const u = {
406
+ prompt_tokens: promptTokens,
407
+ completion_tokens: usageAcc.output,
408
+ total_tokens: promptTokens + usageAcc.output,
409
+ };
410
+ if (usageAcc.cacheRead > 0) u.prompt_tokens_details = { cached_tokens: usageAcc.cacheRead };
411
+ return u;
412
+ };
413
+
364
414
  const mapStop = (r) => r === "tool_use" ? "tool_calls" : r === "max_tokens" ? "length" : "stop";
365
415
 
366
416
  // Normalize Node Readable / web ReadableStream / async iterable to chunks.
@@ -394,6 +444,7 @@ async function forwardAnthropicStreamAsOpenAIChunks(res, stream, requestedModel)
394
444
 
395
445
  switch (ev.type) {
396
446
  case "message_start":
447
+ captureUsage(ev.message?.usage);
397
448
  if (!started) { started = true; res.write(chunk({ role: "assistant", content: "" })); }
398
449
  break;
399
450
  case "content_block_start":
@@ -419,10 +470,11 @@ async function forwardAnthropicStreamAsOpenAIChunks(res, stream, requestedModel)
419
470
  break;
420
471
  }
421
472
  case "message_delta":
473
+ captureUsage(ev.usage);
422
474
  if (ev.delta?.stop_reason) finishReason = mapStop(ev.delta.stop_reason);
423
475
  break;
424
476
  case "message_stop":
425
- res.write(chunk({}, finishReason || "stop"));
477
+ res.write(chunk({}, finishReason || "stop", finalUsage()));
426
478
  res.write("data: [DONE]\n\n");
427
479
  res.end();
428
480
  return;
@@ -447,7 +499,7 @@ async function forwardAnthropicStreamAsOpenAIChunks(res, stream, requestedModel)
447
499
  // truncated stream: send an error chunk, never a fabricated "stop".
448
500
  if (!res.writableEnded) {
449
501
  if (finishReason) {
450
- res.write(chunk({}, finishReason));
502
+ res.write(chunk({}, finishReason, finalUsage()));
451
503
  res.write("data: [DONE]\n\n");
452
504
  } else {
453
505
  logger.warn("Upstream stream ended without message_stop — sending error chunk to OpenAI-surface client");
@@ -542,6 +594,35 @@ router.post("/chat/completions", async (req, res) => {
542
594
  logger.debug({ err: err.message }, "[OpenAI Router] client profile detection failed");
543
595
  }
544
596
 
597
+ // Explicit model/effort pin: Codex's model picker (or any OpenAI-format
598
+ // caller) can name a real OpenAI model + reasoning.effort combo that
599
+ // resolveTierForOpenAIModel recognizes. Mirrors router.js's model-id-pin
600
+ // block for the Anthropic /v1/messages route — this one is needed
601
+ // separately because these OpenAI-shaped handlers call
602
+ // orchestrator.processMessage() directly and never go through router.js's
603
+ // pin/classifier code at all. See ../routing/openai-model-slots.js.
604
+ try {
605
+ const { resolveTierForOpenAIModel } = require("../routing/openai-model-slots");
606
+ const _effort = req.body?.reasoning?.effort ?? req.body?.reasoning_effort ?? null;
607
+ const _pinTier = resolveTierForOpenAIModel(req.body?.model, _effort);
608
+ if (_pinTier) {
609
+ const { getModelTierSelector } = require("../routing");
610
+ const _sel = getModelTierSelector().selectModel(_pinTier, null);
611
+ anthropicRequest._forceProvider = _sel.provider;
612
+ if (_sel.model) anthropicRequest._tierModel = _sel.model;
613
+ anthropicRequest._tierName = _pinTier;
614
+ anthropicRequest._forcedMethod = "openai_model_effort_pin";
615
+ logger.debug({
616
+ model: req.body?.model,
617
+ effort: _effort,
618
+ tier: _pinTier,
619
+ provider: _sel.provider,
620
+ }, "[OpenAI Router] Explicit model/effort pin — scoring bypassed");
621
+ }
622
+ } catch (err) {
623
+ logger.debug({ err: err.message }, "[OpenAI Router] model/effort pin failed");
624
+ }
625
+
545
626
  const session = getSession(sessionId);
546
627
 
547
628
  if (req.body.stream) {
@@ -565,9 +646,11 @@ router.post("/chat/completions", async (req, res) => {
565
646
  session: session,
566
647
  options: {
567
648
  maxSteps: req.body?.max_steps,
568
- clientWantsStream: true
649
+ clientWantsStream: true,
650
+ tenantPolicy: res.locals?.tenantPolicy || null
569
651
  }
570
652
  });
653
+ setServedModelHeaders(res, result?.body?.model);
571
654
 
572
655
  // Phase 2b live stream: reshape Anthropic SSE events into OpenAI
573
656
  // chat.completion.chunk events in flight — first token reaches the
@@ -734,11 +817,33 @@ router.post("/chat/completions", async (req, res) => {
734
817
  delta: {},
735
818
  logprobs: null,
736
819
  finish_reason: openaiResponse.choices[0].finish_reason
737
- }]
820
+ }],
821
+ // Clients (opencode et al.) read token accounting from the final
822
+ // chunk — omitting it here left streamed turns uncounted.
823
+ usage: openaiResponse.usage
738
824
  };
739
825
 
740
826
  logger.debug({ chunk: "finish", finishReason: openaiResponse.choices[0].finish_reason }, "Sending finish chunk");
741
827
  res.write(`data: ${JSON.stringify(finishChunk)}\n\n`);
828
+
829
+ // Trailing usage chunk (choices: [], usage populated) — same shape
830
+ // as the Phase 2b live-stream path above. Previously this buffered
831
+ // synthesis only ever logged openaiResponse.usage (see below) and
832
+ // never wrote it to the client, so streaming clients on this path
833
+ // saw zero token/context usage too.
834
+ if (openaiResponse.usage) {
835
+ const usageChunk = {
836
+ id: openaiResponse.id,
837
+ object: "chat.completion.chunk",
838
+ created: openaiResponse.created,
839
+ model: streamModel,
840
+ system_fingerprint: "fp_lynkr",
841
+ choices: [],
842
+ usage: openaiResponse.usage,
843
+ };
844
+ res.write(`data: ${JSON.stringify(usageChunk)}\n\n`);
845
+ }
846
+
742
847
  res.write("data: [DONE]\n\n");
743
848
 
744
849
  logger.debug({ contentLength: content.length, contentPreview: content.substring(0, 50) }, "=== SSE STREAM COMPLETE ===");
@@ -782,9 +887,11 @@ router.post("/chat/completions", async (req, res) => {
782
887
  headers: req.headers,
783
888
  session: session,
784
889
  options: {
785
- maxSteps: req.body?.max_steps
890
+ maxSteps: req.body?.max_steps,
891
+ tenantPolicy: res.locals?.tenantPolicy || null
786
892
  }
787
893
  });
894
+ setServedModelHeaders(res, result?.body?.model);
788
895
 
789
896
  logger.debug({
790
897
  resultKeys: Object.keys(result || {}),
@@ -839,6 +946,24 @@ router.post("/chat/completions", async (req, res) => {
839
946
  finishReason: openaiResponse.choices[0].finish_reason
840
947
  }, "=== OPENAI CHAT COMPLETION RESPONSE ===");
841
948
 
949
+ // Surface the routed reality: which model actually answered and what
950
+ // it cost (LiteLLM-style headers). Clients that can't price a dynamic
951
+ // router (opencode #24113) get the truth out-of-band.
952
+ const routingMeta = result.body?._routingMeta;
953
+ if (routingMeta?.model) {
954
+ res.set("x-lynkr-routed-model", String(routingMeta.model));
955
+ if (routingMeta.provider) res.set("x-lynkr-routed-provider", String(routingMeta.provider));
956
+ try {
957
+ const { computeCostUsd } = require("../clients/databricks");
958
+ const cost = computeCostUsd(
959
+ routingMeta.model,
960
+ result.body?.usage?.input_tokens || 0,
961
+ result.body?.usage?.output_tokens || 0
962
+ );
963
+ if (cost != null) res.set("x-lynkr-response-cost", cost.toFixed(6));
964
+ } catch { /* cost pricing unavailable — headers are best-effort */ }
965
+ }
966
+
842
967
  res.json(openaiResponse);
843
968
  }
844
969
 
@@ -1018,6 +1143,19 @@ function getConfiguredProviders() {
1018
1143
  });
1019
1144
  }
1020
1145
 
1146
+ if (config.fireworks?.apiKey) {
1147
+ providers.push({
1148
+ name: "fireworks",
1149
+ type: "fireworks-ai",
1150
+ models: [
1151
+ config.fireworks.model || "accounts/fireworks/models/kimi-k2-instruct-0905",
1152
+ "accounts/fireworks/models/kimi-k2-instruct-0905",
1153
+ "accounts/fireworks/models/glm-5p2",
1154
+ "accounts/fireworks/models/deepseek-v3p1"
1155
+ ]
1156
+ });
1157
+ }
1158
+
1021
1159
  if (config.vertex?.projectId) {
1022
1160
  providers.push({
1023
1161
  name: "vertex",
@@ -1043,7 +1181,6 @@ function getConfiguredProviders() {
1043
1181
  router.get("/models", (req, res) => {
1044
1182
  try {
1045
1183
  const providers = getConfiguredProviders();
1046
- const primaryProvider = config.modelProvider?.type || "databricks";
1047
1184
  const timestamp = Math.floor(Date.now() / 1000);
1048
1185
  const models = [];
1049
1186
  const seenModelIds = new Set();
@@ -1480,7 +1617,7 @@ router.post("/embeddings", async (req, res) => {
1480
1617
  const startTime = Date.now();
1481
1618
 
1482
1619
  try {
1483
- const { input, model, encoding_format } = req.body;
1620
+ const { input, model, encoding_format: encodingFormat } = req.body;
1484
1621
 
1485
1622
  if (!input) {
1486
1623
  return res.status(400).json({
@@ -1492,6 +1629,16 @@ router.post("/embeddings", async (req, res) => {
1492
1629
  });
1493
1630
  }
1494
1631
 
1632
+ if (encodingFormat && encodingFormat !== "float" && encodingFormat !== "base64") {
1633
+ return res.status(400).json({
1634
+ error: {
1635
+ message: `Invalid encoding_format: ${encodingFormat}. Must be "float" or "base64".`,
1636
+ type: "invalid_request_error",
1637
+ code: "invalid_parameter"
1638
+ }
1639
+ });
1640
+ }
1641
+
1495
1642
  const inputs = Array.isArray(input) ? input : [input];
1496
1643
 
1497
1644
  logger.debug({
@@ -1565,6 +1712,20 @@ router.post("/embeddings", async (req, res) => {
1565
1712
  totalTokens: embeddingResponse.usage?.total_tokens || 0
1566
1713
  }, "=== EMBEDDINGS RESPONSE ===");
1567
1714
 
1715
+ // Providers return float arrays; honor encoding_format=base64 (OpenAI
1716
+ // spec: base64 of the little-endian float32 buffer) instead of silently
1717
+ // returning floats for every request.
1718
+ if (encodingFormat === "base64" && Array.isArray(embeddingResponse.data)) {
1719
+ for (const item of embeddingResponse.data) {
1720
+ if (Array.isArray(item.embedding)) {
1721
+ item.embedding = Buffer.from(new Float32Array(item.embedding).buffer).toString("base64");
1722
+ }
1723
+ }
1724
+ if ("encoding_format" in embeddingResponse) {
1725
+ embeddingResponse.encoding_format = "base64";
1726
+ }
1727
+ }
1728
+
1568
1729
  res.json(embeddingResponse);
1569
1730
 
1570
1731
  } catch (error) {
@@ -1761,6 +1922,32 @@ router.post("/responses", async (req, res) => {
1761
1922
  logger.debug({ err: err.message }, "[OpenAI Router] client profile detection failed");
1762
1923
  }
1763
1924
 
1925
+ // Explicit model/effort pin — see the identical block in /chat/completions
1926
+ // above for the full rationale. This is the route Codex Desktop actually
1927
+ // uses (wire_api="responses"), so this is the one that matters for its
1928
+ // model picker specifically.
1929
+ try {
1930
+ const { resolveTierForOpenAIModel } = require("../routing/openai-model-slots");
1931
+ const _effort = req.body?.reasoning?.effort ?? req.body?.reasoning_effort ?? null;
1932
+ const _pinTier = resolveTierForOpenAIModel(req.body?.model, _effort);
1933
+ if (_pinTier) {
1934
+ const { getModelTierSelector } = require("../routing");
1935
+ const _sel = getModelTierSelector().selectModel(_pinTier, null);
1936
+ anthropicRequest._forceProvider = _sel.provider;
1937
+ if (_sel.model) anthropicRequest._tierModel = _sel.model;
1938
+ anthropicRequest._tierName = _pinTier;
1939
+ anthropicRequest._forcedMethod = "openai_model_effort_pin";
1940
+ logger.debug({
1941
+ model: req.body?.model,
1942
+ effort: _effort,
1943
+ tier: _pinTier,
1944
+ provider: _sel.provider,
1945
+ }, "[OpenAI Router] Explicit model/effort pin (responses) — scoring bypassed");
1946
+ }
1947
+ } catch (err) {
1948
+ logger.debug({ err: err.message }, "[OpenAI Router] model/effort pin failed (responses)");
1949
+ }
1950
+
1764
1951
  const session = getSession(sessionId);
1765
1952
 
1766
1953
  if (req.body.stream) {
@@ -1775,7 +1962,7 @@ router.post("/responses", async (req, res) => {
1775
1962
 
1776
1963
  // SSE comment keepalive (spec-compliant, ignored by all clients)
1777
1964
  const keepalive = setInterval(() => {
1778
- try { res.write(`: keepalive\n\n`); } catch {}
1965
+ try { res.write(`: keepalive\n\n`); } catch { /* client gone — best-effort keepalive */ }
1779
1966
  }, 2000);
1780
1967
 
1781
1968
  let result;
@@ -1785,9 +1972,11 @@ router.post("/responses", async (req, res) => {
1785
1972
  headers: req.headers,
1786
1973
  session: session,
1787
1974
  options: {
1788
- maxSteps: req.body?.max_steps
1975
+ maxSteps: req.body?.max_steps,
1976
+ tenantPolicy: res.locals?.tenantPolicy || null
1789
1977
  }
1790
1978
  });
1979
+ setServedModelHeaders(res, result?.body?.model);
1791
1980
  } finally {
1792
1981
  clearInterval(keepalive);
1793
1982
  }
@@ -1882,21 +2071,25 @@ router.post("/responses", async (req, res) => {
1882
2071
  if (taskArgs.prompt && taskArgs.prompt.toLowerCase().includes("read")) {
1883
2072
  exploreCmd = "cat README.md 2>/dev/null && cat package.json 2>/dev/null";
1884
2073
  }
1885
- } catch {}
2074
+ } catch (err) {
2075
+ logger.debug({ err: err?.message }, "Failed to parse task tool arguments — using default explore command");
2076
+ }
1886
2077
  }
1887
2078
  converted.push({ ...tc, function: { name: "shell", arguments: JSON.stringify({ command: ["bash", "-c", exploreCmd] }) } });
1888
2079
  continue;
1889
2080
  }
1890
2081
 
1891
2082
  let args = {};
1892
- try { args = JSON.parse(tc.function?.arguments || "{}"); } catch {}
2083
+ try { args = JSON.parse(tc.function?.arguments || "{}"); } catch (err) {
2084
+ logger.debug({ err: err?.message }, "Failed to parse tool call arguments — treating as empty object");
2085
+ }
1893
2086
 
1894
2087
  // Already a shell command — normalise to array format
1895
2088
  if (ln === "shell" || ln === "bash") {
1896
2089
  let cmd = args.command || "";
1897
2090
  // Handle string-encoded arrays: '["bash","-c","ls"]'
1898
2091
  if (typeof cmd === "string" && cmd.trim().startsWith("[")) {
1899
- try { cmd = JSON.parse(cmd); } catch {}
2092
+ try { cmd = JSON.parse(cmd); } catch { /* not a JSON array — keep as plain string */ }
1900
2093
  }
1901
2094
  // Extract the actual command from ["bash", "-c", "actual command"]
1902
2095
  if (Array.isArray(cmd) && cmd.length >= 3 && cmd[0] === "bash" && cmd[1] === "-c") {
@@ -2107,7 +2300,7 @@ router.post("/responses", async (req, res) => {
2107
2300
  sequence_number: 1
2108
2301
  })}\n\n`
2109
2302
  ].join("");
2110
- try { res.end(errorPayload); } catch { try { res.end(); } catch {} }
2303
+ try { res.end(errorPayload); } catch { try { res.end(); } catch { /* res already ended — best-effort */ } }
2111
2304
  }
2112
2305
 
2113
2306
  } else {
@@ -2118,9 +2311,11 @@ router.post("/responses", async (req, res) => {
2118
2311
  headers: req.headers,
2119
2312
  session: session,
2120
2313
  options: {
2121
- maxSteps: req.body?.max_steps
2314
+ maxSteps: req.body?.max_steps,
2315
+ tenantPolicy: res.locals?.tenantPolicy || null
2122
2316
  }
2123
2317
  });
2318
+ setServedModelHeaders(res, result?.body?.model);
2124
2319
 
2125
2320
  const chatResponse = convertAnthropicToOpenAI(result.body, resolveResponseModel(result.body, req.body.model));
2126
2321
  const responsesResponse = convertChatToResponses(chatResponse);
@@ -21,7 +21,6 @@ const router = express.Router();
21
21
  */
22
22
  function getConfiguredProviders() {
23
23
  const providers = [];
24
- const timestamp = Math.floor(Date.now() / 1000);
25
24
 
26
25
  // Check Databricks
27
26
  if (config.databricks?.url && config.databricks?.apiKey) {
@@ -238,6 +237,22 @@ function getConfiguredProviders() {
238
237
  });
239
238
  }
240
239
 
240
+ // Check Fireworks AI (serverless open models)
241
+ if (config.fireworks?.apiKey) {
242
+ providers.push({
243
+ name: "fireworks",
244
+ type: "fireworks-ai",
245
+ baseUrl: config.fireworks.endpoint || "https://api.fireworks.ai/inference/v1",
246
+ enabled: true,
247
+ models: [
248
+ { id: config.fireworks.model || "accounts/fireworks/models/kimi-k2-instruct-0905", name: "Configured Model" },
249
+ { id: "accounts/fireworks/models/kimi-k2-instruct-0905", name: "Kimi K2 Instruct" },
250
+ { id: "accounts/fireworks/models/glm-5p2", name: "GLM 5.2" },
251
+ { id: "accounts/fireworks/models/deepseek-v3p1", name: "DeepSeek V3.1" },
252
+ ]
253
+ });
254
+ }
255
+
241
256
  // Check Vertex AI (Google Cloud)
242
257
  if (config.vertex?.projectId) {
243
258
  const region = config.vertex.region || "us-east5";