From 2c53aa2576f7bb62c0ad2e17efd9d3a9c2274230 Mon Sep 17 00:00:00 2001 From: qian Date: Wed, 5 Aug 2026 00:50:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(relay):=E9=99=90=E6=B5=81=E7=AD=89?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E6=83=85=E5=86=B5=E4=B8=8D=E8=AE=A1=E5=85=A5?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E8=AE=B0=E5=BD=95=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- relay/handler/relay_handler.go | 54 ++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/relay/handler/relay_handler.go b/relay/handler/relay_handler.go index 57d32db..e7f2ecd 100644 --- a/relay/handler/relay_handler.go +++ b/relay/handler/relay_handler.go @@ -604,7 +604,7 @@ func RelayHandler(ctx context.Context, body []byte, path string, headers http.He if billing != nil && preDeductAmount > 0 { _ = billing.SettleFailed(settleCtx, rc.TenantID, rc.RequestID, preDeductAmount) } - recordFailedUsage(provider, rc, selection.ChannelID, v.modelName, v.relayMode, v.isStream, err) + recordFailedUsage(provider, rc, selection, v.modelName, v.relayMode, v.isStream, err) recordChannelError(rc, selection, v.modelName, attempt, true, err, info.LatencyMs()) finalizeTrace(trace, rc, hop, false, attempt, selection, err.Error(), info.LatencyMs()) return nil, v.billingResult, helper.RemapStatusCode(constant.NewUpstreamError(502, "upstream request failed", err), info.ChannelMeta.Settings.StatusCodeMapping) @@ -646,7 +646,7 @@ func RelayHandler(ctx context.Context, body []byte, path string, headers http.He billing.IncrApiKeyQuotaUsed(settleCtx, rc.ApiKeyID, settleResult.ActualCost) } } - recordFailedUsage(provider, rc, selection.ChannelID, v.modelName, v.relayMode, v.isStream, err) + recordFailedUsage(provider, rc, selection, v.modelName, v.relayMode, v.isStream, err) finalizeTrace(trace, rc, hop, false, attempt, selection, err.Error(), info.LatencyMs()) return usage, v.billingResult, err } @@ -692,7 +692,7 @@ func RelayHandler(ctx context.Context, body []byte, path string, headers http.He if billing != nil && preDeductAmount > 0 { _ = billing.SettleFailed(settleCtx, rc.TenantID, rc.RequestID, preDeductAmount) } - recordFailedUsage(provider, rc, selection.ChannelID, v.modelName, v.relayMode, v.isStream, err) + recordFailedUsage(provider, rc, selection, v.modelName, v.relayMode, v.isStream, err) recordChannelError(rc, selection, v.modelName, attempt, true, err, info.LatencyMs()) finalizeTrace(trace, rc, hop, false, attempt, selection, err.Error(), info.LatencyMs()) return nil, v.billingResult, err @@ -845,7 +845,7 @@ func handleChannelUnavailable( fmt.Sprintf("all %d channels failed for model: %s", len(channelErrors), v.modelName), constant.ErrAllChannelsFailed, ) - recordFailedUsage(provider, rc, 0, v.modelName, v.relayMode, v.isStream, allFailedErr) + recordFailedUsage(provider, rc, nil, v.modelName, v.relayMode, v.isStream, allFailedErr) return &channelUnavailableResult{nil, v.billingResult, allFailedErr} } @@ -857,7 +857,9 @@ func handleChannelUnavailable( g.Log().Stack(false).Warningf(ctx, "[RelayHandler] 无可用渠道: model=%s tenant=%d user=%d request=%s%s", v.modelName, rc.TenantID, rc.UserID, rc.RequestID, diagSuffix) noChErr := constant.NewChannelError("no available channel for model: "+v.modelName, err) - recordFailedUsage(provider, rc, 0, v.modelName, v.relayMode, v.isStream, noChErr) + // 无可用渠道时请求未转发到任何上游(与 QPS 限流、权限/额度拒绝等前置拦截同理), + // 不记用量日志,避免零消耗的 error 记录污染请求日志;运营侧的“被拒请求量”已由 + // 上方 Warning 日志、调度器排除明细指标、chn_error_events 覆盖。 return &channelUnavailableResult{nil, v.billingResult, noChErr} } @@ -917,23 +919,31 @@ func (e *RelayErrorWithRateLimit) Error() string { return e.Message } -// recordFailedUsage 记录失败用量 -func recordFailedUsage(provider common.DataProvider, rc *RelayContext, channelID int64, modelName string, relayMode constant.RelayMode, isStream bool, err error) { - provider.RecordUsage(context.Background(), &common.UsageRecord{ - TenantID: rc.TenantID, - UserID: rc.UserID, - ApiKeyID: rc.ApiKeyID, - ProjectID: rc.ProjectID, - ChannelID: channelID, - ModelName: modelName, - RelayMode: int(relayMode), - LatencyMs: 0, - IsStream: isStream, - Success: false, - RequestID: rc.RequestID, - Status: "error", - ErrorMessage: err.Error(), - }) +// recordFailedUsage 记录失败用量。 +// selection 非 nil 时记录具体失败渠道(ID/名称/类型/上游模型),便于在用量日志定位失败渠道; +// nil 表示无单一渠道(全部渠道失败),渠道字段留空,失败详情见 error_message。 +func recordFailedUsage(provider common.DataProvider, rc *RelayContext, selection *common.ChannelSelection, modelName string, relayMode constant.RelayMode, isStream bool, err error) { + record := &common.UsageRecord{ + TenantID: rc.TenantID, + UserID: rc.UserID, + ApiKeyID: rc.ApiKeyID, + ProjectID: rc.ProjectID, + ModelName: modelName, + RequestedModel: modelName, + RelayMode: int(relayMode), + IsStream: isStream, + Success: false, + RequestID: rc.RequestID, + Status: "error", + ErrorMessage: err.Error(), + } + if selection != nil { + record.ChannelID = selection.ChannelID + record.ChannelName = selection.ChannelName + record.ChannelType = selection.ChannelType + record.UpstreamModel = selection.UpstreamModelName + } + provider.RecordUsage(context.Background(), record) } // recordChannelError 记录渠道错误事件到 chn_error_events(异步,不阻塞请求)