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 +36 -14
- package/package.json +1 -1
- package/src/api/router.js +28 -0
- package/src/clients/databricks.js +28 -6
- package/src/clients/routing.js +15 -127
- package/src/routing/complexity-analyzer.js +595 -0
- package/src/routing/index.js +375 -0
- package/test/azure-openai-routing.test.js +27 -1
- package/test/openai-integration.test.js +6 -1
- package/test/routing.test.js +2 -1
package/README.md
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
# Lynkr - Claude Code
|
|
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
|
[](https://www.npmjs.com/package/lynkr)
|
|
4
5
|
[](https://github.com/vishalveerareddy123/homebrew-lynkr)
|
|
@@ -10,13 +11,19 @@
|
|
|
10
11
|
[](https://ollama.ai/)
|
|
11
12
|
[](https://github.com/ggerganov/llama.cpp)
|
|
12
13
|
|
|
13
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
│
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
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) {
|
package/src/clients/routing.js
CHANGED
|
@@ -1,136 +1,24 @@
|
|
|
1
|
-
const config = require("../config");
|
|
2
|
-
const logger = require("../logger");
|
|
3
|
-
const { modelNameSupportsTools } = require("./ollama-utils");
|
|
4
|
-
|
|
5
1
|
/**
|
|
6
|
-
*
|
|
2
|
+
* Request Routing Module
|
|
7
3
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
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
|
-
* @
|
|
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
|
-
|
|
135
|
-
|
|
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
|
};
|