From ee86dbca9d8f1c52698ed03da370e3ad037cab5e Mon Sep 17 00:00:00 2001 From: shaw Date: Fri, 19 Dec 2025 15:59:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(account):=20=E8=B4=A6=E5=8F=B7=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E6=94=AF=E6=8C=81=E9=80=89=E6=8B=A9=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 GET /api/v1/admin/accounts/:id/models 接口获取账号可用模型 - 账号测试弹窗新增模型选择下拉框 - 测试时支持传入 model_id 参数,不传则默认使用 Sonnet - API Key 账号支持根据 model_mapping 映射测试模型 - 将模型常量提取到 claude 包统一管理 --- .../internal/handler/admin/account_handler.go | 67 ++++++++++++++++++- backend/internal/handler/gateway_handler.go | 24 +------ backend/internal/pkg/claude/constants.go | 48 ++++++++++++- backend/internal/server/router.go | 1 + .../internal/service/account_test_service.go | 28 ++++++-- frontend/src/api/admin/accounts.ts | 12 ++++ .../components/account/AccountTestModal.vue | 62 ++++++++++++++--- frontend/src/i18n/locales/en.ts | 1 + frontend/src/i18n/locales/zh.ts | 1 + frontend/src/types/index.ts | 8 +++ 10 files changed, 212 insertions(+), 40 deletions(-) diff --git a/backend/internal/handler/admin/account_handler.go b/backend/internal/handler/admin/account_handler.go index 71804873..4d8cbcb8 100644 --- a/backend/internal/handler/admin/account_handler.go +++ b/backend/internal/handler/admin/account_handler.go @@ -3,6 +3,7 @@ package admin import ( "strconv" + "sub2api/internal/pkg/claude" "sub2api/internal/pkg/response" "sub2api/internal/service" @@ -186,6 +187,11 @@ func (h *AccountHandler) Delete(c *gin.Context) { response.Success(c, gin.H{"message": "Account deleted successfully"}) } +// TestAccountRequest represents the request body for testing an account +type TestAccountRequest struct { + ModelID string `json:"model_id"` +} + // Test handles testing account connectivity with SSE streaming // POST /api/v1/admin/accounts/:id/test func (h *AccountHandler) Test(c *gin.Context) { @@ -195,8 +201,12 @@ func (h *AccountHandler) Test(c *gin.Context) { return } + var req TestAccountRequest + // Allow empty body, model_id is optional + _ = c.ShouldBindJSON(&req) + // Use AccountTestService to test the account with SSE streaming - if err := h.accountTestService.TestAccountConnection(c, accountID); err != nil { + if err := h.accountTestService.TestAccountConnection(c, accountID, req.ModelID); err != nil { // Error already sent via SSE, just log return } @@ -535,3 +545,58 @@ func (h *AccountHandler) SetSchedulable(c *gin.Context) { response.Success(c, account) } + +// GetAvailableModels handles getting available models for an account +// GET /api/v1/admin/accounts/:id/models +func (h *AccountHandler) GetAvailableModels(c *gin.Context) { + accountID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid account ID") + return + } + + account, err := h.adminService.GetAccount(c.Request.Context(), accountID) + if err != nil { + response.NotFound(c, "Account not found") + return + } + + // For OAuth and Setup-Token accounts: return default models + if account.IsOAuth() { + response.Success(c, claude.DefaultModels) + return + } + + // For API Key accounts: return models based on model_mapping + mapping := account.GetModelMapping() + if mapping == nil || len(mapping) == 0 { + // No mapping configured, return default models + response.Success(c, claude.DefaultModels) + return + } + + // Return mapped models (keys of the mapping are the available model IDs) + var models []claude.Model + for requestedModel := range mapping { + // Try to find display info from default models + var found bool + for _, dm := range claude.DefaultModels { + if dm.ID == requestedModel { + models = append(models, dm) + found = true + break + } + } + // If not found in defaults, create a basic entry + if !found { + models = append(models, claude.Model{ + ID: requestedModel, + Type: "model", + DisplayName: requestedModel, + CreatedAt: "", + }) + } + } + + response.Success(c, models) +} diff --git a/backend/internal/handler/gateway_handler.go b/backend/internal/handler/gateway_handler.go index 842fe06e..07d6d981 100644 --- a/backend/internal/handler/gateway_handler.go +++ b/backend/internal/handler/gateway_handler.go @@ -11,6 +11,7 @@ import ( "sub2api/internal/middleware" "sub2api/internal/model" + "sub2api/internal/pkg/claude" "sub2api/internal/service" "github.com/gin-gonic/gin" @@ -285,29 +286,8 @@ func (h *GatewayHandler) waitForSlotWithPing(c *gin.Context, slotType string, id // Models handles listing available models // GET /v1/models func (h *GatewayHandler) Models(c *gin.Context) { - models := []gin.H{ - { - "id": "claude-opus-4-5-20251101", - "type": "model", - "display_name": "Claude Opus 4.5", - "created_at": "2025-11-01T00:00:00Z", - }, - { - "id": "claude-sonnet-4-5-20250929", - "type": "model", - "display_name": "Claude Sonnet 4.5", - "created_at": "2025-09-29T00:00:00Z", - }, - { - "id": "claude-haiku-4-5-20251001", - "type": "model", - "display_name": "Claude Haiku 4.5", - "created_at": "2025-10-01T00:00:00Z", - }, - } - c.JSON(http.StatusOK, gin.H{ - "data": models, + "data": claude.DefaultModels, "object": "list", }) } diff --git a/backend/internal/pkg/claude/constants.go b/backend/internal/pkg/claude/constants.go index 6e33948c..97ad6c83 100644 --- a/backend/internal/pkg/claude/constants.go +++ b/backend/internal/pkg/claude/constants.go @@ -4,9 +4,9 @@ package claude // Beta header 常量 const ( - BetaOAuth = "oauth-2025-04-20" - BetaClaudeCode = "claude-code-20250219" - BetaInterleavedThinking = "interleaved-thinking-2025-05-14" + BetaOAuth = "oauth-2025-04-20" + BetaClaudeCode = "claude-code-20250219" + BetaInterleavedThinking = "interleaved-thinking-2025-05-14" BetaFineGrainedToolStreaming = "fine-grained-tool-streaming-2025-05-14" ) @@ -30,3 +30,45 @@ var DefaultHeaders = map[string]string{ "X-App": "cli", "Anthropic-Dangerous-Direct-Browser-Access": "true", } + +// Model 表示一个 Claude 模型 +type Model struct { + ID string `json:"id"` + Type string `json:"type"` + DisplayName string `json:"display_name"` + CreatedAt string `json:"created_at"` +} + +// DefaultModels Claude Code 客户端支持的默认模型列表 +var DefaultModels = []Model{ + { + ID: "claude-opus-4-5-20251101", + Type: "model", + DisplayName: "Claude Opus 4.5", + CreatedAt: "2025-11-01T00:00:00Z", + }, + { + ID: "claude-sonnet-4-5-20250929", + Type: "model", + DisplayName: "Claude Sonnet 4.5", + CreatedAt: "2025-09-29T00:00:00Z", + }, + { + ID: "claude-haiku-4-5-20251001", + Type: "model", + DisplayName: "Claude Haiku 4.5", + CreatedAt: "2025-10-01T00:00:00Z", + }, +} + +// DefaultModelIDs 返回默认模型的 ID 列表 +func DefaultModelIDs() []string { + ids := make([]string, len(DefaultModels)) + for i, m := range DefaultModels { + ids[i] = m.ID + } + return ids +} + +// DefaultTestModel 测试时使用的默认模型 +const DefaultTestModel = "claude-sonnet-4-5-20250929" diff --git a/backend/internal/server/router.go b/backend/internal/server/router.go index 04f5bfca..9787a3cd 100644 --- a/backend/internal/server/router.go +++ b/backend/internal/server/router.go @@ -189,6 +189,7 @@ func registerRoutes(r *gin.Engine, h *handler.Handlers, s *service.Services, rep accounts.GET("/:id/today-stats", h.Admin.Account.GetTodayStats) accounts.POST("/:id/clear-rate-limit", h.Admin.Account.ClearRateLimit) accounts.POST("/:id/schedulable", h.Admin.Account.SetSchedulable) + accounts.GET("/:id/models", h.Admin.Account.GetAvailableModels) accounts.POST("/batch", h.Admin.Account.BatchCreate) // OAuth routes diff --git a/backend/internal/service/account_test_service.go b/backend/internal/service/account_test_service.go index e5344c1e..3ab659f6 100644 --- a/backend/internal/service/account_test_service.go +++ b/backend/internal/service/account_test_service.go @@ -24,7 +24,6 @@ import ( const ( testClaudeAPIURL = "https://api.anthropic.com/v1/messages" - testModel = "claude-sonnet-4-5-20250929" ) // TestEvent represents a SSE event for account testing @@ -64,9 +63,9 @@ func generateSessionString() string { } // createTestPayload creates a Claude Code style test request payload -func createTestPayload() map[string]interface{} { +func createTestPayload(modelID string) map[string]interface{} { return map[string]interface{}{ - "model": testModel, + "model": modelID, "messages": []map[string]interface{}{ { "role": "user", @@ -101,7 +100,8 @@ func createTestPayload() map[string]interface{} { // TestAccountConnection tests an account's connection by sending a test request // All account types use full Claude Code client characteristics, only auth header differs -func (s *AccountTestService) TestAccountConnection(c *gin.Context, accountID int64) error { +// modelID is optional - if empty, defaults to claude.DefaultTestModel +func (s *AccountTestService) TestAccountConnection(c *gin.Context, accountID int64, modelID string) error { ctx := c.Request.Context() // Get account @@ -110,6 +110,22 @@ func (s *AccountTestService) TestAccountConnection(c *gin.Context, accountID int return s.sendErrorAndEnd(c, "Account not found") } + // Determine the model to use + testModelID := modelID + if testModelID == "" { + testModelID = claude.DefaultTestModel + } + + // For API Key accounts with model mapping, map the model + if account.Type == "apikey" { + mapping := account.GetModelMapping() + if mapping != nil && len(mapping) > 0 { + if mappedModel, exists := mapping[testModelID]; exists { + testModelID = mappedModel + } + } + } + // Determine authentication method and API URL var authToken string var useBearer bool @@ -165,11 +181,11 @@ func (s *AccountTestService) TestAccountConnection(c *gin.Context, accountID int c.Writer.Flush() // Create Claude Code style payload (same for all account types) - payload := createTestPayload() + payload := createTestPayload(testModelID) payloadBytes, _ := json.Marshal(payload) // Send test_start event - s.sendEvent(c, TestEvent{Type: "test_start", Model: testModel}) + s.sendEvent(c, TestEvent{Type: "test_start", Model: testModelID}) req, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(payloadBytes)) if err != nil { diff --git a/frontend/src/api/admin/accounts.ts b/frontend/src/api/admin/accounts.ts index bcb1d40e..d89e787a 100644 --- a/frontend/src/api/admin/accounts.ts +++ b/frontend/src/api/admin/accounts.ts @@ -11,6 +11,7 @@ import type { PaginatedResponse, AccountUsageInfo, WindowStats, + ClaudeModel, } from '@/types'; /** @@ -247,6 +248,16 @@ export async function setSchedulable(id: number, schedulable: boolean): Promise< return data; } +/** + * Get available models for an account + * @param id - Account ID + * @returns List of available models for this account + */ +export async function getAvailableModels(id: number): Promise { + const { data } = await apiClient.get(`/admin/accounts/${id}/models`); + return data; +} + export const accountsAPI = { list, getById, @@ -262,6 +273,7 @@ export const accountsAPI = { getTodayStats, clearRateLimit, setSchedulable, + getAvailableModels, generateAuthUrl, exchangeCode, batchCreate, diff --git a/frontend/src/components/account/AccountTestModal.vue b/frontend/src/components/account/AccountTestModal.vue index ec48f956..1e171558 100644 --- a/frontend/src/components/account/AccountTestModal.vue +++ b/frontend/src/components/account/AccountTestModal.vue @@ -36,6 +36,23 @@ + +
+ + +
+