Files
team-api/relay/common/debug_session.go
T

248 lines
8.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package common
import (
"context"
"net/http"
"github.com/qianfree/team-api/relay/constant"
)
// DebugSession 渠道调试日志会话:挂载于 RelayContext.Debug,整请求存活。
// 重试循环中首个「调试开关开启」的渠道尝试触发创建:捕获段1(客户端请求)并包装段4
// 的客户端响应 writer(保持到请求结束,中间失败尝试不向客户端写字节,累积字节即最终响应)。
// 中间失败尝试经 DebugAttempt.Submit 立即落库(is_final=false);最终尝试经 MarkFinal
// 挂起,由外层 handler 在响应写完后调用 FinalizeAndSubmit 补段4并统一提交(幂等)。
type DebugSession struct {
requestID string
tenantID int64
userID int64
apiKeyID int64
path string
clientReqHeaders map[string]string // 段1(脱敏后)
clientReqBody []byte // 段1 完整请求体
clientWriter *DebugClientWriter // 段4 捕获 writer
pending *DebugAttempt // MarkFinal 挂起的最终尝试
finalized bool
}
// NewDebugSession 创建调试会话
func NewDebugSession(requestID string, tenantID, userID, apiKeyID int64, path string) *DebugSession {
return &DebugSession{
requestID: requestID,
tenantID: tenantID,
userID: userID,
apiKeyID: apiKeyID,
path: path,
}
}
// CaptureClientRequest 捕获段1:客户端请求头(脱敏)+ 完整请求体
func (s *DebugSession) CaptureClientRequest(headers http.Header, body []byte) {
s.clientReqHeaders = MaskHeaders(headers)
s.clientReqBody = body
}
// SetClientWriter 回填段4捕获 writer(由 RelayHandler 包装 rc.Writer 后调用)
func (s *DebugSession) SetClientWriter(w *DebugClientWriter) {
s.clientWriter = w
}
// BeginAttempt 为当前渠道尝试创建捕获上下文(每轮重试一次)
func (s *DebugSession) BeginAttempt(selection *ChannelSelection, modelName, relayModeStr string, isStream bool, retryIndex int) *DebugAttempt {
return &DebugAttempt{
session: s,
Capture: &DebugAttemptCapture{},
channelID: selection.ChannelID,
channelName: selection.ChannelName,
channelType: selection.ChannelType,
modelName: modelName,
upstreamModel: selection.UpstreamModelName,
relayMode: relayModeStr,
isStream: isStream,
retryIndex: retryIndex,
}
}
// BeginTaskAttempt 为异步任务提交创建捕获上下文。任务提交为单次尝试(无重试循环),
// isStream 恒 false,渠道元信息取 ChannelMeta(任务链路经 selectTaskChannel 物化,
// 无 ChannelSelection)。上游请求经适配器 DoRequest(ctx) 携带的捕获器镜像段2/3。
func (s *DebugSession) BeginTaskAttempt(meta *ChannelMeta, modelName, relayModeStr string, retryIndex int) *DebugAttempt {
return &DebugAttempt{
session: s,
Capture: &DebugAttemptCapture{},
channelID: meta.ChannelID,
channelName: meta.ChannelName,
channelType: meta.ChannelType,
modelName: modelName,
upstreamModel: meta.UpstreamModelName,
relayMode: relayModeStr,
isStream: false,
retryIndex: retryIndex,
}
}
// DebugAttempt 单次尝试的调试上下文:会话引用 + 渠道元数据 + 传输层捕获器。
// Capture 经 WithDebugAttempt 挂入 ctx,由 DebugRoundTripper 填充段2/段3。
type DebugAttempt struct {
session *DebugSession
Capture *DebugAttemptCapture
channelID int64
channelName string
channelType int
modelName string
upstreamModel string
relayMode string
isStream bool
retryIndex int
errText string
conversion *DebugConversion // 协议转换信息(CaptureProtocol 快照)
}
// CaptureProtocol 快照协议转换信息(RelayHandler 在 buildRelayInfo 之后调用一次)。
// 上游协议按桥接标志与渠道类型推导:直传 > Responses 桥接 > Responses 直连 > 渠道原生协议。
func (a *DebugAttempt) CaptureProtocol(info *RelayInfo) {
if a == nil || info == nil {
return
}
conv := &DebugConversion{
ClientFormat: string(info.GetOriginalClientFormat()),
}
switch {
case info.ChannelMeta != nil && info.ChannelMeta.Settings.PassThroughBodyEnabled:
conv.UpstreamFormat = conv.ClientFormat
conv.Bridge = "pass_through"
case info.UseResponsesAPI:
conv.UpstreamFormat = string(constant.RelayFormatResponses)
conv.Bridge = "responses_api"
case info.ChannelMeta != nil && info.ChannelMeta.UpstreamSpeaksResponses():
conv.UpstreamFormat = string(constant.RelayFormatResponses)
conv.Bridge = "responses_direct"
default:
conv.UpstreamFormat = nativeFormatOfProvider(a.channelType)
}
for _, f := range info.ConversionChain() {
conv.Chain = append(conv.Chain, string(f))
}
// relaykit 链未覆盖(直传/同构/adaptor 手写转换)且两端协议不同时,兜底记录两端
if len(conv.Chain) == 0 && conv.UpstreamFormat != conv.ClientFormat {
conv.Chain = []string{conv.ClientFormat, conv.UpstreamFormat}
}
a.conversion = conv
}
// nativeFormatOfProvider 渠道类型 → 上游原生协议(展示用途)。
// openai 兼容系渠道占绝大多数,默认 openai;anthropic / gemini 系例外。
func nativeFormatOfProvider(providerType int) string {
switch constant.ProviderType(providerType) {
case constant.ProviderClaude:
return string(constant.RelayFormatClaude)
case constant.ProviderGemini, constant.ProviderVertex:
return string(constant.RelayFormatGemini)
default:
return string(constant.RelayFormatOpenAI)
}
}
// Submit 中间失败尝试:立即经钩子异步落库(is_final=false,段4为空——该尝试未向客户端写出字节)
func (a *DebugAttempt) Submit(err error) {
if a == nil {
return
}
a.errText = debugErrText(err)
if hook := SubmitDebugLog; hook != nil {
hook(context.Background(), a.buildRecord(false, 0, 0))
}
}
// MarkFinal 标记为最终尝试(成功/终止/流中断):挂起等待外层 FinalizeAndSubmit 补段4后提交
func (a *DebugAttempt) MarkFinal(err error) {
if a == nil {
return
}
a.errText = debugErrText(err)
a.session.pending = a
}
// FinalizeAndSubmit 补段4并提交最终尝试记录。由外层 handler 在客户端响应写完后调用
// (recordAudit 收尾处),幂等:重复调用或无挂起尝试时为 no-op。
func (s *DebugSession) FinalizeAndSubmit(totalLatencyMs, firstTokenMs int64) {
if s == nil || s.finalized {
return
}
s.finalized = true
a := s.pending
if a == nil {
return
}
if hook := SubmitDebugLog; hook != nil {
hook(context.Background(), a.buildRecord(true, totalLatencyMs, firstTokenMs))
}
}
// buildRecord 汇总四段与元数据构建落库记录。isFinal=true 时从 clientWriter 补段4。
func (a *DebugAttempt) buildRecord(isFinal bool, totalLatencyMs, firstTokenMs int64) *DebugLogRecord {
s := a.session
snap := a.Capture.snapshot()
rec := &DebugLogRecord{
ChannelID: a.channelID,
ChannelName: a.channelName,
ChannelType: a.channelType,
RequestID: s.requestID,
TenantID: s.tenantID,
UserID: s.userID,
ApiKeyID: s.apiKeyID,
ModelName: a.modelName,
UpstreamModel: a.upstreamModel,
RelayMode: a.relayMode,
InboundPath: s.path,
UpstreamURL: snap.URL,
IsStream: a.isStream,
RetryIndex: a.retryIndex,
IsFinal: isFinal,
UpstreamStatusCode: snap.Status,
Error: a.errText,
// 段1(会话级,全部尝试共享)
ClientReqHeaders: s.clientReqHeaders,
ClientReqBody: s.clientReqBody,
// 段2/段3(传输层捕获)
UpstreamReqHeaders: snap.ReqHeaders,
UpstreamReqBody: snap.ReqBody,
UpstreamRespHeaders: snap.RespHeaders,
UpstreamRespBody: snap.RespBody,
UpstreamLatencyMs: snap.LatencyMs,
Conversion: a.conversion,
}
if snap.Err != "" && rec.Error == "" {
// 传输层错误(连接失败等)未被上层 err 覆盖时兜底保留
rec.Error = snap.Err
}
if isFinal && s.clientWriter != nil {
// 段4:客户端响应
rec.ClientRespHeaders = s.clientWriter.HeaderSnapshot()
rec.ClientRespBody = s.clientWriter.Bytes()
rec.ClientStatusCode = s.clientWriter.StatusCode()
rec.TotalLatencyMs = totalLatencyMs
rec.FirstTokenMs = firstTokenMs
}
return rec
}
func debugErrText(err error) string {
if err == nil {
return ""
}
return err.Error()
}