mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:38:02 +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:
266
src/services/modelService.js
Normal file
266
src/services/modelService.js
Normal file
@@ -0,0 +1,266 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const logger = require('../utils/logger')
|
||||
|
||||
/**
|
||||
* 模型服务
|
||||
* 管理系统支持的 AI 模型列表
|
||||
* 与 pricingService 独立,专注于"支持哪些模型"而不是"如何计费"
|
||||
*/
|
||||
class ModelService {
|
||||
constructor() {
|
||||
this.modelsFile = path.join(process.cwd(), 'data', 'supported_models.json')
|
||||
this.supportedModels = null
|
||||
this.fileWatcher = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化模型服务
|
||||
*/
|
||||
async initialize() {
|
||||
try {
|
||||
this.loadModels()
|
||||
this.setupFileWatcher()
|
||||
logger.success('✅ Model service initialized successfully')
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to initialize model service:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载支持的模型配置
|
||||
*/
|
||||
loadModels() {
|
||||
try {
|
||||
if (fs.existsSync(this.modelsFile)) {
|
||||
const data = fs.readFileSync(this.modelsFile, 'utf8')
|
||||
this.supportedModels = JSON.parse(data)
|
||||
|
||||
const totalModels = Object.values(this.supportedModels).reduce(
|
||||
(sum, config) => sum + config.models.length,
|
||||
0
|
||||
)
|
||||
|
||||
logger.info(`📋 Loaded ${totalModels} supported models from configuration`)
|
||||
} else {
|
||||
logger.warn('⚠️ Supported models file not found, using defaults')
|
||||
this.supportedModels = this.getDefaultModels()
|
||||
|
||||
// 创建默认配置文件
|
||||
this.saveDefaultConfig()
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to load supported models:', error)
|
||||
this.supportedModels = this.getDefaultModels()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认模型配置(后备方案)
|
||||
*/
|
||||
getDefaultModels() {
|
||||
return {
|
||||
claude: {
|
||||
provider: 'anthropic',
|
||||
description: 'Claude models from Anthropic',
|
||||
models: [
|
||||
'claude-sonnet-4-5-20250929',
|
||||
'claude-opus-4-1-20250805',
|
||||
'claude-sonnet-4-20250514',
|
||||
'claude-opus-4-20250514',
|
||||
'claude-3-7-sonnet-20250219',
|
||||
'claude-3-5-sonnet-20241022',
|
||||
'claude-3-5-haiku-20241022',
|
||||
'claude-3-opus-20240229',
|
||||
'claude-3-haiku-20240307'
|
||||
]
|
||||
},
|
||||
openai: {
|
||||
provider: 'openai',
|
||||
description: 'OpenAI GPT models',
|
||||
models: [
|
||||
'gpt-4o',
|
||||
'gpt-4o-mini',
|
||||
'gpt-4.1',
|
||||
'gpt-4.1-mini',
|
||||
'gpt-4.1-nano',
|
||||
'gpt-4-turbo',
|
||||
'gpt-4',
|
||||
'gpt-3.5-turbo',
|
||||
'o3',
|
||||
'o4-mini',
|
||||
'chatgpt-4o-latest'
|
||||
]
|
||||
},
|
||||
gemini: {
|
||||
provider: 'google',
|
||||
description: 'Google Gemini models',
|
||||
models: [
|
||||
'gemini-1.5-pro',
|
||||
'gemini-1.5-flash',
|
||||
'gemini-2.0-flash',
|
||||
'gemini-2.0-flash-exp',
|
||||
'gemini-2.0-flash-thinking',
|
||||
'gemini-2.0-flash-thinking-exp',
|
||||
'gemini-2.0-pro',
|
||||
'gemini-2.5-flash',
|
||||
'gemini-2.5-flash-lite',
|
||||
'gemini-2.5-pro'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存默认配置到文件
|
||||
*/
|
||||
saveDefaultConfig() {
|
||||
try {
|
||||
const dataDir = path.dirname(this.modelsFile)
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true })
|
||||
}
|
||||
|
||||
fs.writeFileSync(this.modelsFile, JSON.stringify(this.supportedModels, null, 2))
|
||||
logger.info('💾 Created default supported_models.json configuration')
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to save default config:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有支持的模型(OpenAI API 格式)
|
||||
*/
|
||||
getAllModels() {
|
||||
const models = []
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
|
||||
for (const [_service, config] of Object.entries(this.supportedModels)) {
|
||||
for (const modelId of config.models) {
|
||||
models.push({
|
||||
id: modelId,
|
||||
object: 'model',
|
||||
created: now,
|
||||
owned_by: config.provider
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return models.sort((a, b) => {
|
||||
// 先按 provider 排序,再按 model id 排序
|
||||
if (a.owned_by !== b.owned_by) {
|
||||
return a.owned_by.localeCompare(b.owned_by)
|
||||
}
|
||||
return a.id.localeCompare(b.id)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 provider 获取模型
|
||||
* @param {string} provider - 'anthropic', 'openai', 'google' 等
|
||||
*/
|
||||
getModelsByProvider(provider) {
|
||||
return this.getAllModels().filter((m) => m.owned_by === provider)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查模型是否被支持
|
||||
* @param {string} modelId - 模型 ID
|
||||
*/
|
||||
isModelSupported(modelId) {
|
||||
if (!modelId) {
|
||||
return false
|
||||
}
|
||||
return this.getAllModels().some((m) => m.id === modelId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型的 provider
|
||||
* @param {string} modelId - 模型 ID
|
||||
*/
|
||||
getModelProvider(modelId) {
|
||||
const model = this.getAllModels().find((m) => m.id === modelId)
|
||||
return model ? model.owned_by : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新加载模型配置
|
||||
*/
|
||||
reloadModels() {
|
||||
logger.info('🔄 Reloading supported models configuration...')
|
||||
this.loadModels()
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置文件监听器(监听配置文件变化)
|
||||
*/
|
||||
setupFileWatcher() {
|
||||
try {
|
||||
// 如果已有监听器,先关闭
|
||||
if (this.fileWatcher) {
|
||||
this.fileWatcher.close()
|
||||
this.fileWatcher = null
|
||||
}
|
||||
|
||||
// 只有文件存在时才设置监听器
|
||||
if (!fs.existsSync(this.modelsFile)) {
|
||||
logger.debug('📋 Models file does not exist yet, skipping file watcher setup')
|
||||
return
|
||||
}
|
||||
|
||||
// 使用 fs.watchFile 监听文件变化
|
||||
const watchOptions = {
|
||||
persistent: true,
|
||||
interval: 60000 // 每60秒检查一次
|
||||
}
|
||||
|
||||
let lastMtime = fs.statSync(this.modelsFile).mtimeMs
|
||||
|
||||
fs.watchFile(this.modelsFile, watchOptions, (curr, _prev) => {
|
||||
if (curr.mtimeMs !== lastMtime) {
|
||||
lastMtime = curr.mtimeMs
|
||||
logger.info('📋 Detected change in supported_models.json, reloading...')
|
||||
this.reloadModels()
|
||||
}
|
||||
})
|
||||
|
||||
// 保存引用以便清理
|
||||
this.fileWatcher = {
|
||||
close: () => fs.unwatchFile(this.modelsFile)
|
||||
}
|
||||
|
||||
logger.info('👁️ File watcher set up for supported_models.json')
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to setup file watcher:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务状态
|
||||
*/
|
||||
getStatus() {
|
||||
const totalModels = this.supportedModels
|
||||
? Object.values(this.supportedModels).reduce((sum, config) => sum + config.models.length, 0)
|
||||
: 0
|
||||
|
||||
return {
|
||||
initialized: this.supportedModels !== null,
|
||||
totalModels,
|
||||
providers: this.supportedModels ? Object.keys(this.supportedModels) : [],
|
||||
fileExists: fs.existsSync(this.modelsFile)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
cleanup() {
|
||||
if (this.fileWatcher) {
|
||||
this.fileWatcher.close()
|
||||
this.fileWatcher = null
|
||||
logger.debug('📋 Model service file watcher closed')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ModelService()
|
||||
Reference in New Issue
Block a user