import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; import * as fs from 'fs'; import * as http from 'http'; import * as os from 'os'; import * as path from 'path'; import { startOpenAICompatProxyServer } from '../../../src/proxy/server/proxy-server'; import type { OpenAICompatProfileConfig } from '../../../src/proxy/profile-router'; let proxyServer: http.Server; let upstreamServer: http.Server; let upstreamSockets = new Set(); let upstreamPort: number; let proxyPort: number; let tempDir: string; let originalTimeoutEnv: string | undefined; let originalCcsHome: string | undefined; function resolveListeningPort(server: http.Server): number { const address = server.address(); if (!address || typeof address === 'string') { throw new Error('Failed to resolve server port'); } return address.port; } async function waitForServerListening(server: http.Server): Promise { if (server.listening) { return resolveListeningPort(server); } return new Promise((resolve, reject) => { const onError = (error: Error) => reject(error); server.once('error', onError); server.once('listening', () => { server.off('error', onError); resolve(resolveListeningPort(server)); }); }); } async function startUpstream( handler: (req: http.IncomingMessage, res: http.ServerResponse) => Promise | void ): Promise { upstreamServer = http.createServer((req, res) => { void Promise.resolve(handler(req, res)); }); upstreamServer.on('connection', (socket) => { upstreamSockets.add(socket); socket.on('close', () => { upstreamSockets.delete(socket); }); }); upstreamServer.listen(0, '127.0.0.1'); upstreamPort = await waitForServerListening(upstreamServer); } async function requestProxy(payload: unknown, signal?: AbortSignal): Promise { return fetch(`http://127.0.0.1:${proxyPort}/v1/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': 'test-proxy-token', }, body: JSON.stringify(payload), signal, }); } beforeEach(async () => { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-proxy-edge-')); originalTimeoutEnv = process.env.CCS_OPENAI_PROXY_REQUEST_TIMEOUT_MS; originalCcsHome = process.env.CCS_HOME; process.env.CCS_HOME = tempDir; }); afterEach(async () => { if (originalTimeoutEnv !== undefined) { process.env.CCS_OPENAI_PROXY_REQUEST_TIMEOUT_MS = originalTimeoutEnv; } else { delete process.env.CCS_OPENAI_PROXY_REQUEST_TIMEOUT_MS; } if (originalCcsHome !== undefined) { process.env.CCS_HOME = originalCcsHome; } else { delete process.env.CCS_HOME; } if (proxyServer) { await new Promise((resolve) => proxyServer.close(() => resolve())); } if (upstreamServer) { for (const socket of upstreamSockets) { socket.destroy(); } upstreamSockets = new Set(); await new Promise((resolve) => upstreamServer.close(() => resolve())); } fs.rmSync(tempDir, { recursive: true, force: true }); }); describe('openai proxy message edge cases', () => { async function startProxyWithHandler( handler: (req: http.IncomingMessage, res: http.ServerResponse) => Promise | void ) { await startUpstream(handler); const profile: OpenAICompatProfileConfig = { profileName: 'hf', settingsPath: '/tmp/hf.settings.json', baseUrl: `http://127.0.0.1:${upstreamPort}`, apiKey: 'hf_token', provider: 'generic-chat-completion-api', model: 'hf-model', }; proxyServer = startOpenAICompatProxyServer({ profile, port: 0, authToken: 'test-proxy-token', }); proxyPort = await waitForServerListening(proxyServer); } it('preserves rate-limit errors from the upstream provider', async () => { await startProxyWithHandler((_req, res) => { res.writeHead(429, { 'Content-Type': 'application/json', 'Retry-After': '9', }); res.end(JSON.stringify({ error: { message: 'rate limited' } })); }); const response = await requestProxy({ model: 'hf-model', messages: [{ role: 'user', content: 'hello' }], }); expect(response.status).toBe(429); expect(response.headers.get('retry-after')).toBe('9'); await expect(response.json()).resolves.toMatchObject({ type: 'error', error: { type: 'rate_limit_error', message: 'rate limited', }, }); }); it('returns api_error when the upstream JSON response has no usable choices', async () => { await startProxyWithHandler((_req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ id: 'chatcmpl_empty', choices: [] })); }); const response = await requestProxy({ model: 'hf-model', messages: [{ role: 'user', content: 'hello' }], }); expect(response.status).toBe(502); await expect(response.json()).resolves.toMatchObject({ type: 'error', error: { type: 'api_error', message: 'Failed to translate OpenAI-compatible JSON response', }, }); }); it('streams thinking deltas and chunked tool-call arguments back as Anthropic SSE', async () => { await startProxyWithHandler((_req, res) => { res.writeHead(200, { 'Content-Type': 'text/event-stream' }); res.write( 'data: {"id":"chatcmpl_1","model":"hf-model","choices":[{"index":0,"delta":{"role":"assistant"}}]}\n\n' ); res.write( 'data: {"id":"chatcmpl_1","model":"hf-model","choices":[{"index":0,"delta":{"reasoning_content":"Need to search first."}}]}\n\n' ); res.write( 'data: {"id":"chatcmpl_1","model":"hf-model","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_1","type":"function","function":{"name":"search","arguments":"{\\"q\\":\\""}}]}}]}\n\n' ); res.write( 'data: {"id":"chatcmpl_1","model":"hf-model","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"docs\\"}"}}]}}]}\n\n' ); res.write( 'data: {"id":"chatcmpl_1","model":"hf-model","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":8,"completion_tokens":6}}\n\n' ); res.end('data: [DONE]\n\n'); }); const response = await requestProxy({ model: 'hf-model', stream: true, messages: [{ role: 'user', content: 'search docs' }], }); const body = await response.text(); expect(response.status).toBe(200); expect(body).toContain('"type":"thinking_delta"'); expect(body).toContain('"type":"tool_use"'); expect(body).toContain('"partial_json":"{\\"q\\":\\""'); expect(body).toContain('"partial_json":"docs\\"}"'); expect(body).toContain('event: message_stop'); }); it('returns a timeout error when the upstream does not respond in time', async () => { process.env.CCS_OPENAI_PROXY_REQUEST_TIMEOUT_MS = '50'; await startProxyWithHandler(async (req, _res) => { await new Promise((resolve) => req.on('close', () => resolve())); }); const response = await requestProxy({ model: 'hf-model', messages: [{ role: 'user', content: 'hello' }], }); expect(response.status).toBe(502); await expect(response.json()).resolves.toMatchObject({ type: 'error', error: { type: 'api_error', message: 'The upstream provider did not respond within 50ms', }, }); }); it('emits an SSE error if the upstream stalls after response headers are sent', async () => { process.env.CCS_OPENAI_PROXY_REQUEST_TIMEOUT_MS = '50'; await startProxyWithHandler((_req, res) => { res.writeHead(200, { 'Content-Type': 'text/event-stream' }); res.write( 'data: {"id":"chatcmpl_1","model":"hf-model","choices":[{"index":0,"delta":{"role":"assistant","content":"partial"}}]}\n\n' ); }); const response = await requestProxy({ model: 'hf-model', stream: true, messages: [{ role: 'user', content: 'hello' }], }); expect(response.status).toBe(200); const body = await response.text(); expect(body).toContain('event: message_start'); expect(body).toContain('event: error'); expect(body).toContain('"message":"Failed to translate OpenAI-compatible SSE response"'); }); it.skipIf(typeof Bun !== 'undefined')( 'aborts the upstream request when the client disconnects mid-flight (Node.js only)', async () => { await startProxyWithHandler(() => {}); await new Promise((resolve) => { const request = http.request( { hostname: '127.0.0.1', port: proxyPort, path: '/v1/messages', method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': 'test-proxy-token', }, }, () => resolve() ); request.write( JSON.stringify({ model: 'hf-model', messages: [{ role: 'user', content: 'hello' }], }) ); request.end(); setTimeout(() => { request.socket?.destroy(); resolve(); }, 50); }); const logPath = path.join(tempDir, '.ccs', 'logs', 'current.jsonl'); await Promise.race([ new Promise((resolve, reject) => { const startedAt = Date.now(); const timer = setInterval(() => { if (fs.existsSync(logPath)) { const content = fs.readFileSync(logPath, 'utf8'); if (content.includes('"event":"request.disconnect"')) { clearInterval(timer); resolve(); return; } } if (Date.now() - startedAt > 1500) { clearInterval(timer); reject(new Error('proxy did not log disconnect cleanup')); } }, 50); }), ]); } ); });