Merge pull request #141 from yicone/main

fix: resolve CORS for browser extensions & enhance OpenAI Responses compatibility
This commit is contained in:
何夕2077 2025-12-27 17:17:08 +08:00 committed by GitHub
commit cdb936cfd6
4 changed files with 188 additions and 8 deletions

View file

@ -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({

View file

@ -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;

91
tests/cors-config.test.js Normal file
View file

@ -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');
});
});

View file

@ -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!');
});
});