lynkr 9.0.1 → 9.0.2
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 +70 -21
- package/bin/cli.js +16 -3
- package/index.js +7 -3
- package/install.sh +3 -3
- package/lynkr-skill.tar.gz +0 -0
- package/native/Cargo.toml +26 -0
- package/native/index.js +29 -0
- package/native/lynkr-native.node +0 -0
- package/native/src/lib.rs +321 -0
- package/package.json +6 -5
- package/src/api/files-multipart.js +30 -0
- package/src/api/files-router.js +81 -0
- package/src/api/openai-router.js +352 -300
- package/src/api/router.js +100 -3
- package/src/cache/prompt.js +13 -0
- package/src/clients/databricks.js +33 -13
- package/src/clients/ollama-utils.js +21 -17
- package/src/clients/openai-format.js +20 -6
- package/src/clients/openrouter-utils.js +42 -37
- package/src/clients/prompt-cache-injection.js +140 -0
- package/src/clients/provider-capabilities.js +41 -0
- package/src/clients/responses-format.js +8 -7
- package/src/clients/standard-tools.js +1 -1
- package/src/clients/xml-tool-extractor.js +307 -0
- package/src/cluster.js +82 -0
- package/src/config/index.js +9 -0
- package/src/context/distill.js +15 -0
- package/src/context/tool-result-compressor.js +563 -0
- package/src/memory/extractor.js +22 -0
- package/src/orchestrator/index.js +101 -199
- package/src/routing/index.js +3 -32
- package/src/routing/telemetry.js +40 -2
- package/src/server.js +12 -0
- package/src/stores/file-store.js +69 -0
- package/src/stores/response-store.js +25 -0
- package/src/tools/index.js +1 -1
- package/src/tools/web.js +1 -1
- package/.lynkr/telemetry.db +0 -0
- package/.lynkr/telemetry.db-shm +0 -0
- package/.lynkr/telemetry.db-wal +0 -0
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
### Run Claude Code, Cursor, and Codex on any model. One proxy, every provider.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/lynkr)
|
|
6
|
-
[](https://github.com/Fast-Editor/Lynkr)
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
[](https://nodejs.org)
|
|
9
9
|
[](https://github.com/vishalveerareddy123/homebrew-lynkr)
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
|
|
12
12
|
<table>
|
|
13
13
|
<tr>
|
|
14
|
-
<td align="center"><strong>
|
|
14
|
+
<td align="center"><strong>12+</strong><br/>LLM Providers</td>
|
|
15
15
|
<td align="center"><strong>60-80%</strong><br/>Cost Reduction</td>
|
|
16
|
-
<td align="center"><strong>
|
|
16
|
+
<td align="center"><strong>699</strong><br/>Tests Passing</td>
|
|
17
17
|
<td align="center"><strong>0</strong><br/>Code Changes Required</td>
|
|
18
18
|
</tr>
|
|
19
19
|
</table>
|
|
@@ -55,6 +55,12 @@ lynkr start
|
|
|
55
55
|
|
|
56
56
|
### Install
|
|
57
57
|
|
|
58
|
+
**One-line install (recommended):**
|
|
59
|
+
```bash
|
|
60
|
+
curl -fsSL https://raw.githubusercontent.com/Fast-Editor/Lynkr/main/install.sh | bash
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Or via npm:**
|
|
58
64
|
```bash
|
|
59
65
|
npm install -g pino-pretty && npm install -g lynkr
|
|
60
66
|
```
|
|
@@ -125,7 +131,24 @@ const { text } = await generateText({
|
|
|
125
131
|
});
|
|
126
132
|
```
|
|
127
133
|
|
|
128
|
-
|
|
134
|
+
**OpenClaw**
|
|
135
|
+
```json
|
|
136
|
+
// openclaw.json
|
|
137
|
+
{
|
|
138
|
+
"models": {
|
|
139
|
+
"providers": [{
|
|
140
|
+
"name": "lynkr",
|
|
141
|
+
"type": "openai-compatible",
|
|
142
|
+
"base_url": "http://localhost:8081/v1",
|
|
143
|
+
"api_key": "any-value",
|
|
144
|
+
"models": ["auto"]
|
|
145
|
+
}]
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
Set `OPENCLAW_MODE=true` in Lynkr's `.env` to show actual provider/model in responses.
|
|
150
|
+
|
|
151
|
+
> Works with any OpenAI-compatible client: Cline, Continue.dev, OpenClaw, KiloCode, and more.
|
|
129
152
|
|
|
130
153
|
---
|
|
131
154
|
|
|
@@ -139,12 +162,16 @@ const { text } = await generateText({
|
|
|
139
162
|
| **MLX Server** | Local | Apple Silicon optimized | **Free** |
|
|
140
163
|
| **AWS Bedrock** | Cloud | 100+ (Claude, Llama, Mistral, Titan) | $$ |
|
|
141
164
|
| **OpenRouter** | Cloud | 100+ (GPT, Claude, Llama, Gemini) | $-$$ |
|
|
142
|
-
| **Databricks** | Cloud | Claude Sonnet 4.5, Opus 4.
|
|
143
|
-
| **Azure OpenAI** | Cloud | GPT-4o,
|
|
165
|
+
| **Databricks** | Cloud | Claude Sonnet 4.5, Opus 4.6 | $$$ |
|
|
166
|
+
| **Azure OpenAI** | Cloud | GPT-4o, o1, o3 | $$$ |
|
|
144
167
|
| **Azure Anthropic** | Cloud | Claude models | $$$ |
|
|
145
|
-
| **OpenAI** | Cloud | GPT-4o,
|
|
168
|
+
| **OpenAI** | Cloud | GPT-4o, o3, o4-mini | $$$ |
|
|
169
|
+
| **Google Vertex** | Cloud | Gemini 2.5 Pro/Flash | $$$ |
|
|
170
|
+
| **Moonshot AI** | Cloud | Kimi K2 Thinking/Turbo | $$ |
|
|
171
|
+
| **Z.AI** | Cloud | GLM-4.7 | $$ |
|
|
172
|
+
| **DeepSeek** | Cloud | DeepSeek Reasoner, R1 | $ |
|
|
146
173
|
|
|
147
|
-
4 local providers for **100% offline, free** usage.
|
|
174
|
+
4 local providers for **100% offline, free** usage. 10+ cloud providers for scale.
|
|
148
175
|
|
|
149
176
|
---
|
|
150
177
|
|
|
@@ -166,6 +193,9 @@ const { text } = await generateText({
|
|
|
166
193
|
| **Transaction fees** | None | None (OSS) / Paid enterprise | 5.5% on credits | Free tier / Paid |
|
|
167
194
|
| **Dependencies** | Node.js only | Python, Prisma, PostgreSQL | N/A | Docker, Python |
|
|
168
195
|
| **Format conversion** | Anthropic <-> OpenAI (automatic) | Automatic | N/A | Automatic |
|
|
196
|
+
| **Code intelligence** | Graphify (19-lang AST graph) | No | No | No |
|
|
197
|
+
| **Routing telemetry** | Built-in (SQLite + REST API) | No | Dashboard | Dashboard |
|
|
198
|
+
| **Admin hot-reload** | Yes (no restart) | Requires restart | N/A | Requires restart |
|
|
169
199
|
| **License** | Apache 2.0 | MIT | Proprietary | MIT (gateway) |
|
|
170
200
|
|
|
171
201
|
**Lynkr's edge:** Purpose-built for AI coding tools. Not a general LLM gateway — a proxy that understands Claude Code, Cursor, and Codex natively, with built-in token optimization, complexity-based routing, and a memory system designed for coding workflows. Installs in one command, runs on Node.js, zero infrastructure required.
|
|
@@ -188,20 +218,29 @@ const { text } = await generateText({
|
|
|
188
218
|
|
|
189
219
|
Lynkr isn't just a passthrough proxy. It's an optimization layer.
|
|
190
220
|
|
|
191
|
-
### Smart Routing
|
|
192
|
-
Routes requests to the right model based on
|
|
221
|
+
### Smart Routing (5-Phase)
|
|
222
|
+
Routes requests to the right model based on 5-phase complexity analysis. Simple questions go to fast/cheap models. Complex architectural tasks go to powerful models. Includes Graphify structural analysis for code-aware routing.
|
|
223
|
+
|
|
224
|
+
- **Complexity scoring** — 15-dimension weighted scoring with agentic workflow detection
|
|
225
|
+
- **Graphify integration** — AST-based knowledge graph detects god nodes, community cohesion, blast radius across 19 languages
|
|
226
|
+
- **Routing telemetry** — every decision recorded with quality scoring (0-100) and latency tracking (P50/P95/P99)
|
|
193
227
|
|
|
194
|
-
### Token Optimization
|
|
228
|
+
### Token Optimization (7 Phases)
|
|
195
229
|
- **Smart tool selection** — only sends tools relevant to the current task
|
|
196
|
-
- **
|
|
230
|
+
- **Code Mode** — replaces 100+ MCP tools with 4 meta-tools (~96% token reduction)
|
|
231
|
+
- **Distill compression** — structural similarity, delta rendering, smart dedup of repetitive tool outputs
|
|
232
|
+
- **Prompt caching** — SHA-256 keyed LRU cache
|
|
197
233
|
- **Memory deduplication** — eliminates repeated information across turns
|
|
198
|
-
- **
|
|
234
|
+
- **History compression** — sliding window with Distill-powered structural dedup
|
|
235
|
+
- **Headroom sidecar** — optional 47-92% ML-based compression (Smart Crusher, CCR, LLMLingua)
|
|
199
236
|
|
|
200
237
|
### Enterprise Resilience
|
|
201
|
-
- **Circuit breakers** — automatic failover
|
|
238
|
+
- **Circuit breakers** — automatic failover with half-open probe recovery
|
|
239
|
+
- **Admin hot-reload** — `POST /v1/admin/reload` reloads config + resets circuit breakers without restart
|
|
202
240
|
- **Load shedding** — graceful degradation under high load
|
|
203
241
|
- **Prometheus metrics** — full observability at `/metrics`
|
|
204
242
|
- **Health checks** — K8s-ready endpoints at `/health`
|
|
243
|
+
- **Performance timer** — per-request timing breakdown with `PERF_TIMER=true`
|
|
205
244
|
|
|
206
245
|
### Memory System
|
|
207
246
|
Titans-inspired long-term memory with surprise-based filtering. The system remembers important context across sessions and forgets noise — reducing token waste from repeated context.
|
|
@@ -214,14 +253,23 @@ SEMANTIC_CACHE_ENABLED=true
|
|
|
214
253
|
SEMANTIC_CACHE_THRESHOLD=0.95
|
|
215
254
|
```
|
|
216
255
|
|
|
217
|
-
### MCP Integration
|
|
218
|
-
Automatic Model Context Protocol server discovery and orchestration. Your MCP tools work through Lynkr without configuration.
|
|
256
|
+
### MCP Integration + Code Mode
|
|
257
|
+
Automatic Model Context Protocol server discovery and orchestration. Your MCP tools work through Lynkr without configuration. Enable Code Mode to replace 100+ MCP tool definitions with 4 lightweight meta-tools:
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
CODE_MODE_ENABLED=true # ~96% reduction in tool-catalog tokens
|
|
261
|
+
```
|
|
219
262
|
|
|
220
263
|
---
|
|
221
264
|
|
|
222
265
|
## Deployment Options
|
|
223
266
|
|
|
224
|
-
**
|
|
267
|
+
**One-line install (recommended)**
|
|
268
|
+
```bash
|
|
269
|
+
curl -fsSL https://raw.githubusercontent.com/Fast-Editor/Lynkr/main/install.sh | bash
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**NPM**
|
|
225
273
|
```bash
|
|
226
274
|
npm install -g lynkr && lynkr start
|
|
227
275
|
```
|
|
@@ -233,7 +281,7 @@ docker-compose up -d
|
|
|
233
281
|
|
|
234
282
|
**Git Clone**
|
|
235
283
|
```bash
|
|
236
|
-
git clone https://github.com/
|
|
284
|
+
git clone https://github.com/Fast-Editor/Lynkr.git
|
|
237
285
|
cd Lynkr && npm install && cp .env.example .env
|
|
238
286
|
npm start
|
|
239
287
|
```
|
|
@@ -251,9 +299,10 @@ brew install lynkr
|
|
|
251
299
|
| Guide | Description |
|
|
252
300
|
|-------|-------------|
|
|
253
301
|
| [Installation](documentation/installation.md) | All installation methods |
|
|
254
|
-
| [Provider Config](documentation/providers.md) | Setup for all
|
|
302
|
+
| [Provider Config](documentation/providers.md) | Setup for all 12+ providers |
|
|
255
303
|
| [Claude Code CLI](documentation/claude-code-cli.md) | Detailed Claude Code integration |
|
|
256
304
|
| [Codex CLI](documentation/codex-cli.md) | Codex config.toml setup |
|
|
305
|
+
| [OpenClaw](documentation/openclaw-integration.md) | OpenClaw integration with tier routing |
|
|
257
306
|
| [Cursor IDE](documentation/cursor-integration.md) | Cursor integration + troubleshooting |
|
|
258
307
|
| [Embeddings](documentation/embeddings.md) | @Codebase semantic search (4 options) |
|
|
259
308
|
| [Token Optimization](documentation/token-optimization.md) | 60-80% cost reduction strategies |
|
|
@@ -293,8 +342,8 @@ Apache 2.0 — See [LICENSE](LICENSE).
|
|
|
293
342
|
|
|
294
343
|
## Community
|
|
295
344
|
|
|
296
|
-
- [GitHub Discussions](https://github.com/
|
|
297
|
-
- [Report Issues](https://github.com/
|
|
345
|
+
- [GitHub Discussions](https://github.com/Fast-Editor/Lynkr/discussions) — Questions and tips
|
|
346
|
+
- [Report Issues](https://github.com/Fast-Editor/Lynkr/issues) — Bug reports and feature requests
|
|
298
347
|
- [NPM Package](https://www.npmjs.com/package/lynkr) — Official package
|
|
299
348
|
- [DeepWiki](https://deepwiki.com/vishalveerareddy123/Lynkr) — AI-powered docs search
|
|
300
349
|
|
package/bin/cli.js
CHANGED
|
@@ -17,11 +17,15 @@ Usage:
|
|
|
17
17
|
lynkr [options]
|
|
18
18
|
|
|
19
19
|
Options:
|
|
20
|
-
-h, --help
|
|
21
|
-
-v, --version
|
|
20
|
+
-h, --help Show this help message
|
|
21
|
+
-v, --version Show version number
|
|
22
|
+
--cluster Enable cluster mode (multi-core)
|
|
23
|
+
--workers N Number of worker processes (default: auto)
|
|
22
24
|
|
|
23
25
|
Environment Variables:
|
|
24
|
-
|
|
26
|
+
CLUSTER_ENABLED=true Enable multi-core cluster mode
|
|
27
|
+
CLUSTER_WORKERS=auto Worker count (auto = CPU cores - 1)
|
|
28
|
+
See .env.example for all configuration options
|
|
25
29
|
|
|
26
30
|
Documentation:
|
|
27
31
|
${pkg.homepage}
|
|
@@ -29,4 +33,13 @@ Documentation:
|
|
|
29
33
|
process.exit(0);
|
|
30
34
|
}
|
|
31
35
|
|
|
36
|
+
// CLI flags for cluster mode
|
|
37
|
+
if (process.argv.includes('--cluster')) {
|
|
38
|
+
process.env.CLUSTER_ENABLED = 'true';
|
|
39
|
+
}
|
|
40
|
+
const workersIdx = process.argv.indexOf('--workers');
|
|
41
|
+
if (workersIdx !== -1 && process.argv[workersIdx + 1]) {
|
|
42
|
+
process.env.CLUSTER_WORKERS = process.argv[workersIdx + 1];
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
require("../index.js");
|
package/index.js
CHANGED
package/install.sh
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
#
|
|
3
3
|
# Lynkr Installation Script
|
|
4
|
-
# Usage: curl -fsSL https://raw.githubusercontent.com/
|
|
4
|
+
# Usage: curl -fsSL https://raw.githubusercontent.com/Fast-Editor/Lynkr/main/install.sh | bash
|
|
5
5
|
#
|
|
6
6
|
# This script installs Lynkr, a self-hosted Claude Code proxy with multi-provider support.
|
|
7
7
|
#
|
|
@@ -125,7 +125,7 @@ create_env_file() {
|
|
|
125
125
|
# Fallback: create minimal .env if .env.example doesn't exist
|
|
126
126
|
cat > "$INSTALL_DIR/.env" << 'EOF'
|
|
127
127
|
# Lynkr Configuration
|
|
128
|
-
# For full options, see: https://github.com/
|
|
128
|
+
# For full options, see: https://github.com/Fast-Editor/Lynkr/blob/main/.env.example
|
|
129
129
|
|
|
130
130
|
# Model Provider (databricks, openai, azure-openai, azure-anthropic, openrouter, ollama, llamacpp)
|
|
131
131
|
MODEL_PROVIDER=ollama
|
|
@@ -247,7 +247,7 @@ print_next_steps() {
|
|
|
247
247
|
echo "💡 ${YELLOW}Tip:${NC} Memory system is enabled by default"
|
|
248
248
|
echo " Lynkr remembers preferences and project context across sessions"
|
|
249
249
|
echo ""
|
|
250
|
-
echo "📚 Documentation: ${BLUE}https://github.com/
|
|
250
|
+
echo "📚 Documentation: ${BLUE}https://github.com/Fast-Editor/Lynkr${NC}"
|
|
251
251
|
echo "💬 Discord: ${BLUE}https://discord.gg/qF7DDxrX${NC}"
|
|
252
252
|
echo ""
|
|
253
253
|
}
|
package/lynkr-skill.tar.gz
CHANGED
|
Binary file
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "lynkr-native"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2024"
|
|
5
|
+
|
|
6
|
+
[lib]
|
|
7
|
+
crate-type = ["cdylib"]
|
|
8
|
+
|
|
9
|
+
[dependencies]
|
|
10
|
+
napi = { version = "3", features = ["napi9", "serde-json"] }
|
|
11
|
+
napi-derive = "3"
|
|
12
|
+
serde = { version = "1", features = ["derive"] }
|
|
13
|
+
serde_json = { version = "1", features = ["preserve_order"] }
|
|
14
|
+
regex = "1"
|
|
15
|
+
sha2 = "0.10"
|
|
16
|
+
hex = "0.4"
|
|
17
|
+
mimalloc = { version = "0.1", default-features = false }
|
|
18
|
+
|
|
19
|
+
[build-dependencies]
|
|
20
|
+
napi-build = "2"
|
|
21
|
+
|
|
22
|
+
[profile.release]
|
|
23
|
+
opt-level = 3
|
|
24
|
+
lto = "fat"
|
|
25
|
+
codegen-units = 1
|
|
26
|
+
strip = true
|
package/native/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lynkr Native — Rust-powered hot-path functions
|
|
3
|
+
*
|
|
4
|
+
* Loads the native .node addon for 10-50x speedup on:
|
|
5
|
+
* - Complexity analysis (regex patterns)
|
|
6
|
+
* - Cache key computation (recursive sort + SHA-256)
|
|
7
|
+
* - Structural similarity (Jaccard on line sets)
|
|
8
|
+
* - Text normalization (ANSI strip + whitespace collapse)
|
|
9
|
+
* - Payload size estimation
|
|
10
|
+
*
|
|
11
|
+
* Falls back to JS implementations if the native addon is unavailable.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
let native = null;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
native = require('./lynkr-native.node');
|
|
18
|
+
} catch {
|
|
19
|
+
// Native addon not available — fall back to JS
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
available: native !== null,
|
|
24
|
+
analyzeComplexityNative: native?.analyzeComplexityNative ?? null,
|
|
25
|
+
computeCacheKey: native?.computeCacheKey ?? null,
|
|
26
|
+
structuralSimilarity: native?.structuralSimilarity ?? null,
|
|
27
|
+
normalizeText: native?.normalizeText ?? null,
|
|
28
|
+
estimatePayloadSize: native?.estimatePayloadSize ?? null,
|
|
29
|
+
};
|
|
Binary file
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
use mimalloc::MiMalloc;
|
|
2
|
+
|
|
3
|
+
#[global_allocator]
|
|
4
|
+
static GLOBAL: MiMalloc = MiMalloc;
|
|
5
|
+
|
|
6
|
+
use napi::bindgen_prelude::*;
|
|
7
|
+
use napi_derive::napi;
|
|
8
|
+
use regex::Regex;
|
|
9
|
+
use sha2::{Digest, Sha256};
|
|
10
|
+
use std::collections::BTreeMap;
|
|
11
|
+
use std::sync::LazyLock;
|
|
12
|
+
|
|
13
|
+
// ── 1. Complexity Analysis (15+ regex patterns at native speed) ─────
|
|
14
|
+
|
|
15
|
+
/// Pre-compiled regex patterns — compiled once, reused forever
|
|
16
|
+
static GREETING_RE: LazyLock<Regex> =
|
|
17
|
+
LazyLock::new(|| Regex::new(r"(?i)^(hi|hello|hey|thanks?|bye|goodbye|good morning|good evening|good afternoon|good night|howdy|greetings|welcome)\b").unwrap());
|
|
18
|
+
|
|
19
|
+
static YES_NO_RE: LazyLock<Regex> =
|
|
20
|
+
LazyLock::new(|| Regex::new(r"(?i)^(yes|no|ok|okay|sure|y|n|yep|nope|yea|nah|affirmative|negative|roger|copy)\s*[.!?]*$").unwrap());
|
|
21
|
+
|
|
22
|
+
static SIMPLE_QUESTION_RE: LazyLock<Regex> =
|
|
23
|
+
LazyLock::new(|| Regex::new(r"(?i)^(what|where|when|who|how|why|which|is|are|do|does|can|could|will|would|should)\b.{0,80}[?]?\s*$").unwrap());
|
|
24
|
+
|
|
25
|
+
static TECHNICAL_RE: LazyLock<Regex> =
|
|
26
|
+
LazyLock::new(|| Regex::new(r"(?i)\b(function|class|module|import|export|async|await|promise|api|database|server|client|component|interface|struct|enum|trait|impl|const|let|var|def|return|throw|catch|try|if|else|for|while|loop|match|switch|case)\b").unwrap());
|
|
27
|
+
|
|
28
|
+
static SECURITY_RE: LazyLock<Regex> =
|
|
29
|
+
LazyLock::new(|| Regex::new(r"(?i)\b(security|audit|vulnerab|exploit|injection|xss|csrf|auth|encrypt|decrypt|certificate|tls|ssl|oauth|jwt|token|permission|privilege|sanitize|escape|hash|salt)\b").unwrap());
|
|
30
|
+
|
|
31
|
+
static ARCHITECTURE_RE: LazyLock<Regex> =
|
|
32
|
+
LazyLock::new(|| Regex::new(r"(?i)\b(architect|design|pattern|microservice|monolith|scale|distributed|event.?driven|cqrs|saga|domain.?driven|hexagonal|clean.?arch|solid|dry|kiss)\b").unwrap());
|
|
33
|
+
|
|
34
|
+
static REFACTOR_RE: LazyLock<Regex> =
|
|
35
|
+
LazyLock::new(|| Regex::new(r"(?i)\b(refactor|restructure|reorganize|rewrite|rearchitect|decompos|extract|consolidat|simplif|clean.?up|tech.?debt)\b").unwrap());
|
|
36
|
+
|
|
37
|
+
static MULTI_FILE_RE: LazyLock<Regex> =
|
|
38
|
+
LazyLock::new(|| Regex::new(r"(?i)\b(all files|every file|entire|codebase|project.?wide|across.?the|multiple files|several files|many files)\b").unwrap());
|
|
39
|
+
|
|
40
|
+
static CONCURRENCY_RE: LazyLock<Regex> =
|
|
41
|
+
LazyLock::new(|| Regex::new(r"(?i)\b(async|await|concurrent|parallel|thread|mutex|lock|deadlock|race.?condition|semaphore|channel|atomic|worker|pool)\b").unwrap());
|
|
42
|
+
|
|
43
|
+
static PERFORMANCE_RE: LazyLock<Regex> =
|
|
44
|
+
LazyLock::new(|| Regex::new(r"(?i)\b(performance|optimize|bottleneck|profil|benchmark|latency|throughput|cache|memory.?leak|cpu|heap|gc|garbage)\b").unwrap());
|
|
45
|
+
|
|
46
|
+
static DATABASE_RE: LazyLock<Regex> =
|
|
47
|
+
LazyLock::new(|| Regex::new(r"(?i)\b(database|sql|query|migration|schema|index|transaction|join|aggregate|stored.?proc|trigger|view|orm|sequelize|prisma|knex|typeorm)\b").unwrap());
|
|
48
|
+
|
|
49
|
+
static REASONING_RE: LazyLock<Regex> =
|
|
50
|
+
LazyLock::new(|| Regex::new(r"(?i)\b(step.?by.?step|think.*through|analyz|compar|trade.?off|pros?.?and?.?cons|evaluat|assess|consider|weigh|reason|logic|deduc)\b").unwrap());
|
|
51
|
+
|
|
52
|
+
static FORCE_CLOUD_RE: LazyLock<Vec<Regex>> = LazyLock::new(|| {
|
|
53
|
+
vec![
|
|
54
|
+
Regex::new(r"(?i)\bsecurity\s+(audit|review)\b").unwrap(),
|
|
55
|
+
Regex::new(r"(?i)\barchitect(ure)?\s+(design|review)\b").unwrap(),
|
|
56
|
+
Regex::new(r"(?i)\b(complete|full|entire)\s+codebase\s+refactor").unwrap(),
|
|
57
|
+
Regex::new(r"(?i)\bcode\s+review\b").unwrap(),
|
|
58
|
+
Regex::new(r"(?i)\bpr\s+review\b").unwrap(),
|
|
59
|
+
Regex::new(r"(?i)\bcomplex\s+debug").unwrap(),
|
|
60
|
+
Regex::new(r"(?i)\bproduction\s+(incident|outage|issue)\b").unwrap(),
|
|
61
|
+
]
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
static FORCE_LOCAL_RE: LazyLock<Vec<Regex>> = LazyLock::new(|| {
|
|
65
|
+
vec![
|
|
66
|
+
Regex::new(r"(?i)^(hi|hello|hey|thanks?|bye|goodbye)\s*[.!?]*$").unwrap(),
|
|
67
|
+
Regex::new(r"(?i)^what\s+time\s+is\s+it").unwrap(),
|
|
68
|
+
Regex::new(r"(?i)^(yes|no|ok|okay|sure|y|n)\s*[.!?]*$").unwrap(),
|
|
69
|
+
Regex::new(r"(?i)^(help|commands?|menu)\s*[.!?]*$").unwrap(),
|
|
70
|
+
]
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
#[napi(object)]
|
|
74
|
+
pub struct ComplexityResult {
|
|
75
|
+
pub score: u32,
|
|
76
|
+
pub force_local: bool,
|
|
77
|
+
pub force_cloud: bool,
|
|
78
|
+
pub token_score: u32,
|
|
79
|
+
pub task_type_score: u32,
|
|
80
|
+
pub code_complexity_score: u32,
|
|
81
|
+
pub reasoning_score: u32,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Analyze request complexity — Rust regex engine is 10-50x faster than JS RegExp
|
|
85
|
+
#[napi]
|
|
86
|
+
pub fn analyze_complexity_native(content: String, token_estimate: u32, tool_count: u32) -> ComplexityResult {
|
|
87
|
+
// Force patterns (short-circuit)
|
|
88
|
+
let force_local = FORCE_LOCAL_RE.iter().any(|re| re.is_match(&content));
|
|
89
|
+
if force_local {
|
|
90
|
+
return ComplexityResult {
|
|
91
|
+
score: 0,
|
|
92
|
+
force_local: true,
|
|
93
|
+
force_cloud: false,
|
|
94
|
+
token_score: 0,
|
|
95
|
+
task_type_score: 0,
|
|
96
|
+
code_complexity_score: 0,
|
|
97
|
+
reasoning_score: 0,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let force_cloud = FORCE_CLOUD_RE.iter().any(|re| re.is_match(&content));
|
|
102
|
+
|
|
103
|
+
// Token score (0-20)
|
|
104
|
+
let token_score = match token_estimate {
|
|
105
|
+
0..500 => 0,
|
|
106
|
+
500..1000 => 4,
|
|
107
|
+
1000..2000 => 8,
|
|
108
|
+
2000..4000 => 12,
|
|
109
|
+
4000..8000 => 16,
|
|
110
|
+
_ => 20,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Tool score (0-20)
|
|
114
|
+
let tool_score = match tool_count {
|
|
115
|
+
0 => 0,
|
|
116
|
+
1..=3 => 4,
|
|
117
|
+
4..=6 => 8,
|
|
118
|
+
7..=10 => 12,
|
|
119
|
+
11..=15 => 16,
|
|
120
|
+
_ => 20,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Task type (0-25)
|
|
124
|
+
let task_type_score = if GREETING_RE.is_match(&content) || YES_NO_RE.is_match(&content) {
|
|
125
|
+
0
|
|
126
|
+
} else if SIMPLE_QUESTION_RE.is_match(&content) {
|
|
127
|
+
3
|
|
128
|
+
} else if REFACTOR_RE.is_match(&content) {
|
|
129
|
+
16
|
|
130
|
+
} else if MULTI_FILE_RE.is_match(&content) {
|
|
131
|
+
22
|
|
132
|
+
} else if force_cloud {
|
|
133
|
+
25
|
|
134
|
+
} else if TECHNICAL_RE.is_match(&content) {
|
|
135
|
+
10
|
|
136
|
+
} else {
|
|
137
|
+
5
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Code complexity (0-20)
|
|
141
|
+
let mut code_score: u32 = 0;
|
|
142
|
+
if MULTI_FILE_RE.is_match(&content) { code_score += 5; }
|
|
143
|
+
if ARCHITECTURE_RE.is_match(&content) { code_score += 5; }
|
|
144
|
+
if SECURITY_RE.is_match(&content) { code_score += 4; }
|
|
145
|
+
if CONCURRENCY_RE.is_match(&content) { code_score += 3; }
|
|
146
|
+
if PERFORMANCE_RE.is_match(&content) { code_score += 3; }
|
|
147
|
+
if DATABASE_RE.is_match(&content) { code_score += 3; }
|
|
148
|
+
let code_complexity_score = code_score.min(20);
|
|
149
|
+
|
|
150
|
+
// Reasoning (0-15)
|
|
151
|
+
let reasoning_score = if REASONING_RE.is_match(&content) { 4 } else { 0 };
|
|
152
|
+
|
|
153
|
+
let total = (token_score + tool_score + task_type_score + code_complexity_score + reasoning_score).min(100);
|
|
154
|
+
|
|
155
|
+
ComplexityResult {
|
|
156
|
+
score: if force_cloud { total.max(76) } else { total },
|
|
157
|
+
force_local: false,
|
|
158
|
+
force_cloud,
|
|
159
|
+
token_score,
|
|
160
|
+
task_type_score,
|
|
161
|
+
code_complexity_score,
|
|
162
|
+
reasoning_score,
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── 2. Cache Key Computation (recursive sort + SHA-256) ─────────────
|
|
167
|
+
|
|
168
|
+
/// Recursively sort all object keys and produce a stable SHA-256 hash.
|
|
169
|
+
/// This is the hot path for prompt cache key generation.
|
|
170
|
+
#[napi]
|
|
171
|
+
pub fn compute_cache_key(json_str: String) -> String {
|
|
172
|
+
let normalized = match serde_json::from_str::<serde_json::Value>(&json_str) {
|
|
173
|
+
Ok(val) => normalize_value(&val),
|
|
174
|
+
Err(_) => {
|
|
175
|
+
// Fallback: hash the raw string
|
|
176
|
+
let mut hasher = Sha256::new();
|
|
177
|
+
hasher.update(json_str.as_bytes());
|
|
178
|
+
return hex::encode(hasher.finalize());
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
let stable = serde_json::to_string(&normalized).unwrap_or(json_str);
|
|
183
|
+
let mut hasher = Sha256::new();
|
|
184
|
+
hasher.update(stable.as_bytes());
|
|
185
|
+
hex::encode(hasher.finalize())
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/// Recursively normalize a JSON value: sort object keys, preserve arrays
|
|
189
|
+
fn normalize_value(val: &serde_json::Value) -> serde_json::Value {
|
|
190
|
+
match val {
|
|
191
|
+
serde_json::Value::Object(map) => {
|
|
192
|
+
let mut sorted = BTreeMap::new();
|
|
193
|
+
for (k, v) in map {
|
|
194
|
+
sorted.insert(k.clone(), normalize_value(v));
|
|
195
|
+
}
|
|
196
|
+
serde_json::Value::Object(sorted.into_iter().collect())
|
|
197
|
+
}
|
|
198
|
+
serde_json::Value::Array(arr) => {
|
|
199
|
+
serde_json::Value::Array(arr.iter().map(normalize_value).collect())
|
|
200
|
+
}
|
|
201
|
+
other => other.clone(),
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── 3. Structural Similarity (Jaccard on line sets) ─────────────────
|
|
206
|
+
|
|
207
|
+
/// Compute Jaccard similarity between two text blocks using normalized line sets.
|
|
208
|
+
/// Used by Distill compression for dedup detection.
|
|
209
|
+
#[napi]
|
|
210
|
+
pub fn structural_similarity(a: String, b: String) -> f64 {
|
|
211
|
+
if a.is_empty() && b.is_empty() {
|
|
212
|
+
return 1.0;
|
|
213
|
+
}
|
|
214
|
+
if a.is_empty() || b.is_empty() {
|
|
215
|
+
return 0.0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let set_a: std::collections::HashSet<&str> = a.lines()
|
|
219
|
+
.map(|l| l.trim())
|
|
220
|
+
.filter(|l| !l.is_empty())
|
|
221
|
+
.collect();
|
|
222
|
+
|
|
223
|
+
let set_b: std::collections::HashSet<&str> = b.lines()
|
|
224
|
+
.map(|l| l.trim())
|
|
225
|
+
.filter(|l| !l.is_empty())
|
|
226
|
+
.collect();
|
|
227
|
+
|
|
228
|
+
if set_a.is_empty() && set_b.is_empty() {
|
|
229
|
+
return 1.0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let intersection = set_a.intersection(&set_b).count();
|
|
233
|
+
let union = set_a.union(&set_b).count();
|
|
234
|
+
|
|
235
|
+
if union == 0 { 0.0 } else { intersection as f64 / union as f64 }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── 4. Text Normalization (ANSI strip + whitespace collapse) ────────
|
|
239
|
+
|
|
240
|
+
static ANSI_RE: LazyLock<Regex> =
|
|
241
|
+
LazyLock::new(|| Regex::new(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])").unwrap());
|
|
242
|
+
|
|
243
|
+
/// Strip ANSI escape codes and normalize whitespace.
|
|
244
|
+
/// Used by Distill compression on every tool result.
|
|
245
|
+
#[napi]
|
|
246
|
+
pub fn normalize_text(text: String) -> String {
|
|
247
|
+
let stripped = ANSI_RE.replace_all(&text, "");
|
|
248
|
+
let normalized = stripped
|
|
249
|
+
.replace("\r\n", "\n")
|
|
250
|
+
.replace('\r', "\n");
|
|
251
|
+
|
|
252
|
+
// Collapse whitespace runs
|
|
253
|
+
let mut result = String::with_capacity(normalized.len());
|
|
254
|
+
let mut prev_space = false;
|
|
255
|
+
let mut newline_count = 0;
|
|
256
|
+
|
|
257
|
+
for ch in normalized.chars() {
|
|
258
|
+
if ch == '\n' {
|
|
259
|
+
newline_count += 1;
|
|
260
|
+
if newline_count <= 2 {
|
|
261
|
+
result.push('\n');
|
|
262
|
+
}
|
|
263
|
+
prev_space = false;
|
|
264
|
+
} else if ch == ' ' || ch == '\t' {
|
|
265
|
+
if !prev_space {
|
|
266
|
+
result.push(' ');
|
|
267
|
+
prev_space = true;
|
|
268
|
+
}
|
|
269
|
+
newline_count = 0;
|
|
270
|
+
} else {
|
|
271
|
+
result.push(ch);
|
|
272
|
+
prev_space = false;
|
|
273
|
+
newline_count = 0;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
result.trim().to_string()
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ── 5. Payload Size Estimation ──────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
/// Estimate payload content size without full JSON serialization.
|
|
283
|
+
/// Scans for base64 image data and text content lengths.
|
|
284
|
+
#[napi]
|
|
285
|
+
pub fn estimate_payload_size(json_str: String) -> u64 {
|
|
286
|
+
let val: serde_json::Value = match serde_json::from_str(&json_str) {
|
|
287
|
+
Ok(v) => v,
|
|
288
|
+
Err(_) => return json_str.len() as u64,
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
let messages = match val.get("messages").and_then(|m| m.as_array()) {
|
|
292
|
+
Some(m) => m,
|
|
293
|
+
None => return 0,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
let mut size: u64 = 0;
|
|
297
|
+
|
|
298
|
+
for msg in messages {
|
|
299
|
+
if let Some(content) = msg.get("content") {
|
|
300
|
+
if let Some(s) = content.as_str() {
|
|
301
|
+
size += s.len() as u64;
|
|
302
|
+
} else if let Some(arr) = content.as_array() {
|
|
303
|
+
for block in arr {
|
|
304
|
+
if let Some(text) = block.get("text").and_then(|t| t.as_str()) {
|
|
305
|
+
size += text.len() as u64;
|
|
306
|
+
}
|
|
307
|
+
if let Some(data) = block.pointer("/source/data").and_then(|d| d.as_str()) {
|
|
308
|
+
size += data.len() as u64;
|
|
309
|
+
}
|
|
310
|
+
if let Some(url) = block.pointer("/image_url/url").and_then(|u| u.as_str()) {
|
|
311
|
+
if url.starts_with("data:") {
|
|
312
|
+
size += url.len() as u64;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
size
|
|
321
|
+
}
|