lynkr 9.0.1 → 9.1.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 +34 -4
- package/bin/lynkr-trajectory.js +136 -0
- package/bin/lynkr-usage.js +219 -0
- package/funding.json +110 -0
- 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/public/dashboard.html +665 -0
- package/src/api/files-multipart.js +30 -0
- package/src/api/files-router.js +81 -0
- package/src/api/middleware/budget.js +19 -1
- package/src/api/middleware/load-shedding.js +17 -0
- package/src/api/openai-router.js +353 -301
- package/src/api/router.js +275 -40
- package/src/cache/prompt.js +13 -0
- package/src/clients/databricks.js +42 -18
- package/src/clients/ollama-utils.js +21 -17
- package/src/clients/openai-format.js +50 -10
- 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 +16 -0
- package/src/context/distill.js +15 -0
- package/src/context/tool-result-compressor.js +563 -0
- package/src/dashboard/api.js +170 -0
- package/src/dashboard/router.js +13 -0
- package/src/headroom/client.js +3 -109
- package/src/headroom/index.js +0 -14
- package/src/memory/extractor.js +22 -0
- package/src/memory/search.js +0 -50
- package/src/orchestrator/index.js +163 -204
- package/src/orchestrator/preflight.js +188 -0
- package/src/routing/index.js +64 -32
- package/src/routing/interaction.js +183 -0
- package/src/routing/risk-analyzer.js +194 -0
- package/src/routing/telemetry.js +47 -2
- package/src/server.js +15 -0
- package/src/stores/file-store.js +104 -0
- package/src/stores/response-store.js +25 -0
- package/src/tools/index.js +1 -1
- package/src/tools/smart-selection.js +11 -2
- package/src/tools/web.js +1 -1
- package/src/training/trajectory-compressor.js +266 -0
- package/src/usage/aggregator.js +206 -0
- package/src/utils/markdown-ansi.js +146 -0
- 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
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
const path = require("path");
|
|
3
4
|
const pkg = require('../package.json');
|
|
4
5
|
|
|
6
|
+
// Subcommands. Dispatched before server boot so `lynkr usage` / `lynkr trajectory`
|
|
7
|
+
// don't start the proxy. Add new subcommands here, not in scattered binaries.
|
|
8
|
+
const SUBCOMMANDS = {
|
|
9
|
+
usage: path.join(__dirname, "lynkr-usage.js"),
|
|
10
|
+
trajectory: path.join(__dirname, "lynkr-trajectory.js"),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const sub = process.argv[2];
|
|
14
|
+
if (sub && Object.prototype.hasOwnProperty.call(SUBCOMMANDS, sub)) {
|
|
15
|
+
process.argv.splice(2, 1); // drop the subcommand token so the script's own arg parser is happy
|
|
16
|
+
require(SUBCOMMANDS[sub]);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
5
20
|
if (process.argv.includes('--version') || process.argv.includes('-v')) {
|
|
6
21
|
console.log(pkg.version);
|
|
7
22
|
process.exit(0);
|
|
@@ -14,14 +29,20 @@ ${pkg.name} v${pkg.version}
|
|
|
14
29
|
${pkg.description}
|
|
15
30
|
|
|
16
31
|
Usage:
|
|
17
|
-
lynkr [options]
|
|
32
|
+
lynkr [options] Start the proxy server (default)
|
|
33
|
+
lynkr usage [options] Show AI spend report and tier-routing savings
|
|
34
|
+
lynkr trajectory [options] Export agent trajectories as JSONL training data
|
|
18
35
|
|
|
19
36
|
Options:
|
|
20
|
-
-h, --help
|
|
21
|
-
-v, --version
|
|
37
|
+
-h, --help Show this help message
|
|
38
|
+
-v, --version Show version number
|
|
39
|
+
--cluster Enable cluster mode (multi-core)
|
|
40
|
+
--workers N Number of worker processes (default: auto)
|
|
22
41
|
|
|
23
42
|
Environment Variables:
|
|
24
|
-
|
|
43
|
+
CLUSTER_ENABLED=true Enable multi-core cluster mode
|
|
44
|
+
CLUSTER_WORKERS=auto Worker count (auto = CPU cores - 1)
|
|
45
|
+
See .env.example for all configuration options
|
|
25
46
|
|
|
26
47
|
Documentation:
|
|
27
48
|
${pkg.homepage}
|
|
@@ -29,4 +50,13 @@ Documentation:
|
|
|
29
50
|
process.exit(0);
|
|
30
51
|
}
|
|
31
52
|
|
|
53
|
+
// CLI flags for cluster mode
|
|
54
|
+
if (process.argv.includes('--cluster')) {
|
|
55
|
+
process.env.CLUSTER_ENABLED = 'true';
|
|
56
|
+
}
|
|
57
|
+
const workersIdx = process.argv.indexOf('--workers');
|
|
58
|
+
if (workersIdx !== -1 && process.argv[workersIdx + 1]) {
|
|
59
|
+
process.env.CLUSTER_WORKERS = process.argv[workersIdx + 1];
|
|
60
|
+
}
|
|
61
|
+
|
|
32
62
|
require("../index.js");
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
/**
|
|
4
|
+
* lynkr trajectory — export agent trajectories from the session DB
|
|
5
|
+
* as JSONL training data.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* lynkr trajectory # stdout, last 30 days
|
|
9
|
+
* lynkr trajectory --since 7d # last 7 days
|
|
10
|
+
* lynkr trajectory --output trajectories.jsonl # write to file
|
|
11
|
+
* lynkr trajectory --tier COMPLEX # only complex sessions
|
|
12
|
+
* lynkr trajectory --anonymize # strip PII / paths / secrets
|
|
13
|
+
* lynkr trajectory --count # just print the row count
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const path = require("path");
|
|
17
|
+
|
|
18
|
+
process.env.WORKSPACE_ROOT = process.env.WORKSPACE_ROOT || path.resolve(__dirname, "..");
|
|
19
|
+
|
|
20
|
+
const compressor = require("../src/training/trajectory-compressor");
|
|
21
|
+
|
|
22
|
+
function parseArgs(argv) {
|
|
23
|
+
const opts = { since: "30d", anonymize: false, output: "-", count: false };
|
|
24
|
+
for (let i = 2; i < argv.length; i++) {
|
|
25
|
+
const a = argv[i];
|
|
26
|
+
const next = argv[i + 1];
|
|
27
|
+
if (a === "--since" && next) {
|
|
28
|
+
opts.since = next;
|
|
29
|
+
i++;
|
|
30
|
+
} else if (a === "--days" && next) {
|
|
31
|
+
opts.since = `${parseInt(next, 10)}d`;
|
|
32
|
+
i++;
|
|
33
|
+
} else if (a === "--tier" && next) {
|
|
34
|
+
opts.tier = next.toUpperCase();
|
|
35
|
+
i++;
|
|
36
|
+
} else if (a === "--output" && next) {
|
|
37
|
+
opts.output = next;
|
|
38
|
+
i++;
|
|
39
|
+
} else if (a === "-o" && next) {
|
|
40
|
+
opts.output = next;
|
|
41
|
+
i++;
|
|
42
|
+
} else if (a === "--anonymize" || a === "--anonymise") {
|
|
43
|
+
opts.anonymize = true;
|
|
44
|
+
} else if (a === "--count") {
|
|
45
|
+
opts.count = true;
|
|
46
|
+
} else if (a === "--help" || a === "-h") {
|
|
47
|
+
printHelp();
|
|
48
|
+
process.exit(0);
|
|
49
|
+
} else if (a === "--format" && next) {
|
|
50
|
+
// Reserved for future formats. Only "jsonl" is supported today.
|
|
51
|
+
if (next !== "jsonl") {
|
|
52
|
+
console.error(`Unsupported --format: ${next}. Only 'jsonl' is supported.`);
|
|
53
|
+
process.exit(2);
|
|
54
|
+
}
|
|
55
|
+
i++;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return opts;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function printHelp() {
|
|
62
|
+
console.log(`Lynkr trajectory exporter — emit JSONL training samples from session history.
|
|
63
|
+
|
|
64
|
+
Usage:
|
|
65
|
+
lynkr trajectory [options]
|
|
66
|
+
|
|
67
|
+
Options:
|
|
68
|
+
--since <window> "7d", "30d", ISO date, or epoch ms (default: 30d)
|
|
69
|
+
--days N Shorthand for --since Nd
|
|
70
|
+
--tier <tier> Filter to one tier: SIMPLE, MEDIUM, COMPLEX, REASONING
|
|
71
|
+
--output, -o <path> Output file (default: stdout, "-")
|
|
72
|
+
--anonymize Strip PII, file paths, API keys, hostnames
|
|
73
|
+
--count Print only the row count, no output
|
|
74
|
+
--format jsonl Output format (only jsonl supported)
|
|
75
|
+
-h, --help Show this help
|
|
76
|
+
|
|
77
|
+
Examples:
|
|
78
|
+
lynkr trajectory --days 7 --output last-week.jsonl
|
|
79
|
+
lynkr trajectory --tier COMPLEX --anonymize -o complex-anon.jsonl
|
|
80
|
+
lynkr trajectory --count
|
|
81
|
+
|
|
82
|
+
Output format (one JSON object per line):
|
|
83
|
+
{
|
|
84
|
+
"session_id": "...",
|
|
85
|
+
"messages": [{"role": "user", "content": "..."}, ...],
|
|
86
|
+
"tool_calls": [...],
|
|
87
|
+
"outcome": "success" | "error",
|
|
88
|
+
"tier": "MEDIUM",
|
|
89
|
+
"complexity_score": 38,
|
|
90
|
+
"model_used": "gpt-4o",
|
|
91
|
+
"provider_used": "azure-openai",
|
|
92
|
+
"tokens_in": 1234,
|
|
93
|
+
"tokens_out": 456,
|
|
94
|
+
"latency_ms": 2400,
|
|
95
|
+
"started_at": "...",
|
|
96
|
+
"ended_at": "..."
|
|
97
|
+
}
|
|
98
|
+
`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function fmtInt(n) {
|
|
102
|
+
return new Intl.NumberFormat("en-US").format(n || 0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function main() {
|
|
106
|
+
const opts = parseArgs(process.argv);
|
|
107
|
+
|
|
108
|
+
if (opts.count) {
|
|
109
|
+
// Quick path — stream-walk the sessions and just count valid trajectories.
|
|
110
|
+
let count = 0;
|
|
111
|
+
compressor.exportJsonl({
|
|
112
|
+
...opts,
|
|
113
|
+
output: { write: () => count++, end: () => {} },
|
|
114
|
+
});
|
|
115
|
+
console.log(`${fmtInt(count)} trajectories`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const isStdout = opts.output === "-";
|
|
120
|
+
const start = Date.now();
|
|
121
|
+
const result = compressor.exportJsonl({
|
|
122
|
+
since: opts.since,
|
|
123
|
+
tier: opts.tier,
|
|
124
|
+
anonymize: opts.anonymize,
|
|
125
|
+
output: opts.output,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (!isStdout) {
|
|
129
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
130
|
+
process.stderr.write(
|
|
131
|
+
`Exported ${fmtInt(result.count)} trajectories to ${result.output} in ${elapsed}s\n`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
main();
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
/**
|
|
4
|
+
* lynkr usage — print AI spend report from routing telemetry.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* lynkr-usage # last 30 days
|
|
8
|
+
* lynkr-usage --days 7
|
|
9
|
+
* lynkr-usage --window 1d
|
|
10
|
+
* lynkr-usage --window all
|
|
11
|
+
* lynkr-usage --json # machine-readable
|
|
12
|
+
* lynkr-usage --flagship gpt-5 # alternative comparison model
|
|
13
|
+
* lynkr-usage --provider moonshot # filter to one provider
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const path = require("path");
|
|
17
|
+
|
|
18
|
+
// Make sure config/logger pick up the workspace root
|
|
19
|
+
process.env.WORKSPACE_ROOT = process.env.WORKSPACE_ROOT || path.resolve(__dirname, "..");
|
|
20
|
+
|
|
21
|
+
const aggregator = require("../src/usage/aggregator");
|
|
22
|
+
|
|
23
|
+
function parseArgs(argv) {
|
|
24
|
+
const opts = { window: "30d", json: false };
|
|
25
|
+
for (let i = 2; i < argv.length; i++) {
|
|
26
|
+
const a = argv[i];
|
|
27
|
+
const next = argv[i + 1];
|
|
28
|
+
if (a === "--json") opts.json = true;
|
|
29
|
+
else if (a === "--days" && next) {
|
|
30
|
+
opts.window = `${parseInt(next, 10)}d`;
|
|
31
|
+
i++;
|
|
32
|
+
} else if (a === "--window" && next) {
|
|
33
|
+
opts.window = next;
|
|
34
|
+
i++;
|
|
35
|
+
} else if (a === "--since" && next) {
|
|
36
|
+
opts.window = next;
|
|
37
|
+
i++;
|
|
38
|
+
} else if (a === "--flagship" && next) {
|
|
39
|
+
opts.flagship = next;
|
|
40
|
+
i++;
|
|
41
|
+
} else if (a === "--provider" && next) {
|
|
42
|
+
opts.provider = next;
|
|
43
|
+
i++;
|
|
44
|
+
} else if (a === "--model" && next) {
|
|
45
|
+
opts.model = next;
|
|
46
|
+
i++;
|
|
47
|
+
} else if (a === "--help" || a === "-h") {
|
|
48
|
+
printHelp();
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return opts;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function printHelp() {
|
|
56
|
+
console.log(`Lynkr usage report — show AI spend and tier-routing savings.
|
|
57
|
+
|
|
58
|
+
Usage:
|
|
59
|
+
lynkr usage [options]
|
|
60
|
+
|
|
61
|
+
Options:
|
|
62
|
+
--days N Window in days (e.g. --days 7)
|
|
63
|
+
--window <preset> Window preset: 1d, 7d, 30d, all (default: 30d)
|
|
64
|
+
--since <iso> Custom start time (ISO 8601 or epoch ms)
|
|
65
|
+
--flagship <model> Comparison model for "savings" math (default: claude-sonnet-4-5-20250929)
|
|
66
|
+
--provider <name> Filter to a single provider
|
|
67
|
+
--model <id> Filter to a single model
|
|
68
|
+
--json Print as JSON instead of a formatted table
|
|
69
|
+
-h, --help Show this help
|
|
70
|
+
|
|
71
|
+
Examples:
|
|
72
|
+
lynkr usage
|
|
73
|
+
lynkr usage --days 7
|
|
74
|
+
lynkr usage --window all --json
|
|
75
|
+
`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const C = {
|
|
79
|
+
reset: "\x1b[0m",
|
|
80
|
+
dim: "\x1b[2m",
|
|
81
|
+
bold: "\x1b[1m",
|
|
82
|
+
green: "\x1b[32m",
|
|
83
|
+
yellow: "\x1b[33m",
|
|
84
|
+
cyan: "\x1b[36m",
|
|
85
|
+
red: "\x1b[31m",
|
|
86
|
+
gray: "\x1b[90m",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
function colour(text, code) {
|
|
90
|
+
if (!process.stdout.isTTY) return text;
|
|
91
|
+
return `${code}${text}${C.reset}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function fmtUSD(n) {
|
|
95
|
+
if (!n) return "$0.00";
|
|
96
|
+
if (n < 0.01) return `$${n.toFixed(4)}`;
|
|
97
|
+
return `$${n.toFixed(2)}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function fmtTokens(n) {
|
|
101
|
+
if (!n) return "0";
|
|
102
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`;
|
|
103
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
|
104
|
+
return String(n);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function fmtInt(n) {
|
|
108
|
+
return new Intl.NumberFormat("en-US").format(n || 0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function pad(s, width, align = "left") {
|
|
112
|
+
s = String(s);
|
|
113
|
+
if (s.length >= width) return s;
|
|
114
|
+
const filler = " ".repeat(width - visibleLength(s));
|
|
115
|
+
return align === "right" ? filler + s : s + filler;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function visibleLength(s) {
|
|
119
|
+
// strip ANSI for column-width math
|
|
120
|
+
return String(s).replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function tableRow(cells, widths, aligns) {
|
|
124
|
+
return cells
|
|
125
|
+
.map((c, i) => pad(c, widths[i], aligns[i] || "left"))
|
|
126
|
+
.join(" ");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function printTable(rows, header, widths, aligns) {
|
|
130
|
+
console.log(colour(tableRow(header, widths, aligns), C.bold));
|
|
131
|
+
console.log(colour(widths.map((w) => "─".repeat(w)).join(" "), C.dim));
|
|
132
|
+
for (const row of rows) {
|
|
133
|
+
console.log(tableRow(row, widths, aligns));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function bucketRows(bucket, widths) {
|
|
138
|
+
return Object.entries(bucket)
|
|
139
|
+
.sort((a, b) => b[1].actualCost - a[1].actualCost)
|
|
140
|
+
.map(([key, b]) => [
|
|
141
|
+
key,
|
|
142
|
+
fmtInt(b.requests),
|
|
143
|
+
fmtTokens(b.totalTokens),
|
|
144
|
+
colour(fmtUSD(b.actualCost), C.cyan),
|
|
145
|
+
colour(fmtUSD(b.flagshipCost), C.gray),
|
|
146
|
+
colour(fmtUSD(b.saved), C.green),
|
|
147
|
+
colour(`${b.savedPercent.toFixed(1)}%`, C.green),
|
|
148
|
+
]);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function printReport(usage) {
|
|
152
|
+
const { window, since, flagship, totals, byTier, byProvider, byModel } = usage;
|
|
153
|
+
|
|
154
|
+
const banner = `Lynkr — Usage Report`;
|
|
155
|
+
console.log("");
|
|
156
|
+
console.log(colour(banner, C.bold));
|
|
157
|
+
console.log(
|
|
158
|
+
colour(
|
|
159
|
+
`window: ${window}${since ? ` since: ${since}` : ""} flagship-comparison: ${flagship}`,
|
|
160
|
+
C.dim
|
|
161
|
+
)
|
|
162
|
+
);
|
|
163
|
+
console.log("");
|
|
164
|
+
|
|
165
|
+
// Summary line
|
|
166
|
+
const headline =
|
|
167
|
+
`${fmtInt(totals.requests)} requests ` +
|
|
168
|
+
`${fmtTokens(totals.totalTokens)} tokens ` +
|
|
169
|
+
`actual ${colour(fmtUSD(totals.actualCost), C.cyan)} ` +
|
|
170
|
+
`flagship-only ${colour(fmtUSD(totals.flagshipCost), C.gray)} ` +
|
|
171
|
+
`saved ${colour(fmtUSD(totals.saved), C.green)} ` +
|
|
172
|
+
colour(`(${totals.savedPercent.toFixed(1)}%)`, C.green);
|
|
173
|
+
console.log(headline);
|
|
174
|
+
if (totals.fallbacks || totals.errors) {
|
|
175
|
+
console.log(
|
|
176
|
+
colour(
|
|
177
|
+
` ${totals.fallbacks} fallback${totals.fallbacks !== 1 ? "s" : ""}, ` +
|
|
178
|
+
`${totals.errors} error${totals.errors !== 1 ? "s" : ""}`,
|
|
179
|
+
C.yellow
|
|
180
|
+
)
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
console.log("");
|
|
184
|
+
|
|
185
|
+
if (totals.requests === 0) {
|
|
186
|
+
console.log(colour("No telemetry yet for this window. Send some requests through Lynkr first.", C.yellow));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const headers = ["", "REQUESTS", "TOKENS", "ACTUAL", "FLAGSHIP", "SAVED", "PCT"];
|
|
191
|
+
const widths = [22, 9, 9, 10, 10, 10, 7];
|
|
192
|
+
const aligns = ["left", "right", "right", "right", "right", "right", "right"];
|
|
193
|
+
|
|
194
|
+
console.log(colour("BY TIER", C.bold));
|
|
195
|
+
printTable(bucketRows(byTier, widths), ["TIER", ...headers.slice(1)], widths, aligns);
|
|
196
|
+
console.log("");
|
|
197
|
+
|
|
198
|
+
console.log(colour("BY PROVIDER", C.bold));
|
|
199
|
+
printTable(bucketRows(byProvider, widths), ["PROVIDER", ...headers.slice(1)], widths, aligns);
|
|
200
|
+
console.log("");
|
|
201
|
+
|
|
202
|
+
console.log(colour("BY MODEL", C.bold));
|
|
203
|
+
printTable(bucketRows(byModel, widths), ["MODEL", ...headers.slice(1)], widths, aligns);
|
|
204
|
+
console.log("");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function main() {
|
|
208
|
+
const opts = parseArgs(process.argv);
|
|
209
|
+
const usage = aggregator.getUsage(opts);
|
|
210
|
+
|
|
211
|
+
if (opts.json) {
|
|
212
|
+
process.stdout.write(JSON.stringify(usage, null, 2) + "\n");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
printReport(usage);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
main();
|