squeezr-ai 1.46.2 → 1.46.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/dist/server.js CHANGED
@@ -1,880 +1,885 @@
1
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { homedir } from 'node:os';
4
- import { Hono } from 'hono';
5
- import { stream, streamSSE } from 'hono/streaming';
6
- import { config, applyMode, runtimeOverrides, anthropicNativeCompactEnabled, effectiveBackend, USER_CONFIG_DIR, USER_CONFIG_PATH } from './config.js';
7
- import { Stats } from './stats.js';
8
- import { DASHBOARD_HTML } from './dashboard.js';
9
- import { getCache, emptySavings } from './compressor.js';
10
- import { compressAnthropicMessages, compressOpenAIMessages, compressGeminiContents, } from './compressor.js';
11
- import { isBypassed, setBypassed, toggleBypassed } from './bypass.js';
12
- import { circuitBreaker } from './circuitBreaker.js';
13
- import { injectExpandToolAnthropic, injectExpandToolOpenAI, handleAnthropicExpandCall, handleOpenAIExpandCall, retrieveOriginal, expandStoreSize, } from './expand.js';
14
- import { compressSystemPrompt } from './systemPrompt.js';
15
- import { anthropicDirectFetch, isAnthropicUrl } from './anthropicDirectFetch.js';
16
- import { sessionCacheSize } from './sessionCache.js';
17
- import { detPatternHits } from './deterministic.js';
18
- import { VERSION } from './version.js';
19
- import { recordRequest, getHistorySessions, getCurrentSession, getProjectAggregates, getAllSessionsForHistory, } from './history.js';
20
- import { updateAnthropicFromHeaders, updateOpenAIFromHeaders, updateGeminiFrom429, addAnthropicUsage, addOpenAIUsage, addGeminiUsage, makeSseUsageParser, maybeRefreshOpenAIBilling, maybeRefreshOpenAISessionLimits, storeKey, storedKey, limitsSnapshot, } from './limits.js';
21
- // ── Project name extraction ────────────────────────────────────────────────────
22
- // Manual project override — set via /squeezr/project endpoint or MCP tool
23
- let manualProject = null;
24
- export function setManualProject(name) {
25
- manualProject = name;
26
- }
27
- export function getManualProject() {
28
- return manualProject;
29
- }
30
- // Reads the CWD from Claude Code's system prompt (injected as <cwd>…</cwd> or
31
- // "current working directory: …") and returns the last path component.
32
- function extractProjectName(body) {
33
- if (manualProject)
34
- return manualProject;
35
- try {
36
- const system = body.system;
37
- let text = '';
38
- if (Array.isArray(system)) {
39
- text = system
40
- .map(s => s.text ?? '')
41
- .join(' ');
42
- }
43
- else if (typeof system === 'string') {
44
- text = system;
45
- }
46
- // Claude Code format: <cwd>/path/to/project</cwd>
47
- const xmlCwd = text.match(/<cwd>([^<]+)<\/cwd>/);
48
- if (xmlCwd) {
49
- const parts = xmlCwd[1].trim().replace(/\\/g, '/').split('/').filter(Boolean);
50
- if (parts.length)
51
- return parts[parts.length - 1];
52
- }
53
- // Plain-text format: "current working directory: /path"
54
- const plainCwd = text.match(/(?:current working directory|cwd)[:\s]+([^\n<]+)/i);
55
- if (plainCwd) {
56
- const parts = plainCwd[1].trim().replace(/\\/g, '/').split('/').filter(Boolean);
57
- if (parts.length)
58
- return parts[parts.length - 1];
59
- }
60
- // Fallback: extract LAST meaningful path segment from system prompt
61
- // e.g. C:\Users\Ramos\Documents\InvoiceApp\src → InvoiceApp
62
- // Only match filesystem paths (not URLs like https://github.com)
63
- const pathMatch = text.match(/(?:[A-Za-z]:[\\/]|\/(?:Users|home|workspace|projects|Documents)[\\/])[^\s<>"*?|]+/i);
64
- if (pathMatch && !pathMatch[0].includes('://')) {
65
- const parts = pathMatch[0].replace(/\\/g, '/').split('/').filter(Boolean);
66
- const skip = new Set([
67
- 'users', 'home', 'documents', 'workspace', 'projects', 'desktop',
68
- 'dev', 'src', 'repos', 'mnt', 'c', 'var', 'tmp', 'opt', 'usr',
69
- 'lib', 'bin', 'etc', 'node_modules', '.claude', '.config',
70
- ]);
71
- for (const pt of parts) {
72
- if (!skip.has(pt.toLowerCase()) && !/^[a-z]:$/i.test(pt) && pt.length > 1)
73
- return pt;
74
- }
75
- if (parts.length)
76
- return parts[parts.length - 1];
77
- }
78
- }
79
- catch { /* ignore */ }
80
- return 'unknown';
81
- }
82
- const ANTHROPIC_API = 'https://api.anthropic.com';
83
- const OPENAI_API = 'https://api.openai.com';
84
- const GOOGLE_API = 'https://generativelanguage.googleapis.com';
85
- const SKIP_REQ_HEADERS = new Set(['host', 'content-length', 'transfer-encoding', 'connection', 'upgrade', 'expect', 'x-squeezr-client', 'x-squeezr-dryrun']);
86
- function readCodexToken() {
87
- try {
88
- const d = JSON.parse(readFileSync(join(homedir(), '.codex', 'auth.json'), 'utf-8'));
89
- return d?.tokens?.access_token ?? null;
90
- }
91
- catch {
92
- return null;
93
- }
94
- }
95
- const SKIP_RESP_HEADERS = new Set(['content-encoding', 'transfer-encoding', 'connection', 'content-length']);
96
- export const stats = new Stats();
97
- function forwardHeaders(headers) {
98
- const out = {};
99
- for (const [k, v] of headers.entries()) {
100
- if (!SKIP_REQ_HEADERS.has(k.toLowerCase()))
101
- out[k] = v;
102
- }
103
- return out;
104
- }
105
- function extractOpenAIKey(headers) {
106
- const auth = headers.get('authorization') ?? '';
107
- return auth.replace(/^bearer\s+/i, '').trim();
108
- }
109
- function extractGoogleKey(headers, url) {
110
- return headers.get('x-goog-api-key') ?? url.searchParams.get('key') ?? '';
111
- }
112
- function detectUpstream(headers) {
113
- if (headers.get('x-goog-api-key'))
114
- return GOOGLE_API;
115
- const auth = headers.get('authorization') ?? '';
116
- if (auth && !headers.get('x-api-key'))
117
- return OPENAI_API;
118
- return ANTHROPIC_API;
119
- }
120
- function estimateChars(data) {
121
- return JSON.stringify(data).length;
122
- }
123
- // Outgoing fetch — uses Node's native fetch for everything EXCEPT
124
- // api.anthropic.com, which is forced through direct DNS so that the system
125
- // hosts file redirect installed by `squeezr enable-claude-desktop` does NOT
126
- // loop the main proxy back to itself (or to the desktop proxy with its
127
- // self-signed cert, which is what was breaking Claude Code in the terminal
128
- // with infinite "Retrying" loops).
129
- //
130
- // This is NOT a state-dependent branch on whether Claude Desktop is enabled
131
- // — that proved fragile. The behaviour is now constant: api.anthropic.com
132
- // ALWAYS uses direct DNS. The result is identical for users who never touch
133
- // Claude Desktop (the hosts file is fine, the direct resolver returns the
134
- // same IPs) and correct for users who do.
135
- function outgoingFetch(url, init) {
136
- if (isAnthropicUrl(url))
137
- return anthropicDirectFetch(url, init);
138
- return fetch(url, init);
139
- }
140
- async function proxyStream(upstream, body, headers, params) {
141
- const url = params?.toString() ? `${upstream}?${params}` : upstream;
142
- return outgoingFetch(url, {
143
- method: 'POST',
144
- headers: { ...headers, 'content-type': 'application/json' },
145
- body: JSON.stringify(body),
146
- });
147
- }
148
- export const app = new Hono();
149
- // ── CORS middleware (required for Cursor IDE and browser-based tools) ─────────
150
- // Cursor's Electron renderer sends OPTIONS preflight before every POST.
151
- // Without this the request is blocked and Cursor shows a network error.
152
- app.use('*', async (c, next) => {
153
- if (c.req.method === 'OPTIONS') {
154
- return c.body(null, 204, {
155
- 'Access-Control-Allow-Origin': '*',
156
- 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
157
- 'Access-Control-Allow-Headers': '*',
158
- 'Access-Control-Max-Age': '86400',
159
- });
160
- }
161
- await next();
162
- c.res.headers.set('Access-Control-Allow-Origin', '*');
163
- c.res.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
164
- c.res.headers.set('Access-Control-Allow-Headers', '*');
165
- });
166
- // ── Client detection from User-Agent ─────────────────────────────────────────
167
- // `hint` comes from the desktop proxy via `x-squeezr-client` and is the
168
- // authoritative source when present (the desktop proxy *knows* which listener
169
- // received the request). Falls back to UA heuristics otherwise.
170
- const VALID_CLIENT_HINTS = new Set([
171
- 'claude_code', 'claude_desktop', 'codex_cli', 'codex_desktop',
172
- 'aider', 'opencode', 'cursor', 'cline', 'windsurf', 'continue',
173
- ]);
174
- function detectAnthropicClient(ua, hint) {
175
- if (hint && VALID_CLIENT_HINTS.has(hint))
176
- return hint;
177
- const u = ua.toLowerCase();
178
- if (u.includes('claude-code') || u.includes('claude_code'))
179
- return 'claude_code';
180
- if (u.includes('claude-desktop') || u.includes('claude desktop') || u.includes('electron'))
181
- return 'claude_desktop';
182
- if (u.includes('aider'))
183
- return 'aider';
184
- if (u.includes('opencode') || u.includes('open-code'))
185
- return 'opencode';
186
- if (u.includes('cursor'))
187
- return 'cursor';
188
- if (u.includes('cline') || u.includes('roo'))
189
- return 'cline';
190
- if (u.includes('windsurf'))
191
- return 'windsurf';
192
- return 'claude_code'; // default: most likely Claude Code if using /v1/messages
193
- }
194
- function detectOpenAIClient(ua, hint) {
195
- if (hint && VALID_CLIENT_HINTS.has(hint))
196
- return hint;
197
- const u = ua.toLowerCase();
198
- if (u.includes('codex'))
199
- return 'codex_desktop';
200
- if (u.includes('cursor'))
201
- return 'cursor';
202
- if (u.includes('continue'))
203
- return 'continue';
204
- if (u.includes('cline') || u.includes('roo'))
205
- return 'cline';
206
- if (u.includes('windsurf'))
207
- return 'windsurf';
208
- if (u.includes('aider'))
209
- return 'aider';
210
- return 'openai_other';
211
- }
212
- // ── Anthropic / Claude Code ───────────────────────────────────────────────────
213
- app.post('/v1/messages', async (c) => {
214
- const body = await c.req.json();
215
- // Support both API key (x-api-key: sk-ant-...) and OAuth bearer token
216
- // (Authorization: Bearer ...) — Claude Code subscription uses OAuth
217
- const apiKey = c.req.header('x-api-key')
218
- ?? c.req.header('authorization')?.replace(/^bearer\s+/i, '').trim()
219
- ?? process.env.ANTHROPIC_API_KEY
220
- ?? '';
221
- const clientId = detectAnthropicClient(c.req.header('user-agent') ?? '', c.req.header('x-squeezr-client'));
222
- const modelId = String(body.model ?? 'unknown');
223
- // Extract project name BEFORE compressing system prompt (compression destroys <cwd> tags)
224
- const project = extractProjectName(body);
225
- const messages = (body.messages ?? []);
226
- const originalChars = estimateChars(messages);
227
- // Dry-run mode: exercises the compression pipeline but does NOT forward to
228
- // upstream. Used by the post-start self-test to verify the request path is
229
- // wired correctly without consuming any API quota.
230
- if (c.req.header('x-squeezr-dryrun') === '1') {
231
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
232
- const [dryMessages, savings] = await compressAnthropicMessages(messages, apiKey, config);
233
- const compressedChars = estimateChars(dryMessages);
234
- return c.json({
235
- identity: 'squeezr',
236
- dry_run: true,
237
- original_chars: originalChars,
238
- compressed_chars: compressedChars,
239
- saved_chars: Math.max(0, originalChars - compressedChars),
240
- savings,
241
- });
242
- }
243
- // Bypass mode: skip all compression, still record request stats
244
- if (isBypassed()) {
245
- stats.recordWithProject(project, originalChars, originalChars, emptySavings(), undefined, clientId, modelId);
246
- recordRequest(project, 0, 0, [], originalChars);
247
- storeKey('anthropic', apiKey);
248
- const fwdHeaders = forwardHeaders(c.req.raw.headers);
249
- if (body.stream) {
250
- const upstream = await proxyStream(`${ANTHROPIC_API}/v1/messages`, body, fwdHeaders);
251
- updateAnthropicFromHeaders(upstream.headers);
252
- for (const [k, v] of upstream.headers.entries()) {
253
- if (!SKIP_RESP_HEADERS.has(k.toLowerCase()))
254
- c.header(k, v);
255
- }
256
- return stream(c, async (s) => {
257
- const reader = upstream.body.getReader();
258
- const decoder = new TextDecoder();
259
- const sseParser = makeSseUsageParser('anthropic', (inp, out) => addAnthropicUsage(inp, out));
260
- while (true) {
261
- const { done, value } = await reader.read();
262
- if (done)
263
- break;
264
- await s.write(value);
265
- sseParser(decoder.decode(value, { stream: true }));
266
- }
267
- });
268
- }
269
- const resp = await outgoingFetch(`${ANTHROPIC_API}/v1/messages`, {
270
- method: 'POST',
271
- headers: { ...fwdHeaders, 'content-type': 'application/json' },
272
- body: JSON.stringify(body),
273
- });
274
- updateAnthropicFromHeaders(resp.headers);
275
- const respBody = await resp.json();
276
- const respHeaders = {};
277
- for (const [k, v] of resp.headers.entries()) {
278
- if (!SKIP_RESP_HEADERS.has(k.toLowerCase()))
279
- respHeaders[k] = v;
280
- }
281
- return c.json(respBody, resp.status, respHeaders);
282
- }
283
- // System prompt compression (handles both string and array formats — Claude Code sends array)
284
- if (config.compressSystemPrompt && !config.dryRun) {
285
- if (typeof body.system === 'string') {
286
- const sp = await compressSystemPrompt(body.system, apiKey, 'haiku');
287
- body.system = sp.text;
288
- stats.recordSystemPromptSaved(sp.originalLen, sp.compressedLen);
289
- }
290
- else if (Array.isArray(body.system)) {
291
- for (const block of body.system) {
292
- if (block.type === 'text' && typeof block.text === 'string') {
293
- const sp = await compressSystemPrompt(block.text, apiKey, 'haiku');
294
- block.text = sp.text;
295
- stats.recordSystemPromptSaved(sp.originalLen, sp.compressedLen);
296
- }
297
- }
298
- }
299
- }
300
- const systemExtraChars = typeof body.system === 'string'
301
- ? body.system.length
302
- : Array.isArray(body.system)
303
- ? body.system.reduce((s, b) => s + (b.text?.length ?? 0), 0)
304
- : 0;
305
- const compT0 = Date.now();
306
- const [compressedMsgs, savings] = await compressAnthropicMessages(messages, apiKey, config, systemExtraChars);
307
- const compLatency = { totalMs: Date.now() - compT0, detMs: savings.detMs, aiMs: savings.aiMs };
308
- body.messages = compressedMsgs;
309
- // Inject expand tool
310
- injectExpandToolAnthropic(body);
311
- const _claudeCompChars = estimateChars(compressedMsgs);
312
- stats.recordWithProject(project, originalChars, _claudeCompChars, savings, compLatency, clientId, modelId);
313
- // Record TOTAL saved (originalChars - compressedChars), not just AI savings
314
- recordRequest(project, Math.max(0, originalChars - _claudeCompChars), savings.compressed, savings.byTool, originalChars);
315
- storeKey('anthropic', apiKey);
316
- const fwdHeaders = forwardHeaders(c.req.raw.headers);
317
- // Anthropic native context compaction beta (compact-2026-01-12)
318
- // When enabled, Anthropic auto-summarizes the conversation server-side
319
- // when input tokens exceed threshold. Stacks with Squeezr's compression.
320
- if (anthropicNativeCompactEnabled()) {
321
- const existingBeta = fwdHeaders['anthropic-beta'] || '';
322
- if (!existingBeta.includes('compact-2026-01-12')) {
323
- fwdHeaders['anthropic-beta'] = existingBeta
324
- ? `${existingBeta},compact-2026-01-12`
325
- : 'compact-2026-01-12';
326
- }
327
- }
328
- if (body.stream) {
329
- const upstream = await proxyStream(`${ANTHROPIC_API}/v1/messages`, body, fwdHeaders);
330
- // Extract rate limit headers immediately (available before body starts)
331
- updateAnthropicFromHeaders(upstream.headers);
332
- // Forward anthropic-ratelimit-* (and other response) headers so Claude Code
333
- // can populate rate_limits in the statusline JSON (issue #4).
334
- for (const [k, v] of upstream.headers.entries()) {
335
- if (!SKIP_RESP_HEADERS.has(k.toLowerCase()))
336
- c.header(k, v);
337
- }
338
- return stream(c, async (s) => {
339
- const reader = upstream.body.getReader();
340
- const decoder = new TextDecoder();
341
- const sseParser = makeSseUsageParser('anthropic', (inp, out) => addAnthropicUsage(inp, out));
342
- while (true) {
343
- const { done, value } = await reader.read();
344
- if (done)
345
- break;
346
- await s.write(value);
347
- sseParser(decoder.decode(value, { stream: true }));
348
- }
349
- });
350
- }
351
- const resp = await outgoingFetch(`${ANTHROPIC_API}/v1/messages`, {
352
- method: 'POST',
353
- headers: { ...fwdHeaders, 'content-type': 'application/json' },
354
- body: JSON.stringify(body),
355
- });
356
- // Extract rate limits and token usage from non-streaming response
357
- updateAnthropicFromHeaders(resp.headers);
358
- const respBody = await resp.json();
359
- if (respBody.usage) {
360
- const u = respBody.usage;
361
- addAnthropicUsage(u.input_tokens ?? 0, u.output_tokens ?? 0);
362
- }
363
- // Handle expand() call if model requested one (track expand rate)
364
- const expandCall = handleAnthropicExpandCall(respBody);
365
- if (expandCall) {
366
- stats.recordExpand(true);
367
- const { toolUseId, original } = expandCall;
368
- const continueMessages = [
369
- ...body.messages,
370
- { role: 'assistant', content: respBody.content },
371
- {
372
- role: 'user',
373
- content: [{ type: 'tool_result', tool_use_id: toolUseId, content: original }],
374
- },
375
- ];
376
- body.messages = continueMessages;
377
- const continuedResp = await fetch(`${ANTHROPIC_API}/v1/messages`, {
378
- method: 'POST',
379
- headers: { ...fwdHeaders, 'content-type': 'application/json' },
380
- body: JSON.stringify(body),
381
- });
382
- updateAnthropicFromHeaders(continuedResp.headers);
383
- const continuedBody = await continuedResp.json();
384
- const continuedHeaders = {};
385
- for (const [k, v] of continuedResp.headers.entries()) {
386
- if (!SKIP_RESP_HEADERS.has(k.toLowerCase()))
387
- continuedHeaders[k] = v;
388
- }
389
- return c.json(continuedBody, continuedResp.status, continuedHeaders);
390
- }
391
- const respHeaders = {};
392
- for (const [k, v] of resp.headers.entries()) {
393
- if (!SKIP_RESP_HEADERS.has(k.toLowerCase()))
394
- respHeaders[k] = v;
395
- }
396
- return c.json(respBody, resp.status, respHeaders);
397
- });
398
- // ── OpenAI / Codex / Ollama ───────────────────────────────────────────────────
399
- app.post('/v1/chat/completions', async (c) => {
400
- const body = await c.req.json();
401
- const openAIKey = extractOpenAIKey(c.req.raw.headers);
402
- const isLocal = config.isLocalKey(openAIKey);
403
- const upstream = isLocal ? `${config.localUpstreamUrl.replace(/\/$/, '')}/v1/chat/completions` : `${OPENAI_API}/v1/chat/completions`;
404
- const oaiClientId = detectOpenAIClient(c.req.header('user-agent') ?? '', c.req.header('x-squeezr-client'));
405
- const oaiModelId = String(body.model ?? 'unknown');
406
- // Extract project name BEFORE compressing system prompt
407
- const oaiProject = extractProjectName(body);
408
- const messages = (body.messages ?? []);
409
- const originalChars = estimateChars(messages);
410
- // Bypass mode: skip all compression, still record request stats
411
- if (isBypassed()) {
412
- stats.recordWithProject(oaiProject, originalChars, originalChars, emptySavings(), undefined, oaiClientId, oaiModelId);
413
- recordRequest(oaiProject, 0, 0, [], originalChars);
414
- if (!isLocal)
415
- storeKey('openai', openAIKey);
416
- const fwdHeaders = forwardHeaders(c.req.raw.headers);
417
- if (body.stream) {
418
- const resp = await fetch(upstream, {
419
- method: 'POST',
420
- headers: { ...fwdHeaders, 'content-type': 'application/json' },
421
- body: JSON.stringify(body),
422
- });
423
- if (!isLocal)
424
- updateOpenAIFromHeaders(resp.headers);
425
- return stream(c, async (s) => {
426
- const reader = resp.body.getReader();
427
- while (true) {
428
- const { done, value } = await reader.read();
429
- if (done)
430
- break;
431
- await s.write(value);
432
- }
433
- });
434
- }
435
- const resp = await fetch(upstream, {
436
- method: 'POST',
437
- headers: { ...fwdHeaders, 'content-type': 'application/json' },
438
- body: JSON.stringify(body),
439
- });
440
- if (!isLocal)
441
- updateOpenAIFromHeaders(resp.headers);
442
- const respBody = await resp.json();
443
- const respHeaders = {};
444
- for (const [k, v] of resp.headers.entries()) {
445
- if (!SKIP_RESP_HEADERS.has(k.toLowerCase()))
446
- respHeaders[k] = v;
447
- }
448
- return c.json(respBody, resp.status, respHeaders);
449
- }
450
- // Compress system message for non-local
451
- if (!isLocal && config.compressSystemPrompt && !config.dryRun) {
452
- const msgs = messages;
453
- if (msgs[0]?.role === 'system' && typeof msgs[0].content === 'string') {
454
- const sp = await compressSystemPrompt(msgs[0].content, openAIKey, 'gpt-mini');
455
- msgs[0].content = sp.text;
456
- stats.recordSystemPromptSaved(sp.originalLen, sp.compressedLen);
457
- }
458
- }
459
- const oaiCompT0 = Date.now();
460
- const [compressedMsgs, savings] = await compressOpenAIMessages(messages, openAIKey, config, isLocal);
461
- const oaiCompLatency = { totalMs: Date.now() - oaiCompT0, detMs: savings.detMs, aiMs: savings.aiMs };
462
- body.messages = compressedMsgs;
463
- if (!isLocal)
464
- injectExpandToolOpenAI(body);
465
- const _oaiCompChars = estimateChars(compressedMsgs);
466
- stats.recordWithProject(oaiProject, originalChars, _oaiCompChars, savings, oaiCompLatency, oaiClientId, oaiModelId);
467
- recordRequest(oaiProject, Math.max(0, originalChars - _oaiCompChars), savings.compressed, savings.byTool, originalChars);
468
- if (!isLocal)
469
- storeKey('openai', openAIKey);
470
- const fwdHeaders = forwardHeaders(c.req.raw.headers);
471
- if (body.stream) {
472
- // Ask OpenAI to include usage in the final chunk (harmless for most clients)
473
- if (!isLocal && !body.stream_options?.include_usage) {
474
- body.stream_options = { ...(body.stream_options ?? {}), include_usage: true };
475
- }
476
- const upstreamResp = await proxyStream(upstream, body, fwdHeaders);
477
- if (!isLocal) {
478
- updateOpenAIFromHeaders(upstreamResp.headers);
479
- maybeRefreshOpenAIBilling(openAIKey).catch(() => { });
480
- }
481
- return stream(c, async (s) => {
482
- const reader = upstreamResp.body.getReader();
483
- const decoder = new TextDecoder();
484
- const sseParser = makeSseUsageParser('openai', (inp, out) => addOpenAIUsage(inp, out));
485
- while (true) {
486
- const { done, value } = await reader.read();
487
- if (done)
488
- break;
489
- await s.write(value);
490
- if (!isLocal)
491
- sseParser(decoder.decode(value, { stream: true }));
492
- }
493
- });
494
- }
495
- const resp = await fetch(upstream, {
496
- method: 'POST',
497
- headers: { ...fwdHeaders, 'content-type': 'application/json' },
498
- body: JSON.stringify(body),
499
- });
500
- if (!isLocal) {
501
- updateOpenAIFromHeaders(resp.headers);
502
- maybeRefreshOpenAIBilling(openAIKey).catch(() => { });
503
- }
504
- const respBody = await resp.json();
505
- if (!isLocal && respBody.usage) {
506
- const u = respBody.usage;
507
- addOpenAIUsage(u.prompt_tokens ?? 0, u.completion_tokens ?? 0);
508
- }
509
- const expandCall = !isLocal ? handleOpenAIExpandCall(respBody) : null;
510
- if (expandCall) {
511
- stats.recordExpand(true);
512
- const { toolCallId, original } = expandCall;
513
- const continueMessages = [
514
- ...body.messages,
515
- respBody.choices[0].message,
516
- { role: 'tool', tool_call_id: toolCallId, content: original },
517
- ];
518
- body.messages = continueMessages;
519
- const continuedResp = await fetch(upstream, {
520
- method: 'POST',
521
- headers: { ...fwdHeaders, 'content-type': 'application/json' },
522
- body: JSON.stringify(body),
523
- });
524
- return c.json(await continuedResp.json(), continuedResp.status);
525
- }
526
- const respHeaders = {};
527
- for (const [k, v] of resp.headers.entries()) {
528
- if (!SKIP_RESP_HEADERS.has(k.toLowerCase()))
529
- respHeaders[k] = v;
530
- }
531
- return c.json(respBody, resp.status, respHeaders);
532
- });
533
- // ── Gemini CLI ────────────────────────────────────────────────────────────────
534
- app.post('/v1beta/models/*', async (c) => {
535
- const body = await c.req.json();
536
- const url = new URL(c.req.url);
537
- const googleKey = extractGoogleKey(c.req.raw.headers, url);
538
- const modelPath = c.req.path.replace('/v1beta/models/', '');
539
- const contents = (body.contents ?? []);
540
- const originalChars = estimateChars(contents);
541
- const geminiProject = extractProjectName(body);
542
- // Gemini model is in the URL path: /v1beta/models/gemini-2.5-pro:generateContent
543
- const geminiModelId = modelPath.split(':')[0] || 'gemini';
544
- // Bypass mode: skip all compression, still record request stats
545
- if (isBypassed()) {
546
- stats.recordWithProject(geminiProject, originalChars, originalChars, emptySavings(), undefined, 'gemini', geminiModelId);
547
- recordRequest(geminiProject, 0, 0, [], originalChars);
548
- const targetUrl = `${GOOGLE_API}/v1beta/models/${modelPath}`;
549
- const fwdHeaders = forwardHeaders(c.req.raw.headers);
550
- const params = url.searchParams;
551
- const paramStr = params.toString();
552
- const resp = await fetch(paramStr ? `${targetUrl}?${paramStr}` : targetUrl, {
553
- method: 'POST',
554
- headers: { ...fwdHeaders, 'content-type': 'application/json' },
555
- body: JSON.stringify(body),
556
- });
557
- const respHeaders = {};
558
- for (const [k, v] of resp.headers.entries()) {
559
- if (!SKIP_RESP_HEADERS.has(k.toLowerCase()))
560
- respHeaders[k] = v;
561
- }
562
- return c.body(await resp.arrayBuffer(), resp.status, respHeaders);
563
- }
564
- // Store gemini key so it's available if backend is later switched to gemini-flash from a different API request
565
- if (googleKey)
566
- storeKey('gemini', googleKey);
567
- const gemCompT0 = Date.now();
568
- const [compressedContents, savings] = await compressGeminiContents(contents, googleKey, config);
569
- const gemCompLatency = { totalMs: Date.now() - gemCompT0, detMs: savings.detMs, aiMs: savings.aiMs };
570
- body.contents = compressedContents;
571
- const _gemCompChars = estimateChars(compressedContents);
572
- stats.recordWithProject(geminiProject, originalChars, _gemCompChars, savings, gemCompLatency, 'gemini', geminiModelId);
573
- recordRequest(geminiProject, Math.max(0, originalChars - _gemCompChars), savings.compressed, savings.byTool, originalChars);
574
- const targetUrl = `${GOOGLE_API}/v1beta/models/${modelPath}`;
575
- const fwdHeaders = forwardHeaders(c.req.raw.headers);
576
- const params = url.searchParams;
577
- if (modelPath.includes('stream')) {
578
- const upstreamResp = await proxyStream(targetUrl, body, fwdHeaders, params);
579
- if (upstreamResp.status === 429)
580
- updateGeminiFrom429(upstreamResp.headers);
581
- return stream(c, async (s) => {
582
- const reader = upstreamResp.body.getReader();
583
- const decoder = new TextDecoder();
584
- // Gemini streaming sends JSON array chunks with usageMetadata, not Anthropic-style SSE
585
- let gemBuf = '';
586
- while (true) {
587
- const { done, value } = await reader.read();
588
- if (done)
589
- break;
590
- await s.write(value);
591
- gemBuf += decoder.decode(value, { stream: true });
592
- const metaMatch = gemBuf.match(/"usageMetadata"\s*:\s*\{[^}]+\}/);
593
- if (metaMatch) {
594
- try {
595
- const meta = JSON.parse(`{${metaMatch[0]}}`);
596
- addGeminiUsage(meta.usageMetadata.promptTokenCount ?? 0, meta.usageMetadata.candidatesTokenCount ?? 0);
597
- }
598
- catch { /* ignore parse errors */ }
599
- gemBuf = '';
600
- }
601
- }
602
- });
603
- }
604
- const paramStr = params.toString();
605
- const resp = await fetch(paramStr ? `${targetUrl}?${paramStr}` : targetUrl, {
606
- method: 'POST',
607
- headers: { ...fwdHeaders, 'content-type': 'application/json' },
608
- body: JSON.stringify(body),
609
- });
610
- if (resp.status === 429)
611
- updateGeminiFrom429(resp.headers);
612
- // Extract Gemini usage from response body
613
- const geminiRespBuf = await resp.arrayBuffer();
614
- try {
615
- const geminiRespJson = JSON.parse(new TextDecoder().decode(geminiRespBuf));
616
- const meta = geminiRespJson.usageMetadata;
617
- if (meta)
618
- addGeminiUsage(meta.promptTokenCount ?? 0, meta.candidatesTokenCount ?? 0);
619
- }
620
- catch { /* ignore */ }
621
- const respHeaders = {};
622
- for (const [k, v] of resp.headers.entries()) {
623
- if (!SKIP_RESP_HEADERS.has(k.toLowerCase()))
624
- respHeaders[k] = v;
625
- }
626
- return c.body(geminiRespBuf, resp.status, respHeaders);
627
- });
628
- // ── Squeezr internal endpoints ────────────────────────────────────────────────
629
- async function buildStatsPayload() {
630
- await maybeRefreshOpenAISessionLimits().catch(() => { });
631
- const session = stats.summary();
632
- // Compute all-time totals by summing ALL history sessions (history.json is the source of truth)
633
- // plus comparing with stats.json — take the maximum to avoid regressions
634
- const allSessions = getAllSessionsForHistory();
635
- const historyTotalSavedTokens = allSessions.reduce((s, r) => s + (r.savedTokens || 0), 0);
636
- const historyTotalOriginalTokens = allSessions.reduce((s, r) => s + (r.originalChars ? Math.round(r.originalChars / 3.5) : 0), 0);
637
- const historyTotalRequests = allSessions.reduce((s, r) => s + (r.requests || 0), 0);
638
- const persisted = Stats.loadGlobal();
639
- const allTimeSavedTokens = Math.max(Math.round(session.total_saved_chars / 3.5), Math.round((persisted.total_saved_chars ?? 0) / 3.5), historyTotalSavedTokens);
640
- const allTimeOriginalTokens = Math.max(Math.round(session.total_original_chars / 3.5), Math.round((persisted.total_original_chars ?? 0) / 3.5), historyTotalOriginalTokens);
641
- const allTimeRequests = Math.max(session.requests, persisted.requests ?? 0, historyTotalRequests);
642
- // Ratio: compute from all-time totals so it matches the all-time Tokens Saved /
643
- // processed cards. The session.savings_pct comes from this-process-only counters
644
- // and shows misleading 0-2% values right after restart when the persisted history
645
- // is multiple orders of magnitude larger than the current session.
646
- const allTimeSavingsPct = allTimeOriginalTokens > 0
647
- ? Math.round((allTimeSavedTokens / allTimeOriginalTokens) * 1000) / 10
648
- : 0;
649
- // Breakdown: prefer persisted all-time values over session-only counters so
650
- // deterministic/dedup/sysprompt numbers stay consistent with the hero cards.
651
- const allTimeBreakdown = {
652
- deterministic: persisted.det_saved_chars ?? (session.breakdown?.deterministic ?? 0),
653
- ai_compression: persisted.ai_saved_chars ?? (session.breakdown?.ai_compression ?? 0),
654
- read_dedup: persisted.dedup_saved_chars ?? (session.breakdown?.read_dedup ?? 0),
655
- system_prompt: persisted.sysprompt_saved_chars ?? (session.breakdown?.system_prompt ?? 0),
656
- overhead: persisted.overhead_chars ?? (session.breakdown?.overhead ?? 0),
657
- ai_calls: persisted.ai_compression_calls ?? (session.breakdown?.ai_calls ?? 0),
658
- };
659
- return {
660
- ...session,
661
- total_original_chars: allTimeOriginalTokens * 3.5,
662
- total_saved_chars: allTimeSavedTokens * 3.5,
663
- total_saved_tokens: allTimeSavedTokens,
664
- requests: allTimeRequests,
665
- savings_pct: allTimeSavingsPct,
666
- breakdown: allTimeBreakdown,
667
- anthropic_native_compact: anthropicNativeCompactEnabled(),
668
- compression_backend: effectiveBackend(),
669
- cache: getCache(config).stats(),
670
- expand_store_size: expandStoreSize(),
671
- session_cache_size: sessionCacheSize(),
672
- dry_run: config.dryRun,
673
- pattern_hits: detPatternHits,
674
- version: VERSION,
675
- port: config.port,
676
- mode: runtimeOverrides.mode,
677
- limits: limitsSnapshot(),
678
- bypassed: isBypassed(),
679
- circuit_breaker: circuitBreaker.snapshot(),
680
- };
681
- }
682
- app.get('/squeezr/stats', (c) => {
683
- return buildStatsPayload().then(d => c.json(d));
684
- });
685
- // ── POST /squeezr/ports — write port config to squeezr.toml ─────────────────
686
- app.post('/squeezr/ports', async (c) => {
687
- const body = await c.req.json();
688
- const { port: newPort, mitm_port: newMitm } = body;
689
- if (!newPort || !newMitm || newPort < 1024 || newMitm < 1024 || newPort === newMitm) {
690
- return c.text('Invalid ports', 400);
691
- }
692
- try {
693
- // Always write to ~/.squeezr/squeezr.toml — the bundled package toml gets
694
- // wiped on every `npm install -g`, which used to silently reset user
695
- // ports back to 8080 + autoscan after every update.
696
- const tomlPath = USER_CONFIG_PATH;
697
- mkdirSync(USER_CONFIG_DIR, { recursive: true });
698
- let content = existsSync(tomlPath) ? readFileSync(tomlPath, 'utf-8') : '[proxy]\n';
699
- // Update or insert [proxy] port and mitm_port
700
- const updateKey = (src, key, val) => {
701
- const re = new RegExp(`^(\\s*${key}\\s*=\\s*)\\d+`, 'm');
702
- return re.test(src) ? src.replace(re, `$1${val}`) : src.replace(/(\[proxy\][^\[]*)/s, `$1${key} = ${val}\n`);
703
- };
704
- if (!content.includes('[proxy]'))
705
- content = '[proxy]\n' + content;
706
- content = updateKey(content, 'port', newPort);
707
- content = updateKey(content, 'mitm_port', newMitm);
708
- writeFileSync(tomlPath, content, 'utf-8');
709
- return c.json({ ok: true, port: newPort, mitm_port: newMitm, toml: tomlPath });
710
- }
711
- catch (err) {
712
- return c.text('Failed to write squeezr.toml: ' + err.message, 500);
713
- }
714
- });
715
- app.get('/squeezr/health', (c) => {
716
- const cb = circuitBreaker.snapshot();
717
- const s = stats.summary();
718
- return c.json({
719
- // Magic identifier so callers can distinguish a real squeezr instance from
720
- // any other HTTP service that happens to answer 200 on this port (e.g. a
721
- // Docker container occupying the configured port).
722
- identity: 'squeezr',
723
- status: 'ok',
724
- version: VERSION,
725
- uptime_seconds: s.uptime_seconds,
726
- mode: runtimeOverrides.mode,
727
- bypassed: isBypassed(),
728
- circuit_breaker: {
729
- state: cb.state,
730
- consecutive_failures: cb.consecutive_failures,
731
- total_trips: cb.total_trips,
732
- last_success_ago_s: cb.last_success_time
733
- ? Math.round((Date.now() - cb.last_success_time) / 1000)
734
- : null,
735
- },
736
- expand_store: {
737
- size: expandStoreSize(),
738
- pressure: expandStoreSize() > 5000 ? 'high' : expandStoreSize() > 1000 ? 'medium' : 'low',
739
- },
740
- compression: {
741
- requests: s.requests,
742
- savings_pct: s.savings_pct,
743
- },
744
- port: config.port,
745
- mitm_port: config.mitmPort,
746
- });
747
- });
748
- // ── Self-test endpoint ─────────────────────────────────────────────────────
749
- // Last self-test results are populated by src/selfTest.ts at startup and on
750
- // demand via GET /squeezr/selftest?run=1.
751
- let lastSelfTest = null;
752
- export function setLastSelfTest(result) {
753
- lastSelfTest = result;
754
- }
755
- app.get('/squeezr/selftest', async (c) => {
756
- if (c.req.query('run') === '1') {
757
- const { runSelfTest } = await import('./selfTest.js');
758
- const result = await runSelfTest({ port: Number(c.req.query('port')) || 0 });
759
- return c.json(result);
760
- }
761
- if (!lastSelfTest) {
762
- return c.json({ status: 'not_run', message: 'Self-test has not been executed yet. Call ?run=1 to execute.' });
763
- }
764
- return c.json(lastSelfTest);
765
- });
766
- // ── Project management ─────────────────────────────────────────────────────
767
- app.get('/squeezr/project', (c) => {
768
- return c.json({ project: getManualProject() ?? stats.currentProjectName() });
769
- });
770
- app.post('/squeezr/project', async (c) => {
771
- const body = await c.req.json();
772
- if (body.project === null || body.project === '') {
773
- setManualProject(null);
774
- return c.json({ project: stats.currentProjectName(), manual: false });
775
- }
776
- if (typeof body.project === 'string') {
777
- setManualProject(body.project);
778
- stats.setProject(body.project);
779
- return c.json({ project: body.project, manual: true });
780
- }
781
- return c.json({ error: 'Invalid project name' }, 400);
782
- });
783
- app.get('/squeezr/expand/:id', (c) => {
784
- const id = c.req.param('id');
785
- const original = retrieveOriginal(id);
786
- stats.recordExpand(!!original);
787
- if (!original)
788
- return c.json({ error: 'Not found or expired' }, 404);
789
- return c.json({ id, content: original });
790
- });
791
- // ── Dashboard + SSE + config ──────────────────────────────────────────────────
792
- app.get('/squeezr/dashboard', (c) => {
793
- return c.html(DASHBOARD_HTML);
794
- });
795
- app.get('/squeezr/events', (c) => {
796
- return streamSSE(c, async (s) => {
797
- await s.writeSSE({ data: JSON.stringify(await buildStatsPayload()) });
798
- while (true) {
799
- await s.sleep(2000);
800
- try {
801
- await s.writeSSE({ data: JSON.stringify(await buildStatsPayload()) });
802
- }
803
- catch {
804
- break;
805
- }
806
- }
807
- });
808
- });
809
- app.get('/squeezr/limits', async (c) => {
810
- await maybeRefreshOpenAISessionLimits().catch(() => { });
811
- return c.json(limitsSnapshot());
812
- });
813
- // ── History + Projects endpoints ──────────────────────────────────────────────
814
- app.get('/squeezr/history', (c) => {
815
- // sessions returns ONLY historical (past) sessions, current is separate
816
- // to avoid double-counting when dashboard does sessions.concat(current)
817
- return c.json({
818
- sessions: getHistorySessions().filter(s => s.id !== getCurrentSession().id),
819
- current: getCurrentSession(),
820
- });
821
- });
822
- app.get('/squeezr/projects', (c) => {
823
- return c.json({ projects: getProjectAggregates() });
824
- });
825
- // ── Control endpoints ─────────────────────────────────────────────────────────
826
- app.post('/squeezr/control/stop', (c) => {
827
- // Respond first, then exit gracefully after a tick
828
- setTimeout(() => process.emit('SIGTERM'), 200);
829
- return c.json({ ok: true, message: 'Squeezr proxy shutting down…' });
830
- });
831
- app.post('/squeezr/config', async (c) => {
832
- const body = await c.req.json();
833
- if (body.mode && ['soft', 'normal', 'aggressive', 'critical'].includes(body.mode)) {
834
- applyMode(body.mode);
835
- }
836
- return c.json({ ok: true, mode: runtimeOverrides.mode });
837
- });
838
- // ── Bypass mode (runtime-only compression toggle) ────────────────────────────
839
- app.get('/squeezr/bypass', (c) => {
840
- return c.json({ bypassed: isBypassed() });
841
- });
842
- app.post('/squeezr/bypass', async (c) => {
843
- try {
844
- const body = await c.req.json().catch(() => ({}));
845
- if (typeof body.enabled === 'boolean') {
846
- setBypassed(body.enabled);
847
- }
848
- else {
849
- toggleBypassed();
850
- }
851
- }
852
- catch {
853
- toggleBypassed();
854
- }
855
- return c.json({ bypassed: isBypassed() });
856
- });
857
- // ── Distillation endpoint — uses captured OAuth token to compress with Opus ──
858
- // Allows external scripts to do high-quality compression using the user's Claude
859
- // Pro/Max subscription (no API key needed). Used by recipes/squeezr-1B for training.
860
- app.post('/squeezr/distill', async (c) => {
861
- const rawBody = await c.req.json().catch(() => ({}));
862
- const text = String(rawBody.text ?? '');
863
- const variant = String(rawBody.variant ?? 'balanced');
864
- const model = String(rawBody.model ?? 'claude-opus-4-7-20260416');
865
- if (!text || text.length < 10) {
866
- return c.json({ error: 'text required' }, 400);
867
- }
868
- const token = storedKey('anthropic');
869
- if (!token) {
870
- return c.json({ error: 'No anthropic OAuth token captured yet. Use Claude Code once first.' }, 401);
871
- }
872
- const VARIANT_PROMPTS = {
873
- conservative: 'CONSERVATIVE compression (30-40% reduction). Keep most detail: all file paths, function names, error messages with line numbers, test failures with assertions, all distinct stack frames. Remove only decorative content, whitespace, duplicate progress indicators.',
874
- balanced: 'BALANCED compression (50-60% reduction). Keep file paths, function names, error messages with line numbers (truncate long messages to essentials), failing test names and assertions, key values. Remove verbose explanations, repeated info, decorative formatting, stack traces beyond first frame.',
875
- aggressive: 'AGGRESSIVE compression (70-85% reduction). Bare essentials only: file paths, identifier names, error type + line, test pass/fail counts + failing test names, single most important value. Remove all explanations, all but critical assertions, all stack frames, all progress/timestamps/decorations.',
876
- };
877
- const variantPrompt = VARIANT_PROMPTS[variant] ?? VARIANT_PROMPTS.balanced;
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { Hono } from 'hono';
5
+ import { stream, streamSSE } from 'hono/streaming';
6
+ import { config, applyMode, runtimeOverrides, anthropicNativeCompactEnabled, effectiveBackend, USER_CONFIG_DIR, USER_CONFIG_PATH } from './config.js';
7
+ import { Stats } from './stats.js';
8
+ import { DASHBOARD_HTML, LOGO_SVG } from './dashboard.js';
9
+ import { getCache, emptySavings } from './compressor.js';
10
+ import { compressAnthropicMessages, compressOpenAIMessages, compressGeminiContents, } from './compressor.js';
11
+ import { isBypassed, setBypassed, toggleBypassed } from './bypass.js';
12
+ import { circuitBreaker } from './circuitBreaker.js';
13
+ import { injectExpandToolAnthropic, injectExpandToolOpenAI, handleAnthropicExpandCall, handleOpenAIExpandCall, retrieveOriginal, expandStoreSize, } from './expand.js';
14
+ import { compressSystemPrompt } from './systemPrompt.js';
15
+ import { anthropicDirectFetch, isAnthropicUrl } from './anthropicDirectFetch.js';
16
+ import { sessionCacheSize } from './sessionCache.js';
17
+ import { detPatternHits } from './deterministic.js';
18
+ import { VERSION } from './version.js';
19
+ import { recordRequest, getHistorySessions, getCurrentSession, getProjectAggregates, getAllSessionsForHistory, } from './history.js';
20
+ import { updateAnthropicFromHeaders, updateOpenAIFromHeaders, updateGeminiFrom429, addAnthropicUsage, addOpenAIUsage, addGeminiUsage, makeSseUsageParser, maybeRefreshOpenAIBilling, maybeRefreshOpenAISessionLimits, storeKey, storedKey, limitsSnapshot, } from './limits.js';
21
+ // ── Project name extraction ────────────────────────────────────────────────────
22
+ // Manual project override — set via /squeezr/project endpoint or MCP tool
23
+ let manualProject = null;
24
+ export function setManualProject(name) {
25
+ manualProject = name;
26
+ }
27
+ export function getManualProject() {
28
+ return manualProject;
29
+ }
30
+ // Reads the CWD from Claude Code's system prompt (injected as <cwd>…</cwd> or
31
+ // "current working directory: …") and returns the last path component.
32
+ function extractProjectName(body) {
33
+ if (manualProject)
34
+ return manualProject;
35
+ try {
36
+ const system = body.system;
37
+ let text = '';
38
+ if (Array.isArray(system)) {
39
+ text = system
40
+ .map(s => s.text ?? '')
41
+ .join(' ');
42
+ }
43
+ else if (typeof system === 'string') {
44
+ text = system;
45
+ }
46
+ // Claude Code format: <cwd>/path/to/project</cwd>
47
+ const xmlCwd = text.match(/<cwd>([^<]+)<\/cwd>/);
48
+ if (xmlCwd) {
49
+ const parts = xmlCwd[1].trim().replace(/\\/g, '/').split('/').filter(Boolean);
50
+ if (parts.length)
51
+ return parts[parts.length - 1];
52
+ }
53
+ // Plain-text format: "current working directory: /path"
54
+ const plainCwd = text.match(/(?:current working directory|cwd)[:\s]+([^\n<]+)/i);
55
+ if (plainCwd) {
56
+ const parts = plainCwd[1].trim().replace(/\\/g, '/').split('/').filter(Boolean);
57
+ if (parts.length)
58
+ return parts[parts.length - 1];
59
+ }
60
+ // Fallback: extract LAST meaningful path segment from system prompt
61
+ // e.g. C:\Users\Ramos\Documents\InvoiceApp\src → InvoiceApp
62
+ // Only match filesystem paths (not URLs like https://github.com)
63
+ const pathMatch = text.match(/(?:[A-Za-z]:[\\/]|\/(?:Users|home|workspace|projects|Documents)[\\/])[^\s<>"*?|]+/i);
64
+ if (pathMatch && !pathMatch[0].includes('://')) {
65
+ const parts = pathMatch[0].replace(/\\/g, '/').split('/').filter(Boolean);
66
+ const skip = new Set([
67
+ 'users', 'home', 'documents', 'workspace', 'projects', 'desktop',
68
+ 'dev', 'src', 'repos', 'mnt', 'c', 'var', 'tmp', 'opt', 'usr',
69
+ 'lib', 'bin', 'etc', 'node_modules', '.claude', '.config',
70
+ ]);
71
+ for (const pt of parts) {
72
+ if (!skip.has(pt.toLowerCase()) && !/^[a-z]:$/i.test(pt) && pt.length > 1)
73
+ return pt;
74
+ }
75
+ if (parts.length)
76
+ return parts[parts.length - 1];
77
+ }
78
+ }
79
+ catch { /* ignore */ }
80
+ return 'unknown';
81
+ }
82
+ const ANTHROPIC_API = 'https://api.anthropic.com';
83
+ const OPENAI_API = 'https://api.openai.com';
84
+ const GOOGLE_API = 'https://generativelanguage.googleapis.com';
85
+ const SKIP_REQ_HEADERS = new Set(['host', 'content-length', 'transfer-encoding', 'connection', 'upgrade', 'expect', 'x-squeezr-client', 'x-squeezr-dryrun']);
86
+ function readCodexToken() {
87
+ try {
88
+ const d = JSON.parse(readFileSync(join(homedir(), '.codex', 'auth.json'), 'utf-8'));
89
+ return d?.tokens?.access_token ?? null;
90
+ }
91
+ catch {
92
+ return null;
93
+ }
94
+ }
95
+ const SKIP_RESP_HEADERS = new Set(['content-encoding', 'transfer-encoding', 'connection', 'content-length']);
96
+ export const stats = new Stats();
97
+ function forwardHeaders(headers) {
98
+ const out = {};
99
+ for (const [k, v] of headers.entries()) {
100
+ if (!SKIP_REQ_HEADERS.has(k.toLowerCase()))
101
+ out[k] = v;
102
+ }
103
+ return out;
104
+ }
105
+ function extractOpenAIKey(headers) {
106
+ const auth = headers.get('authorization') ?? '';
107
+ return auth.replace(/^bearer\s+/i, '').trim();
108
+ }
109
+ function extractGoogleKey(headers, url) {
110
+ return headers.get('x-goog-api-key') ?? url.searchParams.get('key') ?? '';
111
+ }
112
+ function detectUpstream(headers) {
113
+ if (headers.get('x-goog-api-key'))
114
+ return GOOGLE_API;
115
+ const auth = headers.get('authorization') ?? '';
116
+ if (auth && !headers.get('x-api-key'))
117
+ return OPENAI_API;
118
+ return ANTHROPIC_API;
119
+ }
120
+ function estimateChars(data) {
121
+ return JSON.stringify(data).length;
122
+ }
123
+ // Outgoing fetch — uses Node's native fetch for everything EXCEPT
124
+ // api.anthropic.com, which is forced through direct DNS so that the system
125
+ // hosts file redirect installed by `squeezr enable-claude-desktop` does NOT
126
+ // loop the main proxy back to itself (or to the desktop proxy with its
127
+ // self-signed cert, which is what was breaking Claude Code in the terminal
128
+ // with infinite "Retrying" loops).
129
+ //
130
+ // This is NOT a state-dependent branch on whether Claude Desktop is enabled
131
+ // — that proved fragile. The behaviour is now constant: api.anthropic.com
132
+ // ALWAYS uses direct DNS. The result is identical for users who never touch
133
+ // Claude Desktop (the hosts file is fine, the direct resolver returns the
134
+ // same IPs) and correct for users who do.
135
+ function outgoingFetch(url, init) {
136
+ if (isAnthropicUrl(url))
137
+ return anthropicDirectFetch(url, init);
138
+ return fetch(url, init);
139
+ }
140
+ async function proxyStream(upstream, body, headers, params) {
141
+ const url = params?.toString() ? `${upstream}?${params}` : upstream;
142
+ return outgoingFetch(url, {
143
+ method: 'POST',
144
+ headers: { ...headers, 'content-type': 'application/json' },
145
+ body: JSON.stringify(body),
146
+ });
147
+ }
148
+ export const app = new Hono();
149
+ // ── CORS middleware (required for Cursor IDE and browser-based tools) ─────────
150
+ // Cursor's Electron renderer sends OPTIONS preflight before every POST.
151
+ // Without this the request is blocked and Cursor shows a network error.
152
+ app.use('*', async (c, next) => {
153
+ if (c.req.method === 'OPTIONS') {
154
+ return c.body(null, 204, {
155
+ 'Access-Control-Allow-Origin': '*',
156
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
157
+ 'Access-Control-Allow-Headers': '*',
158
+ 'Access-Control-Max-Age': '86400',
159
+ });
160
+ }
161
+ await next();
162
+ c.res.headers.set('Access-Control-Allow-Origin', '*');
163
+ c.res.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
164
+ c.res.headers.set('Access-Control-Allow-Headers', '*');
165
+ });
166
+ // ── Client detection from User-Agent ─────────────────────────────────────────
167
+ // `hint` comes from the desktop proxy via `x-squeezr-client` and is the
168
+ // authoritative source when present (the desktop proxy *knows* which listener
169
+ // received the request). Falls back to UA heuristics otherwise.
170
+ const VALID_CLIENT_HINTS = new Set([
171
+ 'claude_code', 'claude_desktop', 'codex_cli', 'codex_desktop',
172
+ 'aider', 'opencode', 'cursor', 'cline', 'windsurf', 'continue',
173
+ ]);
174
+ function detectAnthropicClient(ua, hint) {
175
+ if (hint && VALID_CLIENT_HINTS.has(hint))
176
+ return hint;
177
+ const u = ua.toLowerCase();
178
+ if (u.includes('claude-code') || u.includes('claude_code'))
179
+ return 'claude_code';
180
+ if (u.includes('claude-desktop') || u.includes('claude desktop') || u.includes('electron'))
181
+ return 'claude_desktop';
182
+ if (u.includes('aider'))
183
+ return 'aider';
184
+ if (u.includes('opencode') || u.includes('open-code'))
185
+ return 'opencode';
186
+ if (u.includes('cursor'))
187
+ return 'cursor';
188
+ if (u.includes('cline') || u.includes('roo'))
189
+ return 'cline';
190
+ if (u.includes('windsurf'))
191
+ return 'windsurf';
192
+ return 'claude_code'; // default: most likely Claude Code if using /v1/messages
193
+ }
194
+ function detectOpenAIClient(ua, hint) {
195
+ if (hint && VALID_CLIENT_HINTS.has(hint))
196
+ return hint;
197
+ const u = ua.toLowerCase();
198
+ if (u.includes('codex'))
199
+ return 'codex_desktop';
200
+ if (u.includes('cursor'))
201
+ return 'cursor';
202
+ if (u.includes('continue'))
203
+ return 'continue';
204
+ if (u.includes('cline') || u.includes('roo'))
205
+ return 'cline';
206
+ if (u.includes('windsurf'))
207
+ return 'windsurf';
208
+ if (u.includes('aider'))
209
+ return 'aider';
210
+ return 'openai_other';
211
+ }
212
+ // ── Anthropic / Claude Code ───────────────────────────────────────────────────
213
+ app.post('/v1/messages', async (c) => {
214
+ const body = await c.req.json();
215
+ // Support both API key (x-api-key: sk-ant-...) and OAuth bearer token
216
+ // (Authorization: Bearer ...) — Claude Code subscription uses OAuth
217
+ const apiKey = c.req.header('x-api-key')
218
+ ?? c.req.header('authorization')?.replace(/^bearer\s+/i, '').trim()
219
+ ?? process.env.ANTHROPIC_API_KEY
220
+ ?? '';
221
+ const clientId = detectAnthropicClient(c.req.header('user-agent') ?? '', c.req.header('x-squeezr-client'));
222
+ const modelId = String(body.model ?? 'unknown');
223
+ // Extract project name BEFORE compressing system prompt (compression destroys <cwd> tags)
224
+ const project = extractProjectName(body);
225
+ const messages = (body.messages ?? []);
226
+ const originalChars = estimateChars(messages);
227
+ // Dry-run mode: exercises the compression pipeline but does NOT forward to
228
+ // upstream. Used by the post-start self-test to verify the request path is
229
+ // wired correctly without consuming any API quota.
230
+ if (c.req.header('x-squeezr-dryrun') === '1') {
231
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
232
+ const [dryMessages, savings] = await compressAnthropicMessages(messages, apiKey, config);
233
+ const compressedChars = estimateChars(dryMessages);
234
+ return c.json({
235
+ identity: 'squeezr',
236
+ dry_run: true,
237
+ original_chars: originalChars,
238
+ compressed_chars: compressedChars,
239
+ saved_chars: Math.max(0, originalChars - compressedChars),
240
+ savings,
241
+ });
242
+ }
243
+ // Bypass mode: skip all compression, still record request stats
244
+ if (isBypassed()) {
245
+ stats.recordWithProject(project, originalChars, originalChars, emptySavings(), undefined, clientId, modelId);
246
+ recordRequest(project, 0, 0, [], originalChars);
247
+ storeKey('anthropic', apiKey);
248
+ const fwdHeaders = forwardHeaders(c.req.raw.headers);
249
+ if (body.stream) {
250
+ const upstream = await proxyStream(`${ANTHROPIC_API}/v1/messages`, body, fwdHeaders);
251
+ updateAnthropicFromHeaders(upstream.headers);
252
+ for (const [k, v] of upstream.headers.entries()) {
253
+ if (!SKIP_RESP_HEADERS.has(k.toLowerCase()))
254
+ c.header(k, v);
255
+ }
256
+ return stream(c, async (s) => {
257
+ const reader = upstream.body.getReader();
258
+ const decoder = new TextDecoder();
259
+ const sseParser = makeSseUsageParser('anthropic', (inp, out) => addAnthropicUsage(inp, out));
260
+ while (true) {
261
+ const { done, value } = await reader.read();
262
+ if (done)
263
+ break;
264
+ await s.write(value);
265
+ sseParser(decoder.decode(value, { stream: true }));
266
+ }
267
+ });
268
+ }
269
+ const resp = await outgoingFetch(`${ANTHROPIC_API}/v1/messages`, {
270
+ method: 'POST',
271
+ headers: { ...fwdHeaders, 'content-type': 'application/json' },
272
+ body: JSON.stringify(body),
273
+ });
274
+ updateAnthropicFromHeaders(resp.headers);
275
+ const respBody = await resp.json();
276
+ const respHeaders = {};
277
+ for (const [k, v] of resp.headers.entries()) {
278
+ if (!SKIP_RESP_HEADERS.has(k.toLowerCase()))
279
+ respHeaders[k] = v;
280
+ }
281
+ return c.json(respBody, resp.status, respHeaders);
282
+ }
283
+ // System prompt compression (handles both string and array formats — Claude Code sends array)
284
+ if (config.compressSystemPrompt && !config.dryRun) {
285
+ if (typeof body.system === 'string') {
286
+ const sp = await compressSystemPrompt(body.system, apiKey, 'haiku');
287
+ body.system = sp.text;
288
+ stats.recordSystemPromptSaved(sp.originalLen, sp.compressedLen);
289
+ }
290
+ else if (Array.isArray(body.system)) {
291
+ for (const block of body.system) {
292
+ if (block.type === 'text' && typeof block.text === 'string') {
293
+ const sp = await compressSystemPrompt(block.text, apiKey, 'haiku');
294
+ block.text = sp.text;
295
+ stats.recordSystemPromptSaved(sp.originalLen, sp.compressedLen);
296
+ }
297
+ }
298
+ }
299
+ }
300
+ const systemExtraChars = typeof body.system === 'string'
301
+ ? body.system.length
302
+ : Array.isArray(body.system)
303
+ ? body.system.reduce((s, b) => s + (b.text?.length ?? 0), 0)
304
+ : 0;
305
+ const compT0 = Date.now();
306
+ const [compressedMsgs, savings] = await compressAnthropicMessages(messages, apiKey, config, systemExtraChars);
307
+ const compLatency = { totalMs: Date.now() - compT0, detMs: savings.detMs, aiMs: savings.aiMs };
308
+ body.messages = compressedMsgs;
309
+ // Inject expand tool
310
+ injectExpandToolAnthropic(body);
311
+ const _claudeCompChars = estimateChars(compressedMsgs);
312
+ stats.recordWithProject(project, originalChars, _claudeCompChars, savings, compLatency, clientId, modelId);
313
+ // Record TOTAL saved (originalChars - compressedChars), not just AI savings
314
+ recordRequest(project, Math.max(0, originalChars - _claudeCompChars), savings.compressed, savings.byTool, originalChars);
315
+ storeKey('anthropic', apiKey);
316
+ const fwdHeaders = forwardHeaders(c.req.raw.headers);
317
+ // Anthropic native context compaction beta (compact-2026-01-12)
318
+ // When enabled, Anthropic auto-summarizes the conversation server-side
319
+ // when input tokens exceed threshold. Stacks with Squeezr's compression.
320
+ if (anthropicNativeCompactEnabled()) {
321
+ const existingBeta = fwdHeaders['anthropic-beta'] || '';
322
+ if (!existingBeta.includes('compact-2026-01-12')) {
323
+ fwdHeaders['anthropic-beta'] = existingBeta
324
+ ? `${existingBeta},compact-2026-01-12`
325
+ : 'compact-2026-01-12';
326
+ }
327
+ }
328
+ if (body.stream) {
329
+ const upstream = await proxyStream(`${ANTHROPIC_API}/v1/messages`, body, fwdHeaders);
330
+ // Extract rate limit headers immediately (available before body starts)
331
+ updateAnthropicFromHeaders(upstream.headers);
332
+ // Forward anthropic-ratelimit-* (and other response) headers so Claude Code
333
+ // can populate rate_limits in the statusline JSON (issue #4).
334
+ for (const [k, v] of upstream.headers.entries()) {
335
+ if (!SKIP_RESP_HEADERS.has(k.toLowerCase()))
336
+ c.header(k, v);
337
+ }
338
+ return stream(c, async (s) => {
339
+ const reader = upstream.body.getReader();
340
+ const decoder = new TextDecoder();
341
+ const sseParser = makeSseUsageParser('anthropic', (inp, out) => addAnthropicUsage(inp, out));
342
+ while (true) {
343
+ const { done, value } = await reader.read();
344
+ if (done)
345
+ break;
346
+ await s.write(value);
347
+ sseParser(decoder.decode(value, { stream: true }));
348
+ }
349
+ });
350
+ }
351
+ const resp = await outgoingFetch(`${ANTHROPIC_API}/v1/messages`, {
352
+ method: 'POST',
353
+ headers: { ...fwdHeaders, 'content-type': 'application/json' },
354
+ body: JSON.stringify(body),
355
+ });
356
+ // Extract rate limits and token usage from non-streaming response
357
+ updateAnthropicFromHeaders(resp.headers);
358
+ const respBody = await resp.json();
359
+ if (respBody.usage) {
360
+ const u = respBody.usage;
361
+ addAnthropicUsage(u.input_tokens ?? 0, u.output_tokens ?? 0);
362
+ }
363
+ // Handle expand() call if model requested one (track expand rate)
364
+ const expandCall = handleAnthropicExpandCall(respBody);
365
+ if (expandCall) {
366
+ stats.recordExpand(true);
367
+ const { toolUseId, original } = expandCall;
368
+ const continueMessages = [
369
+ ...body.messages,
370
+ { role: 'assistant', content: respBody.content },
371
+ {
372
+ role: 'user',
373
+ content: [{ type: 'tool_result', tool_use_id: toolUseId, content: original }],
374
+ },
375
+ ];
376
+ body.messages = continueMessages;
377
+ const continuedResp = await fetch(`${ANTHROPIC_API}/v1/messages`, {
378
+ method: 'POST',
379
+ headers: { ...fwdHeaders, 'content-type': 'application/json' },
380
+ body: JSON.stringify(body),
381
+ });
382
+ updateAnthropicFromHeaders(continuedResp.headers);
383
+ const continuedBody = await continuedResp.json();
384
+ const continuedHeaders = {};
385
+ for (const [k, v] of continuedResp.headers.entries()) {
386
+ if (!SKIP_RESP_HEADERS.has(k.toLowerCase()))
387
+ continuedHeaders[k] = v;
388
+ }
389
+ return c.json(continuedBody, continuedResp.status, continuedHeaders);
390
+ }
391
+ const respHeaders = {};
392
+ for (const [k, v] of resp.headers.entries()) {
393
+ if (!SKIP_RESP_HEADERS.has(k.toLowerCase()))
394
+ respHeaders[k] = v;
395
+ }
396
+ return c.json(respBody, resp.status, respHeaders);
397
+ });
398
+ // ── OpenAI / Codex / Ollama ───────────────────────────────────────────────────
399
+ app.post('/v1/chat/completions', async (c) => {
400
+ const body = await c.req.json();
401
+ const openAIKey = extractOpenAIKey(c.req.raw.headers);
402
+ const isLocal = config.isLocalKey(openAIKey);
403
+ const upstream = isLocal ? `${config.localUpstreamUrl.replace(/\/$/, '')}/v1/chat/completions` : `${OPENAI_API}/v1/chat/completions`;
404
+ const oaiClientId = detectOpenAIClient(c.req.header('user-agent') ?? '', c.req.header('x-squeezr-client'));
405
+ const oaiModelId = String(body.model ?? 'unknown');
406
+ // Extract project name BEFORE compressing system prompt
407
+ const oaiProject = extractProjectName(body);
408
+ const messages = (body.messages ?? []);
409
+ const originalChars = estimateChars(messages);
410
+ // Bypass mode: skip all compression, still record request stats
411
+ if (isBypassed()) {
412
+ stats.recordWithProject(oaiProject, originalChars, originalChars, emptySavings(), undefined, oaiClientId, oaiModelId);
413
+ recordRequest(oaiProject, 0, 0, [], originalChars);
414
+ if (!isLocal)
415
+ storeKey('openai', openAIKey);
416
+ const fwdHeaders = forwardHeaders(c.req.raw.headers);
417
+ if (body.stream) {
418
+ const resp = await fetch(upstream, {
419
+ method: 'POST',
420
+ headers: { ...fwdHeaders, 'content-type': 'application/json' },
421
+ body: JSON.stringify(body),
422
+ });
423
+ if (!isLocal)
424
+ updateOpenAIFromHeaders(resp.headers);
425
+ return stream(c, async (s) => {
426
+ const reader = resp.body.getReader();
427
+ while (true) {
428
+ const { done, value } = await reader.read();
429
+ if (done)
430
+ break;
431
+ await s.write(value);
432
+ }
433
+ });
434
+ }
435
+ const resp = await fetch(upstream, {
436
+ method: 'POST',
437
+ headers: { ...fwdHeaders, 'content-type': 'application/json' },
438
+ body: JSON.stringify(body),
439
+ });
440
+ if (!isLocal)
441
+ updateOpenAIFromHeaders(resp.headers);
442
+ const respBody = await resp.json();
443
+ const respHeaders = {};
444
+ for (const [k, v] of resp.headers.entries()) {
445
+ if (!SKIP_RESP_HEADERS.has(k.toLowerCase()))
446
+ respHeaders[k] = v;
447
+ }
448
+ return c.json(respBody, resp.status, respHeaders);
449
+ }
450
+ // Compress system message for non-local
451
+ if (!isLocal && config.compressSystemPrompt && !config.dryRun) {
452
+ const msgs = messages;
453
+ if (msgs[0]?.role === 'system' && typeof msgs[0].content === 'string') {
454
+ const sp = await compressSystemPrompt(msgs[0].content, openAIKey, 'gpt-mini');
455
+ msgs[0].content = sp.text;
456
+ stats.recordSystemPromptSaved(sp.originalLen, sp.compressedLen);
457
+ }
458
+ }
459
+ const oaiCompT0 = Date.now();
460
+ const [compressedMsgs, savings] = await compressOpenAIMessages(messages, openAIKey, config, isLocal);
461
+ const oaiCompLatency = { totalMs: Date.now() - oaiCompT0, detMs: savings.detMs, aiMs: savings.aiMs };
462
+ body.messages = compressedMsgs;
463
+ if (!isLocal)
464
+ injectExpandToolOpenAI(body);
465
+ const _oaiCompChars = estimateChars(compressedMsgs);
466
+ stats.recordWithProject(oaiProject, originalChars, _oaiCompChars, savings, oaiCompLatency, oaiClientId, oaiModelId);
467
+ recordRequest(oaiProject, Math.max(0, originalChars - _oaiCompChars), savings.compressed, savings.byTool, originalChars);
468
+ if (!isLocal)
469
+ storeKey('openai', openAIKey);
470
+ const fwdHeaders = forwardHeaders(c.req.raw.headers);
471
+ if (body.stream) {
472
+ // Ask OpenAI to include usage in the final chunk (harmless for most clients)
473
+ if (!isLocal && !body.stream_options?.include_usage) {
474
+ body.stream_options = { ...(body.stream_options ?? {}), include_usage: true };
475
+ }
476
+ const upstreamResp = await proxyStream(upstream, body, fwdHeaders);
477
+ if (!isLocal) {
478
+ updateOpenAIFromHeaders(upstreamResp.headers);
479
+ maybeRefreshOpenAIBilling(openAIKey).catch(() => { });
480
+ }
481
+ return stream(c, async (s) => {
482
+ const reader = upstreamResp.body.getReader();
483
+ const decoder = new TextDecoder();
484
+ const sseParser = makeSseUsageParser('openai', (inp, out) => addOpenAIUsage(inp, out));
485
+ while (true) {
486
+ const { done, value } = await reader.read();
487
+ if (done)
488
+ break;
489
+ await s.write(value);
490
+ if (!isLocal)
491
+ sseParser(decoder.decode(value, { stream: true }));
492
+ }
493
+ });
494
+ }
495
+ const resp = await fetch(upstream, {
496
+ method: 'POST',
497
+ headers: { ...fwdHeaders, 'content-type': 'application/json' },
498
+ body: JSON.stringify(body),
499
+ });
500
+ if (!isLocal) {
501
+ updateOpenAIFromHeaders(resp.headers);
502
+ maybeRefreshOpenAIBilling(openAIKey).catch(() => { });
503
+ }
504
+ const respBody = await resp.json();
505
+ if (!isLocal && respBody.usage) {
506
+ const u = respBody.usage;
507
+ addOpenAIUsage(u.prompt_tokens ?? 0, u.completion_tokens ?? 0);
508
+ }
509
+ const expandCall = !isLocal ? handleOpenAIExpandCall(respBody) : null;
510
+ if (expandCall) {
511
+ stats.recordExpand(true);
512
+ const { toolCallId, original } = expandCall;
513
+ const continueMessages = [
514
+ ...body.messages,
515
+ respBody.choices[0].message,
516
+ { role: 'tool', tool_call_id: toolCallId, content: original },
517
+ ];
518
+ body.messages = continueMessages;
519
+ const continuedResp = await fetch(upstream, {
520
+ method: 'POST',
521
+ headers: { ...fwdHeaders, 'content-type': 'application/json' },
522
+ body: JSON.stringify(body),
523
+ });
524
+ return c.json(await continuedResp.json(), continuedResp.status);
525
+ }
526
+ const respHeaders = {};
527
+ for (const [k, v] of resp.headers.entries()) {
528
+ if (!SKIP_RESP_HEADERS.has(k.toLowerCase()))
529
+ respHeaders[k] = v;
530
+ }
531
+ return c.json(respBody, resp.status, respHeaders);
532
+ });
533
+ // ── Gemini CLI ────────────────────────────────────────────────────────────────
534
+ app.post('/v1beta/models/*', async (c) => {
535
+ const body = await c.req.json();
536
+ const url = new URL(c.req.url);
537
+ const googleKey = extractGoogleKey(c.req.raw.headers, url);
538
+ const modelPath = c.req.path.replace('/v1beta/models/', '');
539
+ const contents = (body.contents ?? []);
540
+ const originalChars = estimateChars(contents);
541
+ const geminiProject = extractProjectName(body);
542
+ // Gemini model is in the URL path: /v1beta/models/gemini-2.5-pro:generateContent
543
+ const geminiModelId = modelPath.split(':')[0] || 'gemini';
544
+ // Bypass mode: skip all compression, still record request stats
545
+ if (isBypassed()) {
546
+ stats.recordWithProject(geminiProject, originalChars, originalChars, emptySavings(), undefined, 'gemini', geminiModelId);
547
+ recordRequest(geminiProject, 0, 0, [], originalChars);
548
+ const targetUrl = `${GOOGLE_API}/v1beta/models/${modelPath}`;
549
+ const fwdHeaders = forwardHeaders(c.req.raw.headers);
550
+ const params = url.searchParams;
551
+ const paramStr = params.toString();
552
+ const resp = await fetch(paramStr ? `${targetUrl}?${paramStr}` : targetUrl, {
553
+ method: 'POST',
554
+ headers: { ...fwdHeaders, 'content-type': 'application/json' },
555
+ body: JSON.stringify(body),
556
+ });
557
+ const respHeaders = {};
558
+ for (const [k, v] of resp.headers.entries()) {
559
+ if (!SKIP_RESP_HEADERS.has(k.toLowerCase()))
560
+ respHeaders[k] = v;
561
+ }
562
+ return c.body(await resp.arrayBuffer(), resp.status, respHeaders);
563
+ }
564
+ // Store gemini key so it's available if backend is later switched to gemini-flash from a different API request
565
+ if (googleKey)
566
+ storeKey('gemini', googleKey);
567
+ const gemCompT0 = Date.now();
568
+ const [compressedContents, savings] = await compressGeminiContents(contents, googleKey, config);
569
+ const gemCompLatency = { totalMs: Date.now() - gemCompT0, detMs: savings.detMs, aiMs: savings.aiMs };
570
+ body.contents = compressedContents;
571
+ const _gemCompChars = estimateChars(compressedContents);
572
+ stats.recordWithProject(geminiProject, originalChars, _gemCompChars, savings, gemCompLatency, 'gemini', geminiModelId);
573
+ recordRequest(geminiProject, Math.max(0, originalChars - _gemCompChars), savings.compressed, savings.byTool, originalChars);
574
+ const targetUrl = `${GOOGLE_API}/v1beta/models/${modelPath}`;
575
+ const fwdHeaders = forwardHeaders(c.req.raw.headers);
576
+ const params = url.searchParams;
577
+ if (modelPath.includes('stream')) {
578
+ const upstreamResp = await proxyStream(targetUrl, body, fwdHeaders, params);
579
+ if (upstreamResp.status === 429)
580
+ updateGeminiFrom429(upstreamResp.headers);
581
+ return stream(c, async (s) => {
582
+ const reader = upstreamResp.body.getReader();
583
+ const decoder = new TextDecoder();
584
+ // Gemini streaming sends JSON array chunks with usageMetadata, not Anthropic-style SSE
585
+ let gemBuf = '';
586
+ while (true) {
587
+ const { done, value } = await reader.read();
588
+ if (done)
589
+ break;
590
+ await s.write(value);
591
+ gemBuf += decoder.decode(value, { stream: true });
592
+ const metaMatch = gemBuf.match(/"usageMetadata"\s*:\s*\{[^}]+\}/);
593
+ if (metaMatch) {
594
+ try {
595
+ const meta = JSON.parse(`{${metaMatch[0]}}`);
596
+ addGeminiUsage(meta.usageMetadata.promptTokenCount ?? 0, meta.usageMetadata.candidatesTokenCount ?? 0);
597
+ }
598
+ catch { /* ignore parse errors */ }
599
+ gemBuf = '';
600
+ }
601
+ }
602
+ });
603
+ }
604
+ const paramStr = params.toString();
605
+ const resp = await fetch(paramStr ? `${targetUrl}?${paramStr}` : targetUrl, {
606
+ method: 'POST',
607
+ headers: { ...fwdHeaders, 'content-type': 'application/json' },
608
+ body: JSON.stringify(body),
609
+ });
610
+ if (resp.status === 429)
611
+ updateGeminiFrom429(resp.headers);
612
+ // Extract Gemini usage from response body
613
+ const geminiRespBuf = await resp.arrayBuffer();
614
+ try {
615
+ const geminiRespJson = JSON.parse(new TextDecoder().decode(geminiRespBuf));
616
+ const meta = geminiRespJson.usageMetadata;
617
+ if (meta)
618
+ addGeminiUsage(meta.promptTokenCount ?? 0, meta.candidatesTokenCount ?? 0);
619
+ }
620
+ catch { /* ignore */ }
621
+ const respHeaders = {};
622
+ for (const [k, v] of resp.headers.entries()) {
623
+ if (!SKIP_RESP_HEADERS.has(k.toLowerCase()))
624
+ respHeaders[k] = v;
625
+ }
626
+ return c.body(geminiRespBuf, resp.status, respHeaders);
627
+ });
628
+ // ── Squeezr internal endpoints ────────────────────────────────────────────────
629
+ async function buildStatsPayload() {
630
+ await maybeRefreshOpenAISessionLimits().catch(() => { });
631
+ const session = stats.summary();
632
+ // Compute all-time totals by summing ALL history sessions (history.json is the source of truth)
633
+ // plus comparing with stats.json — take the maximum to avoid regressions
634
+ const allSessions = getAllSessionsForHistory();
635
+ const historyTotalSavedTokens = allSessions.reduce((s, r) => s + (r.savedTokens || 0), 0);
636
+ const historyTotalOriginalTokens = allSessions.reduce((s, r) => s + (r.originalChars ? Math.round(r.originalChars / 3.5) : 0), 0);
637
+ const historyTotalRequests = allSessions.reduce((s, r) => s + (r.requests || 0), 0);
638
+ const persisted = Stats.loadGlobal();
639
+ const allTimeSavedTokens = Math.max(Math.round(session.total_saved_chars / 3.5), Math.round((persisted.total_saved_chars ?? 0) / 3.5), historyTotalSavedTokens);
640
+ const allTimeOriginalTokens = Math.max(Math.round(session.total_original_chars / 3.5), Math.round((persisted.total_original_chars ?? 0) / 3.5), historyTotalOriginalTokens);
641
+ const allTimeRequests = Math.max(session.requests, persisted.requests ?? 0, historyTotalRequests);
642
+ // Ratio: compute from all-time totals so it matches the all-time Tokens Saved /
643
+ // processed cards. The session.savings_pct comes from this-process-only counters
644
+ // and shows misleading 0-2% values right after restart when the persisted history
645
+ // is multiple orders of magnitude larger than the current session.
646
+ const allTimeSavingsPct = allTimeOriginalTokens > 0
647
+ ? Math.round((allTimeSavedTokens / allTimeOriginalTokens) * 1000) / 10
648
+ : 0;
649
+ // Breakdown: prefer persisted all-time values over session-only counters so
650
+ // deterministic/dedup/sysprompt numbers stay consistent with the hero cards.
651
+ const allTimeBreakdown = {
652
+ deterministic: persisted.det_saved_chars ?? (session.breakdown?.deterministic ?? 0),
653
+ ai_compression: persisted.ai_saved_chars ?? (session.breakdown?.ai_compression ?? 0),
654
+ read_dedup: persisted.dedup_saved_chars ?? (session.breakdown?.read_dedup ?? 0),
655
+ system_prompt: persisted.sysprompt_saved_chars ?? (session.breakdown?.system_prompt ?? 0),
656
+ overhead: persisted.overhead_chars ?? (session.breakdown?.overhead ?? 0),
657
+ ai_calls: persisted.ai_compression_calls ?? (session.breakdown?.ai_calls ?? 0),
658
+ };
659
+ return {
660
+ ...session,
661
+ total_original_chars: allTimeOriginalTokens * 3.5,
662
+ total_saved_chars: allTimeSavedTokens * 3.5,
663
+ total_saved_tokens: allTimeSavedTokens,
664
+ requests: allTimeRequests,
665
+ savings_pct: allTimeSavingsPct,
666
+ breakdown: allTimeBreakdown,
667
+ anthropic_native_compact: anthropicNativeCompactEnabled(),
668
+ compression_backend: effectiveBackend(),
669
+ cache: getCache(config).stats(),
670
+ expand_store_size: expandStoreSize(),
671
+ session_cache_size: sessionCacheSize(),
672
+ dry_run: config.dryRun,
673
+ pattern_hits: detPatternHits,
674
+ version: VERSION,
675
+ port: config.port,
676
+ mode: runtimeOverrides.mode,
677
+ limits: limitsSnapshot(),
678
+ bypassed: isBypassed(),
679
+ circuit_breaker: circuitBreaker.snapshot(),
680
+ };
681
+ }
682
+ app.get('/squeezr/stats', (c) => {
683
+ return buildStatsPayload().then(d => c.json(d));
684
+ });
685
+ // ── POST /squeezr/ports — write port config to squeezr.toml ─────────────────
686
+ app.post('/squeezr/ports', async (c) => {
687
+ const body = await c.req.json();
688
+ const { port: newPort, mitm_port: newMitm } = body;
689
+ if (!newPort || !newMitm || newPort < 1024 || newMitm < 1024 || newPort === newMitm) {
690
+ return c.text('Invalid ports', 400);
691
+ }
692
+ try {
693
+ // Always write to ~/.squeezr/squeezr.toml — the bundled package toml gets
694
+ // wiped on every `npm install -g`, which used to silently reset user
695
+ // ports back to 8080 + autoscan after every update.
696
+ const tomlPath = USER_CONFIG_PATH;
697
+ mkdirSync(USER_CONFIG_DIR, { recursive: true });
698
+ let content = existsSync(tomlPath) ? readFileSync(tomlPath, 'utf-8') : '[proxy]\n';
699
+ // Update or insert [proxy] port and mitm_port
700
+ const updateKey = (src, key, val) => {
701
+ const re = new RegExp(`^(\\s*${key}\\s*=\\s*)\\d+`, 'm');
702
+ return re.test(src) ? src.replace(re, `$1${val}`) : src.replace(/(\[proxy\][^\[]*)/s, `$1${key} = ${val}\n`);
703
+ };
704
+ if (!content.includes('[proxy]'))
705
+ content = '[proxy]\n' + content;
706
+ content = updateKey(content, 'port', newPort);
707
+ content = updateKey(content, 'mitm_port', newMitm);
708
+ writeFileSync(tomlPath, content, 'utf-8');
709
+ return c.json({ ok: true, port: newPort, mitm_port: newMitm, toml: tomlPath });
710
+ }
711
+ catch (err) {
712
+ return c.text('Failed to write squeezr.toml: ' + err.message, 500);
713
+ }
714
+ });
715
+ app.get('/squeezr/health', (c) => {
716
+ const cb = circuitBreaker.snapshot();
717
+ const s = stats.summary();
718
+ return c.json({
719
+ // Magic identifier so callers can distinguish a real squeezr instance from
720
+ // any other HTTP service that happens to answer 200 on this port (e.g. a
721
+ // Docker container occupying the configured port).
722
+ identity: 'squeezr',
723
+ status: 'ok',
724
+ version: VERSION,
725
+ uptime_seconds: s.uptime_seconds,
726
+ mode: runtimeOverrides.mode,
727
+ bypassed: isBypassed(),
728
+ circuit_breaker: {
729
+ state: cb.state,
730
+ consecutive_failures: cb.consecutive_failures,
731
+ total_trips: cb.total_trips,
732
+ last_success_ago_s: cb.last_success_time
733
+ ? Math.round((Date.now() - cb.last_success_time) / 1000)
734
+ : null,
735
+ },
736
+ expand_store: {
737
+ size: expandStoreSize(),
738
+ pressure: expandStoreSize() > 5000 ? 'high' : expandStoreSize() > 1000 ? 'medium' : 'low',
739
+ },
740
+ compression: {
741
+ requests: s.requests,
742
+ savings_pct: s.savings_pct,
743
+ },
744
+ port: config.port,
745
+ mitm_port: config.mitmPort,
746
+ });
747
+ });
748
+ // ── Self-test endpoint ─────────────────────────────────────────────────────
749
+ // Last self-test results are populated by src/selfTest.ts at startup and on
750
+ // demand via GET /squeezr/selftest?run=1.
751
+ let lastSelfTest = null;
752
+ export function setLastSelfTest(result) {
753
+ lastSelfTest = result;
754
+ }
755
+ app.get('/squeezr/selftest', async (c) => {
756
+ if (c.req.query('run') === '1') {
757
+ const { runSelfTest } = await import('./selfTest.js');
758
+ const result = await runSelfTest({ port: Number(c.req.query('port')) || 0 });
759
+ return c.json(result);
760
+ }
761
+ if (!lastSelfTest) {
762
+ return c.json({ status: 'not_run', message: 'Self-test has not been executed yet. Call ?run=1 to execute.' });
763
+ }
764
+ return c.json(lastSelfTest);
765
+ });
766
+ // ── Project management ─────────────────────────────────────────────────────
767
+ app.get('/squeezr/project', (c) => {
768
+ return c.json({ project: getManualProject() ?? stats.currentProjectName() });
769
+ });
770
+ app.post('/squeezr/project', async (c) => {
771
+ const body = await c.req.json();
772
+ if (body.project === null || body.project === '') {
773
+ setManualProject(null);
774
+ return c.json({ project: stats.currentProjectName(), manual: false });
775
+ }
776
+ if (typeof body.project === 'string') {
777
+ setManualProject(body.project);
778
+ stats.setProject(body.project);
779
+ return c.json({ project: body.project, manual: true });
780
+ }
781
+ return c.json({ error: 'Invalid project name' }, 400);
782
+ });
783
+ app.get('/squeezr/expand/:id', (c) => {
784
+ const id = c.req.param('id');
785
+ const original = retrieveOriginal(id);
786
+ stats.recordExpand(!!original);
787
+ if (!original)
788
+ return c.json({ error: 'Not found or expired' }, 404);
789
+ return c.json({ id, content: original });
790
+ });
791
+ // ── Dashboard + SSE + config ──────────────────────────────────────────────────
792
+ app.get('/squeezr/dashboard', (c) => {
793
+ return c.html(DASHBOARD_HTML);
794
+ });
795
+ app.get('/squeezr/favicon.svg', (c) => {
796
+ c.header('Content-Type', 'image/svg+xml');
797
+ c.header('Cache-Control', 'public, max-age=86400');
798
+ return c.body(LOGO_SVG);
799
+ });
800
+ app.get('/squeezr/events', (c) => {
801
+ return streamSSE(c, async (s) => {
802
+ await s.writeSSE({ data: JSON.stringify(await buildStatsPayload()) });
803
+ while (true) {
804
+ await s.sleep(2000);
805
+ try {
806
+ await s.writeSSE({ data: JSON.stringify(await buildStatsPayload()) });
807
+ }
808
+ catch {
809
+ break;
810
+ }
811
+ }
812
+ });
813
+ });
814
+ app.get('/squeezr/limits', async (c) => {
815
+ await maybeRefreshOpenAISessionLimits().catch(() => { });
816
+ return c.json(limitsSnapshot());
817
+ });
818
+ // ── History + Projects endpoints ──────────────────────────────────────────────
819
+ app.get('/squeezr/history', (c) => {
820
+ // sessions returns ONLY historical (past) sessions, current is separate
821
+ // to avoid double-counting when dashboard does sessions.concat(current)
822
+ return c.json({
823
+ sessions: getHistorySessions().filter(s => s.id !== getCurrentSession().id),
824
+ current: getCurrentSession(),
825
+ });
826
+ });
827
+ app.get('/squeezr/projects', (c) => {
828
+ return c.json({ projects: getProjectAggregates() });
829
+ });
830
+ // ── Control endpoints ─────────────────────────────────────────────────────────
831
+ app.post('/squeezr/control/stop', (c) => {
832
+ // Respond first, then exit gracefully after a tick
833
+ setTimeout(() => process.emit('SIGTERM'), 200);
834
+ return c.json({ ok: true, message: 'Squeezr proxy shutting down…' });
835
+ });
836
+ app.post('/squeezr/config', async (c) => {
837
+ const body = await c.req.json();
838
+ if (body.mode && ['soft', 'normal', 'aggressive', 'critical'].includes(body.mode)) {
839
+ applyMode(body.mode);
840
+ }
841
+ return c.json({ ok: true, mode: runtimeOverrides.mode });
842
+ });
843
+ // ── Bypass mode (runtime-only compression toggle) ────────────────────────────
844
+ app.get('/squeezr/bypass', (c) => {
845
+ return c.json({ bypassed: isBypassed() });
846
+ });
847
+ app.post('/squeezr/bypass', async (c) => {
848
+ try {
849
+ const body = await c.req.json().catch(() => ({}));
850
+ if (typeof body.enabled === 'boolean') {
851
+ setBypassed(body.enabled);
852
+ }
853
+ else {
854
+ toggleBypassed();
855
+ }
856
+ }
857
+ catch {
858
+ toggleBypassed();
859
+ }
860
+ return c.json({ bypassed: isBypassed() });
861
+ });
862
+ // ── Distillation endpoint uses captured OAuth token to compress with Opus ──
863
+ // Allows external scripts to do high-quality compression using the user's Claude
864
+ // Pro/Max subscription (no API key needed). Used by recipes/squeezr-1B for training.
865
+ app.post('/squeezr/distill', async (c) => {
866
+ const rawBody = await c.req.json().catch(() => ({}));
867
+ const text = String(rawBody.text ?? '');
868
+ const variant = String(rawBody.variant ?? 'balanced');
869
+ const model = String(rawBody.model ?? 'claude-opus-4-7-20260416');
870
+ if (!text || text.length < 10) {
871
+ return c.json({ error: 'text required' }, 400);
872
+ }
873
+ const token = storedKey('anthropic');
874
+ if (!token) {
875
+ return c.json({ error: 'No anthropic OAuth token captured yet. Use Claude Code once first.' }, 401);
876
+ }
877
+ const VARIANT_PROMPTS = {
878
+ conservative: 'CONSERVATIVE compression (30-40% reduction). Keep most detail: all file paths, function names, error messages with line numbers, test failures with assertions, all distinct stack frames. Remove only decorative content, whitespace, duplicate progress indicators.',
879
+ balanced: 'BALANCED compression (50-60% reduction). Keep file paths, function names, error messages with line numbers (truncate long messages to essentials), failing test names and assertions, key values. Remove verbose explanations, repeated info, decorative formatting, stack traces beyond first frame.',
880
+ aggressive: 'AGGRESSIVE compression (70-85% reduction). Bare essentials only: file paths, identifier names, error type + line, test pass/fail counts + failing test names, single most important value. Remove all explanations, all but critical assertions, all stack frames, all progress/timestamps/decorations.',
881
+ };
882
+ const variantPrompt = VARIANT_PROMPTS[variant] ?? VARIANT_PROMPTS.balanced;
878
883
  const prompt = `You are an expert at compressing AI coding tool outputs.
879
884
 
880
885
  ${variantPrompt}
@@ -884,118 +889,118 @@ Output ONLY the compressed text. No preamble, no markdown fences, no explanation
884
889
  ---
885
890
  TOOL OUTPUT TO COMPRESS:
886
891
 
887
- ${text}`;
888
- try {
889
- const authOpts = token.startsWith('sk-') ? { apiKey: token } : { authToken: token };
890
- const { default: Anthropic } = await import('@anthropic-ai/sdk');
891
- const client = new Anthropic({ ...authOpts, baseURL: 'https://api.anthropic.com' });
892
- const t0 = Date.now();
893
- const resp = await client.messages.create({
894
- model,
895
- max_tokens: 4096,
896
- messages: [{ role: 'user', content: prompt }],
897
- });
898
- const compressed = resp.content[0].text.trim();
899
- return c.json({
900
- compressed,
901
- original_length: text.length,
902
- compressed_length: compressed.length,
903
- ratio: 1 - compressed.length / Math.max(text.length, 1),
904
- variant,
905
- model,
906
- latency_ms: Date.now() - t0,
907
- });
908
- }
909
- catch (e) {
910
- const status = e?.status ?? 500;
911
- return c.json({ error: String(e?.message ?? e), status }, status);
912
- }
913
- });
914
- // Get/set compression backend (which AI model compresses tool results)
915
- app.get('/squeezr/backend', (c) => {
916
- return c.json({ backend: effectiveBackend() });
917
- });
918
- app.post('/squeezr/backend', async (c) => {
919
- try {
920
- const body = await c.req.json().catch(() => ({}));
921
- const valid = ['auto', 'local', 'haiku', 'gpt-mini', 'gemini-flash'];
922
- if (body.backend && valid.includes(body.backend)) {
923
- runtimeOverrides.compressionBackend = body.backend;
924
- }
925
- }
926
- catch { /* ignore */ }
927
- return c.json({ backend: effectiveBackend() });
928
- });
929
- // Toggle Anthropic native compaction beta (compact-2026-01-12)
930
- app.get('/squeezr/native-compact', (c) => {
931
- return c.json({ enabled: anthropicNativeCompactEnabled() });
932
- });
933
- app.post('/squeezr/native-compact', async (c) => {
934
- try {
935
- const body = await c.req.json().catch(() => ({}));
936
- if (typeof body.enabled === 'boolean') {
937
- runtimeOverrides.anthropicNativeCompact = body.enabled;
938
- }
939
- else {
940
- runtimeOverrides.anthropicNativeCompact = !anthropicNativeCompactEnabled();
941
- }
942
- }
943
- catch {
944
- runtimeOverrides.anthropicNativeCompact = !anthropicNativeCompactEnabled();
945
- }
946
- return c.json({ enabled: anthropicNativeCompactEnabled() });
947
- });
948
- // ── OAuth token refresh proxy (Codex: set CODEX_REFRESH_TOKEN_URL_OVERRIDE=http://localhost:PORT/oauth/token) ──
949
- app.post('/oauth/token', async (c) => {
950
- const body = await c.req.arrayBuffer();
951
- const resp = await fetch('https://auth.openai.com/oauth/token', {
952
- method: 'POST',
953
- headers: { 'content-type': c.req.header('content-type') ?? 'application/json' },
954
- body,
955
- });
956
- const data = await resp.arrayBuffer();
957
- return c.body(data, resp.status, { 'content-type': 'application/json' });
958
- });
959
- // ── Catch-all ─────────────────────────────────────────────────────────────────
960
- app.all('*', async (c) => {
961
- let upstream = detectUpstream(c.req.raw.headers);
962
- const url = new URL(c.req.url);
963
- const NEEDS_V1 = new Set(['/models', '/engines', '/files', '/embeddings', '/moderations', '/completions', '/edits', '/responses']);
964
- const pathname = NEEDS_V1.has(url.pathname) ? `/v1${url.pathname}` : url.pathname;
965
- // /responses is exclusively an OpenAI Codex endpoint — override upstream regardless
966
- // of what detectUpstream inferred from headers (Codex sends no auth to custom base URLs).
967
- if (pathname === '/v1/responses')
968
- upstream = OPENAI_API;
969
- const targetUrl = `${upstream}${pathname}${url.search}`;
970
- const body = await c.req.arrayBuffer();
971
- const fwdHeaders = forwardHeaders(c.req.raw.headers);
972
- // Inject Codex OAuth token from ~/.codex/auth.json when no auth header present.
973
- if (upstream === OPENAI_API && !fwdHeaders['authorization']) {
974
- const codexToken = readCodexToken();
975
- if (codexToken)
976
- fwdHeaders['authorization'] = `Bearer ${codexToken}`;
977
- }
978
- const resp = await fetch(targetUrl, {
979
- method: c.req.method,
980
- headers: fwdHeaders,
981
- body: body.byteLength > 0 ? body : undefined,
982
- });
983
- const respHeaders = {};
984
- for (const [k, v] of resp.headers.entries()) {
985
- if (!SKIP_RESP_HEADERS.has(k.toLowerCase()))
986
- respHeaders[k] = v;
987
- }
988
- const contentType = resp.headers.get('content-type') ?? '';
989
- if (contentType.includes('text/event-stream')) {
990
- return stream(c, async (s) => {
991
- const reader = resp.body.getReader();
992
- while (true) {
993
- const { done, value } = await reader.read();
994
- if (done)
995
- break;
996
- await s.write(value);
997
- }
998
- });
999
- }
1000
- return c.body(await resp.arrayBuffer(), resp.status, respHeaders);
1001
- });
892
+ ${text}`;
893
+ try {
894
+ const authOpts = token.startsWith('sk-') ? { apiKey: token } : { authToken: token };
895
+ const { default: Anthropic } = await import('@anthropic-ai/sdk');
896
+ const client = new Anthropic({ ...authOpts, baseURL: 'https://api.anthropic.com' });
897
+ const t0 = Date.now();
898
+ const resp = await client.messages.create({
899
+ model,
900
+ max_tokens: 4096,
901
+ messages: [{ role: 'user', content: prompt }],
902
+ });
903
+ const compressed = resp.content[0].text.trim();
904
+ return c.json({
905
+ compressed,
906
+ original_length: text.length,
907
+ compressed_length: compressed.length,
908
+ ratio: 1 - compressed.length / Math.max(text.length, 1),
909
+ variant,
910
+ model,
911
+ latency_ms: Date.now() - t0,
912
+ });
913
+ }
914
+ catch (e) {
915
+ const status = e?.status ?? 500;
916
+ return c.json({ error: String(e?.message ?? e), status }, status);
917
+ }
918
+ });
919
+ // Get/set compression backend (which AI model compresses tool results)
920
+ app.get('/squeezr/backend', (c) => {
921
+ return c.json({ backend: effectiveBackend() });
922
+ });
923
+ app.post('/squeezr/backend', async (c) => {
924
+ try {
925
+ const body = await c.req.json().catch(() => ({}));
926
+ const valid = ['auto', 'local', 'haiku', 'gpt-mini', 'gemini-flash'];
927
+ if (body.backend && valid.includes(body.backend)) {
928
+ runtimeOverrides.compressionBackend = body.backend;
929
+ }
930
+ }
931
+ catch { /* ignore */ }
932
+ return c.json({ backend: effectiveBackend() });
933
+ });
934
+ // Toggle Anthropic native compaction beta (compact-2026-01-12)
935
+ app.get('/squeezr/native-compact', (c) => {
936
+ return c.json({ enabled: anthropicNativeCompactEnabled() });
937
+ });
938
+ app.post('/squeezr/native-compact', async (c) => {
939
+ try {
940
+ const body = await c.req.json().catch(() => ({}));
941
+ if (typeof body.enabled === 'boolean') {
942
+ runtimeOverrides.anthropicNativeCompact = body.enabled;
943
+ }
944
+ else {
945
+ runtimeOverrides.anthropicNativeCompact = !anthropicNativeCompactEnabled();
946
+ }
947
+ }
948
+ catch {
949
+ runtimeOverrides.anthropicNativeCompact = !anthropicNativeCompactEnabled();
950
+ }
951
+ return c.json({ enabled: anthropicNativeCompactEnabled() });
952
+ });
953
+ // ── OAuth token refresh proxy (Codex: set CODEX_REFRESH_TOKEN_URL_OVERRIDE=http://localhost:PORT/oauth/token) ──
954
+ app.post('/oauth/token', async (c) => {
955
+ const body = await c.req.arrayBuffer();
956
+ const resp = await fetch('https://auth.openai.com/oauth/token', {
957
+ method: 'POST',
958
+ headers: { 'content-type': c.req.header('content-type') ?? 'application/json' },
959
+ body,
960
+ });
961
+ const data = await resp.arrayBuffer();
962
+ return c.body(data, resp.status, { 'content-type': 'application/json' });
963
+ });
964
+ // ── Catch-all ─────────────────────────────────────────────────────────────────
965
+ app.all('*', async (c) => {
966
+ let upstream = detectUpstream(c.req.raw.headers);
967
+ const url = new URL(c.req.url);
968
+ const NEEDS_V1 = new Set(['/models', '/engines', '/files', '/embeddings', '/moderations', '/completions', '/edits', '/responses']);
969
+ const pathname = NEEDS_V1.has(url.pathname) ? `/v1${url.pathname}` : url.pathname;
970
+ // /responses is exclusively an OpenAI Codex endpoint — override upstream regardless
971
+ // of what detectUpstream inferred from headers (Codex sends no auth to custom base URLs).
972
+ if (pathname === '/v1/responses')
973
+ upstream = OPENAI_API;
974
+ const targetUrl = `${upstream}${pathname}${url.search}`;
975
+ const body = await c.req.arrayBuffer();
976
+ const fwdHeaders = forwardHeaders(c.req.raw.headers);
977
+ // Inject Codex OAuth token from ~/.codex/auth.json when no auth header present.
978
+ if (upstream === OPENAI_API && !fwdHeaders['authorization']) {
979
+ const codexToken = readCodexToken();
980
+ if (codexToken)
981
+ fwdHeaders['authorization'] = `Bearer ${codexToken}`;
982
+ }
983
+ const resp = await fetch(targetUrl, {
984
+ method: c.req.method,
985
+ headers: fwdHeaders,
986
+ body: body.byteLength > 0 ? body : undefined,
987
+ });
988
+ const respHeaders = {};
989
+ for (const [k, v] of resp.headers.entries()) {
990
+ if (!SKIP_RESP_HEADERS.has(k.toLowerCase()))
991
+ respHeaders[k] = v;
992
+ }
993
+ const contentType = resp.headers.get('content-type') ?? '';
994
+ if (contentType.includes('text/event-stream')) {
995
+ return stream(c, async (s) => {
996
+ const reader = resp.body.getReader();
997
+ while (true) {
998
+ const { done, value } = await reader.read();
999
+ if (done)
1000
+ break;
1001
+ await s.write(value);
1002
+ }
1003
+ });
1004
+ }
1005
+ return c.body(await resp.arrayBuffer(), resp.status, respHeaders);
1006
+ });