import { describe, expect, it, vi } from 'vitest'; import request from 'supertest'; import { createProxyApp } from '../src/proxy.js'; const response = (body, options = {}) => new Response(body, { status: options.status ?? 200, headers: options.headers ?? { 'content-type': 'application/json' }, }); describe('proxy', () => { it('forwards models and rotates credentials', async () => { const fetchImpl = vi.fn() .mockResolvedValueOnce(response('{"object":"list","data":[]}')) .mockResolvedValueOnce(response('{"object":"list","data":[]}')); const app = createProxyApp({ keys: ['key-a', 'key-b'], fetchImpl, upstreamBaseUrl: 'https://upstream.test/v1' }); await request(app).get('/oai/v1/models').expect(200); await request(app).get('/oai/v1/models').expect(200); expect(fetchImpl.mock.calls[0][1].headers.authorization).toBe('Bearer key-a'); expect(fetchImpl.mock.calls[1][1].headers.authorization).toBe('Bearer key-b'); }); it('tries each key until one succeeds', async () => { const fetchImpl = vi.fn() .mockResolvedValueOnce(response('{"error":"bad key"}', { status: 401 })) .mockResolvedValueOnce(response('{"error":"rate limited"}', { status: 429 })) .mockResolvedValueOnce(response('{"object":"list","data":[]}')); const app = createProxyApp({ keys: ['key-a', 'key-b', 'key-c'], fetchImpl }); await request(app).get('/oai/v1/models').expect(200); expect(fetchImpl).toHaveBeenCalledTimes(3); expect(fetchImpl.mock.calls.map(([_, options]) => options.headers.authorization)).toEqual([ 'Bearer key-a', 'Bearer key-b', 'Bearer key-c', ]); }); it('returns the final failure after all keys are exhausted', async () => { const fetchImpl = vi.fn() .mockResolvedValueOnce(response('{"error":"bad key"}', { status: 401 })) .mockResolvedValueOnce(response('{"error":"still bad"}', { status: 401 })); const app = createProxyApp({ keys: ['key-a', 'key-b'], fetchImpl }); await request(app).get('/oai/v1/models').expect(401, '{"error":"still bad"}'); expect(fetchImpl).toHaveBeenCalledTimes(2); }); it('forwards non-streaming chat completions', async () => { const fetchImpl = vi.fn().mockResolvedValue(response('{"id":"completion-1"}')); const app = createProxyApp({ keys: ['key'], fetchImpl }); const body = { model: 'kimi-k3', messages: [{ role: 'user', content: 'Hi' }] }; const result = await request(app).post('/oai/v1/chat/completions').send(body).expect(200); expect(result.body).toEqual({ id: 'completion-1' }); expect(JSON.parse(fetchImpl.mock.calls[0][1].body)).toEqual(body); }); it('enforces the configured context and IP limits and records cost', async () => { const fetchImpl = vi.fn() .mockResolvedValueOnce(response(JSON.stringify({ id: 'c', cost: 0 }))) .mockResolvedValue(response(JSON.stringify({ id: 'c', cost: 10 }))); const app = createProxyApp({ keys: ['key'], fetchImpl, maxContextTokens: 100 }); await request(app).post('/oai/v1/chat/completions').set('X-Real-IP', '198.51.100.1').send({ model: 'm', max_tokens: 101 }).expect(400); await request(app).post('/oai/v1/chat/completions').set('X-Real-IP', '198.51.100.1').send({ model: 'm', max_tokens: 1 }).expect(200); await request(app).post('/oai/v1/chat/completions').set('X-Real-IP', '198.51.100.1').send({ model: 'm', max_tokens: 1 }).expect(200); await request(app).post('/oai/v1/chat/completions').set('X-Real-IP', '198.51.100.1').send({ model: 'm', max_tokens: 1 }).expect(429); }); it('adds the global context limit to model listings', async () => { const fetchImpl = vi.fn().mockResolvedValue(response('{"data":[{"id":"m"}]}')); const app = createProxyApp({ keys: ['key'], fetchImpl, maxContextTokens: 123 }); const result = await request(app).get('/oai/v1/models').expect(200); expect(result.body.data[0]).toMatchObject({ context_length: 123, context_window: 123, max_context_tokens: 123 }); }); it('passes through streaming responses', async () => { const fetchImpl = vi.fn().mockResolvedValue(response('data: {"delta":"Hi"}\n\ndata: [DONE]\n\n', { headers: { 'content-type': 'text/event-stream' }, })); const app = createProxyApp({ keys: ['key'], fetchImpl }); const result = await request(app).post('/oai/v1/chat/completions').send({ stream: true }).expect(200); expect(result.text).toContain('data: [DONE]'); expect(fetchImpl.mock.calls[0][1].headers.accept).toBe('text/event-stream'); }); it('collects the request, chat history, and complete streamed response', async () => { const stream = 'data: {"choices":[{"delta":{"reasoning_content":"Think. ","content":"Hi"}}]}\n\ndata: {"choices":[{"delta":{},"finish_reason":"stop"}]}\n\ndata: [DONE]\n\n'; const fetchImpl = vi.fn().mockResolvedValue(response(stream, { headers: { 'content-type': 'text/event-stream' }, })); const requestLogger = { log: vi.fn() }; const app = createProxyApp({ keys: ['key'], fetchImpl, requestLogger }); const body = { model: 'm', messages: [{ role: 'user', content: 'Hi' }], stream: true }; await request(app) .post('/oai/v1/chat/completions?source=test') .set('X-Real-IP', '198.51.100.10') .send(body) .expect(200); expect(requestLogger.log).toHaveBeenCalledTimes(1); expect(requestLogger.log.mock.calls[0][0]).toMatchObject({ method: 'POST', endpoint: '/oai/v1/chat/completions?source=test', requestBody: body, responseBody: stream, responseStatus: 200, model: 'm', clientIp: '198.51.100.10', stream: true, trainingData: { messages: [ { role: 'user', content: 'Hi' }, { role: 'assistant', content: 'Hi', reasoning_content: 'Think. ' }, ] }, }); expect(requestLogger.log.mock.calls[0][0].requestHeaders['content-type']).toMatch('application/json'); expect(requestLogger.log.mock.calls[0][0].responseHeaders['content-type']).toMatch('text/event-stream'); expect(requestLogger.log.mock.calls[0][0].durationMs).toBeGreaterThanOrEqual(0); expect(requestLogger.log.mock.calls[0][0].trainingData.metadata).toMatchObject({ schema_version: 1, api: 'openai_chat_completions', model: 'm', stream: true, tools: [], response_status: 200, response: { finish_reason: 'stop' }, }); }); it('collects error and not-found responses too', async () => { const requestLogger = { log: vi.fn() }; const app = createProxyApp({ keys: ['key'], requestLogger }); await request(app).get('/missing').expect(404); expect(requestLogger.log).toHaveBeenCalledOnce(); expect(requestLogger.log.mock.calls[0][0]).toMatchObject({ method: 'GET', endpoint: '/missing', requestBody: null, responseStatus: 404, }); expect(requestLogger.log.mock.calls[0][0].responseBody).toContain('Cannot GET /missing'); }); it('translates Responses requests and returns a Responses object', async () => { const fetchImpl = vi.fn().mockResolvedValue(response(JSON.stringify({ id: 'chatcmpl-response', model: 'm', choices: [{ message: { role: 'assistant', content: 'Hello from Responses' }, finish_reason: 'stop' }], usage: { prompt_tokens: 8, completion_tokens: 3 }, }))); const app = createProxyApp({ keys: ['key'], fetchImpl }); const result = await request(app).post('/oai/v1/responses').send({ model: 'm', instructions: 'Be brief', max_output_tokens: 20, input: [{ role: 'user', content: [{ type: 'input_text', text: 'Hello' }] }], }).expect(200); expect(result.body).toMatchObject({ object: 'response', status: 'completed', model: 'm' }); expect(result.body.output[0].content[0]).toMatchObject({ type: 'output_text', text: 'Hello from Responses' }); expect(result.body.usage).toEqual({ input_tokens: 8, output_tokens: 3, total_tokens: 11 }); expect(JSON.parse(fetchImpl.mock.calls[0][1].body)).toEqual({ model: 'm', max_tokens: 20, messages: [{ role: 'system', content: 'Be brief' }, { role: 'user', content: [{ type: 'text', text: 'Hello' }] }], }); }); it('translates Responses streaming events', async () => { const fetchImpl = vi.fn().mockResolvedValue(response([ 'data: {"id":"c1","model":"m","choices":[{"delta":{"role":"assistant","content":"Hi"}}]}', 'data: {"choices":[{"delta":{},"finish_reason":"stop"}]}', 'data: [DONE]', '', ].join('\n\n'), { headers: { 'content-type': 'text/event-stream' } })); const app = createProxyApp({ keys: ['key'], fetchImpl }); const result = await request(app).post('/oai/v1/responses').send({ model: 'm', input: 'Hi', stream: true }).expect(200); expect(result.text).toContain('event: response.created'); expect(result.text).toContain('event: response.output_text.delta'); expect(result.text).toContain('"delta":"Hi"'); expect(result.text).toContain('event: response.completed'); }); it('streams tool calls as completed Responses function_call items', async () => { const fetchImpl = vi.fn().mockResolvedValue(response([ 'data: {"id":"c-tool","model":"m","choices":[{"delta":{"tool_calls":[{"index":0,"id":"call-9","function":{"name":"read_file","arguments":"{\\"path\\":"}}]}}]}', 'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\"main.py\\"}"}}]}}]}', 'data: {"choices":[{"delta":{},"finish_reason":"tool_calls"}]}', 'data: [DONE]', '', ].join('\n\n'), { headers: { 'content-type': 'text/event-stream' } })); const app = createProxyApp({ keys: ['key'], fetchImpl }); const result = await request(app).post('/oai/v1/responses').send({ model: 'm', input: 'explore', stream: true }).expect(200); expect(result.text).toContain('event: response.output_item.added'); expect(result.text).toContain('"name":"read_file"'); expect(result.text).toContain('event: response.function_call_arguments.delta'); expect(result.text).toContain('event: response.function_call_arguments.done'); expect(result.text).toContain('"arguments":"{\\"path\\":\\"main.py\\"}"'); expect(result.text).toContain('event: response.output_item.done'); const completed = result.text.split('\n\n').find((line) => line.startsWith('event: response.completed')); expect(completed).toContain('"type":"function_call"'); expect(completed).toContain('"call_id":"call-9"'); }); it('streams reasoning content as Responses reasoning events', async () => { const fetchImpl = vi.fn().mockResolvedValue(response([ 'data: {"id":"c-reason","model":"m","choices":[{"delta":{"reasoning_content":"think first "}}]}', 'data: {"choices":[{"delta":{"reasoning_content":"then answer"}}]}', 'data: {"choices":[{"delta":{"content":"done"},"finish_reason":"stop"}]}', 'data: [DONE]', '', ].join('\n\n'), { headers: { 'content-type': 'text/event-stream' } })); const app = createProxyApp({ keys: ['key'], fetchImpl }); const result = await request(app).post('/oai/v1/responses').send({ model: 'm', input: 'Hi', stream: true }).expect(200); expect(result.text).toContain('event: response.reasoning_summary_text.delta'); expect(result.text).toContain('"delta":"think first "'); expect(result.text).toContain('"delta":"then answer"'); expect(result.text).toContain('event: response.reasoning_summary_text.done'); expect(result.text).toContain('"output_index":1'); }); it('accepts nested Chat Completions function tools from Codex', async () => { const fetchImpl = vi.fn().mockResolvedValue(response(JSON.stringify({ id: 'chatcmpl-tool', choices: [{ message: { role: 'assistant', content: 'done' }, finish_reason: 'stop' }], usage: {}, }))); const app = createProxyApp({ keys: ['key'], fetchImpl }); await request(app).post('/oai/v1/responses').send({ model: 'm', input: 'lookup', tools: [{ type: 'function', function: { name: 'lookup', parameters: { type: 'object' } } }], }).expect(200); expect(JSON.parse(fetchImpl.mock.calls[0][1].body).tools).toEqual([{ type: 'function', function: { name: 'lookup', parameters: { type: 'object' } }, }]); }); it('propagates upstream errors', async () => { const fetchImpl = vi.fn().mockResolvedValue(response('{"error":"bad key"}', { status: 401 })); const app = createProxyApp({ keys: ['key'], fetchImpl }); await request(app).get('/oai/v1/models').expect(401, '{"error":"bad key"}'); }); it('translates Anthropic messages to chat completions and back', async () => { const fetchImpl = vi.fn().mockResolvedValue(response(JSON.stringify({ id: 'chatcmpl-1', model: 'kimi-k3', choices: [{ message: { role: 'assistant', content: 'Hello back' }, finish_reason: 'stop' }], usage: { prompt_tokens: 12, completion_tokens: 4 }, }))); const app = createProxyApp({ keys: ['key'], fetchImpl }); const body = { model: 'kimi-k3', max_tokens: 100, system: 'Be concise', messages: [{ role: 'user', content: [{ type: 'text', text: 'Hello' }] }], }; const result = await request(app).post('/ant/v1/messages').send(body).expect(200); expect(result.body).toMatchObject({ type: 'message', role: 'assistant', model: 'kimi-k3', stop_reason: 'end_turn' }); expect(result.body.content).toEqual([{ type: 'text', text: 'Hello back' }]); expect(result.body.usage).toEqual({ input_tokens: 12, output_tokens: 4 }); expect(JSON.parse(fetchImpl.mock.calls[0][1].body)).toEqual({ model: 'kimi-k3', max_tokens: 100, messages: [{ role: 'system', content: 'Be concise' }, { role: 'user', content: [{ type: 'text', text: 'Hello' }] }], }); }); it('translates Anthropic tool requests', async () => { const fetchImpl = vi.fn().mockResolvedValue(response(JSON.stringify({ id: 'chatcmpl-2', choices: [{ message: { role: 'assistant', content: null, tool_calls: [{ id: 'call-1', function: { name: 'lookup', arguments: '{"q":"x"}' } }] }, finish_reason: 'tool_calls' }], usage: {}, }))); const app = createProxyApp({ keys: ['key'], fetchImpl }); const body = { model: 'm', max_tokens: 20, messages: [{ role: 'user', content: 'Find x' }], tools: [{ name: 'lookup', description: 'Look up', input_schema: { type: 'object' } }], tool_choice: { type: 'any' } }; const result = await request(app).post('/ant/v1/messages').send(body).expect(200); const upstream = JSON.parse(fetchImpl.mock.calls[0][1].body); expect(upstream.tools[0].function.name).toBe('lookup'); expect(upstream.tool_choice).toBe('required'); expect(result.body.content).toEqual([{ type: 'tool_use', id: 'call-1', name: 'lookup', input: { q: 'x' } }]); expect(result.body.stop_reason).toBe('tool_use'); }); it('translates Anthropic streaming events', async () => { const fetchImpl = vi.fn().mockResolvedValue(response('data: {"id":"c1","model":"m","choices":[{"delta":{"role":"assistant","content":"Hi"}}]}\n\ndata: {"choices":[{"delta":{},"finish_reason":"stop"}]}\n\ndata: [DONE]\n\n', { headers: { 'content-type': 'text/event-stream' } })); const app = createProxyApp({ keys: ['key'], fetchImpl }); const result = await request(app).post('/ant/v1/messages').send({ model: 'm', max_tokens: 10, stream: true, messages: [{ role: 'user', content: 'Hi' }] }).expect(200); expect(result.text).toContain('event: message_start'); expect(result.text).toContain('event: content_block_delta'); expect(result.text).toContain('"text":"Hi"'); expect(result.text).toContain('event: message_stop'); }); it('translates OpenAI reasoning_content into Anthropic thinking events', async () => { const fetchImpl = vi.fn().mockResolvedValue(response([ 'data: {"id":"c2","model":"m","choices":[{"delta":{"role":"assistant","reasoning_content":"thinking "}}]}', 'data: {"choices":[{"delta":{"reasoning_content":"more"}}]}', 'data: {"choices":[{"delta":{"content":"answer"},"finish_reason":"stop"}]}', 'data: [DONE]', '', ].join('\n\n'), { headers: { 'content-type': 'text/event-stream' } })); const app = createProxyApp({ keys: ['key'], fetchImpl }); const result = await request(app).post('/ant/v1/messages').send({ model: 'm', max_tokens: 100, stream: true, messages: [{ role: 'user', content: 'Hi' }] }).expect(200); expect(result.text).toContain('"type":"thinking"'); expect(result.text).toContain('"type":"thinking_delta","thinking":"thinking "'); expect(result.text).toContain('"index":1'); expect(result.text).toContain('"type":"text_delta","text":"answer"'); }); it('returns Anthropic model listings and validates requests', async () => { const fetchImpl = vi.fn().mockResolvedValue(response(JSON.stringify({ data: [{ id: 'm1', name: 'Model One', created: 1700000000 }] }))); const app = createProxyApp({ keys: ['key'], fetchImpl }); const models = await request(app).get('/ant/v1/models').expect(200); expect(models.body.data[0]).toMatchObject({ id: 'm1', display_name: 'Model One', type: 'model' }); await request(app).post('/ant/v1/messages').send({ model: 'm', messages: [] }).expect(400, { type: 'error', error: { type: 'invalid_request_error', message: 'max_tokens is required' }, }); }); it('supports Anthropic SDKs that append /v1 to the configured base URL', async () => { const fetchImpl = vi.fn().mockResolvedValue(response(JSON.stringify({ id: 'chatcmpl-sdk', model: 'm', choices: [{ message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }], usage: {}, }))); const app = createProxyApp({ keys: ['key'], fetchImpl }); await request(app).post('/ant/v1/v1/messages').send({ model: 'm', max_tokens: 100, messages: [{ role: 'user', content: 'hi' }] }).expect(200); }); it('normalizes system, developer, and tool message roles', async () => { const fetchImpl = vi.fn().mockResolvedValue(response(JSON.stringify({ id: 'c', choices: [{ message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }], usage: {} }))); const app = createProxyApp({ keys: ['key'], fetchImpl }); await request(app).post('/ant/v1/messages').send({ model: 'm', max_tokens: 100, messages: [ { role: 'system', content: 'system text' }, { role: 'developer', content: 'developer text' }, { role: 'tool', tool_call_id: 'call-1', content: 'tool result' }, { role: 'user', content: 'hi' }, ], }).expect(200); expect(JSON.parse(fetchImpl.mock.calls[0][1].body).messages.map(({ role }) => role)).toEqual(['system', 'system', 'tool', 'user']); }); it('accepts thinking blocks from prior assistant turns', async () => { const fetchImpl = vi.fn().mockResolvedValue(response(JSON.stringify({ id: 'c3', choices: [{ message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }], usage: {} }))); const app = createProxyApp({ keys: ['key'], fetchImpl }); await request(app).post('/ant/v1/messages').send({ model: 'm', max_tokens: 100, messages: [ { role: 'assistant', content: [{ type: 'thinking', thinking: 'prior reasoning', signature: 'opaque' }, { type: 'text', text: 'prior answer' }] }, { role: 'user', content: 'continue' }, ], }).expect(200); expect(JSON.parse(fetchImpl.mock.calls[0][1].body).messages[0]).toEqual({ role: 'assistant', content: [{ type: 'text', text: 'prior answer' }], reasoning_content: 'prior reasoning', }); }); });