mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-03-29 23:14:57 +00:00
Merge pull request #1042 from learnerLj/fix/claude-console-test-model [skip ci]
fix: respect selected model in claude-console connectivity test
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
|
||||
const CLAUDE_MODELS = [
|
||||
{ value: 'claude-opus-4-6', label: 'Claude Opus 4.6' },
|
||||
{ value: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6' },
|
||||
{ value: 'claude-opus-4-5-20251101', label: 'Claude Opus 4.5' },
|
||||
{ value: 'claude-sonnet-4-5-20250929', label: 'Claude Sonnet 4.5' },
|
||||
{ value: 'claude-sonnet-4-20250514', label: 'Claude Sonnet 4' },
|
||||
|
||||
@@ -485,10 +485,15 @@ router.post('/claude-console-accounts/reset-all-usage', authenticateAdmin, async
|
||||
// 测试Claude Console账户连通性(流式响应)- 复用 claudeConsoleRelayService
|
||||
router.post('/claude-console-accounts/:accountId/test', authenticateAdmin, async (req, res) => {
|
||||
const { accountId } = req.params
|
||||
const model = typeof req.body?.model === 'string' ? req.body.model.trim() : ''
|
||||
|
||||
if (!model) {
|
||||
return res.status(400).json({ error: 'model is required' })
|
||||
}
|
||||
|
||||
try {
|
||||
// 直接调用服务层的测试方法
|
||||
await claudeConsoleRelayService.testAccountConnection(accountId, res)
|
||||
await claudeConsoleRelayService.testAccountConnection(accountId, res, model)
|
||||
} catch (error) {
|
||||
logger.error(`❌ Failed to test Claude Console account:`, error)
|
||||
// 错误已在服务层处理,这里仅做日志记录
|
||||
|
||||
@@ -1455,8 +1455,11 @@ class ClaudeConsoleRelayService {
|
||||
}
|
||||
|
||||
// 🧪 测试账号连接(供Admin API使用)
|
||||
async testAccountConnection(accountId, responseStream) {
|
||||
const { sendStreamTestRequest } = require('../../utils/testPayloadHelper')
|
||||
async testAccountConnection(accountId, responseStream, model) {
|
||||
const {
|
||||
createClaudeTestPayload,
|
||||
sendStreamTestRequest
|
||||
} = require('../../utils/testPayloadHelper')
|
||||
|
||||
try {
|
||||
const account = await claudeConsoleAccountService.getAccount(accountId)
|
||||
@@ -1470,14 +1473,24 @@ class ClaudeConsoleRelayService {
|
||||
const apiUrl = cleanUrl.endsWith('/v1/messages')
|
||||
? cleanUrl
|
||||
: `${cleanUrl}/v1/messages?beta=true`
|
||||
const payload = createClaudeTestPayload(model, { stream: true })
|
||||
|
||||
await sendStreamTestRequest({
|
||||
const extraHeaders = account.userAgent ? { 'User-Agent': account.userAgent } : {}
|
||||
const requestOptions = {
|
||||
apiUrl,
|
||||
authorization: `Bearer ${account.apiKey}`,
|
||||
responseStream,
|
||||
payload,
|
||||
proxyAgent: claudeConsoleAccountService._createProxyAgent(account.proxy),
|
||||
extraHeaders: account.userAgent ? { 'User-Agent': account.userAgent } : {}
|
||||
})
|
||||
extraHeaders
|
||||
}
|
||||
|
||||
if (account.apiKey && account.apiKey.startsWith('sk-ant-')) {
|
||||
requestOptions.extraHeaders['x-api-key'] = account.apiKey
|
||||
} else {
|
||||
requestOptions.authorization = `Bearer ${account.apiKey}`
|
||||
}
|
||||
|
||||
await sendStreamTestRequest(requestOptions)
|
||||
} catch (error) {
|
||||
logger.error(`❌ Test account connection failed:`, error)
|
||||
if (!responseStream.headersSent) {
|
||||
|
||||
@@ -146,7 +146,7 @@ async function sendStreamTestRequest(options) {
|
||||
'Content-Type': 'application/json',
|
||||
'anthropic-version': '2023-06-01',
|
||||
'User-Agent': 'claude-cli/2.0.52 (external, cli)',
|
||||
authorization,
|
||||
...(authorization ? { authorization } : {}),
|
||||
...extraHeaders
|
||||
},
|
||||
timeout,
|
||||
|
||||
73
tests/claudeConsoleAccounts.test.js
Normal file
73
tests/claudeConsoleAccounts.test.js
Normal file
@@ -0,0 +1,73 @@
|
||||
const express = require('express')
|
||||
const request = require('supertest')
|
||||
|
||||
jest.mock('../src/middleware/auth', () => ({
|
||||
authenticateAdmin: (req, res, next) => next()
|
||||
}))
|
||||
|
||||
jest.mock('../src/services/relay/claudeConsoleRelayService', () => ({
|
||||
testAccountConnection: jest.fn(async (accountId, res) =>
|
||||
res.status(200).json({ success: true, accountId })
|
||||
)
|
||||
}))
|
||||
|
||||
jest.mock('../src/services/account/claudeConsoleAccountService', () => ({}))
|
||||
jest.mock('../src/services/accountGroupService', () => ({}))
|
||||
jest.mock('../src/services/apiKeyService', () => ({}))
|
||||
jest.mock('../src/models/redis', () => ({}))
|
||||
jest.mock('../src/utils/logger', () => ({
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
success: jest.fn()
|
||||
}))
|
||||
jest.mock('../src/utils/webhookNotifier', () => ({}))
|
||||
jest.mock('../src/routes/admin/utils', () => ({
|
||||
formatAccountExpiry: jest.fn((account) => account),
|
||||
mapExpiryField: jest.fn((updates) => updates)
|
||||
}))
|
||||
|
||||
const claudeConsoleRelayService = require('../src/services/relay/claudeConsoleRelayService')
|
||||
const claudeConsoleAccountsRouter = require('../src/routes/admin/claudeConsoleAccounts')
|
||||
|
||||
describe('POST /admin/claude-console-accounts/:accountId/test', () => {
|
||||
const buildApp = () => {
|
||||
const app = express()
|
||||
app.use(express.json())
|
||||
app.use('/admin', claudeConsoleAccountsRouter)
|
||||
return app
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('returns 400 when model is missing', async () => {
|
||||
const app = buildApp()
|
||||
|
||||
const response = await request(app)
|
||||
.post('/admin/claude-console-accounts/account-1/test')
|
||||
.send({})
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(response.body).toEqual({ error: 'model is required' })
|
||||
expect(claudeConsoleRelayService.testAccountConnection).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('passes model through to relay service when provided', async () => {
|
||||
const app = buildApp()
|
||||
|
||||
const response = await request(app)
|
||||
.post('/admin/claude-console-accounts/account-1/test')
|
||||
.send({ model: 'claude-sonnet-4-6' })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(claudeConsoleRelayService.testAccountConnection).toHaveBeenCalledTimes(1)
|
||||
expect(claudeConsoleRelayService.testAccountConnection).toHaveBeenCalledWith(
|
||||
'account-1',
|
||||
expect.any(Object),
|
||||
'claude-sonnet-4-6'
|
||||
)
|
||||
})
|
||||
})
|
||||
93
tests/claudeConsoleRelayService.test.js
Normal file
93
tests/claudeConsoleRelayService.test.js
Normal file
@@ -0,0 +1,93 @@
|
||||
jest.mock('../src/utils/logger', () => ({
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn()
|
||||
}))
|
||||
|
||||
jest.mock('../src/services/account/claudeConsoleAccountService', () => ({
|
||||
getAccount: jest.fn(),
|
||||
_createProxyAgent: jest.fn()
|
||||
}))
|
||||
|
||||
jest.mock('../config/config', () => ({}), {
|
||||
virtual: true
|
||||
})
|
||||
jest.mock('../src/models/redis', () => ({}))
|
||||
|
||||
jest.mock('../src/utils/testPayloadHelper', () => ({
|
||||
createClaudeTestPayload: jest.fn(),
|
||||
sendStreamTestRequest: jest.fn()
|
||||
}))
|
||||
|
||||
const claudeConsoleRelayService = require('../src/services/relay/claudeConsoleRelayService')
|
||||
const claudeConsoleAccountService = require('../src/services/account/claudeConsoleAccountService')
|
||||
const { createClaudeTestPayload, sendStreamTestRequest } = require('../src/utils/testPayloadHelper')
|
||||
|
||||
describe('claudeConsoleRelayService.testAccountConnection', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('passes selected model stream payload and bearer auth for non sk-ant key', async () => {
|
||||
claudeConsoleAccountService.getAccount.mockResolvedValue({
|
||||
name: 'Console A1',
|
||||
apiUrl: 'https://console.example.com',
|
||||
apiKey: 'test-key',
|
||||
proxy: null,
|
||||
userAgent: null
|
||||
})
|
||||
claudeConsoleAccountService._createProxyAgent.mockReturnValue(undefined)
|
||||
|
||||
const payload = {
|
||||
model: 'claude-sonnet-4-6',
|
||||
stream: true
|
||||
}
|
||||
createClaudeTestPayload.mockReturnValue(payload)
|
||||
sendStreamTestRequest.mockResolvedValue(undefined)
|
||||
|
||||
const res = {}
|
||||
await claudeConsoleRelayService.testAccountConnection('a1', res, 'claude-sonnet-4-6')
|
||||
|
||||
expect(createClaudeTestPayload).toHaveBeenCalledWith('claude-sonnet-4-6', { stream: true })
|
||||
expect(sendStreamTestRequest).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
payload,
|
||||
authorization: 'Bearer test-key'
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('passes selected model stream payload and x-api-key for sk-ant key', async () => {
|
||||
claudeConsoleAccountService.getAccount.mockResolvedValue({
|
||||
name: 'Console A1',
|
||||
apiUrl: 'https://console.example.com',
|
||||
apiKey: 'sk-ant-test-key',
|
||||
proxy: null,
|
||||
userAgent: null
|
||||
})
|
||||
claudeConsoleAccountService._createProxyAgent.mockReturnValue(undefined)
|
||||
|
||||
const payload = {
|
||||
model: 'claude-sonnet-4-6',
|
||||
stream: true
|
||||
}
|
||||
createClaudeTestPayload.mockReturnValue(payload)
|
||||
sendStreamTestRequest.mockResolvedValue(undefined)
|
||||
|
||||
const res = {}
|
||||
await claudeConsoleRelayService.testAccountConnection('a1', res, 'claude-sonnet-4-6')
|
||||
|
||||
expect(createClaudeTestPayload).toHaveBeenCalledWith('claude-sonnet-4-6', { stream: true })
|
||||
const requestOptions = sendStreamTestRequest.mock.calls[0][0]
|
||||
expect(requestOptions).toEqual(
|
||||
expect.objectContaining({
|
||||
payload,
|
||||
extraHeaders: expect.objectContaining({
|
||||
'x-api-key': 'sk-ant-test-key'
|
||||
})
|
||||
})
|
||||
)
|
||||
expect(requestOptions).not.toHaveProperty('authorization')
|
||||
})
|
||||
})
|
||||
10
tests/modelsConfig.test.js
Normal file
10
tests/modelsConfig.test.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const { CLAUDE_MODELS } = require('../config/models')
|
||||
|
||||
describe('models config', () => {
|
||||
it('places Claude Sonnet 4.6 as the second Claude model option', () => {
|
||||
expect(CLAUDE_MODELS[1]).toEqual({
|
||||
value: 'claude-sonnet-4-6',
|
||||
label: 'Claude Sonnet 4.6'
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user