mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-04-19 15:28:39 +00:00
1
This commit is contained in:
@@ -56,6 +56,7 @@ const PLATFORM_TEST_MODELS = {
|
||||
'claude-console': CLAUDE_MODELS,
|
||||
bedrock: BEDROCK_MODELS,
|
||||
gemini: GEMINI_MODELS,
|
||||
'gemini-api': GEMINI_MODELS,
|
||||
'openai-responses': OPENAI_MODELS,
|
||||
'azure-openai': [],
|
||||
droid: CLAUDE_MODELS,
|
||||
|
||||
@@ -2937,6 +2937,7 @@ async function handleStandardStreamGenerateContent(req, res) {
|
||||
|
||||
module.exports = {
|
||||
// 工具函数
|
||||
buildGeminiApiUrl,
|
||||
generateSessionHash,
|
||||
checkPermissions,
|
||||
ensureGeminiPermission,
|
||||
|
||||
@@ -452,4 +452,164 @@ router.post('/gemini-api-accounts/:id/reset-status', authenticateAdmin, async (r
|
||||
}
|
||||
})
|
||||
|
||||
// 测试 Gemini-API 账户连通性(SSE 流式)
|
||||
const ALLOWED_MAX_TOKENS = [100, 500, 1000, 2000, 4096]
|
||||
const sanitizeMaxTokens = (value) =>
|
||||
ALLOWED_MAX_TOKENS.includes(Number(value)) ? Number(value) : 500
|
||||
|
||||
router.post('/gemini-api-accounts/:accountId/test', authenticateAdmin, async (req, res) => {
|
||||
const { accountId } = req.params
|
||||
const { model = 'gemini-2.5-flash', prompt = 'hi' } = req.body
|
||||
const maxTokens = sanitizeMaxTokens(req.body.maxTokens)
|
||||
const { createGeminiTestPayload, extractErrorMessage } = require('../../utils/testPayloadHelper')
|
||||
const { buildGeminiApiUrl } = require('../../handlers/geminiHandlers')
|
||||
const ProxyHelper = require('../../utils/proxyHelper')
|
||||
const axios = require('axios')
|
||||
|
||||
const abortController = new AbortController()
|
||||
res.on('close', () => abortController.abort())
|
||||
|
||||
const safeWrite = (data) => {
|
||||
if (!res.writableEnded && !res.destroyed) {
|
||||
res.write(data)
|
||||
}
|
||||
}
|
||||
const safeEnd = () => {
|
||||
if (!res.writableEnded && !res.destroyed) {
|
||||
res.end()
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const account = await geminiApiAccountService.getAccount(accountId)
|
||||
if (!account) {
|
||||
return res.status(404).json({ error: 'Account not found' })
|
||||
}
|
||||
if (!account.apiKey) {
|
||||
return res.status(401).json({ error: 'API Key not found or decryption failed' })
|
||||
}
|
||||
|
||||
const baseUrl = account.baseUrl || 'https://generativelanguage.googleapis.com'
|
||||
const apiUrl = buildGeminiApiUrl(baseUrl, model, 'streamGenerateContent', account.apiKey, {
|
||||
stream: true
|
||||
})
|
||||
|
||||
// 设置 SSE 响应头
|
||||
if (res.writableEnded || res.destroyed) {
|
||||
return
|
||||
}
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
'X-Accel-Buffering': 'no'
|
||||
})
|
||||
safeWrite(`data: ${JSON.stringify({ type: 'test_start', message: 'Test started' })}\n\n`)
|
||||
|
||||
const payload = createGeminiTestPayload(model, { prompt, maxTokens })
|
||||
const requestConfig = {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
timeout: 60000,
|
||||
responseType: 'stream',
|
||||
validateStatus: () => true,
|
||||
signal: abortController.signal
|
||||
}
|
||||
|
||||
// 配置代理
|
||||
if (account.proxy) {
|
||||
const agent = ProxyHelper.createProxyAgent(account.proxy)
|
||||
if (agent) {
|
||||
requestConfig.httpsAgent = agent
|
||||
requestConfig.httpAgent = agent
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(apiUrl, payload, requestConfig)
|
||||
|
||||
if (response.status !== 200) {
|
||||
const chunks = []
|
||||
response.data.on('data', (chunk) => chunks.push(chunk))
|
||||
response.data.on('end', () => {
|
||||
const errorData = Buffer.concat(chunks).toString()
|
||||
let errorMsg = `API Error: ${response.status}`
|
||||
try {
|
||||
const json = JSON.parse(errorData)
|
||||
errorMsg = extractErrorMessage(json, errorMsg)
|
||||
} catch {
|
||||
if (errorData.length < 500) {
|
||||
errorMsg = errorData || errorMsg
|
||||
}
|
||||
}
|
||||
safeWrite(
|
||||
`data: ${JSON.stringify({ type: 'test_complete', success: false, error: errorMsg })}\n\n`
|
||||
)
|
||||
safeEnd()
|
||||
})
|
||||
response.data.on('error', () => {
|
||||
safeWrite(
|
||||
`data: ${JSON.stringify({ type: 'test_complete', success: false, error: `API Error: ${response.status}` })}\n\n`
|
||||
)
|
||||
safeEnd()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
let buffer = ''
|
||||
response.data.on('data', (chunk) => {
|
||||
buffer += chunk.toString()
|
||||
const lines = buffer.split('\n')
|
||||
buffer = lines.pop() || ''
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith('data:')) {
|
||||
continue
|
||||
}
|
||||
const jsonStr = line.substring(5).trim()
|
||||
if (!jsonStr || jsonStr === '[DONE]') {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(jsonStr)
|
||||
const text = data.candidates?.[0]?.content?.parts?.[0]?.text
|
||||
if (text) {
|
||||
safeWrite(`data: ${JSON.stringify({ type: 'content', text })}\n\n`)
|
||||
}
|
||||
} catch {
|
||||
// ignore parse errors
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
response.data.on('end', () => {
|
||||
safeWrite(`data: ${JSON.stringify({ type: 'test_complete', success: true })}\n\n`)
|
||||
safeEnd()
|
||||
})
|
||||
|
||||
response.data.on('error', (err) => {
|
||||
safeWrite(
|
||||
`data: ${JSON.stringify({ type: 'test_complete', success: false, error: err.message })}\n\n`
|
||||
)
|
||||
safeEnd()
|
||||
})
|
||||
} catch (axiosError) {
|
||||
if (axiosError.name === 'CanceledError') {
|
||||
return
|
||||
}
|
||||
safeWrite(
|
||||
`data: ${JSON.stringify({ type: 'test_complete', success: false, error: axiosError.message })}\n\n`
|
||||
)
|
||||
safeEnd()
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Gemini-API account test failed:', error)
|
||||
if (!res.headersSent) {
|
||||
return res.status(500).json({ error: 'Test failed', message: error.message })
|
||||
}
|
||||
safeWrite(`data: ${JSON.stringify({ type: 'error', error: error.message })}\n\n`)
|
||||
safeEnd()
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -415,6 +415,9 @@ const pricingService = require('../../services/pricingService')
|
||||
// 获取所有模型价格数据
|
||||
router.get('/models/pricing', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
if (!pricingService.pricingData || Object.keys(pricingService.pricingData).length === 0) {
|
||||
await pricingService.loadPricingData()
|
||||
}
|
||||
const data = pricingService.pricingData
|
||||
res.json({
|
||||
success: true,
|
||||
|
||||
@@ -333,6 +333,7 @@ const platformFallbackModels = {
|
||||
claude: 'claude-sonnet-4-5-20250929',
|
||||
'claude-console': 'claude-sonnet-4-5-20250929',
|
||||
gemini: 'gemini-2.5-pro',
|
||||
'gemini-api': 'gemini-2.5-flash',
|
||||
'openai-responses': 'gpt-5',
|
||||
droid: 'claude-sonnet-4-5-20250929',
|
||||
ccr: 'claude-sonnet-4-5-20250929'
|
||||
@@ -427,6 +428,11 @@ const platformConfigs = {
|
||||
icon: 'fas fa-gem',
|
||||
badge: 'bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-300'
|
||||
},
|
||||
'gemini-api': {
|
||||
label: 'Gemini API',
|
||||
icon: 'fas fa-gem',
|
||||
badge: 'bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-300'
|
||||
},
|
||||
'openai-responses': {
|
||||
label: 'OpenAI Responses',
|
||||
icon: 'fas fa-code',
|
||||
@@ -520,6 +526,7 @@ const getAccountEndpoint = () => {
|
||||
'claude-console': `${APP_CONFIG.apiPrefix}/admin/claude-console-accounts/${props.account.id}/test`,
|
||||
bedrock: `${APP_CONFIG.apiPrefix}/admin/bedrock-accounts/${props.account.id}/test`,
|
||||
gemini: `${APP_CONFIG.apiPrefix}/admin/gemini-accounts/${props.account.id}/test`,
|
||||
'gemini-api': `${APP_CONFIG.apiPrefix}/admin/gemini-api-accounts/${props.account.id}/test`,
|
||||
'openai-responses': `${APP_CONFIG.apiPrefix}/admin/openai-responses-accounts/${props.account.id}/test`,
|
||||
'azure-openai': `${APP_CONFIG.apiPrefix}/admin/azure-openai-accounts/${props.account.id}/test`,
|
||||
droid: `${APP_CONFIG.apiPrefix}/admin/droid-accounts/${props.account.id}/test`,
|
||||
@@ -533,7 +540,9 @@ const startTest = () => {
|
||||
const endpoint = getAccountEndpoint()
|
||||
if (!endpoint) return
|
||||
const authToken = localStorage.getItem('authToken')
|
||||
const useSSE = ['claude', 'claude-console', 'bedrock'].includes(props.account.platform)
|
||||
const useSSE = ['claude', 'claude-console', 'bedrock', 'gemini-api'].includes(
|
||||
props.account.platform
|
||||
)
|
||||
state.sendTestRequest(
|
||||
endpoint,
|
||||
{ model: selectedModel.value },
|
||||
|
||||
@@ -324,8 +324,16 @@ const loadData = async () => {
|
||||
getModelPricingApi(),
|
||||
getModelPricingStatusApi()
|
||||
])
|
||||
if (pricingResult.success) pricingData.value = pricingResult.data
|
||||
if (statusResult.success) pricingStatus.value = statusResult.data
|
||||
if (pricingResult.success) {
|
||||
pricingData.value = pricingResult.data
|
||||
} else {
|
||||
showToast(pricingResult.message || '加载模型价格失败', 'error')
|
||||
}
|
||||
if (statusResult.success) {
|
||||
pricingStatus.value = statusResult.data
|
||||
} else {
|
||||
showToast(statusResult.message || '获取价格状态失败', 'error')
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
|
||||
@@ -2618,6 +2618,7 @@ const supportedTestPlatforms = [
|
||||
'claude-console',
|
||||
'bedrock',
|
||||
'gemini',
|
||||
'gemini-api',
|
||||
'openai-responses',
|
||||
'azure-openai',
|
||||
'droid',
|
||||
|
||||
@@ -1218,6 +1218,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模型价格部分 -->
|
||||
<div v-show="activeSection === 'modelPricing'">
|
||||
<ModelPricingSection />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1797,11 +1802,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模型价格部分 -->
|
||||
<div v-show="activeSection === 'modelPricing'">
|
||||
<ModelPricingSection />
|
||||
</div>
|
||||
|
||||
<!-- ConfirmModal -->
|
||||
<ConfirmModal
|
||||
:cancel-text="confirmModalConfig.cancelText"
|
||||
|
||||
Reference in New Issue
Block a user