diff --git a/src/proxy/server/messages-route.ts b/src/proxy/server/messages-route.ts index 13c329e8..95388e00 100644 --- a/src/proxy/server/messages-route.ts +++ b/src/proxy/server/messages-route.ts @@ -221,8 +221,6 @@ export async function handleProxyMessagesRequest( resolveOpenAIChatCompletionsUrl(upstream.route.profile.baseUrl), buildFetchInit(upstream.route.profile, upstream.body, controller.signal, insecureDispatcher) ); - clearTimeout(timeout); - cleanupDisconnectHandlers(); logger.info('response.received', 'Received upstream response', { profileName: profile.profileName, routedProfileName: upstream.route.profile.profileName, diff --git a/src/proxy/transformers/sse-stream-transformer.ts b/src/proxy/transformers/sse-stream-transformer.ts index 0211e211..dff2b7ce 100644 --- a/src/proxy/transformers/sse-stream-transformer.ts +++ b/src/proxy/transformers/sse-stream-transformer.ts @@ -3,8 +3,8 @@ import { GlmtTransformer } from '../../glmt/glmt-transformer'; import { SSEParser } from '../../glmt/sse-parser'; import type { OpenAIResponse, SSEEvent } from '../../glmt/pipeline'; -const JSON_TRANSLATION_ERROR_MESSAGE = 'Failed to translate Cursor JSON response'; -const STREAM_TRANSLATION_ERROR_MESSAGE = 'Failed to translate Cursor SSE response'; +const JSON_TRANSLATION_ERROR_MESSAGE = 'Failed to translate OpenAI-compatible JSON response'; +const STREAM_TRANSLATION_ERROR_MESSAGE = 'Failed to translate OpenAI-compatible SSE response'; type ResponseHeaders = Headers | Record | Array<[string, string]>; @@ -103,7 +103,7 @@ async function createAnthropicErrorProxyResponse(response: Response): Promise= 400 && response.status < 500 ? 'invalid_request_error' : 'api_error'; - let message = `Cursor request failed with status ${response.status}`; + let message = `Upstream request failed with status ${response.status}`; try { const contentType = (response.headers.get('content-type') || '').toLowerCase(); @@ -145,7 +145,7 @@ async function createAnthropicJsonResponse(response: Response): Promise { type: 'error', error: { type: 'api_error', - message: 'Failed to translate Cursor JSON response', + message: 'Failed to translate OpenAI-compatible JSON response', }, }); }); @@ -202,6 +202,28 @@ describe('openai proxy message edge cases', () => { }); }); + 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('aborts the upstream request when the client disconnects mid-flight', async () => { await startProxyWithHandler(() => {}); diff --git a/tests/unit/cursor/cursor-anthropic-translator.test.ts b/tests/unit/cursor/cursor-anthropic-translator.test.ts index 0abaec2b..9da161fe 100644 --- a/tests/unit/cursor/cursor-anthropic-translator.test.ts +++ b/tests/unit/cursor/cursor-anthropic-translator.test.ts @@ -281,7 +281,7 @@ describe('createAnthropicProxyResponse', () => { }; expect(body.type).toBe('error'); expect(body.error?.type).toBe('api_error'); - expect(body.error?.message).toBe('Failed to translate Cursor JSON response'); + expect(body.error?.message).toBe('Failed to translate OpenAI-compatible JSON response'); }); it('returns 502 when Cursor response is missing choices', async () => { @@ -305,7 +305,7 @@ describe('createAnthropicProxyResponse', () => { }; expect(body.type).toBe('error'); expect(body.error?.type).toBe('api_error'); - expect(body.error?.message).toBe('Failed to translate Cursor JSON response'); + expect(body.error?.message).toBe('Failed to translate OpenAI-compatible JSON response'); }); it('returns 502 when Cursor response has empty choices', async () => { @@ -330,7 +330,7 @@ describe('createAnthropicProxyResponse', () => { }; expect(body.type).toBe('error'); expect(body.error?.type).toBe('api_error'); - expect(body.error?.message).toBe('Failed to translate Cursor JSON response'); + expect(body.error?.message).toBe('Failed to translate OpenAI-compatible JSON response'); }); it('returns Anthropic error envelopes for non-OK upstream JSON errors', async () => { @@ -382,7 +382,7 @@ describe('createAnthropicProxyResponse', () => { }; expect(body.type).toBe('error'); expect(body.error?.type).toBe('api_error'); - expect(body.error?.message).toBe('Failed to translate Cursor JSON response'); + expect(body.error?.message).toBe('Failed to translate OpenAI-compatible JSON response'); }); it('converts OpenAI SSE chunks into Anthropic SSE events', async () => { @@ -420,7 +420,7 @@ describe('createAnthropicProxyResponse', () => { expect(body).toContain('event: error'); expect(body).toContain('"type":"error"'); expect(body).toContain('"error":{"type":"api_error"'); - expect(body).toContain('Failed to translate Cursor SSE response'); + expect(body).toContain('Failed to translate OpenAI-compatible SSE response'); }); it('emits Anthropic-style error events when SSE JSON is malformed', async () => { @@ -435,6 +435,6 @@ describe('createAnthropicProxyResponse', () => { expect(body).toContain('event: error'); expect(body).toContain('"type":"error"'); expect(body).toContain('"error":{"type":"api_error"'); - expect(body).toContain('Failed to translate Cursor SSE response'); + expect(body).toContain('Failed to translate OpenAI-compatible SSE response'); }); });