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.
Files changed (40) hide show
  1. package/README.md +70 -21
  2. package/bin/cli.js +16 -3
  3. package/index.js +7 -3
  4. package/install.sh +3 -3
  5. package/lynkr-skill.tar.gz +0 -0
  6. package/native/Cargo.toml +26 -0
  7. package/native/index.js +29 -0
  8. package/native/lynkr-native.node +0 -0
  9. package/native/src/lib.rs +321 -0
  10. package/package.json +6 -5
  11. package/src/api/files-multipart.js +30 -0
  12. package/src/api/files-router.js +81 -0
  13. package/src/api/openai-router.js +352 -300
  14. package/src/api/router.js +100 -3
  15. package/src/cache/prompt.js +13 -0
  16. package/src/clients/databricks.js +33 -13
  17. package/src/clients/ollama-utils.js +21 -17
  18. package/src/clients/openai-format.js +20 -6
  19. package/src/clients/openrouter-utils.js +42 -37
  20. package/src/clients/prompt-cache-injection.js +140 -0
  21. package/src/clients/provider-capabilities.js +41 -0
  22. package/src/clients/responses-format.js +8 -7
  23. package/src/clients/standard-tools.js +1 -1
  24. package/src/clients/xml-tool-extractor.js +307 -0
  25. package/src/cluster.js +82 -0
  26. package/src/config/index.js +9 -0
  27. package/src/context/distill.js +15 -0
  28. package/src/context/tool-result-compressor.js +563 -0
  29. package/src/memory/extractor.js +22 -0
  30. package/src/orchestrator/index.js +101 -199
  31. package/src/routing/index.js +3 -32
  32. package/src/routing/telemetry.js +40 -2
  33. package/src/server.js +12 -0
  34. package/src/stores/file-store.js +69 -0
  35. package/src/stores/response-store.js +25 -0
  36. package/src/tools/index.js +1 -1
  37. package/src/tools/web.js +1 -1
  38. package/.lynkr/telemetry.db +0 -0
  39. package/.lynkr/telemetry.db-shm +0 -0
  40. 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
  [![npm version](https://img.shields.io/npm/v/lynkr.svg)](https://www.npmjs.com/package/lynkr)
6
- [![Tests](https://img.shields.io/badge/tests-652%20passing-brightgreen)](https://github.com/vishalveerareddy123/Lynkr)
6
+ [![Tests](https://img.shields.io/badge/tests-699%20passing-brightgreen)](https://github.com/Fast-Editor/Lynkr)
7
7
  [![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
8
8
  [![Node.js](https://img.shields.io/badge/node-20%2B-green)](https://nodejs.org)
9
9
  [![Homebrew Tap](https://img.shields.io/badge/homebrew-lynkr-brightgreen.svg)](https://github.com/vishalveerareddy123/homebrew-lynkr)
@@ -11,9 +11,9 @@
11
11
 
12
12
  <table>
13
13
  <tr>
14
- <td align="center"><strong>10+</strong><br/>LLM Providers</td>
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>652</strong><br/>Tests Passing</td>
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
- > Works with any OpenAI-compatible client: Cline, Continue.dev, ClawdBot, KiloCode, and more.
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.5 | $$$ |
143
- | **Azure OpenAI** | Cloud | GPT-4o, GPT-5, o1, o3 | $$$ |
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, o1, o3 | $$$ |
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. 6+ cloud providers for scale.
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 task complexity. Simple questions go to fast/cheap models. Complex architectural tasks go to powerful models. You configure the tiers.
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
- - **Prompt compression** — removes redundant context before sending
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
- - **TOON format** — compact serialization that cuts token count
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 when a provider goes down
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
- **NPM (recommended)**
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/vishalveerareddy123/Lynkr.git
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 10+ providers |
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/vishalveerareddy123/Lynkr/discussions) — Questions and tips
297
- - [Report Issues](https://github.com/vishalveerareddy123/Lynkr/issues) — Bug reports and feature requests
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 Show this help message
21
- -v, --version Show version number
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
- See .env.example for configuration options
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
@@ -1,3 +1,7 @@
1
- const { start } = require("./src/server");
2
-
3
- start();
1
+ if (process.env.CLUSTER_ENABLED === 'true') {
2
+ const { startCluster } = require('./src/cluster');
3
+ startCluster();
4
+ } else {
5
+ const { start } = require('./src/server');
6
+ start();
7
+ }
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/vishalveerareddy123/Lynkr/main/install.sh | bash
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/vishalveerareddy123/Lynkr/blob/main/.env.example
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/vishalveerareddy123/Lynkr${NC}"
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
  }
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
@@ -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
+ }