@sliday/tamp 0.2.1 → 0.2.3
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 +13 -13
- package/bin/tamp.js +106 -20
- package/config.js +11 -11
- package/index.js +20 -2
- package/package.json +1 -1
- package/stats.js +5 -5
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ Gemini CLI ────► │ ──► Google AI API
|
|
|
44
44
|
| `toon` | Columnar [TOON encoding](https://github.com/nicholasgasior/toon-format) | Homogeneous arrays (file listings, routes, deps) |
|
|
45
45
|
| `llmlingua` | Neural text compression via [LLMLingua](https://github.com/microsoft/LLMLingua) sidecar | Natural language text (requires sidecar) |
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
`minify` and `toon` are enabled by default. Add LLMLingua with `TAMP_STAGES=minify,toon,llmlingua`.
|
|
48
48
|
|
|
49
49
|
## Quick Start
|
|
50
50
|
|
|
@@ -92,22 +92,22 @@ All configuration via environment variables:
|
|
|
92
92
|
|
|
93
93
|
| Variable | Default | Description |
|
|
94
94
|
|----------|---------|-------------|
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
95
|
+
| `TAMP_PORT` | `7778` | Proxy listen port |
|
|
96
|
+
| `TAMP_UPSTREAM` | `https://api.anthropic.com` | Default upstream API URL |
|
|
97
|
+
| `TAMP_UPSTREAM_OPENAI` | `https://api.openai.com` | Upstream for OpenAI-format requests |
|
|
98
|
+
| `TAMP_UPSTREAM_GEMINI` | `https://generativelanguage.googleapis.com` | Upstream for Gemini-format requests |
|
|
99
|
+
| `TAMP_STAGES` | `minify` | Comma-separated compression stages |
|
|
100
|
+
| `TAMP_MIN_SIZE` | `200` | Minimum content size (chars) to attempt compression |
|
|
101
|
+
| `TAMP_LOG` | `true` | Enable request logging to stderr |
|
|
102
|
+
| `TAMP_LOG_FILE` | _(none)_ | Write logs to file |
|
|
103
|
+
| `TAMP_MAX_BODY` | `10485760` | Max request body size (bytes) before passthrough |
|
|
104
|
+
| `TAMP_LLMLINGUA_URL` | _(none)_ | LLMLingua sidecar URL for text compression |
|
|
105
105
|
|
|
106
106
|
### Recommended setup
|
|
107
107
|
|
|
108
108
|
```bash
|
|
109
109
|
# Maximum compression
|
|
110
|
-
|
|
110
|
+
TAMP_STAGES=minify,toon,llmlingua TAMP_LLMLINGUA_URL=http://localhost:8788 npx @sliday/tamp
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
## Installation Methods
|
|
@@ -177,7 +177,7 @@ setup.sh One-line installer script
|
|
|
177
177
|
5. The modified body is forwarded to the correct upstream with updated `Content-Length`
|
|
178
178
|
6. The upstream response is streamed back to the client unmodified
|
|
179
179
|
|
|
180
|
-
Bodies exceeding `
|
|
180
|
+
Bodies exceeding `TAMP_MAX_BODY` are piped through without buffering.
|
|
181
181
|
|
|
182
182
|
## Benchmarking
|
|
183
183
|
|
package/bin/tamp.js
CHANGED
|
@@ -1,26 +1,112 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createProxy } from '../index.js'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { spawn } from 'node:child_process'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { dirname, join } from 'node:path'
|
|
3
7
|
|
|
4
|
-
const
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
const root = join(__dirname, '..')
|
|
5
10
|
|
|
6
|
-
|
|
11
|
+
// ANSI colors
|
|
12
|
+
const c = {
|
|
13
|
+
reset: '\x1b[0m',
|
|
14
|
+
bold: '\x1b[1m',
|
|
15
|
+
dim: '\x1b[2m',
|
|
16
|
+
green: '\x1b[32m',
|
|
17
|
+
yellow: '\x1b[33m',
|
|
18
|
+
blue: '\x1b[34m',
|
|
19
|
+
magenta: '\x1b[35m',
|
|
20
|
+
cyan: '\x1b[36m',
|
|
21
|
+
bgGreen: '\x1b[42m',
|
|
22
|
+
bgYellow: '\x1b[43m',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function log(msg = '') { console.error(msg) }
|
|
26
|
+
|
|
27
|
+
function printBanner(config) {
|
|
7
28
|
const url = `http://localhost:${config.port}`
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
|
|
30
|
+
log('')
|
|
31
|
+
log(` ${c.bold}${c.cyan}┌─ Tamp ─────────────────────────────────┐${c.reset}`)
|
|
32
|
+
log(` ${c.cyan}│${c.reset} Proxy: ${c.bold}${c.green}${url}${c.reset}${c.cyan} │${c.reset}`)
|
|
33
|
+
log(` ${c.cyan}│${c.reset} Status: ${c.bgGreen}${c.bold} ● READY ${c.reset}${c.cyan} │${c.reset}`)
|
|
34
|
+
log(` ${c.cyan}│${c.reset} ${c.cyan}│${c.reset}`)
|
|
35
|
+
log(` ${c.cyan}│${c.reset} ${c.bold}Claude Code:${c.reset} ${c.cyan}│${c.reset}`)
|
|
36
|
+
log(` ${c.cyan}│${c.reset} ${c.dim}ANTHROPIC_BASE_URL=${c.reset}${c.yellow}${url}${c.reset} ${c.cyan}│${c.reset}`)
|
|
37
|
+
log(` ${c.cyan}│${c.reset} ${c.cyan}│${c.reset}`)
|
|
38
|
+
log(` ${c.cyan}│${c.reset} ${c.bold}Aider / Cursor / Cline:${c.reset} ${c.cyan}│${c.reset}`)
|
|
39
|
+
log(` ${c.cyan}│${c.reset} ${c.dim}OPENAI_BASE_URL=${c.reset}${c.yellow}${url}${c.reset} ${c.cyan}│${c.reset}`)
|
|
40
|
+
log(` ${c.cyan}└────────────────────────────────────────┘${c.reset}`)
|
|
41
|
+
log('')
|
|
42
|
+
|
|
43
|
+
log(` ${c.bold}Upstreams:${c.reset}`)
|
|
44
|
+
log(` ${c.magenta}anthropic${c.reset} → ${c.dim}${config.upstreams.anthropic}${c.reset}`)
|
|
45
|
+
log(` ${c.magenta}openai${c.reset} → ${c.dim}${config.upstreams.openai}${c.reset}`)
|
|
46
|
+
log(` ${c.magenta}gemini${c.reset} → ${c.dim}${config.upstreams.gemini}${c.reset}`)
|
|
47
|
+
log('')
|
|
48
|
+
|
|
49
|
+
log(` ${c.bold}Compression:${c.reset}`)
|
|
50
|
+
for (const stage of config.stages) {
|
|
51
|
+
const icon = stage === 'llmlingua' ? `${c.green}▸${c.reset}` : `${c.green}▸${c.reset}`
|
|
52
|
+
const label = stage === 'minify' ? 'JSON whitespace removal'
|
|
53
|
+
: stage === 'toon' ? 'TOON columnar encoding'
|
|
54
|
+
: stage === 'llmlingua' ? `LLMLingua-2 neural compression ${c.dim}(${config.llmLinguaUrl})${c.reset}`
|
|
55
|
+
: stage
|
|
56
|
+
log(` ${icon} ${c.cyan}${stage}${c.reset} — ${label}`)
|
|
57
|
+
}
|
|
58
|
+
log('')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --- Auto-start LLMLingua-2 sidecar if needed ---
|
|
62
|
+
let { config: finalConfig, server: finalServer } = createProxy()
|
|
63
|
+
|
|
64
|
+
const needsSidecar = finalConfig.stages.includes('llmlingua') && !finalConfig.llmLinguaUrl
|
|
65
|
+
const venvPython = join(root, 'sidecar', '.venv', 'bin', 'python')
|
|
66
|
+
const serverPy = join(root, 'sidecar', 'server.py')
|
|
67
|
+
const hasSidecar = existsSync(venvPython) && existsSync(serverPy)
|
|
68
|
+
|
|
69
|
+
if (needsSidecar && hasSidecar) {
|
|
70
|
+
const sidecarPort = 8788
|
|
71
|
+
process.env.TAMP_LLMLINGUA_URL = `http://localhost:${sidecarPort}`
|
|
72
|
+
const refreshed = createProxy()
|
|
73
|
+
finalConfig = refreshed.config
|
|
74
|
+
finalServer = refreshed.server
|
|
75
|
+
|
|
76
|
+
log('')
|
|
77
|
+
log(` ${c.yellow}→${c.reset} Starting LLMLingua-2 sidecar ...`)
|
|
78
|
+
|
|
79
|
+
const sidecar = spawn(venvPython, ['-m', 'uvicorn', 'server:app', '--host', '127.0.0.1', '--port', String(sidecarPort)], {
|
|
80
|
+
cwd: join(root, 'sidecar'),
|
|
81
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
let ready = false
|
|
85
|
+
sidecar.stderr.on('data', (d) => {
|
|
86
|
+
const line = d.toString()
|
|
87
|
+
if (!ready && line.includes('Uvicorn running')) {
|
|
88
|
+
ready = true
|
|
89
|
+
log(` ${c.green}✓${c.reset} LLMLingua-2 sidecar ready on ${c.bold}port ${sidecarPort}${c.reset}`)
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
sidecar.on('exit', (code) => {
|
|
94
|
+
if (code !== null && code !== 0) {
|
|
95
|
+
log(` ${c.yellow}✗${c.reset} LLMLingua-2 sidecar exited (code ${code})`)
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
process.on('exit', () => { sidecar?.kill() })
|
|
100
|
+
process.on('SIGINT', () => { sidecar?.kill(); process.exit() })
|
|
101
|
+
process.on('SIGTERM', () => { sidecar?.kill(); process.exit() })
|
|
102
|
+
} else if (needsSidecar && !hasSidecar) {
|
|
103
|
+
log('')
|
|
104
|
+
log(` ${c.yellow}✗${c.reset} LLMLingua-2 sidecar not installed`)
|
|
105
|
+
log(` Run: ${c.cyan}curl -fsSL tamp.dev/setup.sh | bash${c.reset}`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const { config: cfg, server: srv } = { config: finalConfig, server: finalServer }
|
|
109
|
+
|
|
110
|
+
srv.listen(cfg.port, () => {
|
|
111
|
+
printBanner(cfg)
|
|
26
112
|
})
|
package/config.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
export function loadConfig(env = process.env) {
|
|
2
|
-
const stages = (env.
|
|
2
|
+
const stages = (env.TAMP_STAGES || 'minify,toon').split(',').map(s => s.trim()).filter(Boolean)
|
|
3
3
|
return Object.freeze({
|
|
4
|
-
port: parseInt(env.
|
|
5
|
-
upstream: env.
|
|
4
|
+
port: parseInt(env.TAMP_PORT, 10) || 7778,
|
|
5
|
+
upstream: env.TAMP_UPSTREAM || 'https://api.anthropic.com',
|
|
6
6
|
upstreams: Object.freeze({
|
|
7
|
-
anthropic: env.
|
|
8
|
-
openai: env.
|
|
9
|
-
gemini: env.
|
|
7
|
+
anthropic: env.TAMP_UPSTREAM || 'https://api.anthropic.com',
|
|
8
|
+
openai: env.TAMP_UPSTREAM_OPENAI || 'https://api.openai.com',
|
|
9
|
+
gemini: env.TAMP_UPSTREAM_GEMINI || 'https://generativelanguage.googleapis.com',
|
|
10
10
|
}),
|
|
11
|
-
minSize: parseInt(env.
|
|
11
|
+
minSize: parseInt(env.TAMP_MIN_SIZE, 10) || 200,
|
|
12
12
|
stages,
|
|
13
|
-
log: env.
|
|
14
|
-
logFile: env.
|
|
15
|
-
maxBody: parseInt(env.
|
|
13
|
+
log: env.TAMP_LOG !== 'false',
|
|
14
|
+
logFile: env.TAMP_LOG_FILE || null,
|
|
15
|
+
maxBody: parseInt(env.TAMP_MAX_BODY, 10) || 10_485_760,
|
|
16
16
|
cacheSafe: true,
|
|
17
|
-
llmLinguaUrl: env.
|
|
17
|
+
llmLinguaUrl: env.TAMP_LLMLINGUA_URL || null,
|
|
18
18
|
})
|
|
19
19
|
}
|
package/index.js
CHANGED
|
@@ -30,16 +30,25 @@ function forwardRequest(method, upstreamUrl, headers, body, res) {
|
|
|
30
30
|
const upstream = mod.request(opts, (upstreamRes) => {
|
|
31
31
|
res.writeHead(upstreamRes.statusCode, upstreamRes.headers)
|
|
32
32
|
upstreamRes.pipe(res)
|
|
33
|
+
upstreamRes.on('error', (err) => {
|
|
34
|
+
console.error(`[tamp] response stream error: ${err.code || ''} ${err.message}`)
|
|
35
|
+
res.destroy()
|
|
36
|
+
})
|
|
33
37
|
})
|
|
34
38
|
|
|
35
39
|
upstream.on('error', (err) => {
|
|
36
|
-
console.error(`[tamp] upstream error: ${err.message}`)
|
|
40
|
+
console.error(`[tamp] upstream error: ${err.code || ''} ${err.message}`)
|
|
37
41
|
if (!res.headersSent) {
|
|
38
42
|
res.writeHead(502, { 'Content-Type': 'application/json' })
|
|
39
43
|
}
|
|
40
44
|
res.end(JSON.stringify({ error: 'upstream_error', message: err.message }))
|
|
41
45
|
})
|
|
42
46
|
|
|
47
|
+
res.on('error', (err) => {
|
|
48
|
+
console.error(`[tamp] client disconnect: ${err.code || ''} ${err.message}`)
|
|
49
|
+
upstream.destroy()
|
|
50
|
+
})
|
|
51
|
+
|
|
43
52
|
if (body) {
|
|
44
53
|
upstream.end(body)
|
|
45
54
|
} else {
|
|
@@ -65,16 +74,25 @@ function pipeRequest(req, res, upstreamUrl, prefixChunks) {
|
|
|
65
74
|
const upstream = mod.request(opts, (upstreamRes) => {
|
|
66
75
|
res.writeHead(upstreamRes.statusCode, upstreamRes.headers)
|
|
67
76
|
upstreamRes.pipe(res)
|
|
77
|
+
upstreamRes.on('error', (err) => {
|
|
78
|
+
console.error(`[tamp] response stream error: ${err.code || ''} ${err.message}`)
|
|
79
|
+
res.destroy()
|
|
80
|
+
})
|
|
68
81
|
})
|
|
69
82
|
|
|
70
83
|
upstream.on('error', (err) => {
|
|
71
|
-
console.error(`[tamp] upstream error: ${err.message}`)
|
|
84
|
+
console.error(`[tamp] upstream error: ${err.code || ''} ${err.message}`)
|
|
72
85
|
if (!res.headersSent) {
|
|
73
86
|
res.writeHead(502, { 'Content-Type': 'application/json' })
|
|
74
87
|
}
|
|
75
88
|
res.end(JSON.stringify({ error: 'upstream_error', message: err.message }))
|
|
76
89
|
})
|
|
77
90
|
|
|
91
|
+
res.on('error', (err) => {
|
|
92
|
+
console.error(`[tamp] client disconnect: ${err.code || ''} ${err.message}`)
|
|
93
|
+
upstream.destroy()
|
|
94
|
+
})
|
|
95
|
+
|
|
78
96
|
if (prefixChunks) {
|
|
79
97
|
for (const chunk of prefixChunks) {
|
|
80
98
|
upstream.write(chunk)
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"providers.js",
|
|
10
10
|
"stats.js"
|
|
11
11
|
],
|
|
12
|
-
"version": "0.2.
|
|
12
|
+
"version": "0.2.3",
|
|
13
13
|
"description": "Token compression proxy for coding agents. Works with Claude Code, Aider, Cursor, Cline, Windsurf. 33.9% fewer input tokens.",
|
|
14
14
|
"type": "module",
|
|
15
15
|
"main": "index.js",
|
package/stats.js
CHANGED
|
@@ -3,15 +3,15 @@ export function formatRequestLog(stats, session, providerName, url) {
|
|
|
3
3
|
const skipped = stats.filter(s => s.skipped)
|
|
4
4
|
const label = providerName || 'anthropic'
|
|
5
5
|
const path = url || '/v1/messages'
|
|
6
|
-
const lines = [`[
|
|
6
|
+
const lines = [`[tamp] ${label} ${path} — ${stats.length} blocks, ${compressed.length} compressed`]
|
|
7
7
|
|
|
8
8
|
for (const s of stats) {
|
|
9
9
|
if (s.skipped) {
|
|
10
|
-
lines.push(`[
|
|
10
|
+
lines.push(`[tamp] block[${s.index}]: skipped (${s.skipped})`)
|
|
11
11
|
} else if (s.method) {
|
|
12
12
|
const pct = (((s.originalLen - s.compressedLen) / s.originalLen) * 100).toFixed(1)
|
|
13
13
|
const tokInfo = s.originalTokens ? ` ${s.originalTokens}->${s.compressedTokens} tok` : ''
|
|
14
|
-
lines.push(`[
|
|
14
|
+
lines.push(`[tamp] block[${s.index}]: ${s.originalLen}->${s.compressedLen} chars (-${pct}%)${tokInfo} [${s.method}]`)
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -22,12 +22,12 @@ export function formatRequestLog(stats, session, providerName, url) {
|
|
|
22
22
|
if (compressed.length > 0) {
|
|
23
23
|
const pct = (((totalOrig - totalComp) / totalOrig) * 100).toFixed(1)
|
|
24
24
|
const tokPct = totalOrigTok > 0 ? (((totalOrigTok - totalCompTok) / totalOrigTok) * 100).toFixed(1) : '0.0'
|
|
25
|
-
lines.push(`[
|
|
25
|
+
lines.push(`[tamp] total: ${totalOrig}->${totalComp} chars (-${pct}%), ${totalOrigTok}->${totalCompTok} tokens (-${tokPct}%)`)
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
if (session) {
|
|
29
29
|
const totals = session.getTotals()
|
|
30
|
-
lines.push(`[
|
|
30
|
+
lines.push(`[tamp] session: ${totals.totalSaved} chars, ${totals.totalTokensSaved} tokens saved across ${totals.compressionCount} compressions`)
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
return lines.join('\n')
|