feat: 添加 OpenAI API 代理支持

- 新增 /openai/v1/* 路由代理 OpenAI API
- 添加 OPENAI_BASE_URL 环境变量支持
- 更新日志记录支持 provider 字段

使用方式:
- Claude API: /v1/*
- OpenAI API: /openai/v1/*

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
D8D Developer
2026-03-20 14:53:45 +00:00
parent 4037d89392
commit f6f8af6583
3 changed files with 141 additions and 4 deletions

View File

@@ -1,6 +1,7 @@
import 'dotenv/config';
import express from 'express';
import { proxyHandler } from './proxy.js';
import { openaiProxyHandler } from './proxy-openai.js';
import { healthHandler } from './health.js';
const app = express();
@@ -12,14 +13,18 @@ app.use(express.json({ limit: '50mb' }));
// 健康检查端点
app.get('/health', healthHandler);
// 代理所有 /v1/* 路由
// Claude API 代理路由: /v1/*
app.all('/v1/*', proxyHandler);
// OpenAI API 代理路由: /openai/v1/*
app.all('/openai/v1/*', openaiProxyHandler);
// 启动服务器
const server = app.listen(PORT, () => {
console.log(`Claude API Proxy 服务已启动`);
console.log(`API Proxy 服务已启动`);
console.log(`端口: ${PORT}`);
console.log(`上游 API: ${process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com'}`);
console.log(`Claude API: ${process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com'}`);
console.log(`OpenAI API: ${process.env.OPENAI_BASE_URL || 'https://api.openai.com'}`);
console.log(`健康检查: http://localhost:${PORT}/health`);
});

View File

@@ -10,6 +10,7 @@ export interface LogEntry {
duration: number;
stream?: boolean;
error?: string;
provider?: string;
}
export function log(entry: LogEntry): void {
@@ -26,7 +27,7 @@ export function createLogEntry(
path: string,
status: number,
duration: number,
options?: { stream?: boolean; error?: string }
options?: { stream?: boolean; error?: string; provider?: string }
): LogEntry {
return {
time: new Date().toISOString(),

131
src/proxy-openai.ts Normal file
View File

@@ -0,0 +1,131 @@
import { Request, Response } from 'express';
import axios from 'axios';
import { log, createLogEntry } from './logger.js';
// OpenAI 需要透传的 headers
const OPENAI_PROXY_HEADERS = [
'authorization',
'content-type',
'accept',
'openai-organization',
'openai-beta',
];
// 获取 OpenAI 上游 API 基础 URL
function getOpenAIBaseUrl(): string {
return process.env.OPENAI_BASE_URL || 'https://api.openai.com';
}
// 提取需要透传的 headers
function extractProxyHeaders(req: Request): Record<string, string> {
const headers: Record<string, string> = {};
for (const header of OPENAI_PROXY_HEADERS) {
const value = req.headers[header];
if (value) {
headers[header] = Array.isArray(value) ? value[0] : value;
}
}
return headers;
}
// OpenAI 代理处理函数
export async function openaiProxyHandler(req: Request, res: Response): Promise<void> {
const startTime = Date.now();
const method = req.method;
// 移除 /openai 前缀,保留 /v1/... 路径
const requestPath = req.path.replace(/^\/openai/, '');
const baseUrl = getOpenAIBaseUrl();
const targetUrl = `${baseUrl}${requestPath}`;
// 判断是否为流式请求
const isStream = req.body?.stream === true;
// 提取透传 headers
const headers = extractProxyHeaders(req);
try {
const response = await axios({
method: method,
url: targetUrl,
data: req.body,
headers,
responseType: isStream ? 'stream' : 'json',
});
if (isStream) {
// 流式响应处理
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const stream = response.data;
// 监听客户端断开
req.on('close', () => {
stream.destroy();
});
// 监听上游流错误
stream.on('error', (err: Error) => {
console.error('OpenAI 上游流错误:', err.message);
const duration = Date.now() - startTime;
log(createLogEntry(method, requestPath, 500, duration, { stream: true, provider: 'openai', error: err.message }));
if (!res.writableEnded) {
res.end();
}
});
// 管道传输
stream.pipe(res);
// 流结束时记录日志
stream.on('end', () => {
const duration = Date.now() - startTime;
log(createLogEntry(method, requestPath, 200, duration, { stream: true, provider: 'openai' }));
});
} else {
// 非流式响应处理
const duration = Date.now() - startTime;
log(createLogEntry(method, requestPath, response.status, duration, { stream: false, provider: 'openai' }));
res.status(response.status).json(response.data);
}
} catch (error: unknown) {
const duration = Date.now() - startTime;
if (axios.isAxiosError(error)) {
const status = error.response?.status || 500;
const errorData = error.response?.data;
log(createLogEntry(method, requestPath, status, duration, {
stream: isStream,
provider: 'openai',
error: errorData?.error?.message || error.message,
}));
if (isStream && error.response?.data) {
res.status(status).json(errorData);
} else {
res.status(status).json(errorData || {
error: {
message: error.message,
type: 'proxy_error',
},
});
}
} else {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
log(createLogEntry(method, requestPath, 500, duration, {
stream: isStream,
provider: 'openai',
error: errorMessage,
}));
res.status(500).json({
error: {
message: errorMessage,
type: 'internal_error',
},
});
}
}
}