From 2be2ea697e5adb6d4b4dd11114594bf120ca8b4f Mon Sep 17 00:00:00 2001 From: tbphp Date: Sun, 3 Aug 2025 12:29:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AF=B7=E6=B1=82=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=A8=A1=E5=9E=8B=E5=AD=97=E6=AE=B5=20(#113)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 请求日志添加模型字段 * fix: 调整gemini判断模型顺序 --- internal/channel/anthropic_channel.go | 11 ++++ internal/channel/channel.go | 3 + internal/channel/gemini_channel.go | 23 ++++++++ internal/channel/openai_channel.go | 11 ++++ internal/models/types.go | 1 + internal/proxy/server.go | 17 ++++-- internal/services/log_service.go | 3 + web/src/components/logs/LogTable.vue | 84 ++++++++++++++++----------- web/src/types/models.ts | 2 + 9 files changed, 115 insertions(+), 40 deletions(-) diff --git a/internal/channel/anthropic_channel.go b/internal/channel/anthropic_channel.go index 1fac0ad..6e481c7 100644 --- a/internal/channel/anthropic_channel.go +++ b/internal/channel/anthropic_channel.go @@ -61,6 +61,17 @@ func (ch *AnthropicChannel) IsStreamRequest(c *gin.Context, bodyBytes []byte) bo return false } +func (ch *AnthropicChannel) ExtractModel(c *gin.Context, bodyBytes []byte) string { + type modelPayload struct { + Model string `json:"model"` + } + var p modelPayload + if err := json.Unmarshal(bodyBytes, &p); err == nil { + return p.Model + } + return "" +} + // ValidateKey checks if the given API key is valid by making a messages request. func (ch *AnthropicChannel) ValidateKey(ctx context.Context, key string) (bool, error) { upstreamURL := ch.getUpstreamURL() diff --git a/internal/channel/channel.go b/internal/channel/channel.go index 9a03c2d..5db4590 100644 --- a/internal/channel/channel.go +++ b/internal/channel/channel.go @@ -29,6 +29,9 @@ type ChannelProxy interface { // IsStreamRequest checks if the request is for a streaming response, IsStreamRequest(c *gin.Context, bodyBytes []byte) bool + // ExtractModel extracts the model name from the request. + ExtractModel(c *gin.Context, bodyBytes []byte) string + // ValidateKey checks if the given API key is valid. ValidateKey(ctx context.Context, key string) (bool, error) } diff --git a/internal/channel/gemini_channel.go b/internal/channel/gemini_channel.go index b44c0f2..9b4103e 100644 --- a/internal/channel/gemini_channel.go +++ b/internal/channel/gemini_channel.go @@ -71,6 +71,29 @@ func (ch *GeminiChannel) IsStreamRequest(c *gin.Context, bodyBytes []byte) bool return false } +func (ch *GeminiChannel) ExtractModel(c *gin.Context, bodyBytes []byte) string { + // gemini format + path := c.Request.URL.Path + parts := strings.Split(path, "/") + for i, part := range parts { + if part == "models" && i+1 < len(parts) { + modelPart := parts[i+1] + return strings.Split(modelPart, ":")[0] + } + } + + // openai format + type modelPayload struct { + Model string `json:"model"` + } + var p modelPayload + if err := json.Unmarshal(bodyBytes, &p); err == nil && p.Model != "" { + return strings.TrimPrefix(p.Model, "models/") + } + + return "" +} + // ValidateKey checks if the given API key is valid by making a generateContent request. func (ch *GeminiChannel) ValidateKey(ctx context.Context, key string) (bool, error) { upstreamURL := ch.getUpstreamURL() diff --git a/internal/channel/openai_channel.go b/internal/channel/openai_channel.go index df43b1a..650912d 100644 --- a/internal/channel/openai_channel.go +++ b/internal/channel/openai_channel.go @@ -60,6 +60,17 @@ func (ch *OpenAIChannel) IsStreamRequest(c *gin.Context, bodyBytes []byte) bool return false } +func (ch *OpenAIChannel) ExtractModel(c *gin.Context, bodyBytes []byte) string { + type modelPayload struct { + Model string `json:"model"` + } + var p modelPayload + if err := json.Unmarshal(bodyBytes, &p); err == nil { + return p.Model + } + return "" +} + // ValidateKey checks if the given API key is valid by making a chat completion request. func (ch *OpenAIChannel) ValidateKey(ctx context.Context, key string) (bool, error) { upstreamURL := ch.getUpstreamURL() diff --git a/internal/models/types.go b/internal/models/types.go index 7b42006..23a8741 100644 --- a/internal/models/types.go +++ b/internal/models/types.go @@ -83,6 +83,7 @@ type RequestLog struct { GroupID uint `gorm:"not null;index" json:"group_id"` GroupName string `gorm:"type:varchar(255);index" json:"group_name"` KeyValue string `gorm:"type:varchar(700)" json:"key_value"` + Model string `gorm:"type:varchar(255);index" json:"model"` IsSuccess bool `gorm:"not null" json:"is_success"` SourceIP string `gorm:"type:varchar(64)" json:"source_ip"` StatusCode int `gorm:"not null" json:"status_code"` diff --git a/internal/proxy/server.go b/internal/proxy/server.go index e7e840e..62a2e46 100644 --- a/internal/proxy/server.go +++ b/internal/proxy/server.go @@ -114,11 +114,11 @@ func (ps *ProxyServer) executeRequestWithRetry( } logrus.Debugf("Max retries exceeded for group %s after %d attempts. Parsed Error: %s", group.Name, retryCount, logMessage) - ps.logRequest(c, group, &models.APIKey{KeyValue: lastError.KeyValue}, startTime, lastError.StatusCode, retryCount, errors.New(logMessage), isStream, lastError.UpstreamAddr) + ps.logRequest(c, group, &models.APIKey{KeyValue: lastError.KeyValue}, startTime, lastError.StatusCode, retryCount, errors.New(logMessage), isStream, lastError.UpstreamAddr, channelHandler, bodyBytes) } else { response.Error(c, app_errors.ErrMaxRetriesExceeded) logrus.Debugf("Max retries exceeded for group %s after %d attempts.", group.Name, retryCount) - ps.logRequest(c, group, nil, startTime, http.StatusServiceUnavailable, retryCount, app_errors.ErrMaxRetriesExceeded, isStream, "") + ps.logRequest(c, group, nil, startTime, http.StatusServiceUnavailable, retryCount, app_errors.ErrMaxRetriesExceeded, isStream, "", channelHandler, bodyBytes) } return } @@ -127,7 +127,7 @@ func (ps *ProxyServer) executeRequestWithRetry( if err != nil { logrus.Errorf("Failed to select a key for group %s on attempt %d: %v", group.Name, retryCount+1, err) response.Error(c, app_errors.NewAPIError(app_errors.ErrNoKeysAvailable, err.Error())) - ps.logRequest(c, group, nil, startTime, http.StatusServiceUnavailable, retryCount, err, isStream, "") + ps.logRequest(c, group, nil, startTime, http.StatusServiceUnavailable, retryCount, err, isStream, "", channelHandler, bodyBytes) return } @@ -184,7 +184,7 @@ func (ps *ProxyServer) executeRequestWithRetry( if err != nil || (resp != nil && resp.StatusCode >= 400) { if err != nil && app_errors.IsIgnorableError(err) { logrus.Debugf("Client-side ignorable error for key %s, aborting retries: %v", utils.MaskAPIKey(apiKey.KeyValue), err) - ps.logRequest(c, group, apiKey, startTime, 499, retryCount+1, err, isStream, upstreamURL) + ps.logRequest(c, group, apiKey, startTime, 499, retryCount+1, err, isStream, upstreamURL, channelHandler, bodyBytes) return } @@ -227,7 +227,7 @@ func (ps *ProxyServer) executeRequestWithRetry( // ps.keyProvider.UpdateStatus(apiKey, group, true) // 请求成功不再重置成功次数,减少IO消耗 logrus.Debugf("Request for group %s succeeded on attempt %d with key %s", group.Name, retryCount+1, utils.MaskAPIKey(apiKey.KeyValue)) - ps.logRequest(c, group, apiKey, startTime, resp.StatusCode, retryCount+1, nil, isStream, upstreamURL) + ps.logRequest(c, group, apiKey, startTime, resp.StatusCode, retryCount+1, nil, isStream, upstreamURL, channelHandler, bodyBytes) for key, values := range resp.Header { for _, value := range values { @@ -254,6 +254,8 @@ func (ps *ProxyServer) logRequest( finalError error, isStream bool, upstreamAddr string, + channelHandler channel.ChannelProxy, + bodyBytes []byte, ) { if ps.requestLogService == nil { return @@ -274,6 +276,11 @@ func (ps *ProxyServer) logRequest( IsStream: isStream, UpstreamAddr: utils.TruncateString(upstreamAddr, 500), } + + if channelHandler != nil && bodyBytes != nil { + logEntry.Model = channelHandler.ExtractModel(c, bodyBytes) + } + if apiKey != nil { logEntry.KeyValue = apiKey.KeyValue } diff --git a/internal/services/log_service.go b/internal/services/log_service.go index 25e8f48..8fb0f01 100644 --- a/internal/services/log_service.go +++ b/internal/services/log_service.go @@ -45,6 +45,9 @@ func logFiltersScope(c *gin.Context) func(db *gorm.DB) *gorm.DB { } db = db.Where("key_value LIKE ?", likePattern) } + if model := c.Query("model"); model != "" { + db = db.Where("model LIKE ?", "%"+model+"%") + } if isSuccessStr := c.Query("is_success"); isSuccessStr != "" { if isSuccess, err := strconv.ParseBool(isSuccessStr); err == nil { db = db.Where("is_success = ?", isSuccess) diff --git a/web/src/components/logs/LogTable.vue b/web/src/components/logs/LogTable.vue index d5c434b..9d8b1fc 100644 --- a/web/src/components/logs/LogTable.vue +++ b/web/src/components/logs/LogTable.vue @@ -33,6 +33,7 @@ const totalPages = computed(() => Math.ceil(total.value / pageSize.value)); const filters = reactive({ group_name: "", key_value: "", + model: "", is_success: "" as "true" | "false" | "", status_code: "", source_ip: "", @@ -56,6 +57,7 @@ const loadLogs = async () => { page_size: pageSize.value, group_name: filters.group_name || undefined, key_value: filters.key_value || undefined, + model: filters.model || undefined, is_success: filters.is_success === "" ? undefined : filters.is_success === "true", status_code: filters.status_code ? parseInt(filters.status_code, 10) : undefined, source_ip: filters.source_ip || undefined, @@ -130,6 +132,7 @@ const createColumns = () => [ { title: "耗时(ms)", key: "duration_ms", width: 80 }, { title: "重试", key: "retries", width: 50 }, { title: "分组", key: "group_name", width: 120 }, + { title: "模型", key: "model", width: 300 }, { title: "Key", key: "key_value", @@ -196,6 +199,7 @@ const handleSearch = () => { const resetFilters = () => { filters.group_name = ""; filters.key_value = ""; + filters.model = ""; filters.is_success = ""; filters.status_code = ""; filters.source_ip = ""; @@ -209,6 +213,7 @@ const exportLogs = () => { const params: Omit = { group_name: filters.group_name || undefined, key_value: filters.key_value || undefined, + model: filters.model || undefined, is_success: filters.is_success === "" ? undefined : filters.is_success === "true", status_code: filters.status_code ? parseInt(filters.status_code, 10) : undefined, source_ip: filters.source_ip || undefined, @@ -238,21 +243,11 @@ function changePageSize(size: number) {
- -
-
-
@@ -265,17 +260,27 @@ function changePageSize(size: number) { />
-
+
+
+
-
-
- - - 搜索 - - 重置 - - - 导出密钥 - +
+ +
+
+ + + 搜索 + + 重置 + + + 导出密钥 + +
diff --git a/web/src/types/models.ts b/web/src/types/models.ts index 3e44117..1e6d46e 100644 --- a/web/src/types/models.ts +++ b/web/src/types/models.ts @@ -118,6 +118,7 @@ export interface RequestLog { retries: number; group_name?: string; key_value?: string; + model: string; upstream_addr: string; is_stream: boolean; } @@ -139,6 +140,7 @@ export interface LogFilter { page_size?: number; group_name?: string; key_value?: string; + model?: string; is_success?: boolean | null; status_code?: number | null; source_ip?: string;