mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
feat: add intelligent backend routing and model service
- Add modelService for centralized model management - Support dynamic model list from config file (data/supported_models.json) - Include 2025 latest models: GPT-4.1, o3, o4-mini, Gemini 2.5, etc. - File watcher for hot-reload configuration changes - Improve model detection logic in api.js - Priority: modelService lookup → prefix matching fallback - Smart backend routing based on model provider - Add intelligent routing endpoints - /v1/chat/completions: unified OpenAI-compatible endpoint - /v1/completions: legacy format support - Auto-route to Claude/OpenAI/Gemini based on requested model - Add Xcode system prompt support in openaiToClaude - Detect and preserve Xcode-specific system messages - Export handler functions for reuse - openaiClaudeRoutes: export handleChatCompletion - openaiRoutes: export handleResponses 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -11,9 +11,138 @@ const logger = require('../utils/logger')
|
||||
const { getEffectiveModel, parseVendorPrefixedModel } = require('../utils/modelHelper')
|
||||
const sessionHelper = require('../utils/sessionHelper')
|
||||
const { updateRateLimitCounters } = require('../utils/rateLimitHelper')
|
||||
const { handleChatCompletion } = require('./openaiClaudeRoutes')
|
||||
const {
|
||||
handleGenerateContent: geminiHandleGenerateContent,
|
||||
handleStreamGenerateContent: geminiHandleStreamGenerateContent
|
||||
} = require('./geminiRoutes')
|
||||
const openaiRoutes = require('./openaiRoutes')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
// 🔍 根据模型名称检测后端类型
|
||||
function detectBackendFromModel(modelName) {
|
||||
if (!modelName) {
|
||||
return 'claude' // 默认 Claude
|
||||
}
|
||||
|
||||
// 首先尝试使用 modelService 查找模型的 provider
|
||||
try {
|
||||
const modelService = require('../services/modelService')
|
||||
const provider = modelService.getModelProvider(modelName)
|
||||
|
||||
if (provider === 'anthropic') {
|
||||
return 'claude'
|
||||
}
|
||||
if (provider === 'openai') {
|
||||
return 'openai'
|
||||
}
|
||||
if (provider === 'google') {
|
||||
return 'gemini'
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`⚠️ Failed to detect backend from modelService: ${error.message}`)
|
||||
}
|
||||
|
||||
// 降级到前缀匹配作为后备方案
|
||||
const model = modelName.toLowerCase()
|
||||
|
||||
// Claude 模型
|
||||
if (model.startsWith('claude-')) {
|
||||
return 'claude'
|
||||
}
|
||||
|
||||
// OpenAI 模型
|
||||
if (
|
||||
model.startsWith('gpt-') ||
|
||||
model.startsWith('o1-') ||
|
||||
model.startsWith('o3-') ||
|
||||
model === 'chatgpt-4o-latest'
|
||||
) {
|
||||
return 'openai'
|
||||
}
|
||||
|
||||
// Gemini 模型
|
||||
if (model.startsWith('gemini-')) {
|
||||
return 'gemini'
|
||||
}
|
||||
|
||||
// 默认使用 Claude
|
||||
return 'claude'
|
||||
}
|
||||
|
||||
// 🚀 智能后端路由处理器
|
||||
async function routeToBackend(req, res, requestedModel) {
|
||||
const backend = detectBackendFromModel(requestedModel)
|
||||
|
||||
logger.info(`🔀 Routing request - Model: ${requestedModel}, Backend: ${backend}`)
|
||||
|
||||
// 检查权限
|
||||
const permissions = req.apiKey.permissions || 'all'
|
||||
|
||||
if (backend === 'claude') {
|
||||
// Claude 后端:通过 OpenAI 兼容层
|
||||
if (permissions !== 'all' && permissions !== 'claude') {
|
||||
return res.status(403).json({
|
||||
error: {
|
||||
message: 'This API key does not have permission to access Claude',
|
||||
type: 'permission_denied',
|
||||
code: 'permission_denied'
|
||||
}
|
||||
})
|
||||
}
|
||||
await handleChatCompletion(req, res, req.apiKey)
|
||||
} else if (backend === 'openai') {
|
||||
// OpenAI 后端
|
||||
if (permissions !== 'all' && permissions !== 'openai') {
|
||||
return res.status(403).json({
|
||||
error: {
|
||||
message: 'This API key does not have permission to access OpenAI',
|
||||
type: 'permission_denied',
|
||||
code: 'permission_denied'
|
||||
}
|
||||
})
|
||||
}
|
||||
return await openaiRoutes.handleResponses(req, res)
|
||||
} else if (backend === 'gemini') {
|
||||
// Gemini 后端
|
||||
if (permissions !== 'all' && permissions !== 'gemini') {
|
||||
return res.status(403).json({
|
||||
error: {
|
||||
message: 'This API key does not have permission to access Gemini',
|
||||
type: 'permission_denied',
|
||||
code: 'permission_denied'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 转换为 Gemini 格式
|
||||
const geminiRequest = {
|
||||
model: requestedModel,
|
||||
messages: req.body.messages,
|
||||
temperature: req.body.temperature || 0.7,
|
||||
max_tokens: req.body.max_tokens || 4096,
|
||||
stream: req.body.stream || false
|
||||
}
|
||||
|
||||
req.body = geminiRequest
|
||||
|
||||
if (geminiRequest.stream) {
|
||||
return await geminiHandleStreamGenerateContent(req, res)
|
||||
} else {
|
||||
return await geminiHandleGenerateContent(req, res)
|
||||
}
|
||||
} else {
|
||||
return res.status(500).json({
|
||||
error: {
|
||||
message: `Unsupported backend: ${backend}`,
|
||||
type: 'server_error',
|
||||
code: 'unsupported_backend'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function queueRateLimitUpdate(rateLimitInfo, usageSummary, model, context = '') {
|
||||
if (!rateLimitInfo) {
|
||||
return Promise.resolve({ totalTokens: 0, totalCost: 0 })
|
||||
@@ -722,40 +851,23 @@ router.post('/v1/messages', authenticateApiKey, handleMessagesRequest)
|
||||
// 🚀 Claude API messages 端点 - /claude/v1/messages (别名)
|
||||
router.post('/claude/v1/messages', authenticateApiKey, handleMessagesRequest)
|
||||
|
||||
// 📋 模型列表端点 - Claude Code 客户端需要
|
||||
// 📋 模型列表端点 - 支持 Claude, OpenAI, Gemini
|
||||
router.get('/v1/models', authenticateApiKey, async (req, res) => {
|
||||
try {
|
||||
// 返回支持的模型列表
|
||||
const models = [
|
||||
{
|
||||
id: 'claude-3-5-sonnet-20241022',
|
||||
object: 'model',
|
||||
created: 1669599635,
|
||||
owned_by: 'anthropic'
|
||||
},
|
||||
{
|
||||
id: 'claude-3-5-haiku-20241022',
|
||||
object: 'model',
|
||||
created: 1669599635,
|
||||
owned_by: 'anthropic'
|
||||
},
|
||||
{
|
||||
id: 'claude-3-opus-20240229',
|
||||
object: 'model',
|
||||
created: 1669599635,
|
||||
owned_by: 'anthropic'
|
||||
},
|
||||
{
|
||||
id: 'claude-sonnet-4-20250514',
|
||||
object: 'model',
|
||||
created: 1669599635,
|
||||
owned_by: 'anthropic'
|
||||
}
|
||||
]
|
||||
const modelService = require('../services/modelService')
|
||||
|
||||
// 从 modelService 获取所有支持的模型
|
||||
const models = modelService.getAllModels()
|
||||
|
||||
// 可选:根据 API Key 的模型限制过滤
|
||||
let filteredModels = models
|
||||
if (req.apiKey.enableModelRestriction && req.apiKey.restrictedModels?.length > 0) {
|
||||
filteredModels = models.filter((model) => req.apiKey.restrictedModels.includes(model.id))
|
||||
}
|
||||
|
||||
res.json({
|
||||
object: 'list',
|
||||
data: models
|
||||
data: filteredModels
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('❌ Models list error:', error)
|
||||
@@ -766,6 +878,93 @@ router.get('/v1/models', authenticateApiKey, async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
// 🔄 OpenAI 兼容的 chat/completions 端点(智能后端路由)
|
||||
router.post('/v1/chat/completions', authenticateApiKey, async (req, res) => {
|
||||
try {
|
||||
// 验证必需参数
|
||||
if (!req.body.messages || !Array.isArray(req.body.messages) || req.body.messages.length === 0) {
|
||||
return res.status(400).json({
|
||||
error: {
|
||||
message: 'Messages array is required and cannot be empty',
|
||||
type: 'invalid_request_error',
|
||||
code: 'invalid_request'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const requestedModel = req.body.model || 'claude-3-5-sonnet-20241022'
|
||||
req.body.model = requestedModel // 确保模型已设置
|
||||
|
||||
// 使用统一的后端路由处理器
|
||||
await routeToBackend(req, res, requestedModel)
|
||||
} catch (error) {
|
||||
logger.error('❌ OpenAI chat/completions error:', error)
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
error: {
|
||||
message: 'Internal server error',
|
||||
type: 'server_error',
|
||||
code: 'internal_error'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 🔄 OpenAI 兼容的 completions 端点(传统格式,智能后端路由)
|
||||
router.post('/v1/completions', authenticateApiKey, async (req, res) => {
|
||||
try {
|
||||
// 验证必需参数
|
||||
if (!req.body.prompt) {
|
||||
return res.status(400).json({
|
||||
error: {
|
||||
message: 'Prompt is required',
|
||||
type: 'invalid_request_error',
|
||||
code: 'invalid_request'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 将传统 completions 格式转换为 chat 格式
|
||||
const originalBody = req.body
|
||||
const requestedModel = originalBody.model || 'claude-3-5-sonnet-20241022'
|
||||
|
||||
req.body = {
|
||||
model: requestedModel,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: originalBody.prompt
|
||||
}
|
||||
],
|
||||
max_tokens: originalBody.max_tokens,
|
||||
temperature: originalBody.temperature,
|
||||
top_p: originalBody.top_p,
|
||||
stream: originalBody.stream,
|
||||
stop: originalBody.stop,
|
||||
n: originalBody.n || 1,
|
||||
presence_penalty: originalBody.presence_penalty,
|
||||
frequency_penalty: originalBody.frequency_penalty,
|
||||
logit_bias: originalBody.logit_bias,
|
||||
user: originalBody.user
|
||||
}
|
||||
|
||||
// 使用统一的后端路由处理器
|
||||
await routeToBackend(req, res, requestedModel)
|
||||
} catch (error) {
|
||||
logger.error('❌ OpenAI completions error:', error)
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
error: {
|
||||
message: 'Failed to process completion request',
|
||||
type: 'server_error',
|
||||
code: 'internal_error'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 🏥 健康检查端点
|
||||
router.get('/health', async (req, res) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user