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.
Files changed (80) hide show
  1. package/README.md +29 -19
  2. package/bin/cli.js +11 -0
  3. package/bin/lynkr-init.js +14 -1
  4. package/bin/lynkr-usage.js +78 -0
  5. package/bin/wrap.js +60 -35
  6. package/config/difficulty-anchors.json +22 -0
  7. package/package.json +24 -3
  8. package/scripts/audit-log-reader.js +399 -0
  9. package/scripts/calibrate-thresholds.js +38 -157
  10. package/scripts/compact-dictionary.js +204 -0
  11. package/scripts/test-deduplication.js +448 -0
  12. package/scripts/ws7-anchor-replay.js +108 -0
  13. package/skills/lynkr/SKILL.md +195 -0
  14. package/src/agents/context-manager.js +18 -2
  15. package/src/agents/definitions/loader.js +90 -0
  16. package/src/agents/executor.js +24 -2
  17. package/src/agents/index.js +9 -1
  18. package/src/agents/parallel-coordinator.js +2 -2
  19. package/src/agents/reflector.js +11 -1
  20. package/src/api/middleware/loop-guard.js +87 -0
  21. package/src/api/middleware/request-logging.js +5 -64
  22. package/src/api/middleware/session.js +0 -0
  23. package/src/api/openai-router.js +120 -101
  24. package/src/api/providers-handler.js +27 -2
  25. package/src/api/router.js +450 -125
  26. package/src/budget/index.js +2 -19
  27. package/src/cache/semantic.js +9 -0
  28. package/src/clients/databricks.js +459 -146
  29. package/src/clients/gpt-utils.js +11 -105
  30. package/src/clients/openai-format.js +10 -3
  31. package/src/clients/openrouter-utils.js +49 -24
  32. package/src/clients/prompt-cache-injection.js +1 -0
  33. package/src/clients/provider-capabilities.js +1 -1
  34. package/src/clients/responses-format.js +34 -3
  35. package/src/clients/routing.js +15 -0
  36. package/src/config/index.js +36 -2
  37. package/src/context/gcf.js +275 -0
  38. package/src/context/tool-result-compressor.js +51 -9
  39. package/src/dashboard/api.js +1 -0
  40. package/src/logger/index.js +14 -1
  41. package/src/memory/search.js +12 -40
  42. package/src/memory/tools.js +3 -24
  43. package/src/orchestrator/bypass.js +4 -2
  44. package/src/orchestrator/index.js +144 -88
  45. package/src/routing/affinity-store.js +194 -0
  46. package/src/routing/agentic-detector.js +36 -6
  47. package/src/routing/bandit.js +25 -6
  48. package/src/routing/calibration.js +212 -0
  49. package/src/routing/client-profiles.js +292 -0
  50. package/src/routing/complexity-analyzer.js +48 -11
  51. package/src/routing/deescalator.js +148 -0
  52. package/src/routing/degradation.js +109 -0
  53. package/src/routing/feedback.js +157 -0
  54. package/src/routing/index.js +897 -87
  55. package/src/routing/intent-score.js +339 -0
  56. package/src/routing/interaction.js +3 -0
  57. package/src/routing/knn-router.js +70 -21
  58. package/src/routing/model-registry.js +28 -7
  59. package/src/routing/model-tiers.js +25 -2
  60. package/src/routing/reward-pipeline.js +68 -2
  61. package/src/routing/risk-analyzer.js +30 -1
  62. package/src/routing/risk-classifier.js +6 -2
  63. package/src/routing/session-affinity.js +162 -34
  64. package/src/routing/telemetry.js +298 -10
  65. package/src/routing/verifier.js +267 -0
  66. package/src/server.js +66 -21
  67. package/src/sessions/cleanup.js +17 -0
  68. package/src/tools/index.js +1 -15
  69. package/src/tools/smart-selection.js +10 -0
  70. package/src/tools/web-client.js +3 -3
  71. package/.eslintrc.cjs +0 -12
  72. package/benchmark-configs/litellm_config.yaml +0 -86
  73. package/benchmark-configs/lynkr.env +0 -48
  74. package/benchmark-configs/portkey-config.json +0 -60
  75. package/benchmark-configs/portkey-docker.sh +0 -23
  76. package/benchmark-tier-routing.js +0 -449
  77. package/funding.json +0 -110
  78. package/src/api/middleware/validation.js +0 -261
  79. package/src/routing/drift-monitor.js +0 -113
  80. package/src/workers/helpers.js +0 -185
@@ -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
- // Get or create session
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(); // Ensure headers are sent immediately
423
+ res.flushHeaders();
428
424
 
429
425
  try {
430
426
  // For streaming, we need to handle it differently - convert to non-streaming temporarily
431
- // Get non-streaming response from orchestrator
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 Ollama
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
- // Snapshot tool names before the orchestrator can mutate them
1552
- const injectedToolNames = new Set(
1553
- (anthropicRequest.tools || []).map(t => t.name)
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,