Skip to content

Commit 2be2ea6

Browse files
authored
feat: 请求日志添加模型字段 (#113)
* feat: 请求日志添加模型字段 * fix: 调整gemini判断模型顺序
1 parent 2d9a785 commit 2be2ea6

File tree

9 files changed

+110
-35
lines changed

9 files changed

+110
-35
lines changed

internal/channel/anthropic_channel.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,17 @@ func (ch *AnthropicChannel) IsStreamRequest(c *gin.Context, bodyBytes []byte) bo
6161
return false
6262
}
6363

64+
func (ch *AnthropicChannel) ExtractModel(c *gin.Context, bodyBytes []byte) string {
65+
type modelPayload struct {
66+
Model string `json:"model"`
67+
}
68+
var p modelPayload
69+
if err := json.Unmarshal(bodyBytes, &p); err == nil {
70+
return p.Model
71+
}
72+
return ""
73+
}
74+
6475
// ValidateKey checks if the given API key is valid by making a messages request.
6576
func (ch *AnthropicChannel) ValidateKey(ctx context.Context, key string) (bool, error) {
6677
upstreamURL := ch.getUpstreamURL()

internal/channel/channel.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ type ChannelProxy interface {
2929
// IsStreamRequest checks if the request is for a streaming response,
3030
IsStreamRequest(c *gin.Context, bodyBytes []byte) bool
3131

32+
// ExtractModel extracts the model name from the request.
33+
ExtractModel(c *gin.Context, bodyBytes []byte) string
34+
3235
// ValidateKey checks if the given API key is valid.
3336
ValidateKey(ctx context.Context, key string) (bool, error)
3437
}

internal/channel/gemini_channel.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,29 @@ func (ch *GeminiChannel) IsStreamRequest(c *gin.Context, bodyBytes []byte) bool
7171
return false
7272
}
7373

74+
func (ch *GeminiChannel) ExtractModel(c *gin.Context, bodyBytes []byte) string {
75+
// gemini format
76+
path := c.Request.URL.Path
77+
parts := strings.Split(path, "/")
78+
for i, part := range parts {
79+
if part == "models" && i+1 < len(parts) {
80+
modelPart := parts[i+1]
81+
return strings.Split(modelPart, ":")[0]
82+
}
83+
}
84+
85+
// openai format
86+
type modelPayload struct {
87+
Model string `json:"model"`
88+
}
89+
var p modelPayload
90+
if err := json.Unmarshal(bodyBytes, &p); err == nil && p.Model != "" {
91+
return strings.TrimPrefix(p.Model, "models/")
92+
}
93+
94+
return ""
95+
}
96+
7497
// ValidateKey checks if the given API key is valid by making a generateContent request.
7598
func (ch *GeminiChannel) ValidateKey(ctx context.Context, key string) (bool, error) {
7699
upstreamURL := ch.getUpstreamURL()

internal/channel/openai_channel.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,17 @@ func (ch *OpenAIChannel) IsStreamRequest(c *gin.Context, bodyBytes []byte) bool
6060
return false
6161
}
6262

