@sliday/tamp 0.2.0 → 0.2.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 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
- Only `minify` is enabled by default. Enable more with `TOONA_STAGES=minify,toon`.
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
- | `TOONA_PORT` | `7778` | Proxy listen port |
96
- | `TOONA_UPSTREAM` | `https://api.anthropic.com` | Default upstream API URL |
97
- | `TOONA_UPSTREAM_OPENAI` | `https://api.openai.com` | Upstream for OpenAI-format requests |
98
- | `TOONA_UPSTREAM_GEMINI` | `https://generativelanguage.googleapis.com` | Upstream for Gemini-format requests |
99
- | `TOONA_STAGES` | `minify` | Comma-separated compression stages |
100
- | `TOONA_MIN_SIZE` | `200` | Minimum content size (chars) to attempt compression |
101
- | `TOONA_LOG` | `true` | Enable request logging to stderr |
102
- | `TOONA_LOG_FILE` | _(none)_ | Write logs to file |
103
- | `TOONA_MAX_BODY` | `10485760` | Max request body size (bytes) before passthrough |
104
- | `TOONA_LLMLINGUA_URL` | _(none)_ | LLMLingua sidecar URL for text compression |
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
- TOONA_STAGES=minify,toon npx @sliday/tamp
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 `TOONA_MAX_BODY` are piped through without buffering.
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 { config, server } = createProxy()
8
+ const __dirname = dirname(fileURLToPath(import.meta.url))
9
+ const root = join(__dirname, '..')
5
10
 
6
- server.listen(config.port, () => {
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
- console.error('')
9
- console.error(' ┌─ Tamp ─────────────────────────────────┐')
10
- console.error(` │ Proxy: ${url} │`)
11
- console.error(' Status: ● Ready │')
12
- console.error(' │ │')
13
- console.error(' │ Claude Code: │')
14
- console.error(` │ ANTHROPIC_BASE_URL=${url} │`)
15
- console.error(' │ │')
16
- console.error(' │ Aider / Cursor / Cline: │')
17
- console.error(` │ OPENAI_BASE_URL=${url} │`)
18
- console.error(' └────────────────────────────────────────┘')
19
- console.error('')
20
- console.error(` Upstreams:`)
21
- console.error(` anthropic → ${config.upstreams.anthropic}`)
22
- console.error(` openai → ${config.upstreams.openai}`)
23
- console.error(` gemini → ${config.upstreams.gemini}`)
24
- console.error(` Stages: ${config.stages.join(', ')}`)
25
- console.error('')
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
+ const needsSidecar = config.stages.includes('llmlingua') && !config.llmLinguaUrl
63
+ const venvPython = join(root, 'sidecar', '.venv', 'bin', 'python')
64
+ const serverPy = join(root, 'sidecar', 'server.py')
65
+ const hasSidecar = existsSync(venvPython) && existsSync(serverPy)
66
+
67
+ let { config: finalConfig, server: finalServer } = createProxy()
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.TOONA_STAGES || 'minify').split(',').map(s => s.trim()).filter(Boolean)
2
+ const stages = (env.TAMP_STAGES || 'minify,toon').split(',').map(s => s.trim()).filter(Boolean)
3
3
  return Object.freeze({
4
- port: parseInt(env.TOONA_PORT, 10) || 7778,
5
- upstream: env.TOONA_UPSTREAM || 'https://api.anthropic.com',
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.TOONA_UPSTREAM || 'https://api.anthropic.com',
8
- openai: env.TOONA_UPSTREAM_OPENAI || 'https://api.openai.com',
9
- gemini: env.TOONA_UPSTREAM_GEMINI || 'https://generativelanguage.googleapis.com',
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.TOONA_MIN_SIZE, 10) || 200,
11
+ minSize: parseInt(env.TAMP_MIN_SIZE, 10) || 200,
12
12
  stages,
13
- log: env.TOONA_LOG !== 'false',
14
- logFile: env.TOONA_LOG_FILE || null,
15
- maxBody: parseInt(env.TOONA_MAX_BODY, 10) || 10_485_760,
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.TOONA_LLMLINGUA_URL || null,
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)
@@ -94,7 +112,8 @@ return http.createServer(async (req, res) => {
94
112
  }
95
113
 
96
114
  const upstream = config.upstreams?.[provider.name] || config.upstream
97
- const upstreamUrl = new URL(req.url, upstream)
115
+ const reqUrl = provider.normalizeUrl ? provider.normalizeUrl(req.url) : req.url
116
+ const upstreamUrl = new URL(reqUrl, upstream)
98
117
 
99
118
  const chunks = []
100
119
  let size = 0
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "providers.js",
10
10
  "stats.js"
11
11
  ],
12
- "version": "0.2.0",
12
+ "version": "0.2.2",
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/providers.js CHANGED
@@ -51,7 +51,11 @@ const anthropic = {
51
51
  const openai = {
52
52
  name: 'openai',
53
53
  match(method, url) {
54
- return method === 'POST' && url.startsWith('/v1/chat/completions')
54
+ return method === 'POST' && (url.startsWith('/v1/chat/completions') || url.startsWith('/chat/completions'))
55
+ },
56
+ normalizeUrl(url) {
57
+ if (url.startsWith('/chat/completions')) return '/v1' + url
58
+ return url
55
59
  },
56
60
  extract(body) {
57
61
  const targets = []
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 = [`[toona] ${label} ${path} — ${stats.length} blocks, ${compressed.length} compressed`]
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(`[toona] block[${s.index}]: skipped (${s.skipped})`)
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(`[toona] block[${s.index}]: ${s.originalLen}->${s.compressedLen} chars (-${pct}%)${tokInfo} [${s.method}]`)
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(`[toona] total: ${totalOrig}->${totalComp} chars (-${pct}%), ${totalOrigTok}->${totalCompTok} tokens (-${tokPct}%)`)
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(`[toona] session: ${totals.totalSaved} chars, ${totals.totalTokensSaved} tokens saved across ${totals.compressionCount} compressions`)
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')