lynkr 9.6.0 β 9.7.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 +32 -302
- package/bin/cli.js +6 -0
- package/bin/lynkr-init.js +679 -0
- package/bin/wrap.js +686 -0
- package/install.sh +26 -81
- package/package.json +4 -3
- package/scripts/build-knn-index.js +1 -1
- package/scripts/convert-routellm.py +105 -0
- package/src/api/router.js +694 -21
- package/src/auth-mode.js +116 -0
- package/src/clients/databricks.js +416 -67
- package/src/clients/prompt-cache-injection.js +15 -0
- package/src/orchestrator/index.js +120 -60
- package/src/routing/index.js +23 -4
- package/src/routing/knn-router.js +9 -2
- package/src/routing/model-tiers.js +34 -0
package/install.sh
CHANGED
|
@@ -128,63 +128,17 @@ install_dependencies() {
|
|
|
128
128
|
fi
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
#
|
|
131
|
+
# Skip .env creation β the install script runs without a TTY when invoked via
|
|
132
|
+
# `curl | bash`, so the interactive `lynkr init` wizard can't run here. We leave
|
|
133
|
+
# .env unmade so the user is prompted to run `lynkr init` in their own shell
|
|
134
|
+
# afterward, which produces a fully-populated config (~150 keys grouped by
|
|
135
|
+
# section) instead of the old 892-line .env.example dump.
|
|
132
136
|
create_env_file() {
|
|
133
|
-
if [
|
|
134
|
-
print_info "Creating .env configuration file..."
|
|
135
|
-
|
|
136
|
-
# Try to copy from .env.example (comprehensive configuration)
|
|
137
|
-
if [ -f "$INSTALL_DIR/.env.example" ]; then
|
|
138
|
-
cp "$INSTALL_DIR/.env.example" "$INSTALL_DIR/.env"
|
|
139
|
-
print_success "Created .env from .env.example (all features documented)"
|
|
140
|
-
else
|
|
141
|
-
# Fallback: create minimal .env if .env.example doesn't exist
|
|
142
|
-
cat > "$INSTALL_DIR/.env" << 'EOF'
|
|
143
|
-
# Lynkr Configuration
|
|
144
|
-
# For full options, see: https://github.com/Fast-Editor/Lynkr/blob/main/.env.example
|
|
145
|
-
|
|
146
|
-
# Model Provider (databricks, openai, azure-openai, azure-anthropic, openrouter, ollama, llamacpp)
|
|
147
|
-
MODEL_PROVIDER=ollama
|
|
148
|
-
|
|
149
|
-
# Server Configuration
|
|
150
|
-
PORT=8081
|
|
151
|
-
|
|
152
|
-
# Ollama Configuration (default for local development)
|
|
153
|
-
OLLAMA_MODEL=qwen2.5-coder:7b
|
|
154
|
-
OLLAMA_ENDPOINT=http://localhost:11434
|
|
155
|
-
|
|
156
|
-
# Tier-based routing (uncomment and configure to enable)
|
|
157
|
-
# TIER_SIMPLE=ollama:qwen2.5-coder:7b
|
|
158
|
-
# TIER_MEDIUM=ollama:qwen2.5-coder:7b
|
|
159
|
-
# TIER_COMPLEX=ollama:qwen2.5-coder:7b
|
|
160
|
-
# TIER_REASONING=ollama:qwen2.5-coder:7b
|
|
161
|
-
|
|
162
|
-
# Long-Term Memory System (Titans-Inspired) - Enabled by default
|
|
163
|
-
MEMORY_ENABLED=true
|
|
164
|
-
MEMORY_RETRIEVAL_LIMIT=5
|
|
165
|
-
MEMORY_SURPRISE_THRESHOLD=0.3
|
|
166
|
-
|
|
167
|
-
# Uncomment and configure your preferred cloud provider:
|
|
168
|
-
# OPENAI_API_KEY=sk-your-key
|
|
169
|
-
# OPENROUTER_API_KEY=your-key
|
|
170
|
-
# DATABRICKS_API_KEY=your-key
|
|
171
|
-
# DATABRICKS_API_BASE=https://your-workspace.databricks.com
|
|
172
|
-
EOF
|
|
173
|
-
print_success "Created basic .env file"
|
|
174
|
-
fi
|
|
175
|
-
|
|
176
|
-
echo ""
|
|
177
|
-
print_info "π Configuration ready! Key settings:"
|
|
178
|
-
echo " β’ Default provider: Ollama (local, offline)"
|
|
179
|
-
echo " β’ Memory system: Enabled (learns from conversations)"
|
|
180
|
-
echo " β’ Port: 8081"
|
|
181
|
-
echo ""
|
|
182
|
-
print_warning "To use cloud providers (Databricks/OpenAI/Azure):"
|
|
183
|
-
echo " Edit: ${BLUE}nano $INSTALL_DIR/.env${NC}"
|
|
184
|
-
echo " Add your API keys and change MODEL_PROVIDER"
|
|
185
|
-
else
|
|
137
|
+
if [ -f "$INSTALL_DIR/.env" ]; then
|
|
186
138
|
print_warning ".env file already exists, skipping"
|
|
139
|
+
return
|
|
187
140
|
fi
|
|
141
|
+
print_info "Skipping .env creation β run ${BLUE}lynkr init${NC} after install for an interactive setup."
|
|
188
142
|
}
|
|
189
143
|
|
|
190
144
|
# Create symlink for global access
|
|
@@ -224,44 +178,35 @@ print_next_steps() {
|
|
|
224
178
|
print_success "Lynkr installed successfully!"
|
|
225
179
|
echo "=============================="
|
|
226
180
|
echo ""
|
|
227
|
-
echo "π Quick Start
|
|
181
|
+
echo "π Quick Start:"
|
|
228
182
|
echo ""
|
|
229
|
-
echo "
|
|
230
|
-
echo "
|
|
183
|
+
echo " 1. Run the setup wizard:"
|
|
184
|
+
echo " ${BLUE}lynkr init${NC} ${GREEN}β interactive config (4 prompts, ~30 sec)${NC}"
|
|
231
185
|
echo ""
|
|
232
|
-
echo "
|
|
233
|
-
echo "
|
|
186
|
+
echo " The wizard asks for your usage mode (Claude Pro/Max via wrap, or direct"
|
|
187
|
+
echo " API), tier picks across 12 supported providers, credentials for what you"
|
|
188
|
+
echo " chose, and a few routing knobs. It writes a fully-populated .env with"
|
|
189
|
+
echo " production defaults for everything else (caching, compression, policy"
|
|
190
|
+
echo " budgets, MCP sandbox, agents, rate limiting)."
|
|
234
191
|
echo ""
|
|
235
192
|
echo " 2. Start Lynkr:"
|
|
236
|
-
echo " ${BLUE}lynkr${NC}"
|
|
193
|
+
echo " ${BLUE}lynkr${NC} ${GREEN}β run as a proxy server${NC}"
|
|
194
|
+
echo " ${BLUE}lynkr wrap claude${NC} ${GREEN}β OR launch a wrapped AI tool${NC}"
|
|
237
195
|
echo ""
|
|
238
|
-
echo " 3.
|
|
196
|
+
echo " 3. Point your tool at Lynkr:"
|
|
239
197
|
echo " ${BLUE}export ANTHROPIC_BASE_URL=http://localhost:8081${NC}"
|
|
198
|
+
echo " ${BLUE}export ANTHROPIC_API_KEY=any-non-empty-value${NC}"
|
|
240
199
|
echo " ${BLUE}claude${NC}"
|
|
241
200
|
echo ""
|
|
242
|
-
echo " ${YELLOW}
|
|
243
|
-
echo "
|
|
244
|
-
echo ""
|
|
245
|
-
echo "
|
|
246
|
-
echo " ${BLUE}nano $INSTALL_DIR/.env${NC}"
|
|
247
|
-
echo ""
|
|
248
|
-
echo " Update these lines:"
|
|
249
|
-
echo " ${BLUE}MODEL_PROVIDER=databricks${NC} ${GREEN}β Change from 'ollama'${NC}"
|
|
250
|
-
echo " ${BLUE}DATABRICKS_API_KEY=dapi_xxxxx${NC} ${GREEN}β Add your key${NC}"
|
|
251
|
-
echo " ${BLUE}DATABRICKS_API_BASE=https://your-workspace.databricks.com${NC}"
|
|
252
|
-
echo ""
|
|
253
|
-
echo " 2. Start Lynkr:"
|
|
254
|
-
echo " ${BLUE}lynkr${NC}"
|
|
255
|
-
echo ""
|
|
256
|
-
echo " 3. Configure Claude Code CLI:"
|
|
257
|
-
echo " ${BLUE}export ANTHROPIC_BASE_URL=http://localhost:8081${NC}"
|
|
258
|
-
echo " ${BLUE}export ANTHROPIC_API_KEY=any-non-empty-value${NC} ${GREEN}β Placeholder${NC}"
|
|
259
|
-
echo " ${BLUE}claude${NC}"
|
|
201
|
+
echo " ${YELLOW}Manual configuration (alternative)${NC}"
|
|
202
|
+
echo " ββββββββββββββββββββββββββββββββββββββββββββ"
|
|
203
|
+
echo " Copy ${BLUE}.env.example${NC} to ${BLUE}.env${NC} and edit by hand if you prefer."
|
|
204
|
+
echo " The 892-line template documents every available knob."
|
|
260
205
|
echo ""
|
|
261
206
|
echo "ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
|
|
262
207
|
echo ""
|
|
263
|
-
echo "π‘ ${YELLOW}Tip:${NC} Memory system
|
|
264
|
-
echo "
|
|
208
|
+
echo "π‘ ${YELLOW}Tip:${NC} Memory system, prompt caching, and TOON compression are all on"
|
|
209
|
+
echo " by default. The wizard's defaults match a production-grade Lynkr setup."
|
|
265
210
|
echo ""
|
|
266
211
|
echo "π Documentation: ${BLUE}https://github.com/Fast-Editor/Lynkr${NC}"
|
|
267
212
|
echo "π¬ Discord: ${BLUE}https://discord.gg/qF7DDxrX${NC}"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lynkr",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.7.1",
|
|
4
4
|
"description": "Self-hosted LLM gateway and tier-routing proxy for Claude Code, Cursor, and Codex. Routes across Ollama, AWS Bedrock, OpenRouter, Databricks, Azure OpenAI, llama.cpp, and LM Studio with prompt caching, MCP tools, and 60-80% cost savings.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"dev": "nodemon index.js",
|
|
17
17
|
"lint": "eslint src index.js",
|
|
18
18
|
"test": "npm run test:unit && npm run test:performance",
|
|
19
|
-
"test:unit": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/routing.test.js test/hybrid-routing-integration.test.js test/web-tools.test.js test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js test/azure-openai-config.test.js test/azure-openai-format-conversion.test.js test/azure-openai-routing.test.js test/azure-openai-streaming.test.js test/azure-openai-error-resilience.test.js test/azure-openai-integration.test.js test/openai-integration.test.js test/toon-compression.test.js test/llamacpp-integration.test.js test/resilience.test.js test/telemetry-routing.test.js test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js test/distill.test.js test/large-payload.test.js test/code-mode.test.js test/prompt-cache-injection.test.js test/risk-analyzer.test.js test/interaction-block.test.js test/preflight.test.js test/token-reduction.test.js test/session-affinity.test.js test/model-registry-cost.test.js test/task-decomposition.test.js test/output-format-guard.test.js test/tier-fallback.test.js",
|
|
19
|
+
"test:unit": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/routing.test.js test/hybrid-routing-integration.test.js test/web-tools.test.js test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js test/azure-openai-config.test.js test/azure-openai-format-conversion.test.js test/azure-openai-routing.test.js test/azure-openai-streaming.test.js test/azure-openai-error-resilience.test.js test/azure-openai-integration.test.js test/openai-integration.test.js test/toon-compression.test.js test/llamacpp-integration.test.js test/resilience.test.js test/telemetry-routing.test.js test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js test/distill.test.js test/large-payload.test.js test/code-mode.test.js test/prompt-cache-injection.test.js test/risk-analyzer.test.js test/interaction-block.test.js test/preflight.test.js test/token-reduction.test.js test/session-affinity.test.js test/model-registry-cost.test.js test/task-decomposition.test.js test/output-format-guard.test.js test/tier-fallback.test.js test/wrap.test.js test/init.test.js",
|
|
20
20
|
"test:memory": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js",
|
|
21
21
|
"test:new-features": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js",
|
|
22
22
|
"test:performance": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node test/hybrid-routing-performance.test.js && DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node test/performance-tests.js",
|
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
"express": "^5.1.0",
|
|
80
80
|
"express-rate-limit": "^8.2.1",
|
|
81
81
|
"fast-glob": "^3.3.2",
|
|
82
|
+
"graphify": "^1.0.0",
|
|
82
83
|
"hnswlib-node": "^3.0.0",
|
|
83
84
|
"js-tiktoken": "^1.0.20",
|
|
84
85
|
"js-yaml": "^4.1.1",
|
|
@@ -89,7 +90,7 @@
|
|
|
89
90
|
"undici": "^6.22.0"
|
|
90
91
|
},
|
|
91
92
|
"optionalDependencies": {
|
|
92
|
-
"better-sqlite3": "^12.
|
|
93
|
+
"better-sqlite3": "^12.11.1",
|
|
93
94
|
"dockerode": "^4.0.2",
|
|
94
95
|
"tree-sitter": "^0.21.1",
|
|
95
96
|
"tree-sitter-javascript": "^0.21.0",
|
|
@@ -51,7 +51,7 @@ async function _readTelemetry(days) {
|
|
|
51
51
|
return db
|
|
52
52
|
.prepare(
|
|
53
53
|
`SELECT request_text AS query, provider, model, quality_score AS quality,
|
|
54
|
-
cost,
|
|
54
|
+
cost_usd AS cost, latency_ms AS latency, tier
|
|
55
55
|
FROM routing_telemetry
|
|
56
56
|
WHERE timestamp >= ?
|
|
57
57
|
AND quality_score IS NOT NULL
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Convert routellm/gpt4_dataset (train.jsonl) β Lynkr kNN bootstrap JSONL.
|
|
4
|
+
|
|
5
|
+
Stream-downloads from HuggingFace and maps mixtral_score (1-5) to your
|
|
6
|
+
TIER_* model config so the kNN index learns to recommend YOUR real models,
|
|
7
|
+
not RouteLLM's pool (mixtral / gpt-4).
|
|
8
|
+
|
|
9
|
+
Output format matches scripts/build-knn-index.js bootstrap input:
|
|
10
|
+
{"query": "...", "provider": "ollama", "model": "minimax-m2.5:cloud",
|
|
11
|
+
"quality": 95, "cost": 0.001, "latency": 200, "tier": "SIMPLE"}
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
.venv/bin/python scripts/convert-routellm.py [--limit 30000] -o routellm.jsonl
|
|
15
|
+
"""
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
import urllib.request
|
|
20
|
+
import ssl
|
|
21
|
+
|
|
22
|
+
# Map mixtral_score β user's tier config.
|
|
23
|
+
# Spread is intentionally aggressive: only score=5 (Mixtral truly nailed it)
|
|
24
|
+
# stays SIMPLE; score=4 escalates to MEDIUM so the kNN index has enough MEDIUM
|
|
25
|
+
# examples to actually recommend non-SIMPLE tiers (vs the previous mapping
|
|
26
|
+
# where 86% of records were SIMPLE and kNN never disagreed with the heuristic).
|
|
27
|
+
SCORE_TO_ROUTE = {
|
|
28
|
+
5: {"tier": "SIMPLE", "provider": "ollama", "model": "minimax-m2.5:cloud", "quality": 95, "cost": 0.001, "latency": 200},
|
|
29
|
+
4: {"tier": "MEDIUM", "provider": "azure-anthropic", "model": "claude-sonnet-4-6", "quality": 80, "cost": 0.003, "latency": 1500},
|
|
30
|
+
3: {"tier": "MEDIUM", "provider": "azure-anthropic", "model": "claude-sonnet-4-6", "quality": 70, "cost": 0.003, "latency": 1500},
|
|
31
|
+
2: {"tier": "COMPLEX", "provider": "azure-anthropic", "model": "claude-sonnet-4-6", "quality": 55, "cost": 0.003, "latency": 1500},
|
|
32
|
+
1: {"tier": "REASONING", "provider": "azure-anthropic", "model": "claude-opus-4-8", "quality": 40, "cost": 0.015, "latency": 3500},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
URL = "https://huggingface.co/datasets/routellm/gpt4_dataset/resolve/main/train.jsonl"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main():
|
|
39
|
+
p = argparse.ArgumentParser()
|
|
40
|
+
p.add_argument("--output", "-o", default="data/routellm-bootstrap.jsonl")
|
|
41
|
+
p.add_argument("--limit", type=int, default=None,
|
|
42
|
+
help="Only convert first N rows")
|
|
43
|
+
args = p.parse_args()
|
|
44
|
+
|
|
45
|
+
print(f"Streaming {URL} β¦", file=sys.stderr)
|
|
46
|
+
# Some macOS Python builds have SSL issues with huggingface.co β fall back
|
|
47
|
+
# to unverified context if needed. The data is public, no auth.
|
|
48
|
+
ctx = ssl.create_default_context()
|
|
49
|
+
try:
|
|
50
|
+
with urllib.request.urlopen(URL, context=ctx, timeout=60) as resp:
|
|
51
|
+
_stream(resp, args.output, args.limit)
|
|
52
|
+
except (ssl.SSLError, OSError) as e:
|
|
53
|
+
print(f"SSL strict failed ({e}); retrying with unverified context", file=sys.stderr)
|
|
54
|
+
ctx = ssl._create_unverified_context()
|
|
55
|
+
with urllib.request.urlopen(URL, context=ctx, timeout=60) as resp:
|
|
56
|
+
_stream(resp, args.output, args.limit)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _stream(resp, out_path, limit):
|
|
60
|
+
written = 0
|
|
61
|
+
by_score = {s: 0 for s in SCORE_TO_ROUTE}
|
|
62
|
+
skipped = 0
|
|
63
|
+
with open(out_path, "w") as out:
|
|
64
|
+
for line in resp:
|
|
65
|
+
if limit and written >= limit:
|
|
66
|
+
break
|
|
67
|
+
try:
|
|
68
|
+
row = json.loads(line)
|
|
69
|
+
except Exception:
|
|
70
|
+
skipped += 1
|
|
71
|
+
continue
|
|
72
|
+
score = row.get("mixtral_score")
|
|
73
|
+
if score not in SCORE_TO_ROUTE:
|
|
74
|
+
skipped += 1
|
|
75
|
+
continue
|
|
76
|
+
route = SCORE_TO_ROUTE[score]
|
|
77
|
+
rec = {
|
|
78
|
+
"query": row.get("prompt", ""),
|
|
79
|
+
"provider": route["provider"],
|
|
80
|
+
"model": route["model"],
|
|
81
|
+
"quality": route["quality"],
|
|
82
|
+
"cost": route["cost"],
|
|
83
|
+
"latency": route["latency"],
|
|
84
|
+
"tier": route["tier"],
|
|
85
|
+
}
|
|
86
|
+
if not rec["query"]:
|
|
87
|
+
skipped += 1
|
|
88
|
+
continue
|
|
89
|
+
out.write(json.dumps(rec) + "\n")
|
|
90
|
+
written += 1
|
|
91
|
+
by_score[score] += 1
|
|
92
|
+
if written % 5000 == 0:
|
|
93
|
+
print(f" {written} writtenβ¦", file=sys.stderr)
|
|
94
|
+
|
|
95
|
+
print(f"\nWrote {written} records to {out_path} ({skipped} skipped)", file=sys.stderr)
|
|
96
|
+
print(f"By mixtral_score: {by_score}", file=sys.stderr)
|
|
97
|
+
print(f"\nTier distribution:", file=sys.stderr)
|
|
98
|
+
for score, route in SCORE_TO_ROUTE.items():
|
|
99
|
+
n = by_score[score]
|
|
100
|
+
if n:
|
|
101
|
+
print(f" {route['tier']:>10} {route['provider']}:{route['model']:30s} n={n}", file=sys.stderr)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
main()
|