diff --git a/backend/internal/handler/admin/account_handler.go b/backend/internal/handler/admin/account_handler.go index 4d8cbcb8..5b3ac4f6 100644 --- a/backend/internal/handler/admin/account_handler.go +++ b/backend/internal/handler/admin/account_handler.go @@ -241,16 +241,20 @@ func (h *AccountHandler) Refresh(c *gin.Context) { return } - // Update account credentials - newCredentials := map[string]interface{}{ - "access_token": tokenInfo.AccessToken, - "token_type": tokenInfo.TokenType, - "expires_in": tokenInfo.ExpiresIn, - "expires_at": tokenInfo.ExpiresAt, - "refresh_token": tokenInfo.RefreshToken, - "scope": tokenInfo.Scope, + // Copy existing credentials to preserve non-token settings (e.g., intercept_warmup_requests) + newCredentials := make(map[string]interface{}) + for k, v := range account.Credentials { + newCredentials[k] = v } + // Update token-related fields + newCredentials["access_token"] = tokenInfo.AccessToken + newCredentials["token_type"] = tokenInfo.TokenType + newCredentials["expires_in"] = tokenInfo.ExpiresIn + newCredentials["expires_at"] = tokenInfo.ExpiresAt + newCredentials["refresh_token"] = tokenInfo.RefreshToken + newCredentials["scope"] = tokenInfo.Scope + updatedAccount, err := h.adminService.UpdateAccount(c.Request.Context(), accountID, &service.UpdateAccountInput{ Credentials: newCredentials, }) diff --git a/backend/internal/handler/gateway_handler.go b/backend/internal/handler/gateway_handler.go index 07d6d981..41f91872 100644 --- a/backend/internal/handler/gateway_handler.go +++ b/backend/internal/handler/gateway_handler.go @@ -7,6 +7,7 @@ import ( "io" "log" "net/http" + "strings" "time" "sub2api/internal/middleware" @@ -127,6 +128,16 @@ func (h *GatewayHandler) Messages(c *gin.Context) { return } + // 检查预热请求拦截(在账号选择后、转发前检查) + if account.IsInterceptWarmupEnabled() && isWarmupRequest(body) { + if req.Stream { + sendMockWarmupStream(c, req.Model) + } else { + sendMockWarmupResponse(c, req.Model) + } + return + } + // 3. 获取账号并发槽位 accountReleaseFunc, err := h.acquireAccountSlotWithWait(c, account, req.Stream, &streamStarted) if err != nil { @@ -489,3 +500,89 @@ func (h *GatewayHandler) CountTokens(c *gin.Context) { return } } + +// isWarmupRequest 检测是否为预热请求(标题生成、Warmup等) +func isWarmupRequest(body []byte) bool { + // 快速检查:如果body不包含关键字,直接返回false + bodyStr := string(body) + if !strings.Contains(bodyStr, "title") && !strings.Contains(bodyStr, "Warmup") { + return false + } + + // 解析完整请求 + var req struct { + Messages []struct { + Content []struct { + Type string `json:"type"` + Text string `json:"text"` + } `json:"content"` + } `json:"messages"` + System []struct { + Text string `json:"text"` + } `json:"system"` + } + if err := json.Unmarshal(body, &req); err != nil { + return false + } + + // 检查 messages 中的标题提示模式 + for _, msg := range req.Messages { + for _, content := range msg.Content { + if content.Type == "text" { + if strings.Contains(content.Text, "Please write a 5-10 word title for the following conversation:") || + content.Text == "Warmup" { + return true + } + } + } + } + + // 检查 system 中的标题提取模式 + for _, system := range req.System { + if strings.Contains(system.Text, "nalyze if this message indicates a new conversation topic. If it does, extract a 2-3 word title") { + return true + } + } + + return false +} + +// sendMockWarmupStream 发送流式 mock 响应(用于预热请求拦截) +func sendMockWarmupStream(c *gin.Context, model string) { + c.Header("Content-Type", "text/event-stream") + c.Header("Cache-Control", "no-cache") + c.Header("Connection", "keep-alive") + c.Header("X-Accel-Buffering", "no") + + events := []string{ + `event: message_start` + "\n" + `data: {"message":{"content":[],"id":"msg_mock_warmup","model":"` + model + `","role":"assistant","stop_reason":null,"stop_sequence":null,"type":"message","usage":{"input_tokens":10,"output_tokens":0}},"type":"message_start"}`, + `event: content_block_start` + "\n" + `data: {"content_block":{"text":"","type":"text"},"index":0,"type":"content_block_start"}`, + `event: content_block_delta` + "\n" + `data: {"delta":{"text":"New","type":"text_delta"},"index":0,"type":"content_block_delta"}`, + `event: content_block_delta` + "\n" + `data: {"delta":{"text":" Conversation","type":"text_delta"},"index":0,"type":"content_block_delta"}`, + `event: content_block_stop` + "\n" + `data: {"index":0,"type":"content_block_stop"}`, + `event: message_delta` + "\n" + `data: {"delta":{"stop_reason":"end_turn","stop_sequence":null},"type":"message_delta","usage":{"input_tokens":10,"output_tokens":2}}`, + `event: message_stop` + "\n" + `data: {"type":"message_stop"}`, + } + + for _, event := range events { + _, _ = c.Writer.WriteString(event + "\n\n") + c.Writer.Flush() + time.Sleep(20 * time.Millisecond) + } +} + +// sendMockWarmupResponse 发送非流式 mock 响应(用于预热请求拦截) +func sendMockWarmupResponse(c *gin.Context, model string) { + c.JSON(http.StatusOK, gin.H{ + "id": "msg_mock_warmup", + "type": "message", + "role": "assistant", + "model": model, + "content": []gin.H{{"type": "text", "text": "New Conversation"}}, + "stop_reason": "end_turn", + "usage": gin.H{ + "input_tokens": 10, + "output_tokens": 2, + }, + }) +} diff --git a/backend/internal/model/account.go b/backend/internal/model/account.go index e42c813f..3040cf8f 100644 --- a/backend/internal/model/account.go +++ b/backend/internal/model/account.go @@ -263,3 +263,17 @@ func (a *Account) ShouldHandleErrorCode(statusCode int) bool { } return false } + +// IsInterceptWarmupEnabled 检查是否启用预热请求拦截 +// 启用后,标题生成、Warmup等预热请求将返回mock响应,不消耗上游token +func (a *Account) IsInterceptWarmupEnabled() bool { + if a.Credentials == nil { + return false + } + if v, ok := a.Credentials["intercept_warmup_requests"]; ok { + if enabled, ok := v.(bool); ok { + return enabled + } + } + return false +} diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue index 513f9933..09c6c253 100644 --- a/frontend/src/components/account/CreateAccountModal.vue +++ b/frontend/src/components/account/CreateAccountModal.vue @@ -418,6 +418,31 @@ + +
{{ t('admin.accounts.interceptWarmupRequestsDesc') }}
+{{ t('admin.accounts.interceptWarmupRequestsDesc') }}
+