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
package/src/api/openai-router.js
CHANGED
|
@@ -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";
|