import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { buildCountTokensBodies, getAllowedModelBases, isPxpipeSupportedGptModel, isPxpipeSupportedModel, setAllowedModelBases, shouldTransformAnthropicMessages, transformAnthropicMessages, transformOpenAIChatCompletions, } from '../src/core/index.js'; const enc = new TextEncoder(); const dec = new TextDecoder(); // Tests below assert DEFAULT model-scope behavior, which assumes PXPIPE_MODELS is unset. // Snapshot and clear any ambient value (e.g. a dev shell that still exports PXPIPE_MODELS) // before each test so the suite is deterministic regardless of the environment it runs in, // then restore the original afterward. The per-test override cases still work: they see an // unset var, set their own value, and clean up. let ambientPxpipeModels: string | undefined; beforeEach(() => { ambientPxpipeModels = process.env.PXPIPE_MODELS; delete process.env.PXPIPE_MODELS; }); afterEach(() => { if (ambientPxpipeModels === undefined) delete process.env.PXPIPE_MODELS; else process.env.PXPIPE_MODELS = ambientPxpipeModels; }); describe('public library API', () => { it('recognizes Fable 5 (with suffix aliases) as the default scope; Opus is OFF by default', () => { expect(isPxpipeSupportedModel('claude-fable-5')).toBe(true); expect(isPxpipeSupportedModel('claude-fable-5-high')).toBe(true); expect(isPxpipeSupportedModel('google/gemini-3.6-flash')).toBe(true); expect(isPxpipeSupportedModel('untrusted/google/gemini-3.6-flash')).toBe(false); // Opus 4.8 is OPT-IN, not in the default scope — same pipeline/render as // Fable, but it reads imaged content at a tax (FINDINGS.md 2026-06-16), so // the default doesn't silently compress the operator's main driver. Enable // it via PXPIPE_MODELS or the dashboard "compress models" chips. expect(isPxpipeSupportedModel('claude-opus-4-8')).toBe(false); // older Opus + other families are not in the default scope expect(isPxpipeSupportedModel('claude-opus-4-7')).toBe(false); expect(isPxpipeSupportedModel('claude-opus-4-6')).toBe(false); expect(isPxpipeSupportedModel('claude-mythos-5')).toBe(false); expect(isPxpipeSupportedModel('claude-fable-50')).toBe(false); expect(isPxpipeSupportedModel('claude-sonnet-4-7')).toBe(false); expect(isPxpipeSupportedModel(null)).toBe(false); }); it('strips bracketed variant tags like [1m] before matching', () => { expect(isPxpipeSupportedModel('claude-fable-5[1m]')).toBe(true); expect(isPxpipeSupportedModel('claude-fable-5-high[1m]')).toBe(true); expect(isPxpipeSupportedModel('claude-opus-4-8[1m]')).toBe(false); // Opus opt-in, off by default // a non-scoped base is still rejected even with a variant tag expect(isPxpipeSupportedModel('claude-opus-4-7[1m]')).toBe(false); }); it('honors PXPIPE_MODELS to override the default scope', () => { const prev = process.env.PXPIPE_MODELS; try { // narrow to Fable only process.env.PXPIPE_MODELS = 'claude-fable-5'; expect(isPxpipeSupportedModel('claude-fable-5')).toBe(true); expect(isPxpipeSupportedModel('claude-opus-4-8')).toBe(false); // re-point to a different set process.env.PXPIPE_MODELS = 'claude-fable-5,claude-opus-4-7'; expect(isPxpipeSupportedModel('claude-opus-4-7')).toBe(true); expect(isPxpipeSupportedModel('claude-opus-4-8')).toBe(false); // not in this set } finally { if (prev === undefined) delete process.env.PXPIPE_MODELS; else process.env.PXPIPE_MODELS = prev; } }); it('honors the dashboard runtime override (setAllowedModelBases) over env/default', () => { try { // override takes precedence over the env/default scope setAllowedModelBases(['claude-fable-5', 'claude-opus-4-8']); expect(getAllowedModelBases()).toEqual(['claude-fable-5', 'claude-opus-4-8']); expect(isPxpipeSupportedModel('claude-opus-4-8')).toBe(true); // opted in at runtime // empty list = compress nothing setAllowedModelBases([]); expect(isPxpipeSupportedModel('claude-fable-5')).toBe(false); // null clears the override → back to the Fable-only default setAllowedModelBases(null); expect(isPxpipeSupportedModel('claude-fable-5')).toBe(true); expect(isPxpipeSupportedGptModel('gpt-5.6-sol')).toBe(false); expect(isPxpipeSupportedGptModel('grok-4.5')).toBe(false); expect(isPxpipeSupportedModel('claude-opus-4-8')).toBe(false); } finally { setAllowedModelBases(null); // never leak the override into other tests } }); it('keeps GPT 5.6 Sol aliases opt-in by default', () => { expect(isPxpipeSupportedGptModel('gpt-5')).toBe(false); expect(isPxpipeSupportedGptModel('gpt-5.5')).toBe(false); expect(isPxpipeSupportedGptModel('gpt-5.5-codex')).toBe(false); expect(isPxpipeSupportedGptModel('gpt-5.6')).toBe(false); expect(isPxpipeSupportedGptModel('gpt-5.6-sol')).toBe(false); expect(isPxpipeSupportedGptModel('gpt-5.6-sol-codex')).toBe(false); expect(isPxpipeSupportedGptModel('gpt-5.6-terra')).toBe(false); expect(isPxpipeSupportedGptModel('gpt-5-mini')).toBe(false); expect(isPxpipeSupportedGptModel('gpt-4o')).toBe(false); process.env.PXPIPE_MODELS = 'gpt-5.6-sol'; expect(isPxpipeSupportedGptModel('gpt-5.6-sol')).toBe(true); expect(isPxpipeSupportedGptModel('gpt-5.6-sol-codex')).toBe(true); expect(isPxpipeSupportedGptModel('gpt-5.6-sol[1m]')).toBe(true); expect(isPxpipeSupportedGptModel('gpt-5.6-sol-codex[1m]')).toBe(true); expect(isPxpipeSupportedGptModel('gpt-5.6')).toBe(false); expect(isPxpipeSupportedGptModel('gpt-5.6-terra')).toBe(false); }); it('keeps Grok and Sol opt-in by default', () => { // Grok remains opt-in because its arithmetic, gist, and state results are // below the Fable bar. const prev = process.env.PXPIPE_MODELS; try { delete process.env.PXPIPE_MODELS; expect(isPxpipeSupportedGptModel('grok-4.5')).toBe(false); expect(isPxpipeSupportedGptModel('grok-4')).toBe(false); expect(isPxpipeSupportedGptModel('grok-4.20')).toBe(false); expect(getAllowedModelBases()).not.toContain('grok-4.5'); expect(getAllowedModelBases()).toEqual(['claude-fable-5', 'gemini-3.6-flash']); process.env.PXPIPE_MODELS = 'claude-fable-5,gpt-5.6-sol,grok-4.5'; expect(isPxpipeSupportedGptModel('grok-4.5')).toBe(true); expect(isPxpipeSupportedGptModel('grok-4.5-fast')).toBe(true); // -suffix alias expect(isPxpipeSupportedGptModel('gpt-5.6-sol')).toBe(true); } finally { if (prev === undefined) delete process.env.PXPIPE_MODELS; else process.env.PXPIPE_MODELS = prev; } }); it('honors the single PXPIPE_MODELS scope for GPT families', () => { const prev = process.env.PXPIPE_MODELS; try { // Explicit Claude-only scope disables GPT imaging. process.env.PXPIPE_MODELS = 'claude-fable-5'; expect(isPxpipeSupportedGptModel('gpt-5.5')).toBe(false); expect(isPxpipeSupportedGptModel('gpt-5.6-sol')).toBe(false); // Mixed CSV selects exactly those bases across families. process.env.PXPIPE_MODELS = 'claude-fable-5,gpt-5.6-sol'; expect(isPxpipeSupportedGptModel('gpt-5.5')).toBe(false); expect(isPxpipeSupportedGptModel('gpt-5.6-sol')).toBe(true); expect(isPxpipeSupportedModel('claude-fable-5')).toBe(true); // `off` disables everything. process.env.PXPIPE_MODELS = 'off'; expect(isPxpipeSupportedGptModel('gpt-5.6-sol')).toBe(false); expect(isPxpipeSupportedModel('claude-fable-5')).toBe(false); } finally { if (prev === undefined) delete process.env.PXPIPE_MODELS; else process.env.PXPIPE_MODELS = prev; } }); it('reports applicability with route/method/body gates', () => { expect(shouldTransformAnthropicMessages({ model: 'claude-fable-5', method: 'POST', path: '/v1/messages', bodyBytes: 10, })).toEqual({ eligible: true, reason: 'eligible' }); expect(shouldTransformAnthropicMessages({ model: 'claude-fable-5', method: 'GET', path: '/v1/messages', bodyBytes: 10, }).reason).toBe('unsupported_method'); // Provider-prefixed routes createProxy() also transforms must be eligible // here too — the old endsWith('/v1/messages') check rejected /anthropic/messages. for (const path of ['/anthropic/v1/messages', '/anthropic/messages']) { expect(shouldTransformAnthropicMessages({ model: 'claude-fable-5', method: 'POST', path, bodyBytes: 10, })).toEqual({ eligible: true, reason: 'eligible' }); } expect(shouldTransformAnthropicMessages({ model: 'claude-fable-5', method: 'POST', path: '/v1/messages/count_tokens', bodyBytes: 10, }).reason).toBe('unsupported_path'); }); it('builds count_tokens probe bodies from a messages body', () => { const body = enc.encode(JSON.stringify({ model: 'claude-opus-4-7', max_tokens: 1024, stream: true, system: [{ type: 'text', text: 'sys' }], tools: [{ name: 't', description: 'd', input_schema: { type: 'object' } }], messages: [ { role: 'user', content: 'hello' }, { role: 'assistant', content: [ { type: 'text', text: 'cached', cache_control: { type: 'ephemeral', ttl: '1h' } }, { type: 'text', text: 'tail' }, ], }, ], })); const probes = buildCountTokensBodies(body); expect(probes.fullBody).toBeInstanceOf(Uint8Array); const full = JSON.parse(dec.decode(probes.fullBody!)) as Record; expect(full.model).toBe('claude-opus-4-7'); expect(full.max_tokens).toBeUndefined(); expect(full.stream).toBeUndefined(); expect(Array.isArray(full.messages)).toBe(true); expect(probes.cacheablePrefixBody).toBeInstanceOf(Uint8Array); const prefix = JSON.parse(dec.decode(probes.cacheablePrefixBody!)) as { messages: Array<{ content: unknown }> }; const last = prefix.messages.at(-1)!; expect(Array.isArray(last.content)).toBe(true); expect((last.content as unknown[])).toHaveLength(1); }); it('cacheable-prefix probe body pairs orphan tool_use blocks with synthetic tool_result', () => { const body = enc.encode(JSON.stringify({ model: 'claude-opus-4-7', messages: [ { role: 'user', content: 'hi' }, { role: 'assistant', content: [ { type: 'text', text: 'thinking' }, { type: 'tool_use', id: 'toolu_orphan_a', name: 'read', input: {} }, ], }, { role: 'user', content: [ { type: 'tool_result', tool_use_id: 'toolu_orphan_a', content: 'result' }, { type: 'text', text: 'next turn please', cache_control: { type: 'ephemeral' } }, ], }, { role: 'assistant', content: [ { type: 'tool_use', id: 'toolu_orphan_b', name: 'read', input: {} }, ], }, // tool_result for toolu_orphan_b would be in the dropped tail ], })); const probes = buildCountTokensBodies(body); expect(probes.cacheablePrefixBody).toBeInstanceOf(Uint8Array); const prefix = JSON.parse(dec.decode(probes.cacheablePrefixBody!)) as { messages: Array<{ role: string; content: unknown }>; }; // Truncation kept up to and including the cache_control-bearing block, // which sits in messages[2]. The cached-prefix should NOT include msg[3] // (the orphan tool_use), but if it did, the synthetic tool_result must // pair it. Either way: no orphan tool_use ids may remain unpaired. const allBlocks = prefix.messages.flatMap((m) => Array.isArray(m.content) ? (m.content as Array<{ type?: string }>) : [], ); const orphanUses = allBlocks .filter((b) => b.type === 'tool_use') .map((b) => (b as { id?: string }).id); const results = new Set( allBlocks .filter((b) => b.type === 'tool_result') .map((b) => (b as { tool_use_id?: string }).tool_use_id), ); for (const id of orphanUses) { expect(results.has(id)).toBe(true); } }); it('wraps the transformer with model gating and cache ownership metadata', async () => { const unsupported = enc.encode(JSON.stringify({ model: 'claude-sonnet-4-6', system: 'x'.repeat(20_000), messages: [{ role: 'user', content: 'hello' }], })); const skipped = await transformAnthropicMessages({ body: unsupported, model: 'claude-sonnet-4-6' }); expect(skipped.applied).toBe(false); expect(skipped.reason).toBe('unsupported_model'); expect(skipped.body).toBe(unsupported); const supported = enc.encode(JSON.stringify({ model: 'claude-fable-5', system: 'Important system instruction. '.repeat(1200), tools: [{ name: 'read_file', description: 'Read a file from disk. '.repeat(200), input_schema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] }, }], messages: [{ role: 'user', content: 'hello' }], })); const transformed = await transformAnthropicMessages({ body: supported, model: 'claude-fable-5' }); expect(transformed.applied).toBe(true); expect(transformed.reason).toBe('applied'); expect(transformed.info.compressedChars).toBeGreaterThan(0); expect(transformed.info.imageCount).toBeGreaterThan(0); // Task #21: pxpipe never adds its own cache_control markers. // The caller sent zero markers, so the rewritten body also has zero. expect(transformed.cache.ownsCacheControl).toBe(false); expect(transformed.cache.markerCount).toBe(0); }); it('preserves the exact Claude Code OAuth identity as the first system block', async () => { const identity = "You are Claude Code, Anthropic's official CLI for Claude."; const body = enc.encode(JSON.stringify({ model: 'claude-fable-5', system: [ { type: 'text', text: identity }, { type: 'text', text: 'Detailed Claude Code operating instructions. '.repeat(1200), cache_control: { type: 'ephemeral' }, }, ], tools: [{ name: 'read_file', description: 'Read a file from disk. '.repeat(200), input_schema: { type: 'object', properties: { path: { type: 'string' } } }, }], messages: [{ role: 'user', content: 'hello' }], })); const transformed = await transformAnthropicMessages({ body, model: 'claude-fable-5' }); expect(transformed.applied).toBe(true); expect(transformed.info.imageCount).toBeGreaterThan(0); const out = JSON.parse(dec.decode(transformed.body)) as { system?: Array<{ type: string; text?: string; cache_control?: unknown }>; }; expect(out.system?.[0]).toEqual({ type: 'text', text: identity }); expect(out.system?.[0]?.cache_control).toBeUndefined(); expect(transformed.info.imageSourceText).not.toContain(identity); }); it('transforms GPT 5.5 chat completions using OpenAI image_url blocks', async () => { const body = enc.encode(JSON.stringify({ model: 'gpt-5.5', messages: [ { role: 'system', content: 'System instruction. '.repeat(700) }, { role: 'developer', content: 'Developer instruction. '.repeat(400) }, { role: 'user', content: 'hello' }, ], tools: [{ type: 'function', function: { name: 'read_file', description: 'Read a file from disk. '.repeat(100), parameters: { type: 'object', description: 'Long root description.', properties: { path: { type: 'string', description: 'Path to read.' }, }, required: ['path'], }, }, }], })); const transformed = await transformOpenAIChatCompletions(body, { charsPerToken: 1, minCompressChars: 1, }); expect(transformed.info.compressed).toBe(true); expect(transformed.info.imageCount).toBeGreaterThan(0); const out = JSON.parse(dec.decode(transformed.body)) as any; const firstUser = out.messages.find((m: any) => m.role === 'user'); expect(Array.isArray(firstUser.content)).toBe(true); expect(firstUser.content[0].type).toBe('image_url'); expect(firstUser.content[0].image_url.url).toMatch(/^data:image\/png;base64,/); expect(out.messages[0].content).toContain('rendered into image'); expect(out.tools[0].function.description).toBe('Read a file from disk. '.repeat(100)); expect(out.tools[0].function.parameters.description).toBeUndefined(); expect(out.tools[0].function.parameters.properties.path.description).toBeUndefined(); expect(JSON.stringify(out)).not.toContain('cache_control'); }); });