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:
Wesley Liddick
2026-03-02 20:28:20 +08:00
committed by GitHub
7 changed files with 203 additions and 8 deletions

View File

@@ -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' },

View File

@@ -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)
// 错误已在服务层处理,这里仅做日志记录

View File

@@ -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) {

View File

@@ -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,

View 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'
)
})
})

View 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')
})
})

View 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'
})
})
})