import { describe, expect, it } from 'bun:test'; import { ProxyRequestTransformer } from '../../../../src/proxy/transformers/request-transformer'; describe('ProxyRequestTransformer regressions', () => { it('drops assistant messages that only contain stripped thinking blocks', () => { const result = new ProxyRequestTransformer().transform({ messages: [ { role: 'assistant', content: [ { type: 'thinking', text: 'internal' }, { type: 'redacted_thinking', text: 'hidden' }, ], }, ], }); expect(result.messages).toEqual([]); }); it('maps adaptive thinking through output_config effort for OpenAI-compatible upstreams', () => { const result = new ProxyRequestTransformer().transform({ messages: [{ role: 'user', content: 'hello' }], thinking: { type: 'adaptive' }, output_config: { effort: 'max' }, }); expect(result.reasoning_effort).toBe('high'); expect(result.reasoning).toBeUndefined(); }); it('explicitly normalizes anthropic xhigh adaptive effort for OpenAI-compatible upstreams', () => { const result = new ProxyRequestTransformer().transform({ messages: [{ role: 'user', content: 'hello' }], thinking: { type: 'adaptive' }, output_config: { effort: 'xhigh' }, }); expect(result.reasoning_effort).toBe('high'); expect(result.reasoning).toBeUndefined(); }); it('rejects unsupported thinking types instead of silently dropping them', () => { expect(() => new ProxyRequestTransformer().transform({ messages: [{ role: 'user', content: 'hello' }], thinking: { type: 'typo' }, }) ).toThrow('thinking.type must be "enabled", "adaptive", or "disabled"'); }); it('keeps Anthropic role validation for tool_use, image, and tool_result blocks', () => { expect(() => new ProxyRequestTransformer().transform({ messages: [{ role: 'user', content: [{ type: 'tool_use', name: 'search', input: {} }] }], }) ).toThrow('tool_use requires assistant role'); expect(() => new ProxyRequestTransformer().transform({ messages: [ { role: 'assistant', content: [ { type: 'image', source: { type: 'url', url: 'https://example.com/image.png' }, }, ], }, ], }) ).toThrow('image requires user role'); expect(() => new ProxyRequestTransformer().transform({ messages: [ { role: 'assistant', content: [{ type: 'tool_result', tool_use_id: 'toolu_1', content: 'nope' }], }, ], }) ).toThrow('tool_result requires user role'); }); it('rejects orphaned, incomplete, or mixed-order tool_result blocks', () => { expect(() => new ProxyRequestTransformer().transform({ messages: [ { role: 'user', content: [{ type: 'tool_result', tool_use_id: 'toolu_1', content: 'orphan' }], }, ], }) ).toThrow('tool_result requires a preceding assistant tool_use'); expect(() => new ProxyRequestTransformer().transform({ messages: [ { role: 'assistant', content: [ { type: 'tool_use', id: 'toolu_1', name: 'search', input: { q: 'docs' } }, { type: 'tool_use', id: 'toolu_2', name: 'open', input: { url: 'https://example.com' } }, ], }, { role: 'user', content: [{ type: 'tool_result', tool_use_id: 'toolu_1', content: 'partial' }], }, ], }) ).toThrow('must provide tool_result blocks for all pending tool_use ids'); expect(() => new ProxyRequestTransformer().transform({ messages: [ { role: 'assistant', content: [{ type: 'tool_use', id: 'toolu_1', name: 'vision', input: { detail: 'high' } }], }, { role: 'user', content: [ { type: 'text', text: 'Here you go' }, { type: 'tool_result', tool_use_id: 'toolu_1', content: 'result' }, ], }, ], }) ).toThrow('text is not allowed before tool_result blocks for pending tool_use ids'); expect(() => new ProxyRequestTransformer().transform({ messages: [ { role: 'assistant', content: [{ type: 'tool_use', id: 'toolu_1', name: 'vision', input: { detail: 'high' } }], }, { role: 'user', content: [ { type: 'tool_result', tool_use_id: 'toolu_1', content: 'result' }, { type: 'text', text: 'follow-up' }, ], }, ], }) ).not.toThrow(); expect(() => new ProxyRequestTransformer().transform({ messages: [ { role: 'assistant', content: [ { type: 'tool_use', id: 'toolu_1', name: 'search', input: { q: 'docs' } }, { type: 'tool_use', id: 'toolu_2', name: 'open', input: { url: 'https://example.com' } }, ], }, { role: 'user', content: [ { type: 'tool_result', tool_use_id: 'toolu_1', content: 'partial' }, { type: 'text', text: 'follow-up' }, { type: 'tool_result', tool_use_id: 'toolu_2', content: 'done' }, ], }, ], }) ).toThrow('text is not allowed between tool_result blocks for pending tool_use ids'); expect(() => new ProxyRequestTransformer().transform({ messages: [ { role: 'assistant', content: [{ type: 'tool_use', id: 'toolu_1', name: 'vision', input: { detail: 'high' } }], }, { role: 'user', content: 'plain follow-up', }, ], }) ).toThrow('must start with tool_result blocks for pending tool_use ids'); }); it('converts tool_result image blocks to text placeholders for OpenAI-compatible tool messages', () => { const result = new ProxyRequestTransformer().transform({ messages: [ { role: 'assistant', content: [{ type: 'tool_use', id: 'toolu_1', name: 'vision', input: { detail: 'high' } }], }, { role: 'user', content: [ { type: 'tool_result', tool_use_id: 'toolu_1', content: [ { type: 'text', text: 'screenshot captured' }, { type: 'image', source: { type: 'url', url: 'https://storage.example.com/error.png?X-Amz-Signature=secret' }, }, { type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'ZmFrZQ==' }, }, ], }, ], }, ], }); expect(result.messages[1]).toEqual({ role: 'tool', tool_call_id: 'toolu_1', content: 'screenshot captured\n[tool_result image omitted: url image payload]\n[tool_result image omitted: image/png base64 payload]', }); expect(result.messages[1]?.content).not.toContain('https://storage.example.com'); expect(result.messages[1]?.content).not.toContain('X-Amz-Signature'); expect(result.messages[1]?.content).not.toContain('secret'); }); it('rejects unsupported assistant blocks instead of silently dropping them', () => { expect(() => new ProxyRequestTransformer().transform({ messages: [ { role: 'assistant', content: [{ type: 'server_tool_use', id: 'srv_1' }], }, ], }) ).toThrow('type "server_tool_use" is not supported'); }); it('translates url images and tool_choice while coalescing repeated turns', () => { const result = new ProxyRequestTransformer().transform({ tool_choice: { type: 'tool', name: 'vision', disable_parallel_tool_use: true, }, tools: [{ name: 'vision', description: 'Inspect image', input_schema: { type: 'object' } }], messages: [ { role: 'user', content: [{ type: 'image', source: { type: 'url', url: 'https://example.com/cat.png' } }], }, { role: 'user', content: [{ type: 'text', text: 'Describe it' }] }, { role: 'assistant', content: [{ type: 'text', text: 'Checking' }], }, { role: 'assistant', content: [{ type: 'tool_use', id: 'toolu_1', name: 'vision', input: { detail: 'high' } }], }, { role: 'user', content: [ { type: 'tool_result', tool_use_id: 'toolu_1', is_error: true, content: [{ type: 'text', text: 'fetch failed' }], }, ], }, ], }); expect(result.tool_choice).toEqual({ type: 'function', function: { name: 'vision' }, }); expect(result.parallel_tool_calls).toBe(false); expect(result.messages[0]).toEqual({ role: 'user', content: [ { type: 'image_url', image_url: { url: 'https://example.com/cat.png' } }, { type: 'text', text: 'Describe it' }, ], }); expect(result.messages[1]).toEqual({ role: 'assistant', content: 'Checking', tool_calls: [ { id: 'toolu_1', type: 'function', function: { name: 'vision', arguments: '{"detail":"high"}', }, }, ], }); expect(result.messages[2]).toEqual({ role: 'tool', tool_call_id: 'toolu_1', content: 'Error: fetch failed', }); }); it('defaults tools to auto tool_choice when none is specified', () => { const result = new ProxyRequestTransformer().transform({ messages: [{ role: 'user', content: 'hello' }], tools: [{ name: 'search', description: 'Search docs', input_schema: { type: 'object' } }], }); expect(result.tool_choice).toBe('auto'); }); });