63+
func (ch *OpenAIChannel) ExtractModel(c *gin.Context, bodyBytes []byte) string {
64+
type modelPayload struct {
65+
Model string `json:"model"`
66+
}
67+
var p modelPayload
68+
if err := json.Unmarshal(bodyBytes, &p); err == nil {
69+
return p.Model
70+
}
71+
return ""
72+
}
73+
6374
// ValidateKey checks if the given API key is valid by making a chat completion request.
6475
func (ch *OpenAIChannel) ValidateKey(ctx context.Context, key string) (bool, error) {
6576
upstreamURL := ch.getUpstreamURL()

internal/models/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ type RequestLog struct {
8383
GroupID uint `gorm:"not null;index" json:"group_id"`
8484
GroupName string `gorm:"type:varchar(255);index" json:"group_name"`
8585
KeyValue string `gorm:"type:varchar(700)" json:"key_value"`
86+
Model string `gorm:"type:varchar(255);index" json:"model"`
8687
IsSuccess bool `gorm:"not null" json:"is_success"`
8788
SourceIP string `gorm:"type:varchar(64)" json:"source_ip"`
8889
StatusCode int `gorm:"not null" json:"status_code"`

internal/proxy/server.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,11 @@ func (ps *ProxyServer) executeRequestWithRetry(
114114
}
115115
logrus.Debugf("Max retries exceeded for group %s after %d attempts. Parsed Error: %s", group.Name, retryCount, logMessage)
116116

117-
ps.logRequest(c, group, &models.APIKey{KeyValue: lastError.KeyValue}, startTime, lastError.StatusCode, retryCount, errors.New(logMessage), isStream, lastError.UpstreamAddr)
117+
ps.logRequest(c, group, &models.APIKey{KeyValue: lastError.KeyValue}, startTime, lastError.StatusCode, retryCount, errors.New(logMessage), isStream, lastError.UpstreamAddr, channelHandler, bodyBytes)
118118
} else {
119119
response.Error(c, app_errors.ErrMaxRetriesExceeded)
120120
logrus.Debugf("Max retries exceeded for group %s after %d attempts.", group.Name, retryCount)
121-
ps.logRequest(c, group, nil, startTime, http.StatusServiceUnavailable, retryCount, app_errors.ErrMaxRetriesExceeded, isStream, "")
121+
ps.logRequest(c, group, nil, startTime, http.StatusServiceUnavailable, retryCount, app_errors.ErrMaxRetriesExceeded, isStream, "", channelHandler, bodyBytes)
122122
}
123123
return
124124
}
@@ -127,7 +127,7 @@ func (ps *ProxyServer) executeRequestWithRetry(
127127
if err != nil {
128128
logrus.Errorf("Failed to select a key for group %s on attempt %d: %v", group.Name, retryCount+1, err)
129129
response.Error(c, app_errors.NewAPIError(app_errors.ErrNoKeysAvailable, err.Error()))
130-
ps.logRequest(c, group, nil, startTime, http.StatusServiceUnavailable, retryCount, err, isStream, "")
130+
ps.logRequest(c, group, nil, startTime, http.StatusServiceUnavailable, retryCount, err, isStream, "", channelHandler, bodyBytes)
131131
return
132132
}
133133

@@ -184,7 +184,7 @@ func (ps *ProxyServer) executeRequestWithRetry(
184184
if err != nil || (resp != nil && resp.StatusCode >= 400) {
185185
if err != nil && app_errors.IsIgnorableError(err) {
186186
logrus.Debugf("Client-side ignorable error for key %s, aborting retries: %v", utils.MaskAPIKey(apiKey.KeyValue), err)
187-
ps.logRequest(c, group, apiKey, startTime, 499, retryCount+1, err, isStream, upstreamURL)
187+
ps.logRequest(c, group, apiKey, startTime, 499, retryCount+1, err, isStream, upstreamURL, channelHandler, bodyBytes)
188188
return
189189
}
190190

@@ -227,7 +227,7 @@ func (ps *ProxyServer) executeRequestWithRetry(
227227

228228
// ps.keyProvider.UpdateStatus(apiKey, group, true) // 请求成功不再重置成功次数,减少IO消耗
229229
logrus.Debugf("Request for group %s succeeded on attempt %d with key %s", group.Name, retryCount+1, utils.MaskAPIKey(apiKey.KeyValue))
230-
ps.logRequest(c, group, apiKey, startTime, resp.StatusCode, retryCount+1, nil, isStream, upstreamURL)
230+
ps.logRequest(c, group, apiKey, startTime, resp.StatusCode, retryCount+1, nil, isStream, upstreamURL, channelHandler, bodyBytes)
231231

232232
for key, values := range resp.Header {
233233
for _, value := range values {
@@ -254,6 +254,8 @@ func (ps *ProxyServer) logRequest(
254254
finalError error,
255255
isStream bool,
256256
upstreamAddr string,
257+
channelHandler channel.ChannelProxy,
258+
bodyBytes []byte,
257259
) {
258260
if ps.requestLogService == nil {
259261
return
@@ -274,6 +276,11 @@ func (ps *ProxyServer) logRequest(
274276
IsStream: isStream,
275277
UpstreamAddr: utils.TruncateString(upstreamAddr, 500),
276278
}
279+
280+
if channelHandler != nil && bodyBytes != nil {
281+
logEntry.Model = channelHandler.ExtractModel(c, bodyBytes)
282+
}
283+
277284
if apiKey != nil {
278285
logEntry.KeyValue = apiKey.KeyValue
279286
}

internal/services/log_service.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ func logFiltersScope(c *gin.Context) func(db *gorm.DB) *gorm.DB {
4545
}
4646
db = db.Where("key_value LIKE ?", likePattern)
4747
}
48+
if model := c.Query("model"); model != "" {
49+
db = db.Where("model LIKE ?", "%"+model+"%")
50+
}
4851
if isSuccessStr := c.Query("is_success"); isSuccessStr != "" {
4952
if isSuccess, err := strconv.ParseBool(isSuccessStr); err == nil {
5053
db = db.Where("is_success = ?", isSuccess)

web/src/components/logs/LogTable.vue

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const totalPages = computed(() => Math.ceil(total.value / pageSize.value));
3333
const filters = reactive({
3434
group_name: "",
3535
key_value: "",
36+
model: "",
3637
is_success: "" as "true" | "false" | "",
3738
status_code: "",
3839
source_ip: "",
@@ -56,6 +57,7 @@ const loadLogs = async () => {
5657
page_size: pageSize.value,
5758
group_name: filters.group_name || undefined,
5859
key_value: filters.key_value || undefined,
60+
model: filters.model || undefined,
5961
is_success: filters.is_success === "" ? undefined : filters.is_success === "true",
6062
status_code: filters.status_code ? parseInt(filters.status_code, 10) : undefined,
6163
source_ip: filters.source_ip || undefined,
@@ -130,6 +132,7 @@ const createColumns = () => [
130132
{ title: "耗时(ms)", key: "duration_ms", width: 80 },
131133
{ title: "重试", key: "retries", width: 50 },
132134
{ title: "分组", key: "group_name", width: 120 },
135+
{ title: "模型", key: "model", width: 300 },
133136
{
134137
title: "Key",
135138
key: "key_value",
@@ -196,6 +199,7 @@ const handleSearch = () => {
196199
const resetFilters = () => {
197200
filters.group_name = "";
198201
filters.key_value = "";
202+
filters.model = "";
199203
filters.is_success = "";
200204
filters.status_code = "";
201205
filters.source_ip = "";
@@ -209,6 +213,7 @@ const exportLogs = () => {
209213
const params: Omit<LogFilter, "page" | "page_size"> = {
210214
group_name: filters.group_name || undefined,
211215
key_value: filters.key_value || undefined,
216+
model: filters.model || undefined,
212217
is_success: filters.is_success === "" ? undefined : filters.is_success === "true",
213218
status_code: filters.status_code ? parseInt(filters.status_code, 10) : undefined,
214219
source_ip: filters.source_ip || undefined,
@@ -238,44 +243,44 @@ function changePageSize(size: number) {
238243
<div class="filter-row">
239244
<div class="filter-grid">
240245
<div class="filter-item">
241-
<n-input
242-
v-model:value="filters.group_name"
243-
placeholder="分组名"
246+
<n-select
247+
v-model:value="filters.is_success"
248+
:options="successOptions"
244249
size="small"
245-
clearable
246-
@keyup.enter="handleSearch"
250+
@update:value="handleSearch"
247251
/>
248252
</div>
249253
<div class="filter-item">
250254
<n-input
251-
v-model:value="filters.key_value"
252-
placeholder="密钥"
255+
v-model:value="filters.status_code"
256+
placeholder="状态码"
253257
size="small"
254258
clearable
255259
@keyup.enter="handleSearch"
256260
/>
257261
</div>
258262
<div class="filter-item">
259263
<n-input
260-
v-model:value="filters.status_code"
261-
placeholder="状态码"
264+
v-model:value="filters.group_name"
265+
placeholder="分组名"
262266
size="small"
263267
clearable
264268
@keyup.enter="handleSearch"
265269
/>
266270
</div>
267271
<div class="filter-item">
268-
<n-select
269-
v-model:value="filters.is_success"
270-
:options="successOptions"
272+
<n-input
273+
v-model:value="filters.model"
274+
placeholder="模型"
271275
size="small"
272-
@update:value="handleSearch"
276+
clearable
277+
@keyup.enter="handleSearch"
273278
/>
274279
</div>
275280
<div class="filter-item">
276281
<n-input
277-
v-model:value="filters.error_contains"
278-
placeholder="错误信息"
282+
v-model:value="filters.key_value"
283+
placeholder="密钥"
279284
size="small"
280285
clearable
281286
@keyup.enter="handleSearch"
@@ -299,21 +304,30 @@ function changePageSize(size: number) {
299304
placeholder="结束时间"
300305
/>
301306
</div>
302-
</div>
303-
<div class="filter-actions">
304-
<n-button ghost size="small" :disabled="loading" @click="handleSearch">
305-
<template #icon>
306-
<n-icon :component="Search" />
307-
</template>
308-
搜索
309-
</n-button>
310-
<n-button size="small" @click="resetFilters">重置</n-button>
311-
<n-button size="small" type="primary" ghost @click="exportLogs">
312-
<template #icon>
313-
<n-icon :component="DownloadOutline" />
314-
</template>
315-
导出密钥
316-
</n-button>
307+
<div class="filter-item">
308+
<n-input
309+
v-model:value="filters.error_contains"
310+
placeholder="错误信息"
311+
size="small"
312+
clearable
313+
@keyup.enter="handleSearch"
314+
/>
315+
</div>
316+
<div class="filter-actions">
317+
<n-button ghost size="small" :disabled="loading" @click="handleSearch">
318+
<template #icon>
319+
<n-icon :component="Search" />
320+
</template>
321+
搜索
322+
</n-button>
323+
<n-button size="small" @click="resetFilters">重置</n-button>
324+
<n-button size="small" type="primary" ghost @click="exportLogs">
325+
<template #icon>
326+
<n-icon :component="DownloadOutline" />
327+
</template>
328+
导出密钥
329+
</n-button>
330+
</div>
317331
</div>
318332
</div>
319333
</div>

web/src/types/models.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export interface RequestLog {
118118
retries: number;
119119
group_name?: string;
120120
key_value?: string;
121+
model: string;
121122
upstream_addr: string;
122123
is_stream: boolean;
123124
}
@@ -139,6 +140,7 @@ export interface LogFilter {
139140
page_size?: number;
140141
group_name?: string;
141142
key_value?: string;
143+
model?: string;
142144
is_success?: boolean | null;
143145
status_code?: number | null;
144146
source_ip?: string;

0 commit comments

Comments
 (0)