mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
415 lines
12 KiB
JavaScript
415 lines
12 KiB
JavaScript
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-5',
|
||
'gpt-5-mini',
|
||
'gpt-35-turbo',
|
||
'gpt-35-turbo-16k',
|
||
'codex-mini'
|
||
],
|
||
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'
|
||
}
|
||
})
|
||
}
|
||
}
|
||
})
|
||
|
||
// 处理响应请求 (gpt-5, gpt-5-mini, codex-mini models)
|
||
router.post('/responses', authenticateApiKey, async (req, res) => {
|
||
const requestId = `azure_resp_${Date.now()}_${crypto.randomBytes(8).toString('hex')}`
|
||
const sessionId = req.sessionId || req.headers['x-session-id'] || null
|
||
|
||
logger.info(`🚀 Azure OpenAI Responses 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: 'responses'
|
||
})
|
||
|
||
// 处理流式响应
|
||
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 responses 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
|