feat: 新增支持Azure OpenAI账户

This commit is contained in:
shaw
2025-08-23 10:30:29 +08:00
parent e4e72ba5e9
commit 74bcb99142
11 changed files with 1884 additions and 7 deletions

View File

@@ -5,6 +5,7 @@ const claudeConsoleAccountService = require('../services/claudeConsoleAccountSer
const bedrockAccountService = require('../services/bedrockAccountService')
const geminiAccountService = require('../services/geminiAccountService')
const openaiAccountService = require('../services/openaiAccountService')
const azureOpenaiAccountService = require('../services/azureOpenaiAccountService')
const accountGroupService = require('../services/accountGroupService')
const redis = require('../models/redis')
const { authenticateAdmin } = require('../middleware/auth')
@@ -5227,4 +5228,291 @@ router.put(
}
)
// 🌐 Azure OpenAI 账户管理
// 获取所有 Azure OpenAI 账户
router.get('/azure-openai-accounts', authenticateAdmin, async (req, res) => {
try {
const accounts = await azureOpenaiAccountService.getAllAccounts()
res.json({
success: true,
data: accounts
})
} catch (error) {
logger.error('Failed to fetch Azure OpenAI accounts:', error)
res.status(500).json({
success: false,
message: 'Failed to fetch accounts',
error: error.message
})
}
})
// 创建 Azure OpenAI 账户
router.post('/azure-openai-accounts', authenticateAdmin, async (req, res) => {
try {
const {
name,
description,
accountType,
azureEndpoint,
apiVersion,
deploymentName,
apiKey,
supportedModels,
proxy,
groupId,
priority,
isActive,
schedulable
} = req.body
// 验证必填字段
if (!name) {
return res.status(400).json({
success: false,
message: 'Account name is required'
})
}
if (!azureEndpoint) {
return res.status(400).json({
success: false,
message: 'Azure endpoint is required'
})
}
if (!apiKey) {
return res.status(400).json({
success: false,
message: 'API key is required'
})
}
if (!deploymentName) {
return res.status(400).json({
success: false,
message: 'Deployment name is required'
})
}
// 验证 Azure endpoint 格式
if (!azureEndpoint.match(/^https:\/\/[\w-]+\.openai\.azure\.com$/)) {
return res.status(400).json({
success: false,
message:
'Invalid Azure OpenAI endpoint format. Expected: https://your-resource.openai.azure.com'
})
}
// 测试连接
try {
const testUrl = `${azureEndpoint}/openai/deployments/${deploymentName}?api-version=${apiVersion || '2024-02-01'}`
await axios.get(testUrl, {
headers: {
'api-key': apiKey
},
timeout: 5000
})
} catch (testError) {
if (testError.response?.status === 404) {
logger.warn('Azure OpenAI deployment not found, but continuing with account creation')
} else if (testError.response?.status === 401) {
return res.status(400).json({
success: false,
message: 'Invalid API key or unauthorized access'
})
}
}
const account = await azureOpenaiAccountService.createAccount({
name,
description,
accountType: accountType || 'shared',
azureEndpoint,
apiVersion: apiVersion || '2024-02-01',
deploymentName,
apiKey,
supportedModels,
proxy,
groupId,
priority: priority || 50,
isActive: isActive !== false,
schedulable: schedulable !== false
})
res.json({
success: true,
data: account,
message: 'Azure OpenAI account created successfully'
})
} catch (error) {
logger.error('Failed to create Azure OpenAI account:', error)
res.status(500).json({
success: false,
message: 'Failed to create account',
error: error.message
})
}
})
// 更新 Azure OpenAI 账户
router.put('/azure-openai-accounts/:id', authenticateAdmin, async (req, res) => {
try {
const { id } = req.params
const updates = req.body
const account = await azureOpenaiAccountService.updateAccount(id, updates)
res.json({
success: true,
data: account,
message: 'Azure OpenAI account updated successfully'
})
} catch (error) {
logger.error('Failed to update Azure OpenAI account:', error)
res.status(500).json({
success: false,
message: 'Failed to update account',
error: error.message
})
}
})
// 删除 Azure OpenAI 账户
router.delete('/azure-openai-accounts/:id', authenticateAdmin, async (req, res) => {
try {
const { id } = req.params
await azureOpenaiAccountService.deleteAccount(id)
res.json({
success: true,
message: 'Azure OpenAI account deleted successfully'
})
} catch (error) {
logger.error('Failed to delete Azure OpenAI account:', error)
res.status(500).json({
success: false,
message: 'Failed to delete account',
error: error.message
})
}
})
// 切换 Azure OpenAI 账户状态
router.put('/azure-openai-accounts/:id/toggle', authenticateAdmin, async (req, res) => {
try {
const { id } = req.params
const account = await azureOpenaiAccountService.getAccount(id)
if (!account) {
return res.status(404).json({
success: false,
message: 'Account not found'
})
}
const newStatus = account.isActive === 'true' ? 'false' : 'true'
await azureOpenaiAccountService.updateAccount(id, { isActive: newStatus })
res.json({
success: true,
message: `Account ${newStatus === 'true' ? 'activated' : 'deactivated'} successfully`,
isActive: newStatus === 'true'
})
} catch (error) {
logger.error('Failed to toggle Azure OpenAI account status:', error)
res.status(500).json({
success: false,
message: 'Failed to toggle account status',
error: error.message
})
}
})
// 切换 Azure OpenAI 账户调度状态
router.put(
'/azure-openai-accounts/:accountId/toggle-schedulable',
authenticateAdmin,
async (req, res) => {
try {
const { accountId } = req.params
const result = await azureOpenaiAccountService.toggleSchedulable(accountId)
return res.json({
success: true,
schedulable: result.schedulable,
message: result.schedulable ? '已启用调度' : '已禁用调度'
})
} catch (error) {
logger.error('切换 Azure OpenAI 账户调度状态失败:', error)
return res.status(500).json({
success: false,
message: '切换调度状态失败',
error: error.message
})
}
}
)
// 健康检查单个 Azure OpenAI 账户
router.post('/azure-openai-accounts/:id/health-check', authenticateAdmin, async (req, res) => {
try {
const { id } = req.params
const healthResult = await azureOpenaiAccountService.healthCheckAccount(id)
res.json({
success: true,
data: healthResult
})
} catch (error) {
logger.error('Failed to perform health check:', error)
res.status(500).json({
success: false,
message: 'Failed to perform health check',
error: error.message
})
}
})
// 批量健康检查所有 Azure OpenAI 账户
router.post('/azure-openai-accounts/health-check-all', authenticateAdmin, async (req, res) => {
try {
const healthResults = await azureOpenaiAccountService.performHealthChecks()
res.json({
success: true,
data: healthResults
})
} catch (error) {
logger.error('Failed to perform batch health check:', error)
res.status(500).json({
success: false,
message: 'Failed to perform batch health check',
error: error.message
})
}
})
// 迁移 API Keys 以支持 Azure OpenAI
router.post('/migrate-api-keys-azure', authenticateAdmin, async (req, res) => {
try {
const migratedCount = await azureOpenaiAccountService.migrateApiKeysForAzureSupport()
res.json({
success: true,
message: `Successfully migrated ${migratedCount} API keys for Azure OpenAI support`
})
} catch (error) {
logger.error('Failed to migrate API keys:', error)
res.status(500).json({
success: false,
message: 'Failed to migrate API keys',
error: error.message
})
}
})
module.exports = router

