@sliday/tamp 0.1.1 → 0.2.1
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 +53 -26
- package/bin/tamp.js +15 -9
- package/compress.js +14 -45
- package/config.js +5 -0
- package/index.js +16 -7
- package/package.json +3 -2
- package/providers.js +151 -0
- package/stats.js +4 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Tamp
|
|
2
2
|
|
|
3
|
-
**Token compression proxy for
|
|
3
|
+
**Token compression proxy for coding agents.** 33.9% fewer input tokens, zero code changes. Works with Claude Code, Aider, Cursor, Cline, Windsurf, and any OpenAI-compatible agent.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
npx @sliday/tamp
|
|
@@ -14,18 +14,28 @@ curl -fsSL https://tamp.dev/setup.sh | bash
|
|
|
14
14
|
|
|
15
15
|
## How It Works
|
|
16
16
|
|
|
17
|
-
Tamp
|
|
17
|
+
Tamp auto-detects your agent's API format and compresses tool result blocks before forwarding upstream. Source code, error results, and non-JSON content pass through untouched.
|
|
18
18
|
|
|
19
19
|
```
|
|
20
20
|
Claude Code ──► Tamp (localhost:7778) ──► Anthropic API
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
Aider/Cursor ──► │ ──► OpenAI API
|
|
22
|
+
Gemini CLI ────► │ ──► Google AI API
|
|
23
|
+
│
|
|
24
|
+
├─ JSON → minify whitespace
|
|
25
|
+
├─ Arrays → TOON columnar encoding
|
|
26
|
+
├─ Line-numbered → strip prefixes + minify
|
|
27
|
+
├─ Source code → passthrough
|
|
28
|
+
└─ Errors → skip
|
|
27
29
|
```
|
|
28
30
|
|
|
31
|
+
### Supported API Formats
|
|
32
|
+
|
|
33
|
+
| Format | Endpoint | Agents |
|
|
34
|
+
|--------|----------|--------|
|
|
35
|
+
| Anthropic Messages | `POST /v1/messages` | Claude Code |
|
|
36
|
+
| OpenAI Chat Completions | `POST /v1/chat/completions` | Aider, Cursor, Cline, Windsurf, OpenCode |
|
|
37
|
+
| Google Gemini | `POST .../generateContent` | Gemini CLI |
|
|
38
|
+
|
|
29
39
|
### Compression Stages
|
|
30
40
|
|
|
31
41
|
| Stage | What it does | When it applies |
|
|
@@ -45,24 +55,36 @@ npx @sliday/tamp
|
|
|
45
55
|
```
|
|
46
56
|
|
|
47
57
|
```
|
|
48
|
-
┌─ Tamp
|
|
49
|
-
│ Proxy: http://localhost:7778
|
|
50
|
-
│ Status: ● Ready
|
|
51
|
-
│
|
|
52
|
-
│
|
|
53
|
-
│
|
|
54
|
-
│
|
|
55
|
-
|
|
58
|
+
┌─ Tamp ─────────────────────────────────┐
|
|
59
|
+
│ Proxy: http://localhost:7778 │
|
|
60
|
+
│ Status: ● Ready │
|
|
61
|
+
│ │
|
|
62
|
+
│ Claude Code: │
|
|
63
|
+
│ ANTHROPIC_BASE_URL=http://localhost:7778
|
|
64
|
+
│ │
|
|
65
|
+
│ Aider / Cursor / Cline: │
|
|
66
|
+
│ OPENAI_BASE_URL=http://localhost:7778
|
|
67
|
+
└────────────────────────────────────────┘
|
|
56
68
|
```
|
|
57
69
|
|
|
58
|
-
### 2. Point
|
|
70
|
+
### 2. Point your agent at the proxy
|
|
59
71
|
|
|
72
|
+
**Claude Code:**
|
|
60
73
|
```bash
|
|
61
74
|
export ANTHROPIC_BASE_URL=http://localhost:7778
|
|
62
75
|
claude
|
|
63
76
|
```
|
|
64
77
|
|
|
65
|
-
|
|
78
|
+
**Aider:**
|
|
79
|
+
```bash
|
|
80
|
+
export OPENAI_API_BASE=http://localhost:7778
|
|
81
|
+
aider
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Cursor / Cline / Windsurf:**
|
|
85
|
+
Set the API base URL to `http://localhost:7778` in your editor's settings.
|
|
86
|
+
|
|
87
|
+
That's it. Use your agent as normal — Tamp compresses silently in the background.
|
|
66
88
|
|
|
67
89
|
## Configuration
|
|
68
90
|
|
|
@@ -71,7 +93,9 @@ All configuration via environment variables:
|
|
|
71
93
|
| Variable | Default | Description |
|
|
72
94
|
|----------|---------|-------------|
|
|
73
95
|
| `TOONA_PORT` | `7778` | Proxy listen port |
|
|
74
|
-
| `TOONA_UPSTREAM` | `https://api.anthropic.com` |
|
|
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 |
|
|
75
99
|
| `TOONA_STAGES` | `minify` | Comma-separated compression stages |
|
|
76
100
|
| `TOONA_MIN_SIZE` | `200` | Minimum content size (chars) to attempt compression |
|
|
77
101
|
| `TOONA_LOG` | `true` | Enable request logging to stderr |
|
|
@@ -136,7 +160,8 @@ Tamp only compresses the **last user message** in each request (the most recent
|
|
|
136
160
|
```
|
|
137
161
|
bin/tamp.js CLI entry point
|
|
138
162
|
index.js HTTP proxy server
|
|
139
|
-
|
|
163
|
+
providers.js API format adapters (Anthropic, OpenAI, Gemini) + auto-detection
|
|
164
|
+
compress.js Compression pipeline (compressRequest, compressText)
|
|
140
165
|
detect.js Content classification (classifyContent, tryParseJSON, stripLineNumbers)
|
|
141
166
|
config.js Environment-based configuration
|
|
142
167
|
stats.js Session statistics and request logging
|
|
@@ -145,11 +170,12 @@ setup.sh One-line installer script
|
|
|
145
170
|
|
|
146
171
|
### How the proxy works
|
|
147
172
|
|
|
148
|
-
1.
|
|
149
|
-
2.
|
|
150
|
-
3.
|
|
151
|
-
4.
|
|
152
|
-
5. The
|
|
173
|
+
1. `detectProvider()` auto-detects the API format from the request path
|
|
174
|
+
2. Unrecognized requests are piped through unmodified
|
|
175
|
+
3. Matched requests are buffered, parsed, and tool results are extracted via the provider adapter
|
|
176
|
+
4. Extracted blocks are classified and compressed
|
|
177
|
+
5. The modified body is forwarded to the correct upstream with updated `Content-Length`
|
|
178
|
+
6. The upstream response is streamed back to the client unmodified
|
|
153
179
|
|
|
154
180
|
Bodies exceeding `TOONA_MAX_BODY` are piped through without buffering.
|
|
155
181
|
|
|
@@ -183,7 +209,8 @@ node --test test/compress.test.js
|
|
|
183
209
|
### Test files
|
|
184
210
|
|
|
185
211
|
```
|
|
186
|
-
test/compress.test.js Compression pipeline tests
|
|
212
|
+
test/compress.test.js Compression pipeline tests (Anthropic + OpenAI formats)
|
|
213
|
+
test/providers.test.js Provider adapter + auto-detection tests
|
|
187
214
|
test/detect.test.js Content classification tests
|
|
188
215
|
test/config.test.js Configuration loading tests
|
|
189
216
|
test/proxy.test.js HTTP proxy integration tests
|
package/bin/tamp.js
CHANGED
|
@@ -4,17 +4,23 @@ import { createProxy } from '../index.js'
|
|
|
4
4
|
const { config, server } = createProxy()
|
|
5
5
|
|
|
6
6
|
server.listen(config.port, () => {
|
|
7
|
+
const url = `http://localhost:${config.port}`
|
|
7
8
|
console.error('')
|
|
8
|
-
console.error(' ┌─ Tamp
|
|
9
|
-
console.error(` │ Proxy:
|
|
10
|
-
console.error(' │ Status: ● Ready
|
|
11
|
-
console.error(' │
|
|
12
|
-
console.error(' │
|
|
13
|
-
console.error(` │
|
|
14
|
-
console.error(' │
|
|
15
|
-
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(' └────────────────────────────────────────┘')
|
|
16
19
|
console.error('')
|
|
17
|
-
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}`)
|
|
18
24
|
console.error(` Stages: ${config.stages.join(', ')}`)
|
|
19
25
|
console.error('')
|
|
20
26
|
})
|
package/compress.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { encode } from '@toon-format/toon'
|
|
2
2
|
import { countTokens } from '@anthropic-ai/tokenizer'
|
|
3
3
|
import { tryParseJSON, classifyContent, stripLineNumbers } from './detect.js'
|
|
4
|
+
import { anthropic } from './providers.js'
|
|
4
5
|
|
|
5
6
|
export function compressText(text, config) {
|
|
6
7
|
if (text.length < config.minSize) return null
|
|
@@ -62,54 +63,22 @@ async function compressBlock(text, config) {
|
|
|
62
63
|
return sync
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
export async function
|
|
66
|
+
export async function compressRequest(body, config, provider) {
|
|
67
|
+
const targets = provider.extract(body)
|
|
66
68
|
const stats = []
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
for (let i = body.messages.length - 1; i >= 0; i--) {
|
|
71
|
-
if (body.messages[i].role === 'user') { lastUserIdx = i; break }
|
|
72
|
-
}
|
|
73
|
-
if (lastUserIdx === -1) return { body, stats }
|
|
74
|
-
|
|
75
|
-
const msg = body.messages[lastUserIdx]
|
|
76
|
-
const debug = config.log
|
|
77
|
-
|
|
78
|
-
if (typeof msg.content === 'string') {
|
|
79
|
-
const result = await compressBlock(msg.content, config)
|
|
69
|
+
for (const target of targets) {
|
|
70
|
+
if (target.skip) { stats.push({ index: target.index, skipped: target.skip }); continue }
|
|
71
|
+
const result = await compressBlock(target.text, config)
|
|
80
72
|
if (result) {
|
|
81
|
-
|
|
82
|
-
stats.push({ index:
|
|
83
|
-
}
|
|
84
|
-
} else if (Array.isArray(msg.content)) {
|
|
85
|
-
for (let i = 0; i < msg.content.length; i++) {
|
|
86
|
-
const block = msg.content[i]
|
|
87
|
-
if (block.type !== 'tool_result') continue
|
|
88
|
-
if (block.is_error) { stats.push({ index: i, skipped: 'error' }); continue }
|
|
89
|
-
|
|
90
|
-
if (typeof block.content === 'string') {
|
|
91
|
-
if (debug) {
|
|
92
|
-
const cls = classifyContent(block.content)
|
|
93
|
-
const len = block.content.length
|
|
94
|
-
console.error(`[toona] debug block[${i}]: type=${cls} len=${len} tool_use_id=${block.tool_use_id || '?'}`)
|
|
95
|
-
}
|
|
96
|
-
const result = await compressBlock(block.content, config)
|
|
97
|
-
if (result) { block.content = result.text; stats.push({ index: i, ...result }) }
|
|
98
|
-
} else if (Array.isArray(block.content)) {
|
|
99
|
-
for (const sub of block.content) {
|
|
100
|
-
if (sub.type === 'text') {
|
|
101
|
-
if (debug) {
|
|
102
|
-
const cls = classifyContent(sub.text)
|
|
103
|
-
const len = sub.text.length
|
|
104
|
-
console.error(`[toona] debug sub-block: type=${cls} len=${len}`)
|
|
105
|
-
}
|
|
106
|
-
const result = await compressBlock(sub.text, config)
|
|
107
|
-
if (result) { sub.text = result.text; stats.push({ index: i, ...result }) }
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
73
|
+
target.compressed = result.text
|
|
74
|
+
stats.push({ index: target.index, ...result })
|
|
111
75
|
}
|
|
112
76
|
}
|
|
113
|
-
|
|
77
|
+
provider.apply(body, targets)
|
|
114
78
|
return { body, stats }
|
|
115
79
|
}
|
|
80
|
+
|
|
81
|
+
export async function compressMessages(body, config) {
|
|
82
|
+
if (!body?.messages?.length) return { body, stats: [] }
|
|
83
|
+
return compressRequest(body, config, anthropic)
|
|
84
|
+
}
|
package/config.js
CHANGED
|
@@ -3,6 +3,11 @@ export function loadConfig(env = process.env) {
|
|
|
3
3
|
return Object.freeze({
|
|
4
4
|
port: parseInt(env.TOONA_PORT, 10) || 7778,
|
|
5
5
|
upstream: env.TOONA_UPSTREAM || 'https://api.anthropic.com',
|
|
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',
|
|
10
|
+
}),
|
|
6
11
|
minSize: parseInt(env.TOONA_MIN_SIZE, 10) || 200,
|
|
7
12
|
stages,
|
|
8
13
|
log: env.TOONA_LOG !== 'false',
|
package/index.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import http from 'node:http'
|
|
2
2
|
import https from 'node:https'
|
|
3
3
|
import { loadConfig } from './config.js'
|
|
4
|
-
import {
|
|
4
|
+
import { compressRequest } from './compress.js'
|
|
5
|
+
import { detectProvider } from './providers.js'
|
|
5
6
|
import { createSession, formatRequestLog } from './stats.js'
|
|
6
7
|
|
|
7
8
|
export function createProxy(overrides = {}) {
|
|
8
|
-
const
|
|
9
|
+
const base = loadConfig()
|
|
10
|
+
const config = { ...base, ...overrides }
|
|
11
|
+
if (overrides.upstream && !overrides.upstreams) {
|
|
12
|
+
config.upstreams = { anthropic: overrides.upstream, openai: overrides.upstream, gemini: overrides.upstream }
|
|
13
|
+
}
|
|
9
14
|
const session = createSession()
|
|
10
15
|
return { config, session, server: _createServer(config, session) }
|
|
11
16
|
}
|
|
@@ -81,13 +86,17 @@ function pipeRequest(req, res, upstreamUrl, prefixChunks) {
|
|
|
81
86
|
|
|
82
87
|
return http.createServer(async (req, res) => {
|
|
83
88
|
if (config.log) console.error(`[tamp] ${req.method} ${req.url}`)
|
|
84
|
-
const
|
|
85
|
-
const isMessages = req.method === 'POST' && req.url.startsWith('/v1/messages')
|
|
89
|
+
const provider = detectProvider(req.method, req.url)
|
|
86
90
|
|
|
87
|
-
if (!
|
|
91
|
+
if (!provider) {
|
|
92
|
+
const upstreamUrl = new URL(req.url, config.upstream)
|
|
88
93
|
return pipeRequest(req, res, upstreamUrl)
|
|
89
94
|
}
|
|
90
95
|
|
|
96
|
+
const upstream = config.upstreams?.[provider.name] || config.upstream
|
|
97
|
+
const reqUrl = provider.normalizeUrl ? provider.normalizeUrl(req.url) : req.url
|
|
98
|
+
const upstreamUrl = new URL(reqUrl, upstream)
|
|
99
|
+
|
|
91
100
|
const chunks = []
|
|
92
101
|
let size = 0
|
|
93
102
|
let overflow = false
|
|
@@ -113,12 +122,12 @@ return http.createServer(async (req, res) => {
|
|
|
113
122
|
|
|
114
123
|
try {
|
|
115
124
|
const parsed = JSON.parse(rawBody.toString('utf-8'))
|
|
116
|
-
const { body, stats } = await
|
|
125
|
+
const { body, stats } = await compressRequest(parsed, config, provider)
|
|
117
126
|
finalBody = Buffer.from(JSON.stringify(body), 'utf-8')
|
|
118
127
|
|
|
119
128
|
if (config.log && stats.length) {
|
|
120
129
|
session.record(stats)
|
|
121
|
-
console.error(formatRequestLog(stats, session))
|
|
130
|
+
console.error(formatRequestLog(stats, session, provider.name, req.url))
|
|
122
131
|
}
|
|
123
132
|
} catch (err) {
|
|
124
133
|
if (config.log) console.error(`[tamp] passthrough (parse error): ${err.message}`)
|
package/package.json
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
"compress.js",
|
|
7
7
|
"config.js",
|
|
8
8
|
"detect.js",
|
|
9
|
+
"providers.js",
|
|
9
10
|
"stats.js"
|
|
10
11
|
],
|
|
11
|
-
"version": "0.
|
|
12
|
-
"description": "Token compression proxy for Claude Code.
|
|
12
|
+
"version": "0.2.1",
|
|
13
|
+
"description": "Token compression proxy for coding agents. Works with Claude Code, Aider, Cursor, Cline, Windsurf. 33.9% fewer input tokens.",
|
|
13
14
|
"type": "module",
|
|
14
15
|
"main": "index.js",
|
|
15
16
|
"bin": {
|
package/providers.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const anthropic = {
|
|
2
|
+
name: 'anthropic',
|
|
3
|
+
match(method, url) {
|
|
4
|
+
return method === 'POST' && url.startsWith('/v1/messages')
|
|
5
|
+
},
|
|
6
|
+
extract(body) {
|
|
7
|
+
const targets = []
|
|
8
|
+
if (!body?.messages?.length) return targets
|
|
9
|
+
|
|
10
|
+
let lastUserIdx = -1
|
|
11
|
+
for (let i = body.messages.length - 1; i >= 0; i--) {
|
|
12
|
+
if (body.messages[i].role === 'user') { lastUserIdx = i; break }
|
|
13
|
+
}
|
|
14
|
+
if (lastUserIdx === -1) return targets
|
|
15
|
+
|
|
16
|
+
const msg = body.messages[lastUserIdx]
|
|
17
|
+
|
|
18
|
+
if (typeof msg.content === 'string') {
|
|
19
|
+
targets.push({ path: ['messages', lastUserIdx, 'content'], text: msg.content })
|
|
20
|
+
} else if (Array.isArray(msg.content)) {
|
|
21
|
+
for (let i = 0; i < msg.content.length; i++) {
|
|
22
|
+
const block = msg.content[i]
|
|
23
|
+
if (block.type !== 'tool_result') continue
|
|
24
|
+
if (block.is_error) { targets.push({ skip: 'error', index: i }); continue }
|
|
25
|
+
|
|
26
|
+
if (typeof block.content === 'string') {
|
|
27
|
+
targets.push({ path: ['messages', lastUserIdx, 'content', i, 'content'], text: block.content, index: i })
|
|
28
|
+
} else if (Array.isArray(block.content)) {
|
|
29
|
+
for (let j = 0; j < block.content.length; j++) {
|
|
30
|
+
const sub = block.content[j]
|
|
31
|
+
if (sub.type === 'text') {
|
|
32
|
+
targets.push({ path: ['messages', lastUserIdx, 'content', i, 'content', j, 'text'], text: sub.text, index: i })
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return targets
|
|
39
|
+
},
|
|
40
|
+
apply(body, targets) {
|
|
41
|
+
for (const t of targets) {
|
|
42
|
+
if (t.skip || !t.compressed) continue
|
|
43
|
+
let obj = body
|
|
44
|
+
const path = t.path
|
|
45
|
+
for (let i = 0; i < path.length - 1; i++) obj = obj[path[i]]
|
|
46
|
+
obj[path[path.length - 1]] = t.compressed
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const openai = {
|
|
52
|
+
name: 'openai',
|
|
53
|
+
match(method, url) {
|
|
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
|
|
59
|
+
},
|
|
60
|
+
extract(body) {
|
|
61
|
+
const targets = []
|
|
62
|
+
if (!body?.messages?.length) return targets
|
|
63
|
+
|
|
64
|
+
// Find last assistant message with tool_calls
|
|
65
|
+
let lastAssistantIdx = -1
|
|
66
|
+
for (let i = body.messages.length - 1; i >= 0; i--) {
|
|
67
|
+
if (body.messages[i].role === 'assistant' && body.messages[i].tool_calls?.length) {
|
|
68
|
+
lastAssistantIdx = i
|
|
69
|
+
break
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (lastAssistantIdx === -1) return targets
|
|
73
|
+
|
|
74
|
+
// Collect all subsequent role:tool messages
|
|
75
|
+
for (let i = lastAssistantIdx + 1; i < body.messages.length; i++) {
|
|
76
|
+
const msg = body.messages[i]
|
|
77
|
+
if (msg.role !== 'tool') break
|
|
78
|
+
if (typeof msg.content === 'string') {
|
|
79
|
+
targets.push({ path: ['messages', i, 'content'], text: msg.content, index: i })
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return targets
|
|
83
|
+
},
|
|
84
|
+
apply(body, targets) {
|
|
85
|
+
for (const t of targets) {
|
|
86
|
+
if (t.skip || !t.compressed) continue
|
|
87
|
+
let obj = body
|
|
88
|
+
const path = t.path
|
|
89
|
+
for (let i = 0; i < path.length - 1; i++) obj = obj[path[i]]
|
|
90
|
+
obj[path[path.length - 1]] = t.compressed
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const gemini = {
|
|
96
|
+
name: 'gemini',
|
|
97
|
+
match(method, url) {
|
|
98
|
+
return method === 'POST' && url.includes('generateContent')
|
|
99
|
+
},
|
|
100
|
+
extract(body) {
|
|
101
|
+
const targets = []
|
|
102
|
+
if (!body?.contents?.length) return targets
|
|
103
|
+
|
|
104
|
+
// Find last content with functionResponse parts
|
|
105
|
+
for (let ci = body.contents.length - 1; ci >= 0; ci--) {
|
|
106
|
+
const content = body.contents[ci]
|
|
107
|
+
if (!content.parts?.length) continue
|
|
108
|
+
for (let pi = 0; pi < content.parts.length; pi++) {
|
|
109
|
+
const part = content.parts[pi]
|
|
110
|
+
if (!part.functionResponse?.response) continue
|
|
111
|
+
const resp = part.functionResponse.response
|
|
112
|
+
const text = typeof resp === 'string' ? resp : JSON.stringify(resp, null, 2)
|
|
113
|
+
targets.push({
|
|
114
|
+
path: ['contents', ci, 'parts', pi, 'functionResponse', 'response'],
|
|
115
|
+
text,
|
|
116
|
+
index: pi,
|
|
117
|
+
wasObject: typeof resp !== 'string',
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
if (targets.length) break
|
|
121
|
+
}
|
|
122
|
+
return targets
|
|
123
|
+
},
|
|
124
|
+
apply(body, targets) {
|
|
125
|
+
for (const t of targets) {
|
|
126
|
+
if (t.skip || !t.compressed) continue
|
|
127
|
+
let obj = body
|
|
128
|
+
const path = t.path
|
|
129
|
+
for (let i = 0; i < path.length - 1; i++) obj = obj[path[i]]
|
|
130
|
+
// If original was object, try to parse compressed back to object
|
|
131
|
+
if (t.wasObject) {
|
|
132
|
+
try {
|
|
133
|
+
obj[path[path.length - 1]] = JSON.parse(t.compressed)
|
|
134
|
+
continue
|
|
135
|
+
} catch { /* fall through to string */ }
|
|
136
|
+
}
|
|
137
|
+
obj[path[path.length - 1]] = t.compressed
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const providers = [anthropic, openai, gemini]
|
|
143
|
+
|
|
144
|
+
export function detectProvider(method, url) {
|
|
145
|
+
for (const p of providers) {
|
|
146
|
+
if (p.match(method, url)) return p
|
|
147
|
+
}
|
|
148
|
+
return null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export { anthropic, openai, gemini }
|
package/stats.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
export function formatRequestLog(stats, session) {
|
|
1
|
+
export function formatRequestLog(stats, session, providerName, url) {
|
|
2
2
|
const compressed = stats.filter(s => s.method)
|
|
3
3
|
const skipped = stats.filter(s => s.skipped)
|
|
4
|
-
const
|
|
4
|
+
const label = providerName || 'anthropic'
|
|
5
|
+
const path = url || '/v1/messages'
|
|
6
|
+
const lines = [`[toona] ${label} ${path} — ${stats.length} blocks, ${compressed.length} compressed`]
|
|
5
7
|
|
|
6
8
|
for (const s of stats) {
|
|
7
9
|
if (s.skipped) {
|