lynkr 4.2.1 → 4.3.1

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 CHANGED
@@ -1,4 +1,5 @@
1
- # Lynkr - Claude Code Proxy with Multi-Provider Support
1
+ # Lynkr - Run Cursor, Cline, Continue, and Claude Code on any model.
2
+ ## One universal LLM proxy for AI coding tools.
2
3
 
3
4
  [![npm version](https://img.shields.io/npm/v/lynkr.svg)](https://www.npmjs.com/package/lynkr)
4
5
  [![Homebrew Tap](https://img.shields.io/badge/homebrew-lynkr-brightgreen.svg)](https://github.com/vishalveerareddy123/homebrew-lynkr)
@@ -10,13 +11,19 @@
10
11
  [![Ollama Compatible](https://img.shields.io/badge/Ollama-Compatible-brightgreen)](https://ollama.ai/)
11
12
  [![llama.cpp Compatible](https://img.shields.io/badge/llama.cpp-Compatible-blue)](https://github.com/ggerganov/llama.cpp)
12
13
 
13
- > **Production-ready Claude Code proxy supporting 9+ LLM providers with 60-80% cost reduction through token optimization.**
14
-
14
+ ### Use Case
15
+ ```
16
+ Cursor / Cline / Continue / Claude Code
17
+
18
+ Lynkr
19
+
20
+ Local LLMs | OpenRouter | Azure | Databricks
21
+ ```
15
22
  ---
16
23
 
17
24
  ## Overview
18
25
 
19
- Lynkr is a **self-hosted proxy server** that unlocks Claude Code CLI and Cursor IDE by enabling:
26
+ Lynkr is a **self-hosted proxy server** that unlocks Claude Code CLI , Cursor IDE and Codex Cli by enabling:
20
27
 
21
28
  - 🚀 **Any LLM Provider** - Databricks, AWS Bedrock (100+ models), OpenRouter (100+ models), Ollama (local), llama.cpp, Azure OpenAI, Azure Anthropic, OpenAI, LM Studio
22
29
  - 💰 **60-80% Cost Reduction** - Built-in token optimization with smart tool selection, prompt caching, and memory deduplication
@@ -64,14 +71,9 @@ nano .env
64
71
  npm start
65
72
  ```
66
73
 
67
- **Option 3: Homebrew (macOS/Linux)**
68
- ```bash
69
- brew tap vishalveerareddy123/lynkr
70
- brew install lynkr
71
- lynkr start
72
- ```
73
74
 
74
- **Option 4: Docker**
75
+
76
+ **Option 3: Docker**
75
77
  ```bash
76
78
  docker-compose up -d
77
79
  ```
@@ -136,7 +138,27 @@ Configure Cursor IDE to use Lynkr:
136
138
  - @Codebase search: Requires [embeddings setup](documentation/embeddings.md)
137
139
 
138
140
  📖 **[Full Cursor Setup Guide](documentation/cursor-integration.md)** | **[Embeddings Configuration](documentation/embeddings.md)**
139
-
141
+ ---
142
+ ## Codex CLI with Lynkr
143
+ Configure Codex Cli to use Lynkr
144
+ Option 1: **Environment Variable (simplest)**
145
+ ```
146
+ export OPENAI_BASE_URL=http://localhost:8081/v1
147
+ export OPENAI_API_KEY=dummy
148
+ codex
149
+ ```
150
+
151
+ Option 2: **Config File (~/.codex/config.toml)**
152
+ ```
153
+ model_provider = "lynkr"
154
+
155
+ [model_providers.lynkr]
156
+ name = "Lynkr Proxy"
157
+ base_url = "http://localhost:8081/v1"
158
+ env_key = "OPENAI_API_KEY"
159
+ ```
160
+
161
+ ## Lynkr also supports Cline, Continue.dev and other OpenAI compatible tools.
140
162
  ---
141
163
 
142
164
  ## Documentation
@@ -198,7 +220,7 @@ Configure Cursor IDE to use Lynkr:
198
220
 
199
221
  ```
200
222
  ┌─────────────────┐
201
- Claude Code CLI or Cursor IDE
223
+ AI Tools
202
224
  └────────┬────────┘
203
225
  │ Anthropic/OpenAI Format
204
226
 
@@ -251,7 +273,7 @@ export MODEL_PROVIDER=openrouter
251
273
  export OPENROUTER_API_KEY=sk-or-v1-your-key
252
274
  npm start
253
275
  ```
254
-
276
+ ** You can setup multiple models like local models
255
277
  📖 **[More Examples](documentation/providers.md#quick-start-examples)**
256
278
 
257
279
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lynkr",
3
- "version": "4.2.1",
3
+ "version": "4.3.1",
4
4
  "description": "Self-hosted Claude Code & Cursor proxy with Databricks,AWS BedRock,Azure adapters, openrouter, Ollama,llamacpp,LM Studio, workspace tooling, and MCP integration.",
5
5
  "main": "index.js",
6
6
  "bin": {
package/src/api/router.js CHANGED
@@ -4,6 +4,7 @@ const { getSession } = require("../sessions");
4
4
  const metrics = require("../metrics");
5
5
  const { createRateLimiter } = require("./middleware/rate-limiter");
6
6
  const openaiRouter = require("./openai-router");
7
+ const { getRoutingHeaders, getRoutingStats, analyzeComplexity } = require("../routing");
7
8
 
8
9
  const router = express.Router();
9
10
 
@@ -59,6 +60,15 @@ router.get("/health", (req, res) => {
59
60
  res.json({ status: "ok" });
60
61
  });
61
62
 
63
+ // Routing stats endpoint (Phase 3: Metrics)
64
+ router.get("/routing/stats", (req, res) => {
65
+ const stats = getRoutingStats();
66
+ res.json({
67
+ status: "ok",
68
+ stats: stats || { message: "No routing decisions recorded yet" },
69
+ });
70
+ });
71
+
62
72
  router.get("/debug/session", (req, res) => {
63
73
  if (!req.sessionId) {
64
74
  return res.status(400).json({ error: "missing_session_id", message: "Provide x-session-id header" });
@@ -109,6 +119,16 @@ router.post("/v1/messages", rateLimiter, async (req, res, next) => {
109
119
  const wantsStream = Boolean(req.query?.stream === 'true' || req.body?.stream);
110
120
  const hasTools = Array.isArray(req.body?.tools) && req.body.tools.length > 0;
111
121
 
122
+ // Analyze complexity for routing headers (Phase 3)
123
+ const complexity = analyzeComplexity(req.body);
124
+ const routingHeaders = getRoutingHeaders({
125
+ provider: complexity.recommendation === 'local' ? 'ollama' : 'cloud',
126
+ score: complexity.score,
127
+ threshold: complexity.threshold,
128
+ method: 'complexity',
129
+ reason: complexity.breakdown?.taskType?.reason || complexity.recommendation,
130
+ });
131
+
112
132
  // For true streaming: only support non-tool requests for MVP
113
133
  // Tool requests require buffering for agent loop
114
134
  if (wantsStream && !hasTools) {
@@ -118,6 +138,7 @@ router.post("/v1/messages", rateLimiter, async (req, res, next) => {
118
138
  "Content-Type": "text/event-stream",
119
139
  "Cache-Control": "no-cache",
120
140
  Connection: "keep-alive",
141
+ ...routingHeaders, // Include routing headers
121
142
  });
122
143
  if (typeof res.flushHeaders === "function") {
123
144
  res.flushHeaders();
@@ -386,6 +407,13 @@ router.post("/v1/messages", rateLimiter, async (req, res, next) => {
386
407
  return;
387
408
  }
388
409
 
410
+ // Add routing headers (Phase 3)
411
+ Object.entries(routingHeaders).forEach(([key, value]) => {
412
+ if (value !== undefined) {
413
+ res.setHeader(key, value);
414
+ }
415
+ });
416
+
389
417
  if (result.headers) {
390
418
  Object.entries(result.headers).forEach(([key, value]) => {
391
419
  if (value !== undefined) {
@@ -911,19 +911,34 @@ async function invokeBedrock(body) {
911
911
  }
912
912
 
913
913
  async function invokeModel(body, options = {}) {
914
- const { determineProvider, isFallbackEnabled, getFallbackProvider } = require("./routing");
914
+ const { determineProvider, isFallbackEnabled, getFallbackProvider, analyzeComplexity } = require("./routing");
915
915
  const metricsCollector = getMetricsCollector();
916
916
  const registry = getCircuitBreakerRegistry();
917
917
 
918
- // Determine provider based on routing logic
918
+ // Analyze complexity and determine provider
919
+ const complexityAnalysis = analyzeComplexity(body);
919
920
  const initialProvider = options.forceProvider ?? determineProvider(body);
920
921
  const preferOllama = config.modelProvider?.preferOllama ?? false;
921
922
 
923
+ // Build routing decision object for response headers
924
+ const routingDecision = {
925
+ provider: initialProvider,
926
+ score: complexityAnalysis.score,
927
+ threshold: complexityAnalysis.threshold,
928
+ mode: complexityAnalysis.mode,
929
+ recommendation: complexityAnalysis.recommendation,
930
+ method: complexityAnalysis.score !== undefined ? 'complexity' : 'static',
931
+ taskType: complexityAnalysis.breakdown?.taskType?.reason,
932
+ };
933
+
922
934
  logger.debug({
923
935
  initialProvider,
924
936
  preferOllama,
925
937
  fallbackEnabled: isFallbackEnabled(),
926
938
  toolCount: Array.isArray(body?.tools) ? body.tools.length : 0,
939
+ complexityScore: complexityAnalysis.score,
940
+ complexityThreshold: complexityAnalysis.threshold,
941
+ recommendation: complexityAnalysis.recommendation,
927
942
  }, "Provider routing decision");
928
943
 
929
944
  metricsCollector.recordProviderRouting(initialProvider);
@@ -979,10 +994,11 @@ async function invokeModel(body, options = {}) {
979
994
  }
980
995
  }
981
996
 
982
- // Return result with provider info for proper response conversion
997
+ // Return result with provider info and routing decision for headers
983
998
  return {
984
999
  ...result,
985
- actualProvider: initialProvider
1000
+ actualProvider: initialProvider,
1001
+ routingDecision,
986
1002
  };
987
1003
 
988
1004
  } catch (err) {
@@ -1061,10 +1077,16 @@ async function invokeModel(body, options = {}) {
1061
1077
  totalLatency: Date.now() - startTime,
1062
1078
  }, "Fallback to cloud provider succeeded");
1063
1079
 
1064
- // Return result with actual provider used (fallback provider)
1080
+ // Return result with actual provider used (fallback provider) and routing decision
1065
1081
  return {
1066
1082
  ...fallbackResult,
1067
- actualProvider: fallbackProvider
1083
+ actualProvider: fallbackProvider,
1084
+ routingDecision: {
1085
+ ...routingDecision,
1086
+ provider: fallbackProvider,
1087
+ method: 'fallback',
1088
+ fallbackReason: reason,
1089
+ },
1068
1090
  };
1069
1091
 
1070
1092
  } catch (fallbackErr) {
@@ -1,136 +1,24 @@
1
- const config = require("../config");
2
- const logger = require("../logger");
3
- const { modelNameSupportsTools } = require("./ollama-utils");
4
-
5
1
  /**
6
- * Determine provider based on request complexity
2
+ * Request Routing Module
7
3
  *
8
- * Routing Rules:
9
- * 1. If PREFER_OLLAMA is false, route based on MODEL_PROVIDER
10
- * 2. If no tools OR tool count < threshold, route to Ollama
11
- * 3. If tools present AND model doesn't support tools, route to cloud
12
- * 4. If tool count >= threshold, route to cloud for better performance
4
+ * Determines the optimal provider for handling requests based on
5
+ * complexity analysis and configuration.
13
6
  *
14
- * @param {Object} payload - Request payload with tools array
15
- * @returns {string} Provider to use ('ollama' or fallback provider)
16
- */
17
- function determineProvider(payload) {
18
- const preferOllama = config.modelProvider?.preferOllama ?? false;
19
-
20
- // If not in preference mode, use static configuration
21
- if (!preferOllama) {
22
- return config.modelProvider?.type ?? "databricks";
23
- }
24
-
25
- // Count tools in request
26
- const toolCount = Array.isArray(payload?.tools) ? payload.tools.length : 0;
27
- const maxToolsForOllama = config.modelProvider?.ollamaMaxToolsForRouting ?? 3;
28
- const maxToolsForOpenRouter = config.modelProvider?.openRouterMaxToolsForRouting ?? 15;
29
-
30
- // Check if Ollama model supports tools when tools are present
31
- if (toolCount > 0) {
32
- const ollamaModel = config.ollama?.model;
33
- const supportsTools = modelNameSupportsTools(ollamaModel);
34
-
35
- // Only route to fallback if it's enabled AND model doesn't support tools
36
- if (!supportsTools && isFallbackEnabled()) {
37
- const fallback = config.modelProvider?.fallbackProvider ?? "databricks";
38
- logger.debug(
39
- { toolCount, ollamaModel, supportsTools: false, decision: fallback },
40
- "Routing to cloud (model doesn't support tools)"
41
- );
42
- return fallback;
43
- }
44
- }
45
-
46
- // No tools or simple requests → Ollama
47
- if (toolCount === 0 || toolCount < maxToolsForOllama) {
48
- logger.debug(
49
- { toolCount, maxToolsForOllama, decision: "ollama" },
50
- "Routing to Ollama (simple request)"
51
- );
52
- return "ollama";
53
- }
54
-
55
- // Moderate tool count → OpenRouter, OpenAI, or Azure OpenAI (if configured and fallback enabled)
56
- if (toolCount < maxToolsForOpenRouter && isFallbackEnabled()) {
57
- if (config.openrouter?.apiKey) {
58
- logger.debug(
59
- { toolCount, maxToolsForOllama, maxToolsForOpenRouter, decision: "openrouter" },
60
- "Routing to OpenRouter (moderate tools)"
61
- );
62
- return "openrouter";
63
- } else if (config.openai?.apiKey) {
64
- logger.debug(
65
- { toolCount, maxToolsForOllama, maxToolsForOpenRouter, decision: "openai" },
66
- "Routing to OpenAI (moderate tools)"
67
- );
68
- return "openai";
69
- } else if (config.azureOpenAI?.apiKey) {
70
- logger.debug(
71
- { toolCount, maxToolsForOllama, maxToolsForOpenRouter, decision: "azure-openai" },
72
- "Routing to Azure OpenAI (moderate tools)"
73
- );
74
- return "azure-openai";
75
- } else if (config.llamacpp?.endpoint) {
76
- logger.debug(
77
- { toolCount, maxToolsForOllama, maxToolsForOpenRouter, decision: "llamacpp" },
78
- "Routing to llama.cpp (moderate tools)"
79
- );
80
- return "llamacpp";
81
- } else if (config.lmstudio?.endpoint) {
82
- logger.debug(
83
- { toolCount, maxToolsForOllama, maxToolsForOpenRouter, decision: "lmstudio" },
84
- "Routing to LM Studio (moderate tools)"
85
- );
86
- return "lmstudio";
87
- } else if (config.bedrock?.apiKey) {
88
- logger.debug(
89
- { toolCount, maxToolsForOllama, maxToolsForOpenRouter, decision: "bedrock" },
90
- "Routing to AWS Bedrock (moderate tools)"
91
- );
92
- return "bedrock";
93
- }
94
- }
95
-
96
- // Heavy tool count → cloud (only if fallback is enabled)
97
- if (isFallbackEnabled()) {
98
- const fallback = config.modelProvider?.fallbackProvider ?? "databricks";
99
- logger.debug(
100
- { toolCount, maxToolsForOpenRouter, decision: fallback },
101
- "Routing to cloud (heavy tools)"
102
- );
103
- return fallback;
104
- }
105
-
106
- // Fallback disabled, route to Ollama regardless of complexity
107
- logger.debug(
108
- { toolCount, maxToolsForOllama, fallbackEnabled: false, decision: "ollama" },
109
- "Routing to Ollama (fallback disabled)"
110
- );
111
- return "ollama";
112
- }
113
-
114
- /**
115
- * Check if fallback is enabled for the current configuration
7
+ * This module re-exports the smart routing system for backward compatibility.
8
+ * All routing logic is now in src/routing/index.js
116
9
  *
117
- * @returns {boolean} True if fallback is enabled
10
+ * @module clients/routing
118
11
  */
119
- function isFallbackEnabled() {
120
- return config.modelProvider?.fallbackEnabled !== false;
121
- }
122
12
 
123
- /**
124
- * Get the fallback provider
125
- *
126
- * @returns {string} Fallback provider name (e.g., "databricks", "azure-anthropic")
127
- */
128
- function getFallbackProvider() {
129
- return config.modelProvider?.fallbackProvider ?? "databricks";
130
- }
13
+ const smartRouting = require('../routing');
131
14
 
15
+ // Re-export all functions from smart routing
132
16
  module.exports = {
133
- determineProvider,
134
- isFallbackEnabled,
135
- getFallbackProvider,
17
+ determineProvider: smartRouting.determineProvider,
18
+ determineProviderSmart: smartRouting.determineProviderSmart,
19
+ isFallbackEnabled: smartRouting.isFallbackEnabled,
20
+ getFallbackProvider: smartRouting.getFallbackProvider,
21
+ getRoutingHeaders: smartRouting.getRoutingHeaders,
22
+ getRoutingStats: smartRouting.getRoutingStats,
23
+ analyzeComplexity: smartRouting.analyzeComplexity,
136
24
  };