View File

@@ -0,0 +1,318 @@
const express = require('express')
const router = express.Router()
const logger = require('../utils/logger')
const { authenticateApiKey } = require('../middleware/auth')
const azureOpenaiAccountService = require('../services/azureOpenaiAccountService')
const azureOpenaiRelayService = require('../services/azureOpenaiRelayService')
const apiKeyService = require('../services/apiKeyService')
const crypto = require('crypto')
// 支持的模型列表 - 基于真实的 Azure OpenAI 模型
const ALLOWED_MODELS = {
CHAT_MODELS: [
'gpt-4',
'gpt-4-turbo',
'gpt-4o',
'gpt-4o-mini',
'gpt-35-turbo',
'gpt-35-turbo-16k'
],
EMBEDDING_MODELS: ['text-embedding-ada-002', 'text-embedding-3-small', 'text-embedding-3-large']
}
const ALL_ALLOWED_MODELS = [...ALLOWED_MODELS.CHAT_MODELS, ...ALLOWED_MODELS.EMBEDDING_MODELS]
// Azure OpenAI 稳定 API 版本
// const AZURE_API_VERSION = '2024-02-01' // 当前未使用,保留以备后用
// 原子使用统计报告器
class AtomicUsageReporter {
constructor() {
this.reportedUsage = new Set()
this.pendingReports = new Map()
}
async reportOnce(requestId, usageData, apiKeyId, modelToRecord, accountId) {
if (this.reportedUsage.has(requestId)) {
logger.debug(`Usage already reported for request: ${requestId}`)
return false
}
// 防止并发重复报告
if (this.pendingReports.has(requestId)) {
return this.pendingReports.get(requestId)
}
const reportPromise = this._performReport(
requestId,
usageData,
apiKeyId,
modelToRecord,
accountId
)
this.pendingReports.set(requestId, reportPromise)
try {
const result = await reportPromise
this.reportedUsage.add(requestId)
return result
} finally {
this.pendingReports.delete(requestId)
// 清理过期的已报告记录
setTimeout(() => this.reportedUsage.delete(requestId), 60 * 1000) // 1分钟后清理
}
}
async _performReport(requestId, usageData, apiKeyId, modelToRecord, accountId) {
try {
const inputTokens = usageData.prompt_tokens || usageData.input_tokens || 0
const outputTokens = usageData.completion_tokens || usageData.output_tokens || 0
const cacheCreateTokens =
usageData.prompt_tokens_details?.cache_creation_tokens ||
usageData.input_tokens_details?.cache_creation_tokens ||
0
const cacheReadTokens =
usageData.prompt_tokens_details?.cached_tokens ||
usageData.input_tokens_details?.cached_tokens ||
0
await apiKeyService.recordUsage(
apiKeyId,
inputTokens,
outputTokens,
cacheCreateTokens,
cacheReadTokens,
modelToRecord,
accountId
)
// 同步更新 Azure 账户的 lastUsedAt 和累计使用量
try {
const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens
if (accountId) {
await azureOpenaiAccountService.updateAccountUsage(accountId, totalTokens)
}
} catch (acctErr) {
logger.warn(`Failed to update Azure account usage for ${accountId}: ${acctErr.message}`)
}
logger.info(
`📊 Azure OpenAI Usage recorded for ${requestId}: ` +
`model=${modelToRecord}, ` +
`input=${inputTokens}, output=${outputTokens}, ` +
`cache_create=${cacheCreateTokens}, cache_read=${cacheReadTokens}`
)
return true
} catch (error) {
logger.error('Failed to report Azure OpenAI usage:', error)
return false
}
}
}
const usageReporter = new AtomicUsageReporter()
// 健康检查
router.get('/health', (req, res) => {
res.status(200).json({
status: 'healthy',
service: 'azure-openai-relay',
timestamp: new Date().toISOString()
})
})
// 获取可用模型列表(兼容 OpenAI API
router.get('/models', authenticateApiKey, async (req, res) => {
try {
const models = ALL_ALLOWED_MODELS.map((model) => ({
id: `azure/${model}`,
object: 'model',
created: Date.now(),
owned_by: 'azure-openai'
}))
res.json({
object: 'list',
data: models
})
} catch (error) {
logger.error('Error fetching Azure OpenAI models:', error)
res.status(500).json({ error: { message: 'Failed to fetch models' } })
}
})
// 处理聊天完成请求
router.post('/chat/completions', authenticateApiKey, async (req, res) => {
const requestId = `azure_req_${Date.now()}_${crypto.randomBytes(8).toString('hex')}`
const sessionId = req.sessionId || req.headers['x-session-id'] || null
logger.info(`🚀 Azure OpenAI Chat Request ${requestId}`, {
apiKeyId: req.apiKey?.id,
sessionId,
model: req.body.model,
stream: req.body.stream || false,
messages: req.body.messages?.length || 0
})
try {
// 获取绑定的 Azure OpenAI 账户
let account = null
if (req.apiKey?.azureOpenaiAccountId) {
account = await azureOpenaiAccountService.getAccount(req.apiKey.azureOpenaiAccountId)
if (!account) {
logger.warn(`Bound Azure OpenAI account not found: ${req.apiKey.azureOpenaiAccountId}`)
}
}
// 如果没有绑定账户或账户不可用,选择一个可用账户
if (!account || account.isActive !== 'true') {
account = await azureOpenaiAccountService.selectAvailableAccount(sessionId)
}
// 发送请求到 Azure OpenAI
const response = await azureOpenaiRelayService.handleAzureOpenAIRequest({
account,
requestBody: req.body,
headers: req.headers,
isStream: req.body.stream || false,
endpoint: 'chat/completions'
})
// 处理流式响应
if (req.body.stream) {
await azureOpenaiRelayService.handleStreamResponse(response, res, {
onEnd: async ({ usageData, actualModel }) => {
if (usageData) {
const modelToRecord = actualModel || req.body.model || 'unknown'
await usageReporter.reportOnce(
requestId,
usageData,
req.apiKey.id,
modelToRecord,
account.id
)
}
},
onError: (error) => {
logger.error(`Stream error for request ${requestId}:`, error)
}
})
} else {
// 处理非流式响应
const { usageData, actualModel } = azureOpenaiRelayService.handleNonStreamResponse(
response,
res
)
if (usageData) {
const modelToRecord = actualModel || req.body.model || 'unknown'
await usageReporter.reportOnce(
requestId,
usageData,
req.apiKey.id,
modelToRecord,
account.id
)
}
}
} catch (error) {
logger.error(`Azure OpenAI request failed ${requestId}:`, error)
if (!res.headersSent) {
const statusCode = error.response?.status || 500
const errorMessage =
error.response?.data?.error?.message || error.message || 'Internal server error'
res.status(statusCode).json({
error: {
message: errorMessage,
type: 'azure_openai_error',
code: error.code || 'unknown'
}
})
}
}
})
// 处理嵌入请求
router.post('/embeddings', authenticateApiKey, async (req, res) => {
const requestId = `azure_embed_${Date.now()}_${crypto.randomBytes(8).toString('hex')}`
const sessionId = req.sessionId || req.headers['x-session-id'] || null
logger.info(`🚀 Azure OpenAI Embeddings Request ${requestId}`, {
apiKeyId: req.apiKey?.id,
sessionId,
model: req.body.model,
input: Array.isArray(req.body.input) ? req.body.input.length : 1
})
try {
// 获取绑定的 Azure OpenAI 账户
let account = null
if (req.apiKey?.azureOpenaiAccountId) {
account = await azureOpenaiAccountService.getAccount(req.apiKey.azureOpenaiAccountId)
if (!account) {
logger.warn(`Bound Azure OpenAI account not found: ${req.apiKey.azureOpenaiAccountId}`)
}
}
// 如果没有绑定账户或账户不可用,选择一个可用账户
if (!account || account.isActive !== 'true') {
account = await azureOpenaiAccountService.selectAvailableAccount(sessionId)
}
// 发送请求到 Azure OpenAI
const response = await azureOpenaiRelayService.handleAzureOpenAIRequest({
account,
requestBody: req.body,
headers: req.headers,
isStream: false,
endpoint: 'embeddings'
})
// 处理响应
const { usageData, actualModel } = azureOpenaiRelayService.handleNonStreamResponse(
response,
res
)
if (usageData) {
const modelToRecord = actualModel || req.body.model || 'unknown'
await usageReporter.reportOnce(requestId, usageData, req.apiKey.id, modelToRecord, account.id)
}
} catch (error) {
logger.error(`Azure OpenAI embeddings request failed ${requestId}:`, error)
if (!res.headersSent) {
const statusCode = error.response?.status || 500
const errorMessage =
error.response?.data?.error?.message || error.message || 'Internal server error'
res.status(statusCode).json({
error: {
message: errorMessage,
type: 'azure_openai_error',
code: error.code || 'unknown'
}
})
}
}
})
// 获取使用统计
router.get('/usage', authenticateApiKey, async (req, res) => {
try {
const { start_date, end_date } = req.query
const usage = await apiKeyService.getUsageStats(req.apiKey.id, start_date, end_date)
res.json({
object: 'usage',
data: usage
})
} catch (error) {
logger.error('Error fetching Azure OpenAI usage:', error)
res.status(500).json({ error: { message: 'Failed to fetch usage data' } })
}
})
module.exports = router