From d541c36b478c8c3fe60409a15f387c8a4525d5fa Mon Sep 17 00:00:00 2001 From: yicone Date: Sat, 27 Dec 2025 17:01:02 +0800 Subject: [PATCH] fix: resolve CORS for browser extensions & enhance OpenAI Responses compatibility --- .../strategies/OpenAIResponsesConverter.js | 17 +++- src/request-handler.js | 8 +- tests/cors-config.test.js | 91 +++++++++++++++++++ tests/openai-responses-converter.test.js | 80 ++++++++++++++++ 4 files changed, 188 insertions(+), 8 deletions(-) create mode 100644 tests/cors-config.test.js create mode 100644 tests/openai-responses-converter.test.js diff --git a/src/converters/strategies/OpenAIResponsesConverter.js b/src/converters/strategies/OpenAIResponsesConverter.js index 2c926dd..5847320 100644 --- a/src/converters/strategies/OpenAIResponsesConverter.js +++ b/src/converters/strategies/OpenAIResponsesConverter.js @@ -382,11 +382,18 @@ export class OpenAIResponsesConverter extends BaseConverter { // 处理 input 数组中的消息 if (responsesRequest.input && Array.isArray(responsesRequest.input)) { responsesRequest.input.forEach(item => { - if (item.type === 'message') { - const content = item.content - .filter(c => c.type === 'input_text') - .map(c => c.text) - .join('\n'); + // 如果 item 没有 type 属性,默认为 message + // 或者 item.type 明确为 message + if (!item.type || item.type === 'message') { + let content = ''; + if (Array.isArray(item.content)) { + content = item.content + .filter(c => c.type === 'input_text') + .map(c => c.text) + .join('\n'); + } else if (typeof item.content === 'string') { + content = item.content; + } if (content) { geminiRequest.contents.push({ diff --git a/src/request-handler.js b/src/request-handler.js index 81defc7..9a11838 100644 --- a/src/request-handler.js +++ b/src/request-handler.js @@ -23,11 +23,13 @@ export function createRequestHandler(config, providerPoolManager) { let path = requestUrl.pathname; const method = req.method; + // Set CORS headers for all requests + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-goog-api-key, Model-Provider'); + // Handle CORS preflight requests if (method === 'OPTIONS') { - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-goog-api-key, Model-Provider'); res.writeHead(204); res.end(); return; diff --git a/tests/cors-config.test.js b/tests/cors-config.test.js new file mode 100644 index 0000000..5e6e757 --- /dev/null +++ b/tests/cors-config.test.js @@ -0,0 +1,91 @@ +import { jest } from '@jest/globals'; + +// Mock `open` module before importing anything that uses it +jest.mock('open', () => ({ + default: jest.fn() +})); + +// Now import the module under test +import { createRequestHandler } from '../src/request-handler.js'; + +describe('CORS Configuration', () => { + let mockConfig; + let mockProviderPoolManager; + let handler; + + beforeEach(() => { + mockConfig = { + MODEL_PROVIDER: 'mock-provider', + REQUIRED_API_KEY: 'mock-key', + providerPools: {} + }; + + mockProviderPoolManager = { + getPool: () => null + }; + + handler = createRequestHandler(mockConfig, mockProviderPoolManager); + }); + + test('should set CORS headers for POST requests', async () => { + const headers = {}; + const req = { + url: '/v1/test', + method: 'POST', + headers: { + host: 'localhost:3000' + } + }; + + const res = { + setHeader: (name, value) => { + headers[name] = value; + }, + writeHead: (statusCode, h) => { + if (h) Object.assign(headers, h); + }, + end: () => {} + }; + + try { + await handler(req, res); + } catch (e) { + // Expected to fail/error due to mock environment, but headers should be set + } + + expect(headers['Access-Control-Allow-Origin']).toBe('*'); + expect(headers['Access-Control-Allow-Methods']).toBe('GET, POST, PUT, DELETE, OPTIONS'); + expect(headers['Access-Control-Allow-Headers']).toBe('Content-Type, Authorization, x-goog-api-key, Model-Provider'); + }); + + test('should set CORS headers for OPTIONS requests', async () => { + const headers = {}; + const req = { + url: '/v1/test', + method: 'OPTIONS', + headers: { + host: 'localhost:3000' + } + }; + + const res = { + setHeader: (name, value) => { + headers[name] = value; + }, + writeHead: (statusCode, h) => { + if (h) Object.assign(headers, h); + }, + end: () => {} + }; + + try { + await handler(req, res); + } catch (e) { + // Expected + } + + expect(headers['Access-Control-Allow-Origin']).toBe('*'); + expect(headers['Access-Control-Allow-Methods']).toBe('GET, POST, PUT, DELETE, OPTIONS'); + expect(headers['Access-Control-Allow-Headers']).toBe('Content-Type, Authorization, x-goog-api-key, Model-Provider'); + }); +}); diff --git a/tests/openai-responses-converter.test.js b/tests/openai-responses-converter.test.js new file mode 100644 index 0000000..9aada0e --- /dev/null +++ b/tests/openai-responses-converter.test.js @@ -0,0 +1,80 @@ +import { OpenAIResponsesConverter } from '../src/converters/strategies/OpenAIResponsesConverter.js'; + +describe('OpenAIResponsesConverter', () => { + let converter; + + beforeEach(() => { + converter = new OpenAIResponsesConverter(); + }); + + test('toGeminiRequest should handle input without explicit type as message', () => { + const responsesRequest = { + "model": "gemini-2.0-flash-exp", + "input": [ + { + "role": "user", + "content": [ + { + "type": "input_text", + "text": "Hello, world!" + } + ] + } + ], + "max_output_tokens": 200 + }; + + const geminiRequest = converter.toGeminiRequest(responsesRequest); + + expect(geminiRequest.contents).toBeDefined(); + expect(geminiRequest.contents.length).toBe(1); + expect(geminiRequest.contents[0].role).toBe('user'); + expect(geminiRequest.contents[0].parts[0].text).toBe('Hello, world!'); + }); + + test('toGeminiRequest should handle string content in input', () => { + const responsesRequest = { + "model": "gemini-2.0-flash-exp", + "input": [ + { + "role": "user", + "content": "Hello, world!" + } + ], + "max_output_tokens": 200 + }; + + const geminiRequest = converter.toGeminiRequest(responsesRequest); + + expect(geminiRequest.contents).toBeDefined(); + expect(geminiRequest.contents.length).toBe(1); + expect(geminiRequest.contents[0].role).toBe('user'); + expect(geminiRequest.contents[0].parts[0].text).toBe('Hello, world!'); + }); + + test('toGeminiRequest should handle input with explicit message type', () => { + const responsesRequest = { + "model": "gemini-2.0-flash-exp", + "input": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "Hello, world!" + } + ] + } + ], + "max_output_tokens": 200 + }; + + const geminiRequest = converter.toGeminiRequest(responsesRequest); + + expect(geminiRequest.contents).toBeDefined(); + expect(geminiRequest.contents.length).toBe(1); + expect(geminiRequest.contents[0].role).toBe('user'); + expect(geminiRequest.contents[0].parts[0].text).toBe('Hello, world!'); + }); +});