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:
11
src/index.ts
11
src/index.ts
@@ -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`);
|
||||
});
|
||||
|
||||
|
||||
@@ -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
131
src/proxy-openai.ts
Normal 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',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user