Files
pxpipe/tests/proxy-usage.test.ts
T
teamchong f199640bdc ship Unifont @ 10px multilingual rendering + 400 diagnosis fixes
- Switch font: JetBrains Mono @ 15px → Unifont 16.0.04 @ 10px. Sparse
  atlas with double-width EAW dispatch for CJK. Full BMP profile
  (35,501 glyphs) covers Latin/Cyrillic/Greek/Hebrew/Arabic/CJK/Hangul
  plus Letterlike, Misc Technical, Block Elements, Geometric Shapes,
  Misc Symbols, Dingbats, Enclosed Alphanumerics, all box-drawing.
- Fix cell-height bug (was hardcoded 9; now probed from font metrics
  via Math.ceil(maxAscent+maxDescent)). Previously clipped the top
  2 rows off every CJK glyph since the original Unifont ship.
- Expand tabs to spaces with 4-column tab stops, EAW-aware. Was the
  silent-drop source for 99.6% of dropped chars in production.
- Add dropped_codepoints_top telemetry (top 20 per event, only emitted
  when drops > 0) so future coverage gaps surface in events.jsonl.
- Raise minToolResultChars 2000→5000 and minReminderChars 1000→2000
  to stop net-losing tokens on small compressions, per ~2,500 tok/img
  empirical measurement (N=33 cold-miss events).
- Fix dashboard per-image-cost formula: was pngBytes/375 (~190 tok),
  now imageCount * 2500 (the measured empirical rate). Reduces the
  inflated "tokens saved" headline to honest numbers.
- Schema-stub fix in compressSchemas path: preserveSchemaShell()
  retains type / properties / required / enum / oneOf / anyOf / $ref /
  numeric constraints recursively (depth 20), strips only descriptions
  and metadata. Prevents 400 errors on first-tool-call requests.
- Capture full gzipped 4xx body + per-event req_body_sha8 for upstream
  error diagnosis. Sidecar to ~/.pixelpipe/4xx-bodies/ when over 32 KB
  inline cap. Workers-safe via CompressionStream + Web Crypto.
- scripts/restart.sh: pnpm-aware one-command kill+rebuild+start with
  graceful SIGTERM→SIGKILL escalation, build-failure abort, port-in-use
  precheck, --no-build flag for fast iteration. Shell-tested.
- Bundle 1.58 MB gzipped on Node. 91/91 vitest + 4/4 shell tests.
2026-05-18 21:28:25 -04:00

