lynkr 9.7.2 → 9.9.0
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 +29 -19
- package/bin/cli.js +11 -0
- package/bin/lynkr-init.js +14 -1
- package/bin/lynkr-usage.js +78 -0
- package/bin/wrap.js +60 -35
- package/config/difficulty-anchors.json +22 -0
- package/package.json +24 -3
- package/scripts/audit-log-reader.js +399 -0
- package/scripts/calibrate-thresholds.js +38 -157
- package/scripts/compact-dictionary.js +204 -0
- package/scripts/test-deduplication.js +448 -0
- package/scripts/ws7-anchor-replay.js +108 -0
- package/skills/lynkr/SKILL.md +195 -0
- package/src/agents/context-manager.js +18 -2
- package/src/agents/definitions/loader.js +90 -0
- package/src/agents/executor.js +24 -2
- package/src/agents/index.js +9 -1
- package/src/agents/parallel-coordinator.js +2 -2
- package/src/agents/reflector.js +11 -1
- package/src/api/middleware/loop-guard.js +87 -0
- package/src/api/middleware/request-logging.js +5 -64
- package/src/api/middleware/session.js +0 -0
- package/src/api/openai-router.js +120 -101
- package/src/api/providers-handler.js +27 -2
- package/src/api/router.js +450 -125
- package/src/budget/index.js +2 -19
- package/src/cache/semantic.js +9 -0
- package/src/clients/databricks.js +459 -146
- package/src/clients/gpt-utils.js +11 -105
- package/src/clients/openai-format.js +10 -3
- package/src/clients/openrouter-utils.js +49 -24
- package/src/clients/prompt-cache-injection.js +1 -0
- package/src/clients/provider-capabilities.js +1 -1
- package/src/clients/responses-format.js +34 -3
- package/src/clients/routing.js +15 -0
- package/src/config/index.js +36 -2
- package/src/context/gcf.js +275 -0
- package/src/context/tool-result-compressor.js +51 -9
- package/src/dashboard/api.js +1 -0
- package/src/logger/index.js +14 -1
- package/src/memory/search.js +12 -40
- package/src/memory/tools.js +3 -24
- package/src/orchestrator/bypass.js +4 -2
- package/src/orchestrator/index.js +144 -88
- package/src/routing/affinity-store.js +194 -0
- package/src/routing/agentic-detector.js +36 -6
- package/src/routing/bandit.js +25 -6
- package/src/routing/calibration.js +212 -0
- package/src/routing/client-profiles.js +292 -0
- package/src/routing/complexity-analyzer.js +48 -11
- package/src/routing/deescalator.js +148 -0
- package/src/routing/degradation.js +109 -0
- package/src/routing/feedback.js +157 -0
- package/src/routing/index.js +897 -87
- package/src/routing/intent-score.js +339 -0
- package/src/routing/interaction.js +3 -0
- package/src/routing/knn-router.js +70 -21
- package/src/routing/model-registry.js +28 -7
- package/src/routing/model-tiers.js +25 -2
- package/src/routing/reward-pipeline.js +68 -2
- package/src/routing/risk-analyzer.js +30 -1
- package/src/routing/risk-classifier.js +6 -2
- package/src/routing/session-affinity.js +162 -34
- package/src/routing/telemetry.js +298 -10
- package/src/routing/verifier.js +267 -0
- package/src/server.js +66 -21
- package/src/sessions/cleanup.js +17 -0
- package/src/tools/index.js +1 -15
- package/src/tools/smart-selection.js +10 -0
- package/src/tools/web-client.js +3 -3
- package/.eslintrc.cjs +0 -12
- package/benchmark-configs/litellm_config.yaml +0 -86
- package/benchmark-configs/lynkr.env +0 -48
- package/benchmark-configs/portkey-config.json +0 -60
- package/benchmark-configs/portkey-docker.sh +0 -23
- package/benchmark-tier-routing.js +0 -449
- package/funding.json +0 -110
- package/src/api/middleware/validation.js +0 -261
- package/src/routing/drift-monitor.js +0 -113
- package/src/workers/helpers.js +0 -185
package/src/api/openai-router.js
CHANGED
|
@@ -24,6 +24,7 @@ const {
|
|
|
24
24
|
convertAnthropicToOpenAI
|
|
25
25
|
} = require("../clients/openai-format");
|
|
26
26
|
const { IDE_SAFE_TOOLS } = require("../clients/standard-tools");
|
|
27
|
+
const clientProfiles = require("../routing/client-profiles");
|
|
27
28
|
|
|
28
29
|
const router = express.Router();
|
|
29
30
|
|
|
@@ -53,7 +54,6 @@ function detectClient(headers) {
|
|
|
53
54
|
const userAgent = (headers?.["user-agent"] || "").toLowerCase();
|
|
54
55
|
const clientHeader = (headers?.["x-client"] || headers?.["x-client-name"] || "").toLowerCase();
|
|
55
56
|
|
|
56
|
-
// Check user-agent and custom headers
|
|
57
57
|
if (userAgent.includes("codex") || clientHeader.includes("codex") || userAgent.includes("openai-codex")) {
|
|
58
58
|
return "codex";
|
|
59
59
|
}
|
|
@@ -297,14 +297,12 @@ function mapToolForClient(toolName, argsJson, clientType) {
|
|
|
297
297
|
|
|
298
298
|
const clientMappings = CLIENT_TOOL_MAPPINGS[clientType];
|
|
299
299
|
if (!clientMappings) {
|
|
300
|
-
// Unknown client - return as-is
|
|
301
300
|
return { name: toolName, arguments: argsJson };
|
|
302
301
|
}
|
|
303
302
|
|
|
304
303
|
const mapping = clientMappings[toolName];
|
|
305
304
|
if (mapping) {
|
|
306
305
|
const mappedArgs = mapping.mapArgs(args);
|
|
307
|
-
// Remove undefined values
|
|
308
306
|
Object.keys(mappedArgs).forEach(key => {
|
|
309
307
|
if (mappedArgs[key] === undefined) {
|
|
310
308
|
delete mappedArgs[key];
|
|
@@ -331,12 +329,23 @@ function mapToolForClient(toolName, argsJson, clientType) {
|
|
|
331
329
|
* OpenAI-compatible chat completions endpoint.
|
|
332
330
|
* Converts OpenAI format → Anthropic → processes → converts back to OpenAI format.
|
|
333
331
|
*/
|
|
332
|
+
// Visible routing badge (LYNKR_VISIBLE_ROUTING=true) for OpenAI-format
|
|
333
|
+
// clients (Codex, Cursor). Mirrors the Anthropic router's badge; sourced
|
|
334
|
+
// from the _routingMeta the orchestrator attaches to every response.
|
|
335
|
+
function lynkrBadge(resultBody) {
|
|
336
|
+
const config = require("../config");
|
|
337
|
+
if (!config.routing?.visibleInteraction) return "";
|
|
338
|
+
const m = resultBody?._routingMeta;
|
|
339
|
+
if (!m || !m.tier) return "";
|
|
340
|
+
const score = typeof m.score === "number" ? ` · score ${m.score}` : "";
|
|
341
|
+
return `*[Lynkr] ${m.tier} → ${m.model || "—"} (${m.provider || "—"})${score}*\n\n`;
|
|
342
|
+
}
|
|
343
|
+
|
|
334
344
|
router.post("/chat/completions", async (req, res) => {
|
|
335
345
|
const startTime = Date.now();
|
|
336
346
|
const sessionId = req.headers["x-session-id"] || req.headers["authorization"]?.split(" ")[1] || "openai-session";
|
|
337
347
|
|
|
338
348
|
try {
|
|
339
|
-
// Validate request body exists
|
|
340
349
|
if (!req.body || typeof req.body !== 'object') {
|
|
341
350
|
logger.error({ body: req.body, bodyType: typeof req.body }, "Invalid or missing request body");
|
|
342
351
|
return res.status(400).json({
|
|
@@ -348,7 +357,6 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
348
357
|
});
|
|
349
358
|
}
|
|
350
359
|
|
|
351
|
-
// Validate required fields
|
|
352
360
|
if (!req.body.messages || !Array.isArray(req.body.messages)) {
|
|
353
361
|
logger.error({ hasMessages: !!req.body.messages }, "Missing or invalid messages array");
|
|
354
362
|
return res.status(400).json({
|
|
@@ -360,32 +368,6 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
360
368
|
});
|
|
361
369
|
}
|
|
362
370
|
|
|
363
|
-
// DEBUG: Log full message details to diagnose Codex caching issue
|
|
364
|
-
const messagesSummary = (req.body.messages || []).map((m, i) => ({
|
|
365
|
-
index: i,
|
|
366
|
-
role: m.role,
|
|
367
|
-
contentPreview: typeof m.content === 'string'
|
|
368
|
-
? m.content.substring(0, 200)
|
|
369
|
-
: (m.content == null ? null : (JSON.stringify(m.content) ?? '').substring(0, 200))
|
|
370
|
-
}));
|
|
371
|
-
|
|
372
|
-
logger.debug({
|
|
373
|
-
endpoint: "/v1/chat/completions",
|
|
374
|
-
model: req.body.model,
|
|
375
|
-
messageCount: req.body.messages?.length,
|
|
376
|
-
stream: req.body.stream || false,
|
|
377
|
-
hasTools: !!req.body.tools,
|
|
378
|
-
toolCount: req.body.tools?.length || 0,
|
|
379
|
-
hasMessages: !!req.body.messages,
|
|
380
|
-
messagesType: typeof req.body.messages,
|
|
381
|
-
requestBodyKeys: Object.keys(req.body),
|
|
382
|
-
// Log first 500 chars of body for debugging
|
|
383
|
-
requestBodyPreview: JSON.stringify(req.body).substring(0, 500),
|
|
384
|
-
// DEBUG: Full messages breakdown
|
|
385
|
-
messages: messagesSummary
|
|
386
|
-
}, "=== OPENAI CHAT COMPLETION REQUEST ===");
|
|
387
|
-
|
|
388
|
-
// Convert OpenAI request to Anthropic format
|
|
389
371
|
const anthropicRequest = convertOpenAIToAnthropic(req.body);
|
|
390
372
|
|
|
391
373
|
// Inject tools if client didn't send any.
|
|
@@ -414,22 +396,35 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
414
396
|
}, "=== INJECTING TOOLS ===");
|
|
415
397
|
}
|
|
416
398
|
|
|
417
|
-
//
|
|
399
|
+
// WS3 — detect the client harness on the OpenAI-compat paths too (the
|
|
400
|
+
// /v1/messages router already does this). Runs after tool injection so
|
|
401
|
+
// the fingerprint sees the final tools array either way: goose/Cursor
|
|
402
|
+
// send their own loadout, tool-less clients get IDE_SAFE_TOOLS injected
|
|
403
|
+
// and fingerprint as claude-code. Without this the agentic detector
|
|
404
|
+
// counts the harness baseline as intent and floors every request —
|
|
405
|
+
// including "hi" — at the ITERATIVE minTier (COMPLEX).
|
|
406
|
+
try {
|
|
407
|
+
const profile = clientProfiles.detectClient({
|
|
408
|
+
headers: req.headers,
|
|
409
|
+
payload: anthropicRequest,
|
|
410
|
+
});
|
|
411
|
+
if (profile) anthropicRequest._clientProfile = profile;
|
|
412
|
+
} catch (err) {
|
|
413
|
+
logger.debug({ err: err.message }, "[OpenAI Router] client profile detection failed");
|
|
414
|
+
}
|
|
415
|
+
|
|
418
416
|
const session = getSession(sessionId);
|
|
419
417
|
|
|
420
|
-
// Handle streaming vs non-streaming
|
|
421
418
|
if (req.body.stream) {
|
|
422
|
-
// Set up SSE headers for streaming
|
|
423
419
|
res.setHeader("Content-Type", "text/event-stream");
|
|
424
420
|
res.setHeader("Cache-Control", "no-cache");
|
|
425
421
|
res.setHeader("Connection", "keep-alive");
|
|
426
422
|
res.setHeader("X-Accel-Buffering", "no"); // Prevent nginx buffering
|
|
427
|
-
res.flushHeaders();
|
|
423
|
+
res.flushHeaders();
|
|
428
424
|
|
|
429
425
|
try {
|
|
430
426
|
// For streaming, we need to handle it differently - convert to non-streaming temporarily
|
|
431
|
-
|
|
432
|
-
anthropicRequest.stream = false; // Force non-streaming from orchestrator
|
|
427
|
+
anthropicRequest.stream = false;
|
|
433
428
|
|
|
434
429
|
const result = await orchestrator.processMessage({
|
|
435
430
|
payload: anthropicRequest,
|
|
@@ -440,7 +435,6 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
440
435
|
}
|
|
441
436
|
});
|
|
442
437
|
|
|
443
|
-
// Check if we have a valid response body
|
|
444
438
|
logger.debug({
|
|
445
439
|
hasResult: !!result,
|
|
446
440
|
resultKeys: result ? Object.keys(result) : null,
|
|
@@ -459,11 +453,9 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
459
453
|
throw new Error("Invalid response from orchestrator");
|
|
460
454
|
}
|
|
461
455
|
|
|
462
|
-
// Convert to OpenAI format
|
|
463
456
|
const streamModel = resolveResponseModel(result.body, req.body.model);
|
|
464
457
|
const openaiResponse = convertAnthropicToOpenAI(result.body, streamModel);
|
|
465
458
|
|
|
466
|
-
// Debug: Log what we're about to stream
|
|
467
459
|
logger.debug({
|
|
468
460
|
openaiResponseId: openaiResponse.id,
|
|
469
461
|
messageContent: openaiResponse.choices[0]?.message?.content?.substring(0, 100),
|
|
@@ -475,10 +467,9 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
475
467
|
}, "=== PREPARING TO STREAM ===");
|
|
476
468
|
|
|
477
469
|
// Simulate streaming by sending the complete response as chunks
|
|
478
|
-
const content = openaiResponse.choices[0].message.content || "";
|
|
470
|
+
const content = lynkrBadge(result.body) + (openaiResponse.choices[0].message.content || "");
|
|
479
471
|
let toolCalls = openaiResponse.choices[0].message.tool_calls;
|
|
480
472
|
|
|
481
|
-
// Map tool names for known IDE clients
|
|
482
473
|
if (clientType !== "unknown" && toolCalls && toolCalls.length > 0) {
|
|
483
474
|
toolCalls = toolCalls.map(tc => {
|
|
484
475
|
const mapped = mapToolForClient(tc.function?.name || "", tc.function?.arguments || "{}", clientType);
|
|
@@ -496,7 +487,6 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
496
487
|
}, "Tool names mapped for streaming chat/completions");
|
|
497
488
|
}
|
|
498
489
|
|
|
499
|
-
// Send start chunk with role
|
|
500
490
|
const startChunk = {
|
|
501
491
|
id: openaiResponse.id,
|
|
502
492
|
object: "chat.completion.chunk",
|
|
@@ -517,7 +507,6 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
517
507
|
logger.warn("Start chunk write returned false (backpressure)");
|
|
518
508
|
}
|
|
519
509
|
|
|
520
|
-
// Send content in a single chunk (or character by character for true streaming simulation)
|
|
521
510
|
if (content) {
|
|
522
511
|
const contentChunk = {
|
|
523
512
|
id: openaiResponse.id,
|
|
@@ -536,7 +525,6 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
536
525
|
logger.debug({ contentPreview: content.substring(0, 50), writeOk: contentWriteOk }, "Sent content chunk");
|
|
537
526
|
}
|
|
538
527
|
|
|
539
|
-
// Send tool calls if present
|
|
540
528
|
if (toolCalls && toolCalls.length > 0) {
|
|
541
529
|
for (const toolCall of toolCalls) {
|
|
542
530
|
const toolChunk = {
|
|
@@ -565,7 +553,6 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
565
553
|
}
|
|
566
554
|
}
|
|
567
555
|
|
|
568
|
-
// Send finish chunk
|
|
569
556
|
const finishChunk = {
|
|
570
557
|
id: openaiResponse.id,
|
|
571
558
|
object: "chat.completion.chunk",
|
|
@@ -584,7 +571,6 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
584
571
|
res.write(`data: ${JSON.stringify(finishChunk)}\n\n`);
|
|
585
572
|
res.write("data: [DONE]\n\n");
|
|
586
573
|
|
|
587
|
-
// Ensure data is flushed before ending
|
|
588
574
|
logger.debug({ contentLength: content.length, contentPreview: content.substring(0, 50) }, "=== SSE STREAM COMPLETE ===");
|
|
589
575
|
res.end();
|
|
590
576
|
|
|
@@ -601,7 +587,6 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
601
587
|
stack: streamError.stack
|
|
602
588
|
}, "=== STREAMING ERROR ===");
|
|
603
589
|
|
|
604
|
-
// Send error in OpenAI streaming format
|
|
605
590
|
const errorChunk = {
|
|
606
591
|
id: `chatcmpl-error-${Date.now()}`,
|
|
607
592
|
object: "chat.completion.chunk",
|
|
@@ -622,7 +607,6 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
622
607
|
res.end();
|
|
623
608
|
}
|
|
624
609
|
} else {
|
|
625
|
-
// Non-streaming mode
|
|
626
610
|
const result = await orchestrator.processMessage({
|
|
627
611
|
payload: anthropicRequest,
|
|
628
612
|
headers: req.headers,
|
|
@@ -632,7 +616,6 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
632
616
|
}
|
|
633
617
|
});
|
|
634
618
|
|
|
635
|
-
// Debug logging
|
|
636
619
|
logger.debug({
|
|
637
620
|
resultKeys: Object.keys(result || {}),
|
|
638
621
|
hasBody: !!result?.body,
|
|
@@ -640,10 +623,12 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
640
623
|
bodyKeys: result?.body ? Object.keys(result.body) : null
|
|
641
624
|
}, "Orchestrator result structure");
|
|
642
625
|
|
|
643
|
-
// Convert Anthropic response to OpenAI format
|
|
644
626
|
const openaiResponse = convertAnthropicToOpenAI(result.body, resolveResponseModel(result.body, req.body.model));
|
|
627
|
+
const _badge = lynkrBadge(result.body);
|
|
628
|
+
if (_badge && openaiResponse.choices?.[0]?.message) {
|
|
629
|
+
openaiResponse.choices[0].message.content = _badge + (openaiResponse.choices[0].message.content || "");
|
|
630
|
+
}
|
|
645
631
|
|
|
646
|
-
// Map tool names for known IDE clients
|
|
647
632
|
if (clientType !== "unknown" && openaiResponse.choices?.[0]?.message?.tool_calls?.length > 0) {
|
|
648
633
|
openaiResponse.choices[0].message.tool_calls = openaiResponse.choices[0].message.tool_calls.map(tc => {
|
|
649
634
|
const mapped = mapToolForClient(tc.function?.name || "", tc.function?.arguments || "{}", clientType);
|
|
@@ -679,7 +664,6 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
679
664
|
duration: Date.now() - startTime
|
|
680
665
|
}, "OpenAI chat completion error");
|
|
681
666
|
|
|
682
|
-
// Return OpenAI-format error
|
|
683
667
|
res.status(500).json({
|
|
684
668
|
error: {
|
|
685
669
|
message: error.message || "Internal server error",
|
|
@@ -696,9 +680,7 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
696
680
|
*/
|
|
697
681
|
function getConfiguredProviders() {
|
|
698
682
|
const providers = [];
|
|
699
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
700
683
|
|
|
701
|
-
// Check Databricks
|
|
702
684
|
if (config.databricks?.url && config.databricks?.apiKey) {
|
|
703
685
|
providers.push({
|
|
704
686
|
name: "databricks",
|
|
@@ -711,7 +693,6 @@ function getConfiguredProviders() {
|
|
|
711
693
|
});
|
|
712
694
|
}
|
|
713
695
|
|
|
714
|
-
// Check AWS Bedrock
|
|
715
696
|
if (config.bedrock?.apiKey) {
|
|
716
697
|
const bedrockModels = [config.bedrock.modelId];
|
|
717
698
|
if (config.bedrock.modelId?.includes("claude")) {
|
|
@@ -728,7 +709,6 @@ function getConfiguredProviders() {
|
|
|
728
709
|
});
|
|
729
710
|
}
|
|
730
711
|
|
|
731
|
-
// Check Azure Anthropic
|
|
732
712
|
if (config.azureAnthropic?.endpoint && config.azureAnthropic?.apiKey) {
|
|
733
713
|
providers.push({
|
|
734
714
|
name: "azure-anthropic",
|
|
@@ -737,7 +717,6 @@ function getConfiguredProviders() {
|
|
|
737
717
|
});
|
|
738
718
|
}
|
|
739
719
|
|
|
740
|
-
// Check Azure OpenAI
|
|
741
720
|
if (config.azureOpenAI?.endpoint && config.azureOpenAI?.apiKey) {
|
|
742
721
|
providers.push({
|
|
743
722
|
name: "azure-openai",
|
|
@@ -752,7 +731,6 @@ function getConfiguredProviders() {
|
|
|
752
731
|
});
|
|
753
732
|
}
|
|
754
733
|
|
|
755
|
-
// Check OpenAI
|
|
756
734
|
if (config.openai?.apiKey) {
|
|
757
735
|
providers.push({
|
|
758
736
|
name: "openai",
|
|
@@ -766,7 +744,6 @@ function getConfiguredProviders() {
|
|
|
766
744
|
});
|
|
767
745
|
}
|
|
768
746
|
|
|
769
|
-
// Check OpenRouter
|
|
770
747
|
if (config.openrouter?.apiKey) {
|
|
771
748
|
providers.push({
|
|
772
749
|
name: "openrouter",
|
|
@@ -781,7 +758,21 @@ function getConfiguredProviders() {
|
|
|
781
758
|
});
|
|
782
759
|
}
|
|
783
760
|
|
|
784
|
-
// Check
|
|
761
|
+
// Check Eden AI (OpenAI-compatible gateway, EU/GDPR)
|
|
762
|
+
if (config.edenai?.apiKey) {
|
|
763
|
+
providers.push({
|
|
764
|
+
name: "edenai",
|
|
765
|
+
type: "edenai",
|
|
766
|
+
models: [
|
|
767
|
+
config.edenai.model || "openai/gpt-4o-mini",
|
|
768
|
+
"anthropic/claude-sonnet-4-5",
|
|
769
|
+
"openai/gpt-4o",
|
|
770
|
+
"openai/gpt-4o-mini",
|
|
771
|
+
"google/gemini-2.5-flash"
|
|
772
|
+
]
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
785
776
|
if (config.ollama?.endpoint) {
|
|
786
777
|
providers.push({
|
|
787
778
|
name: "ollama",
|
|
@@ -790,7 +781,6 @@ function getConfiguredProviders() {
|
|
|
790
781
|
});
|
|
791
782
|
}
|
|
792
783
|
|
|
793
|
-
// Check llama.cpp
|
|
794
784
|
if (config.llamacpp?.endpoint) {
|
|
795
785
|
providers.push({
|
|
796
786
|
name: "llamacpp",
|
|
@@ -799,7 +789,6 @@ function getConfiguredProviders() {
|
|
|
799
789
|
});
|
|
800
790
|
}
|
|
801
791
|
|
|
802
|
-
// Check LM Studio
|
|
803
792
|
if (config.lmstudio?.endpoint) {
|
|
804
793
|
providers.push({
|
|
805
794
|
name: "lmstudio",
|
|
@@ -808,7 +797,6 @@ function getConfiguredProviders() {
|
|
|
808
797
|
});
|
|
809
798
|
}
|
|
810
799
|
|
|
811
|
-
// Check Z.AI (Zhipu)
|
|
812
800
|
if (config.zai?.apiKey) {
|
|
813
801
|
providers.push({
|
|
814
802
|
name: "zai",
|
|
@@ -822,7 +810,6 @@ function getConfiguredProviders() {
|
|
|
822
810
|
});
|
|
823
811
|
}
|
|
824
812
|
|
|
825
|
-
// Check Moonshot AI (Kimi)
|
|
826
813
|
if (config.moonshot?.apiKey) {
|
|
827
814
|
providers.push({
|
|
828
815
|
name: "moonshot",
|
|
@@ -834,7 +821,6 @@ function getConfiguredProviders() {
|
|
|
834
821
|
});
|
|
835
822
|
}
|
|
836
823
|
|
|
837
|
-
// Check Vertex AI (Google Cloud)
|
|
838
824
|
if (config.vertex?.projectId) {
|
|
839
825
|
providers.push({
|
|
840
826
|
name: "vertex",
|
|
@@ -865,10 +851,8 @@ router.get("/models", (req, res) => {
|
|
|
865
851
|
const models = [];
|
|
866
852
|
const seenModelIds = new Set();
|
|
867
853
|
|
|
868
|
-
// Collect models from all providers
|
|
869
854
|
for (const provider of providers) {
|
|
870
855
|
for (const modelId of provider.models) {
|
|
871
|
-
// Create unique key to avoid duplicates
|
|
872
856
|
const uniqueKey = `${provider.name}:${modelId}`;
|
|
873
857
|
if (seenModelIds.has(uniqueKey)) continue;
|
|
874
858
|
seenModelIds.add(uniqueKey);
|
|
@@ -886,7 +870,6 @@ router.get("/models", (req, res) => {
|
|
|
886
870
|
}
|
|
887
871
|
}
|
|
888
872
|
|
|
889
|
-
// Add embedding models if embeddings are configured
|
|
890
873
|
const embeddingConfig = determineEmbeddingProvider();
|
|
891
874
|
if (embeddingConfig) {
|
|
892
875
|
let embeddingModelId;
|
|
@@ -900,6 +883,9 @@ router.get("/models", (req, res) => {
|
|
|
900
883
|
case "openrouter":
|
|
901
884
|
embeddingModelId = embeddingConfig.model;
|
|
902
885
|
break;
|
|
886
|
+
case "edenai":
|
|
887
|
+
embeddingModelId = embeddingConfig.model;
|
|
888
|
+
break;
|
|
903
889
|
case "openai":
|
|
904
890
|
embeddingModelId = embeddingConfig.model || "text-embedding-ada-002";
|
|
905
891
|
break;
|
|
@@ -993,6 +979,18 @@ function determineEmbeddingProvider(requestedModel = null) {
|
|
|
993
979
|
endpoint: "https://openrouter.ai/api/v1/embeddings"
|
|
994
980
|
};
|
|
995
981
|
|
|
982
|
+
case "edenai":
|
|
983
|
+
if (!config.edenai?.apiKey) {
|
|
984
|
+
logger.warn("EMBEDDINGS_PROVIDER=edenai but EDENAI_API_KEY not set");
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
return {
|
|
988
|
+
provider: "edenai",
|
|
989
|
+
model: requestedModel || config.edenai.embeddingsModel,
|
|
990
|
+
apiKey: config.edenai.apiKey,
|
|
991
|
+
endpoint: "https://api.edenai.run/v3/embeddings"
|
|
992
|
+
};
|
|
993
|
+
|
|
996
994
|
case "openai":
|
|
997
995
|
if (!config.openai?.apiKey) {
|
|
998
996
|
logger.warn("EMBEDDINGS_PROVIDER=openai but OPENAI_API_KEY not set");
|
|
@@ -1019,6 +1017,15 @@ function determineEmbeddingProvider(requestedModel = null) {
|
|
|
1019
1017
|
};
|
|
1020
1018
|
}
|
|
1021
1019
|
|
|
1020
|
+
if (chatProvider === "edenai" && config.edenai?.apiKey) {
|
|
1021
|
+
return {
|
|
1022
|
+
provider: "edenai",
|
|
1023
|
+
model: requestedModel || config.edenai.embeddingsModel,
|
|
1024
|
+
apiKey: config.edenai.apiKey,
|
|
1025
|
+
endpoint: "https://api.edenai.run/v3/embeddings"
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1022
1029
|
if (chatProvider === "ollama" && config.ollama?.embeddingsModel) {
|
|
1023
1030
|
return {
|
|
1024
1031
|
provider: "ollama",
|
|
@@ -1086,7 +1093,6 @@ async function generateOllamaEmbeddings(inputs, embeddingConfig) {
|
|
|
1086
1093
|
inputCount: inputs.length
|
|
1087
1094
|
}, "Generating embeddings with Ollama");
|
|
1088
1095
|
|
|
1089
|
-
// Ollama doesn't support batch, so we need to process one by one
|
|
1090
1096
|
const embeddings = [];
|
|
1091
1097
|
|
|
1092
1098
|
for (let i = 0; i < inputs.length; i++) {
|
|
@@ -1127,7 +1133,6 @@ async function generateOllamaEmbeddings(inputs, embeddingConfig) {
|
|
|
1127
1133
|
}
|
|
1128
1134
|
}
|
|
1129
1135
|
|
|
1130
|
-
// Convert to OpenAI format
|
|
1131
1136
|
return {
|
|
1132
1137
|
object: "list",
|
|
1133
1138
|
data: embeddings,
|
|
@@ -1208,9 +1213,6 @@ async function generateLlamaCppEmbeddings(inputs, embeddingConfig) {
|
|
|
1208
1213
|
}
|
|
1209
1214
|
}
|
|
1210
1215
|
|
|
1211
|
-
/**
|
|
1212
|
-
* Generate embeddings using OpenRouter
|
|
1213
|
-
*/
|
|
1214
1216
|
async function generateOpenRouterEmbeddings(inputs, embeddingConfig) {
|
|
1215
1217
|
const { model, apiKey, endpoint } = embeddingConfig;
|
|
1216
1218
|
|
|
@@ -1242,9 +1244,6 @@ async function generateOpenRouterEmbeddings(inputs, embeddingConfig) {
|
|
|
1242
1244
|
return await response.json();
|
|
1243
1245
|
}
|
|
1244
1246
|
|
|
1245
|
-
/**
|
|
1246
|
-
* Generate embeddings using OpenAI
|
|
1247
|
-
*/
|
|
1248
1247
|
async function generateOpenAIEmbeddings(inputs, embeddingConfig) {
|
|
1249
1248
|
const { model, apiKey, endpoint } = embeddingConfig;
|
|
1250
1249
|
|
|
@@ -1286,7 +1285,6 @@ router.post("/embeddings", async (req, res) => {
|
|
|
1286
1285
|
try {
|
|
1287
1286
|
const { input, model, encoding_format } = req.body;
|
|
1288
1287
|
|
|
1289
|
-
// Validate input
|
|
1290
1288
|
if (!input) {
|
|
1291
1289
|
return res.status(400).json({
|
|
1292
1290
|
error: {
|
|
@@ -1297,7 +1295,6 @@ router.post("/embeddings", async (req, res) => {
|
|
|
1297
1295
|
});
|
|
1298
1296
|
}
|
|
1299
1297
|
|
|
1300
|
-
// Convert input to array if string
|
|
1301
1298
|
const inputs = Array.isArray(input) ? input : [input];
|
|
1302
1299
|
|
|
1303
1300
|
logger.debug({
|
|
@@ -1307,7 +1304,6 @@ router.post("/embeddings", async (req, res) => {
|
|
|
1307
1304
|
inputLengths: inputs.map(i => i.length)
|
|
1308
1305
|
}, "=== OPENAI EMBEDDINGS REQUEST ===");
|
|
1309
1306
|
|
|
1310
|
-
// Determine which provider to use for embeddings
|
|
1311
1307
|
const embeddingConfig = determineEmbeddingProvider(model);
|
|
1312
1308
|
|
|
1313
1309
|
if (!embeddingConfig) {
|
|
@@ -1321,7 +1317,6 @@ router.post("/embeddings", async (req, res) => {
|
|
|
1321
1317
|
});
|
|
1322
1318
|
}
|
|
1323
1319
|
|
|
1324
|
-
// Route to appropriate provider
|
|
1325
1320
|
let embeddingResponse;
|
|
1326
1321
|
|
|
1327
1322
|
try {
|
|
@@ -1338,6 +1333,11 @@ router.post("/embeddings", async (req, res) => {
|
|
|
1338
1333
|
embeddingResponse = await generateOpenRouterEmbeddings(inputs, embeddingConfig);
|
|
1339
1334
|
break;
|
|
1340
1335
|
|
|
1336
|
+
case "edenai":
|
|
1337
|
+
// Eden AI embeddings are OpenAI-compatible (Bearer + {model,input}).
|
|
1338
|
+
embeddingResponse = await generateOpenAIEmbeddings(inputs, embeddingConfig);
|
|
1339
|
+
break;
|
|
1340
|
+
|
|
1341
1341
|
case "openai":
|
|
1342
1342
|
embeddingResponse = await generateOpenAIEmbeddings(inputs, embeddingConfig);
|
|
1343
1343
|
break;
|
|
@@ -1368,7 +1368,6 @@ router.post("/embeddings", async (req, res) => {
|
|
|
1368
1368
|
totalTokens: embeddingResponse.usage?.total_tokens || 0
|
|
1369
1369
|
}, "=== EMBEDDINGS RESPONSE ===");
|
|
1370
1370
|
|
|
1371
|
-
// Return embeddings in OpenAI format
|
|
1372
1371
|
res.json(embeddingResponse);
|
|
1373
1372
|
|
|
1374
1373
|
} catch (error) {
|
|
@@ -1401,7 +1400,6 @@ router.post("/responses", async (req, res) => {
|
|
|
1401
1400
|
try {
|
|
1402
1401
|
const { convertResponsesToChat, convertChatToResponses } = require("../clients/responses-format");
|
|
1403
1402
|
|
|
1404
|
-
// Comprehensive debug logging
|
|
1405
1403
|
logger.debug({
|
|
1406
1404
|
endpoint: "/v1/responses",
|
|
1407
1405
|
inputType: typeof req.body.input,
|
|
@@ -1441,7 +1439,6 @@ router.post("/responses", async (req, res) => {
|
|
|
1441
1439
|
}
|
|
1442
1440
|
}
|
|
1443
1441
|
|
|
1444
|
-
// Convert Responses API to Chat Completions format
|
|
1445
1442
|
const chatRequest = convertResponsesToChat(req.body);
|
|
1446
1443
|
|
|
1447
1444
|
logger.debug({
|
|
@@ -1453,7 +1450,6 @@ router.post("/responses", async (req, res) => {
|
|
|
1453
1450
|
}))
|
|
1454
1451
|
}, "After Responses→Chat conversion");
|
|
1455
1452
|
|
|
1456
|
-
// Convert to Anthropic format
|
|
1457
1453
|
const anthropicRequest = convertOpenAIToAnthropic(chatRequest);
|
|
1458
1454
|
|
|
1459
1455
|
// Normalize tool_use names in conversation history to client format.
|
|
@@ -1548,17 +1544,26 @@ router.post("/responses", async (req, res) => {
|
|
|
1548
1544
|
anthropicRequest.tools = anthropicRequest.tools.filter(t => !RESPONSES_EXCLUDED.has(t.name));
|
|
1549
1545
|
}
|
|
1550
1546
|
|
|
1551
|
-
//
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1547
|
+
// WS3 — detect the client harness on the OpenAI-compat paths too (the
|
|
1548
|
+
// /v1/messages router already does this). Runs after tool injection so
|
|
1549
|
+
// the fingerprint sees the final tools array either way: goose/Cursor
|
|
1550
|
+
// send their own loadout, tool-less clients get IDE_SAFE_TOOLS injected
|
|
1551
|
+
// and fingerprint as claude-code. Without this the agentic detector
|
|
1552
|
+
// counts the harness baseline as intent and floors every request —
|
|
1553
|
+
// including "hi" — at the ITERATIVE minTier (COMPLEX).
|
|
1554
|
+
try {
|
|
1555
|
+
const profile = clientProfiles.detectClient({
|
|
1556
|
+
headers: req.headers,
|
|
1557
|
+
payload: anthropicRequest,
|
|
1558
|
+
});
|
|
1559
|
+
if (profile) anthropicRequest._clientProfile = profile;
|
|
1560
|
+
} catch (err) {
|
|
1561
|
+
logger.debug({ err: err.message }, "[OpenAI Router] client profile detection failed");
|
|
1562
|
+
}
|
|
1555
1563
|
|
|
1556
|
-
// Get session
|
|
1557
1564
|
const session = getSession(sessionId);
|
|
1558
1565
|
|
|
1559
|
-
// Handle streaming vs non-streaming
|
|
1560
1566
|
if (req.body.stream) {
|
|
1561
|
-
// Set up SSE headers for streaming
|
|
1562
1567
|
res.setHeader("Content-Type", "text/event-stream");
|
|
1563
1568
|
res.setHeader("Cache-Control", "no-cache");
|
|
1564
1569
|
res.setHeader("Connection", "keep-alive");
|
|
@@ -1594,7 +1599,6 @@ router.post("/responses", async (req, res) => {
|
|
|
1594
1599
|
terminationReason: result?.terminationReason
|
|
1595
1600
|
}, "=== ORCHESTRATOR RESULT FOR RESPONSES API ===");
|
|
1596
1601
|
|
|
1597
|
-
// Convert back: Anthropic → OpenAI → Responses
|
|
1598
1602
|
const responsesModel = resolveResponseModel(result.body, req.body.model);
|
|
1599
1603
|
|
|
1600
1604
|
// Guard: if orchestrator returned an error body, surface it as text
|
|
@@ -1643,16 +1647,32 @@ router.post("/responses", async (req, res) => {
|
|
|
1643
1647
|
}
|
|
1644
1648
|
}
|
|
1645
1649
|
|
|
1650
|
+
if (content) content = lynkrBadge(result.body) + content;
|
|
1651
|
+
|
|
1646
1652
|
// Universal tool→shell converter: ensures every tool call becomes
|
|
1647
1653
|
// a "shell" command that Codex (or any client) can execute.
|
|
1648
1654
|
// Server-side tools (Task, WebSearch, etc.) are dropped entirely.
|
|
1649
1655
|
if (toolCalls.length > 0) {
|
|
1650
1656
|
const SERVER_TOOLS = new Set(["task", "websearch", "webfetch", "web_search", "web_fetch", "web_agent", "askuserquestion", "todowrite"]);
|
|
1657
|
+
// Tool names the client itself declared (e.g. Codex v0.142's
|
|
1658
|
+
// exec_command/update_plan) must go back verbatim — renaming them
|
|
1659
|
+
// to "shell" gets rejected as "unsupported call". The shell
|
|
1660
|
+
// conversion below only applies to Lynkr-injected tool names.
|
|
1661
|
+
const clientToolNames = new Set(
|
|
1662
|
+
(req.body.tools || [])
|
|
1663
|
+
.filter((t) => t && (t.name || t.function?.name))
|
|
1664
|
+
.map((t) => t.name || t.function.name)
|
|
1665
|
+
);
|
|
1651
1666
|
const converted = [];
|
|
1652
1667
|
for (const tc of toolCalls) {
|
|
1653
1668
|
const name = tc.function?.name || "";
|
|
1654
1669
|
const ln = name.toLowerCase();
|
|
1655
1670
|
|
|
1671
|
+
if (clientToolNames.has(name)) {
|
|
1672
|
+
converted.push(tc);
|
|
1673
|
+
continue;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1656
1676
|
// Convert server-side tools to useful shell equivalents
|
|
1657
1677
|
if (SERVER_TOOLS.has(ln)) {
|
|
1658
1678
|
let exploreCmd = "ls -la && head -100 README.md 2>/dev/null && cat package.json 2>/dev/null && ls src/ 2>/dev/null";
|
|
@@ -1731,7 +1751,6 @@ router.post("/responses", async (req, res) => {
|
|
|
1731
1751
|
chunks.push(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
1732
1752
|
};
|
|
1733
1753
|
|
|
1734
|
-
// response.created
|
|
1735
1754
|
sse("response.created", {
|
|
1736
1755
|
type: "response.created",
|
|
1737
1756
|
response: {
|
|
@@ -1741,7 +1760,6 @@ router.post("/responses", async (req, res) => {
|
|
|
1741
1760
|
sequence_number: sequenceNumber++
|
|
1742
1761
|
});
|
|
1743
1762
|
|
|
1744
|
-
// response.in_progress
|
|
1745
1763
|
sse("response.in_progress", {
|
|
1746
1764
|
type: "response.in_progress",
|
|
1747
1765
|
response: {
|
|
@@ -1753,7 +1771,6 @@ router.post("/responses", async (req, res) => {
|
|
|
1753
1771
|
|
|
1754
1772
|
const outputItems = [];
|
|
1755
1773
|
|
|
1756
|
-
// Function call events
|
|
1757
1774
|
for (const toolCall of toolCalls) {
|
|
1758
1775
|
const toolCallId = toolCall.id || `call_${Date.now()}_${outputIndex}`;
|
|
1759
1776
|
const functionName = toolCall.function?.name || "unknown";
|
|
@@ -1785,7 +1802,6 @@ router.post("/responses", async (req, res) => {
|
|
|
1785
1802
|
outputIndex++;
|
|
1786
1803
|
}
|
|
1787
1804
|
|
|
1788
|
-
// Text content events
|
|
1789
1805
|
if (content) {
|
|
1790
1806
|
sse("response.output_item.added", {
|
|
1791
1807
|
type: "response.output_item.added", output_index: outputIndex,
|
|
@@ -1847,7 +1863,6 @@ router.post("/responses", async (req, res) => {
|
|
|
1847
1863
|
sequence_number: sequenceNumber++
|
|
1848
1864
|
});
|
|
1849
1865
|
|
|
1850
|
-
// Write entire payload and close in one call
|
|
1851
1866
|
const payload = chunks.join("");
|
|
1852
1867
|
res.end(payload);
|
|
1853
1868
|
|
|
@@ -1858,7 +1873,9 @@ router.post("/responses", async (req, res) => {
|
|
|
1858
1873
|
messages: chatRequest?.messages || [],
|
|
1859
1874
|
assistantContent: content || null,
|
|
1860
1875
|
});
|
|
1861
|
-
} catch {
|
|
1876
|
+
} catch (err) {
|
|
1877
|
+
logger.warn({ err }, "Failed to store response for previous_response_id continuity");
|
|
1878
|
+
}
|
|
1862
1879
|
|
|
1863
1880
|
logger.info({
|
|
1864
1881
|
duration: Date.now() - startTime,
|
|
@@ -1894,7 +1911,6 @@ router.post("/responses", async (req, res) => {
|
|
|
1894
1911
|
}
|
|
1895
1912
|
|
|
1896
1913
|
} else {
|
|
1897
|
-
// Non-streaming response
|
|
1898
1914
|
anthropicRequest.stream = false;
|
|
1899
1915
|
|
|
1900
1916
|
const result = await orchestrator.processMessage({
|
|
@@ -1906,7 +1922,6 @@ router.post("/responses", async (req, res) => {
|
|
|
1906
1922
|
}
|
|
1907
1923
|
});
|
|
1908
1924
|
|
|
1909
|
-
// Convert back: Anthropic → OpenAI → Responses
|
|
1910
1925
|
const chatResponse = convertAnthropicToOpenAI(result.body, resolveResponseModel(result.body, req.body.model));
|
|
1911
1926
|
const responsesResponse = convertChatToResponses(chatResponse);
|
|
1912
1927
|
|
|
@@ -1920,6 +1935,10 @@ router.post("/responses", async (req, res) => {
|
|
|
1920
1935
|
.replace(/\\t/g, '\t');
|
|
1921
1936
|
}
|
|
1922
1937
|
|
|
1938
|
+
if (responsesResponse.content) {
|
|
1939
|
+
responsesResponse.content = lynkrBadge(result.body) + responsesResponse.content;
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1923
1942
|
logger.info({
|
|
1924
1943
|
duration: Date.now() - startTime,
|
|
1925
1944
|
contentLength: responsesResponse.content?.length || 0,
|