lynkr 9.6.0 → 9.7.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 +36 -0
- package/bin/cli.js +2 -0
- package/bin/wrap.js +686 -0
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lynkr",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.7.0",
|
|
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/
|
|
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/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",
|
|
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()
|