383 lines
11 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { createProxy, type ProxyEvent } from '../src/core/proxy.js';
/** Tiny in-process mock upstream — accepts any request and returns whatever
* the test fixture configured. Lets us assert that the proxy correctly
* extracts Anthropic's usage block from both SSE and JSON responses without
* touching the network. */
function mockUpstream(handler: (req: Request) => Promise<Response> | Response) {
// Patch globalThis.fetch for the duration of the test.
const real = globalThis.fetch;
globalThis.fetch = ((req: Request | string | URL, init?: RequestInit) => {
const r = req instanceof Request ? req : new Request(String(req), init);
return Promise.resolve(handler(r));
}) as typeof fetch;
return () => {
globalThis.fetch = real;
};
}
const SAMPLE_REQ_BODY = JSON.stringify({
model: 'claude-3-5-haiku-latest',
messages: [{ role: 'user', content: 'hi' }],
system: 'short',
});
describe('proxy usage extraction', () => {
it('extracts usage tokens from a non-stream JSON response', async () => {
const restore = mockUpstream(
() =>
new Response(
JSON.stringify({
id: 'msg_1',
type: 'message',
role: 'assistant',
content: [{ type: 'text', text: 'hello' }],
usage: {
input_tokens: 123,
output_tokens: 7,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 100,
},
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
),
);
let captured: ProxyEvent | undefined;
const proxy = createProxy({
transform: {},
onRequest: (e) => {
captured = e;
},
});
const res = await proxy(
new Request('http://localhost/v1/messages', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: SAMPLE_REQ_BODY,
}),
);
// Drain the client-side body so the tee is forced to finish.
await res.text();
// Give the onRequest callback a tick to fire (it's behind a void promise).
await new Promise((r) => setTimeout(r, 20));
restore();
expect(captured).toBeDefined();
expect(captured!.usage?.input_tokens).toBe(123);
expect(captured!.usage?.output_tokens).toBe(7);
expect(captured!.usage?.cache_read_input_tokens).toBe(100);
expect(captured!.firstByteMs).toBeTypeOf('number');
});
it('extracts usage tokens from an SSE stream (message_start event)', async () => {
const sseBody =
'event: message_start\n' +
'data: ' +
JSON.stringify({
type: 'message_start',
message: {
id: 'msg_2',
type: 'message',
role: 'assistant',
content: [],
usage: {
input_tokens: 42,
output_tokens: 0,
cache_creation_input_tokens: 5000,
cache_read_input_tokens: 0,
},
},
}) +
'\n\n' +
'event: content_block_delta\n' +
'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"hi"}}\n\n' +
'event: message_stop\n' +
'data: {"type":"message_stop"}\n\n';
const restore = mockUpstream(
() =>
new Response(sseBody, {
status: 200,
headers: { 'content-type': 'text/event-stream' },
}),
);
let captured: ProxyEvent | undefined;
const proxy = createProxy({
transform: {},
onRequest: (e) => {
captured = e;
},
});
const res = await proxy(
new Request('http://localhost/v1/messages', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: SAMPLE_REQ_BODY,
}),
);
await res.text();
await new Promise((r) => setTimeout(r, 20));
restore();
expect(captured).toBeDefined();
expect(captured!.usage?.input_tokens).toBe(42);
expect(captured!.usage?.cache_creation_input_tokens).toBe(5000);
});
it('fires the event with undefined usage when the response is an error', async () => {
const restore = mockUpstream(
() =>
new Response(JSON.stringify({ error: { type: 'overloaded_error' } }), {
status: 529,
headers: { 'content-type': 'application/json' },
}),
);
let captured: ProxyEvent | undefined;
const proxy = createProxy({
transform: {},
onRequest: (e) => {
captured = e;
},
});
const res = await proxy(
new Request('http://localhost/v1/messages', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: SAMPLE_REQ_BODY,
}),
);
await res.text();
await new Promise((r) => setTimeout(r, 20));
restore();
expect(captured).toBeDefined();
expect(captured!.status).toBe(529);
expect(captured!.usage).toBeUndefined();
// 5xx: we synthesize our own message upstream, so no errorBody capture.
expect(captured!.errorBody).toBeUndefined();
});
it('captures upstream error body for 4xx responses (up to 2 KiB)', async () => {
const upstreamErr = {
type: 'error',
error: {
type: 'invalid_request_error',
message: 'messages.5.content.0.tool_use_id: unknown tool_use id',
},
};
const restore = mockUpstream(
() =>
new Response(JSON.stringify(upstreamErr), {
status: 400,
headers: { 'content-type': 'application/json' },
}),
);
let captured: ProxyEvent | undefined;
const proxy = createProxy({
transform: {},
onRequest: (e) => {
captured = e;
},
});
const res = await proxy(
new Request('http://localhost/v1/messages', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: SAMPLE_REQ_BODY,
}),
);
// Drain the client side so the tee can complete.
const clientBody = await res.text();
await new Promise((r) => setTimeout(r, 20));
restore();
expect(captured).toBeDefined();
expect(captured!.status).toBe(400);
expect(captured!.usage).toBeUndefined();
expect(captured!.errorBody).toBe(JSON.stringify(upstreamErr));
// Client must still receive the full body unchanged.
expect(clientBody).toBe(JSON.stringify(upstreamErr));
});
it('caps the captured 4xx error body at ~2 KiB', async () => {
const huge = 'x'.repeat(10_000);
const restore = mockUpstream(
() =>
new Response(huge, {
status: 400,
headers: { 'content-type': 'text/plain' },
}),
);
let captured: ProxyEvent | undefined;
const proxy = createProxy({
transform: {},
onRequest: (e) => {
captured = e;
},
});
const res = await proxy(
new Request('http://localhost/v1/messages', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: SAMPLE_REQ_BODY,
}),
);
await res.text();
await new Promise((r) => setTimeout(r, 20));
restore();
expect(captured).toBeDefined();
expect(captured!.errorBody).toBeDefined();
expect(captured!.errorBody!.length).toBe(2048);
});
/** Decompress a gzip Uint8Array back to bytes — mirror of proxy's gzipBytes. */
async function gunzipBytes(buf: Uint8Array): Promise<Uint8Array> {
const stream = new Response(buf as BufferSource).body!.pipeThrough(
new DecompressionStream('gzip'),
);
return new Uint8Array(await new Response(stream).arrayBuffer());
}
it('captures the FULL gzipped transformed body on 4xx + sets reqBodySha8', async () => {
// Pair with errorBody so a future debugger can reconstruct
// "we sent X, Anthropic said Y" from the JSONL alone. We gzip the body
// so even a 170 KiB transformed payload fits inline once base64'd
// (typical PNG-heavy bodies compress to <10% of source).
const restore = mockUpstream(
() =>
new Response(JSON.stringify({ error: { type: 'bad' } }), {
status: 400,
headers: { 'content-type': 'application/json' },
}),
);
let captured: ProxyEvent | undefined;
const proxy = createProxy({
transform: {},
onRequest: (e) => {
captured = e;
},
});
const res = await proxy(
new Request('http://localhost/v1/messages', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: SAMPLE_REQ_BODY,
}),
);
await res.text();
await new Promise((r) => setTimeout(r, 20));
restore();
expect(captured).toBeDefined();
expect(captured!.status).toBe(400);
// Hash lands on every event, not just 4xx.
expect(captured!.reqBodySha8).toMatch(/^[0-9a-f]{8}$/);
// Gzipped body is present, has the gzip magic header, and decompresses
// back to the transformed JSON we sent upstream.
expect(captured!.reqBodyGz).toBeDefined();
expect(captured!.reqBodyGz![0]).toBe(0x1f);
expect(captured!.reqBodyGz![1]).toBe(0x8b);
const decoded = new TextDecoder().decode(
await gunzipBytes(captured!.reqBodyGz!),
);
const parsed = JSON.parse(decoded);
expect(parsed.model).toBe('claude-3-5-haiku-latest');
expect(parsed.messages[0].role).toBe('user');
});
it('does NOT gzip the request body on 2xx (but still sets reqBodySha8)', async () => {
const restore = mockUpstream(
() =>
new Response(JSON.stringify({
id: 'x', type: 'message', role: 'assistant',
content: [{ type: 'text', text: 'ok' }],
model: 'x', stop_reason: 'end_turn',
usage: { input_tokens: 1, output_tokens: 1 },
}), {
status: 200,
headers: { 'content-type': 'application/json' },
}),
);
let captured: ProxyEvent | undefined;
const proxy = createProxy({
transform: {},
onRequest: (e) => {
captured = e;
},
});
const res = await proxy(
new Request('http://localhost/v1/messages', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: SAMPLE_REQ_BODY,
}),
);
await res.text();
await new Promise((r) => setTimeout(r, 20));
restore();
expect(captured).toBeDefined();
expect(captured!.status).toBe(200);
// Hash lands on every event.
expect(captured!.reqBodySha8).toMatch(/^[0-9a-f]{8}$/);
// But the gzipped body itself is only captured on 4xx.
expect(captured!.reqBodyGz).toBeUndefined();
});
it('reqBodySha8 is identical across two requests with the same body', async () => {
// Correlation use-case: spot "same payload sometimes works, sometimes
// fails" patterns in events.jsonl.
let restore = mockUpstream(
() =>
new Response('{"x":1}', {
status: 400,
headers: { 'content-type': 'application/json' },
}),
);
const captures: ProxyEvent[] = [];
const proxy = createProxy({
transform: {},
onRequest: (e) => {
captures.push(e);
},
});
for (let i = 0; i < 2; i++) {
const res = await proxy(
new Request('http://localhost/v1/messages', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: SAMPLE_REQ_BODY,
}),
);
await res.text();
}
await new Promise((r) => setTimeout(r, 20));
restore();
expect(captures.length).toBe(2);
expect(captures[0]!.reqBodySha8).toBeDefined();
expect(captures[0]!.reqBodySha8).toBe(captures[1]!.reqBodySha8);
});
});