Files
sub2api/backend/internal/service/openai_ws_v2/passthrough_relay.go
T
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

1108 lines
32 KiB
Go

package openai_ws_v2
import (
"context"
"errors"
"io"
"net"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
coderws "github.com/coder/websocket"
"github.com/tidwall/gjson"
)
type FrameConn interface {
ReadFrame(ctx context.Context) (coderws.MessageType, []byte, error)
WriteFrame(ctx context.Context, msgType coderws.MessageType, payload []byte) error
Close() error
}
type Usage struct {
InputTokens int
OutputTokens int
CacheCreationInputTokens int
CacheReadInputTokens int
ImageOutputTokens int
}
type RelayResult struct {
RequestModel string
ResponseModel string
ResponseModelConflict bool
Usage Usage
RequestID string
TerminalEventType string
FirstTokenMs *int
Duration time.Duration
ClientToUpstreamFrames int64
UpstreamToClientFrames int64
DroppedDownstreamFrames int64
}
type RelayTurnResult struct {
RequestModel string
ResponseModel string
ResponseModelConflict bool
Usage Usage
RequestID string
TerminalEventType string
StartedAt time.Time
Duration time.Duration
FirstTokenMs *int
}
type RelayExit struct {
Stage string
Err error
Graceful bool
WroteDownstream bool
}
type RelayOptions struct {
WriteTimeout time.Duration
IdleTimeout time.Duration
UpstreamDrainTimeout time.Duration
FirstTurnStartedAt time.Time
TakeNextTurnStartedAt func() time.Time
FirstMessageType coderws.MessageType
FirstMessageSent bool
StartClientAfterFirstDownstream bool
OnUsageParseFailure func(eventType string, usageRaw string)
OnTurnComplete func(turn RelayTurnResult)
BeforeWriteClient func(msgType coderws.MessageType, payload []byte, wroteDownstream bool) error
BeforeClientWrite func(msgType coderws.MessageType, payload []byte)
AfterClientWrite func(msgType coderws.MessageType, payload []byte, writeErr error)
BeforeRelayCancel func(exit RelayExit)
ReadClientFrame func(ctx context.Context, clientConn FrameConn) (coderws.MessageType, []byte, error)
OnTrace func(event RelayTraceEvent)
Now func() time.Time
}
type RelayTraceEvent struct {
Stage string
Direction string
MessageType string
PayloadBytes int
Graceful bool
WroteDownstream bool
Error string
}
type relayState struct {
usage Usage
requestModelMu sync.RWMutex
requestModel string
pendingTurnStart atomic.Pointer[time.Time]
lastResponseID string
lastResponseModel string
responseConflict bool
terminalEventType string
firstTokenMs *int
turnTimingByID map[string]*relayTurnTiming
activeTurn *relayTurnTiming
}
type relayExitSignal struct {
stage string
err error
graceful bool
wroteDownstream bool
}
type observedUpstreamEvent struct {
terminal bool
eventType string
responseID string
usage Usage
startedAt time.Time
responseModel string
responseConflict bool
duration time.Duration
firstToken *int
}
type relayTurnTiming struct {
startAt time.Time
firstTokenMs *int
firstResponseModel string
terminalResponseModel string
responseModelConflict bool
}
func Relay(
ctx context.Context,
clientConn FrameConn,
upstreamConn FrameConn,
firstClientMessage []byte,
options RelayOptions,
) (RelayResult, *RelayExit) {
result := RelayResult{RequestModel: strings.TrimSpace(gjson.GetBytes(firstClientMessage, "model").String())}
if clientConn == nil || upstreamConn == nil {
return result, &RelayExit{Stage: "relay_init", Err: errors.New("relay connection is nil")}
}
if ctx == nil {
ctx = context.Background()
}
nowFn := options.Now
if nowFn == nil {
nowFn = time.Now
}
writeTimeout := options.WriteTimeout
if writeTimeout <= 0 {
writeTimeout = 2 * time.Minute
}
drainTimeout := options.UpstreamDrainTimeout
if drainTimeout <= 0 {
drainTimeout = 1200 * time.Millisecond
}
firstMessageType := options.FirstMessageType
if firstMessageType != coderws.MessageBinary {
firstMessageType = coderws.MessageText
}
startAt := nowFn()
state := &relayState{requestModel: result.RequestModel}
if isClientResponseCreateFrame(firstMessageType, firstClientMessage) {
firstTurnStartedAt := options.FirstTurnStartedAt
if firstTurnStartedAt.IsZero() {
firstTurnStartedAt = startAt
}
state.setPendingTurnStartedAt(firstTurnStartedAt)
}
onTrace := options.OnTrace
relayCtx, relayCancel := context.WithCancel(ctx)
defer relayCancel()
lastActivity := atomic.Int64{}
lastActivity.Store(nowFn().UnixNano())
markActivity := func() {
lastActivity.Store(nowFn().UnixNano())
}
writeUpstream := func(msgType coderws.MessageType, payload []byte) error {
writeCtx, cancel := context.WithTimeout(relayCtx, writeTimeout)
defer cancel()
return upstreamConn.WriteFrame(writeCtx, msgType, payload)
}
writeClientFrameUpstream := func(msgType coderws.MessageType, payload []byte) error {
if isClientResponseCreateFrame(msgType, payload) {
state.setRequestModel(strings.TrimSpace(gjson.GetBytes(payload, "model").String()))
turnStartedAt := time.Time{}
if options.TakeNextTurnStartedAt != nil {
turnStartedAt = options.TakeNextTurnStartedAt()
}
if turnStartedAt.IsZero() {
turnStartedAt = nowFn()
}
state.setPendingTurnStartedAt(turnStartedAt)
}
return writeUpstream(msgType, payload)
}
writeClient := func(msgType coderws.MessageType, payload []byte) error {
// 下行写超时故意不挂在 relayCtx 上:coder/websocket 在已武装的 write
// ctx 被取消时会直接硬关连接(context.AfterFunc 的 stop 不等待执行中
// 的回调),外部取消若落在一次已成功写入的解除武装窗口内,会连同尚未
// 发出的 close 帧一起冲掉,客户端只能看到裸 EOF 而收不到关闭码。与读
// 侧 conn.Read(context.Background()) 同理,取消路径的连接回收由各退出
// 分支的显式 Close/CloseNow 兜底。
writeCtx, cancel := context.WithTimeout(context.Background(), writeTimeout)
defer cancel()
return clientConn.WriteFrame(writeCtx, msgType, payload)
}
clientToUpstreamFrames := &atomic.Int64{}
upstreamToClientFrames := &atomic.Int64{}
droppedDownstreamFrames := &atomic.Int64{}
emitRelayTrace(onTrace, RelayTraceEvent{
Stage: "relay_start",
PayloadBytes: len(firstClientMessage),
MessageType: relayMessageTypeString(firstMessageType),
})
if options.FirstMessageSent {
emitRelayTrace(onTrace, RelayTraceEvent{
Stage: "write_first_message_skipped",
Direction: "client_to_upstream",
MessageType: relayMessageTypeString(firstMessageType),
PayloadBytes: len(firstClientMessage),
})
} else {
if err := writeUpstream(firstMessageType, firstClientMessage); err != nil {
result.Duration = nowFn().Sub(startAt)
emitRelayTrace(onTrace, RelayTraceEvent{
Stage: "write_first_message_failed",
Direction: "client_to_upstream",
MessageType: relayMessageTypeString(firstMessageType),
PayloadBytes: len(firstClientMessage),
Error: err.Error(),
})
return result, &RelayExit{Stage: "write_upstream", Err: err}
}
emitRelayTrace(onTrace, RelayTraceEvent{
Stage: "write_first_message_ok",
Direction: "client_to_upstream",
MessageType: relayMessageTypeString(firstMessageType),
PayloadBytes: len(firstClientMessage),
})
}
clientToUpstreamFrames.Add(1)
markActivity()
exitCh := make(chan relayExitSignal, 3)
dropDownstreamWrites := atomic.Bool{}
clientReaderStarted := atomic.Bool{}
startClientReader := func() {
if !clientReaderStarted.CompareAndSwap(false, true) {
return
}
go runClientToUpstream(relayCtx, clientConn, options.ReadClientFrame, writeClientFrameUpstream, markActivity, clientToUpstreamFrames, onTrace, exitCh)
}
if !options.StartClientAfterFirstDownstream {
startClientReader()
}
go runUpstreamToClient(
relayCtx,
upstreamConn,
writeClient,
startAt,
nowFn,
state,
options.OnUsageParseFailure,
options.OnTurnComplete,
options.BeforeWriteClient,
options.BeforeClientWrite,
options.AfterClientWrite,
func(msgType coderws.MessageType, payload []byte) {
if options.StartClientAfterFirstDownstream {
startClientReader()
}
},
&dropDownstreamWrites,
upstreamToClientFrames,
droppedDownstreamFrames,
markActivity,
onTrace,
exitCh,
)
go runIdleWatchdog(relayCtx, nowFn, options.IdleTimeout, &lastActivity, onTrace, exitCh)
firstExit := <-exitCh
// An outer ingress cancellation is a control-plane close, not a graceful
// upstream disconnect. Leave the client connection open here so the
// adapter can emit the precise lease/request close code. Internal
// relayCancel does not cancel ctx and therefore does not take this path.
if ctx.Err() != nil {
firstExit.graceful = false
}
emitRelayTrace(onTrace, RelayTraceEvent{
Stage: "first_exit",
Direction: relayDirectionFromStage(firstExit.stage),
Graceful: firstExit.graceful,
WroteDownstream: firstExit.wroteDownstream,
Error: relayErrorString(firstExit.err),
})
if options.BeforeRelayCancel != nil {
options.BeforeRelayCancel(RelayExit{
Stage: firstExit.stage,
Err: firstExit.err,
Graceful: firstExit.graceful,
WroteDownstream: firstExit.wroteDownstream,
})
}
combinedWroteDownstream := firstExit.wroteDownstream
secondExit := relayExitSignal{graceful: true}
hasSecondExit := false
// 客户端断开后尽力继续读取上游短窗口,捕获延迟 usage/terminal 事件用于计费。
if firstExit.stage == "read_client" && firstExit.graceful {
dropDownstreamWrites.Store(true)
secondExit, hasSecondExit = waitRelayExit(exitCh, drainTimeout)
} else {
relayCancel()
_ = upstreamConn.Close()
if clientReaderStarted.Load() {
secondExit, hasSecondExit = waitRelayExit(exitCh, 200*time.Millisecond)
}
}
if hasSecondExit {
combinedWroteDownstream = combinedWroteDownstream || secondExit.wroteDownstream
emitRelayTrace(onTrace, RelayTraceEvent{
Stage: "second_exit",
Direction: relayDirectionFromStage(secondExit.stage),
Graceful: secondExit.graceful,
WroteDownstream: secondExit.wroteDownstream,
Error: relayErrorString(secondExit.err),
})
}
relayCancel()
_ = upstreamConn.Close()
enrichResult(&result, state, nowFn().Sub(startAt))
result.ClientToUpstreamFrames = clientToUpstreamFrames.Load()
result.UpstreamToClientFrames = upstreamToClientFrames.Load()
result.DroppedDownstreamFrames = droppedDownstreamFrames.Load()
if options.FirstMessageSent && firstExit.stage == "read_client" && firstExit.graceful {
emitRelayTrace(onTrace, RelayTraceEvent{
Stage: "relay_client_closed",
Graceful: true,
WroteDownstream: combinedWroteDownstream,
})
return result, nil
}
if firstExit.stage == "read_client" && firstExit.graceful {
stage := "client_disconnected"
exitErr := firstExit.err
if hasSecondExit && !secondExit.graceful {
stage = secondExit.stage
exitErr = secondExit.err
}
if exitErr == nil {
exitErr = io.EOF
}
emitRelayTrace(onTrace, RelayTraceEvent{
Stage: "relay_exit",
Direction: relayDirectionFromStage(stage),
Graceful: false,
WroteDownstream: combinedWroteDownstream,
Error: relayErrorString(exitErr),
})
return result, &RelayExit{
Stage: stage,
Err: exitErr,
WroteDownstream: combinedWroteDownstream,
}
}
if firstExit.graceful && (!hasSecondExit || secondExit.graceful) {
emitRelayTrace(onTrace, RelayTraceEvent{
Stage: "relay_complete",
Graceful: true,
WroteDownstream: combinedWroteDownstream,
})
_ = clientConn.Close()
return result, nil
}
if !firstExit.graceful {
emitRelayTrace(onTrace, RelayTraceEvent{
Stage: "relay_exit",
Direction: relayDirectionFromStage(firstExit.stage),
Graceful: false,
WroteDownstream: combinedWroteDownstream,
Error: relayErrorString(firstExit.err),
})
return result, &RelayExit{
Stage: firstExit.stage,
Err: firstExit.err,
WroteDownstream: combinedWroteDownstream,
}
}
if hasSecondExit && !secondExit.graceful {
emitRelayTrace(onTrace, RelayTraceEvent{
Stage: "relay_exit",
Direction: relayDirectionFromStage(secondExit.stage),
Graceful: false,
WroteDownstream: combinedWroteDownstream,
Error: relayErrorString(secondExit.err),
})
return result, &RelayExit{
Stage: secondExit.stage,
Err: secondExit.err,
WroteDownstream: combinedWroteDownstream,
}
}
if options.FirstMessageSent {
emitRelayTrace(onTrace, RelayTraceEvent{
Stage: "relay_client_closed",
Graceful: true,
WroteDownstream: combinedWroteDownstream,
})
return result, nil
}
emitRelayTrace(onTrace, RelayTraceEvent{
Stage: "relay_complete",
Graceful: true,
WroteDownstream: combinedWroteDownstream,
})
_ = clientConn.Close()
return result, nil
}
func isClientResponseCreateFrame(msgType coderws.MessageType, payload []byte) bool {
if msgType != coderws.MessageText && msgType != coderws.MessageBinary {
return false
}
return strings.TrimSpace(gjson.GetBytes(payload, "type").String()) == "response.create"
}
func runClientToUpstream(
ctx context.Context,
clientConn FrameConn,
readClientFrame func(context.Context, FrameConn) (coderws.MessageType, []byte, error),
writeUpstream func(msgType coderws.MessageType, payload []byte) error,
markActivity func(),
forwardedFrames *atomic.Int64,
onTrace func(event RelayTraceEvent),
exitCh chan<- relayExitSignal,
) {
if readClientFrame == nil {
readClientFrame = func(ctx context.Context, conn FrameConn) (coderws.MessageType, []byte, error) {
return conn.ReadFrame(ctx)
}
}
for {
msgType, payload, err := readClientFrame(ctx, clientConn)
if err != nil {
emitRelayTrace(onTrace, RelayTraceEvent{
Stage: "read_client_failed",
Direction: "client_to_upstream",
Error: err.Error(),
Graceful: isDisconnectError(err),
})
exitCh <- relayExitSignal{stage: "read_client", err: err, graceful: isDisconnectError(err)}
return
}
markActivity()
if err := writeUpstream(msgType, payload); err != nil {
emitRelayTrace(onTrace, RelayTraceEvent{
Stage: "write_upstream_failed",
Direction: "client_to_upstream",
MessageType: relayMessageTypeString(msgType),
PayloadBytes: len(payload),
Error: err.Error(),
})
exitCh <- relayExitSignal{stage: "write_upstream", err: err}
return
}
if forwardedFrames != nil {
forwardedFrames.Add(1)
}
markActivity()
}
}
func runUpstreamToClient(
ctx context.Context,
upstreamConn FrameConn,
writeClient func(msgType coderws.MessageType, payload []byte) error,
startAt time.Time,
nowFn func() time.Time,
state *relayState,
onUsageParseFailure func(eventType string, usageRaw string),
onTurnComplete func(turn RelayTurnResult),
beforeWriteClient func(msgType coderws.MessageType, payload []byte, wroteDownstream bool) error,
beforeClientWrite func(msgType coderws.MessageType, payload []byte),
afterClientWrite func(msgType coderws.MessageType, payload []byte, writeErr error),
afterWriteClient func(msgType coderws.MessageType, payload []byte),
dropDownstreamWrites *atomic.Bool,
forwardedFrames *atomic.Int64,
droppedFrames *atomic.Int64,
markActivity func(),
onTrace func(event RelayTraceEvent),
exitCh chan<- relayExitSignal,
) {
wroteDownstream := false
for {
msgType, payload, err := upstreamConn.ReadFrame(ctx)
if err != nil {
emitRelayTrace(onTrace, RelayTraceEvent{
Stage: "read_upstream_failed",
Direction: "upstream_to_client",
Error: err.Error(),
Graceful: isDisconnectError(err),
WroteDownstream: wroteDownstream,
})
exitCh <- relayExitSignal{
stage: "read_upstream",
err: err,
graceful: isDisconnectError(err),
wroteDownstream: wroteDownstream,
}
return
}
markActivity()
if beforeWriteClient != nil {
if err := beforeWriteClient(msgType, payload, wroteDownstream); err != nil {
emitRelayTrace(onTrace, RelayTraceEvent{
Stage: "upstream_message_rejected",
Direction: "upstream_to_client",
MessageType: relayMessageTypeString(msgType),
PayloadBytes: len(payload),
WroteDownstream: wroteDownstream,
Error: err.Error(),
})
exitCh <- relayExitSignal{
stage: "upstream_message",
err: err,
wroteDownstream: wroteDownstream,
}
return
}
}
observedEvent := observedUpstreamEvent{}
switch msgType {
case coderws.MessageText:
observedEvent = observeUpstreamMessage(state, payload, startAt, nowFn, onUsageParseFailure)
case coderws.MessageBinary:
// binary frame 直接透传,不进入 JSON 观测路径(避免无效解析开销)。
}
emitTurnComplete(onTurnComplete, state, observedEvent)
if dropDownstreamWrites != nil && dropDownstreamWrites.Load() {
if droppedFrames != nil {
droppedFrames.Add(1)
}
emitRelayTrace(onTrace, RelayTraceEvent{
Stage: "drop_downstream_frame",
Direction: "upstream_to_client",
MessageType: relayMessageTypeString(msgType),
PayloadBytes: len(payload),
WroteDownstream: wroteDownstream,
})
if observedEvent.terminal {
exitCh <- relayExitSignal{
stage: "drain_terminal",
graceful: true,
wroteDownstream: wroteDownstream,
}
return
}
markActivity()
continue
}
if beforeClientWrite != nil {
beforeClientWrite(msgType, payload)
}
writeErr := writeClient(msgType, payload)
if afterClientWrite != nil {
afterClientWrite(msgType, payload, writeErr)
}
if writeErr != nil {
emitRelayTrace(onTrace, RelayTraceEvent{
Stage: "write_client_failed",
Direction: "upstream_to_client",
MessageType: relayMessageTypeString(msgType),
PayloadBytes: len(payload),
WroteDownstream: wroteDownstream,
Error: writeErr.Error(),
})
exitCh <- relayExitSignal{stage: "write_client", err: writeErr, wroteDownstream: wroteDownstream}
return
}
wroteDownstream = true
if afterWriteClient != nil {
afterWriteClient(msgType, payload)
}
if forwardedFrames != nil {
forwardedFrames.Add(1)
}
markActivity()
}
}
func runIdleWatchdog(
ctx context.Context,
nowFn func() time.Time,
idleTimeout time.Duration,
lastActivity *atomic.Int64,
onTrace func(event RelayTraceEvent),
exitCh chan<- relayExitSignal,
) {
if idleTimeout <= 0 {
return
}
checkInterval := minDuration(idleTimeout/4, 5*time.Second)
if checkInterval < time.Second {
checkInterval = time.Second
}
ticker := time.NewTicker(checkInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
last := time.Unix(0, lastActivity.Load())
if nowFn().Sub(last) < idleTimeout {
continue
}
emitRelayTrace(onTrace, RelayTraceEvent{
Stage: "idle_timeout_triggered",
Direction: "watchdog",
Error: context.DeadlineExceeded.Error(),
})
exitCh <- relayExitSignal{stage: "idle_timeout", err: context.DeadlineExceeded}
return
}
}
}
func emitRelayTrace(onTrace func(event RelayTraceEvent), event RelayTraceEvent) {
if onTrace == nil {
return
}
onTrace(event)
}
func relayMessageTypeString(msgType coderws.MessageType) string {
switch msgType {
case coderws.MessageText:
return "text"
case coderws.MessageBinary:
return "binary"
default:
return "unknown(" + strconv.Itoa(int(msgType)) + ")"
}
}
func relayDirectionFromStage(stage string) string {
switch stage {
case "read_client", "write_upstream":
return "client_to_upstream"
case "read_upstream", "write_client", "drain_terminal":
return "upstream_to_client"
case "idle_timeout":
return "watchdog"
default:
return ""
}
}
func relayErrorString(err error) string {
if err == nil {
return ""
}
return err.Error()
}
func observeUpstreamMessage(
state *relayState,
message []byte,
startAt time.Time,
nowFn func() time.Time,
onUsageParseFailure func(eventType string, usageRaw string),
) observedUpstreamEvent {
if state == nil || len(message) == 0 {
return observedUpstreamEvent{}
}
values := gjson.GetManyBytes(message, "type", "response.id", "response_id", "id")
eventType := strings.TrimSpace(values[0].String())
if eventType == "" {
return observedUpstreamEvent{}
}
responseID := strings.TrimSpace(values[1].String())
if responseID == "" {
responseID = strings.TrimSpace(values[2].String())
}
// 仅 terminal 事件兜底读取顶层 id,避免把 event_id 当成 response_id 关联到 turn。
if responseID == "" && isTerminalEvent(eventType) {
responseID = strings.TrimSpace(values[3].String())
}
now := nowFn()
if state.firstTokenMs == nil && isTokenEvent(eventType) {
ms := int(now.Sub(startAt).Milliseconds())
if ms >= 0 {
state.firstTokenMs = &ms
}
if state.activeTurn != nil && state.activeTurn.firstTokenMs == nil {
tms := int(now.Sub(state.activeTurn.startAt).Milliseconds())
if tms >= 0 {
state.activeTurn.firstTokenMs = &tms
}
}
}
parsedUsage := parseUsageAndAccumulate(state, message, eventType, onUsageParseFailure)
observed := observedUpstreamEvent{
eventType: eventType,
responseID: responseID,
usage: parsedUsage,
}
var turnTiming *relayTurnTiming
if responseID != "" {
turnTiming = openAIWSRelayGetOrInitTurnTiming(state, responseID, now)
if turnTiming != nil && turnTiming.firstTokenMs == nil && isTokenEvent(eventType) {
ms := int(now.Sub(turnTiming.startAt).Milliseconds())
if ms >= 0 {
turnTiming.firstTokenMs = &ms
}
}
} else {
turnTiming = state.activeTurn
}
observeRelayTurnResponseModel(turnTiming, firstRelayResponseModel(message), isTerminalEvent(eventType))
if !isTerminalEvent(eventType) {
return observed
}
observed.terminal = true
state.terminalEventType = eventType
if responseID != "" {
state.lastResponseID = responseID
if turnTiming, ok := openAIWSRelayDeleteTurnTiming(state, responseID); ok {
observed.responseModel = relayTurnResponseModel(&turnTiming)
observed.responseConflict = turnTiming.responseModelConflict
state.lastResponseModel = observed.responseModel
state.responseConflict = observed.responseConflict
duration := now.Sub(turnTiming.startAt)
if duration < 0 {
duration = 0
}
observed.startedAt = turnTiming.startAt
observed.duration = duration
observed.firstToken = openAIWSRelayCloneIntPtr(turnTiming.firstTokenMs)
}
}
return observed
}
func emitTurnComplete(
onTurnComplete func(turn RelayTurnResult),
state *relayState,
observed observedUpstreamEvent,
) {
if onTurnComplete == nil || !observed.terminal {
return
}
responseID := strings.TrimSpace(observed.responseID)
if responseID == "" {
return
}
requestModel := ""
if state != nil {
requestModel = state.currentRequestModel()
}
onTurnComplete(RelayTurnResult{
RequestModel: requestModel,
ResponseModel: observed.responseModel,
ResponseModelConflict: observed.responseConflict,
Usage: observed.usage,
RequestID: responseID,
TerminalEventType: observed.eventType,
StartedAt: observed.startedAt,
Duration: observed.duration,
FirstTokenMs: openAIWSRelayCloneIntPtr(observed.firstToken),
})
}
func firstRelayResponseModel(message []byte) string {
if len(message) == 0 {
return ""
}
values := gjson.GetManyBytes(message, "response.model", "model")
for _, value := range values {
if value.Type != gjson.String {
continue
}
if model := strings.TrimSpace(value.String()); model != "" {
return model
}
}
return ""
}
func observeRelayTurnResponseModel(turn *relayTurnTiming, model string, terminal bool) {
if turn == nil {
return
}
model = strings.TrimSpace(model)
if model == "" {
return
}
current := relayTurnResponseModel(turn)
if current != "" && !strings.EqualFold(current, model) {
turn.responseModelConflict = true
}
if terminal {
turn.terminalResponseModel = model
return
}
if turn.firstResponseModel == "" {
turn.firstResponseModel = model
}
}
func relayTurnResponseModel(turn *relayTurnTiming) string {
if turn == nil {
return ""
}
if turn.terminalResponseModel != "" {
return turn.terminalResponseModel
}
return turn.firstResponseModel
}
func openAIWSRelayGetOrInitTurnTiming(state *relayState, responseID string, now time.Time) *relayTurnTiming {
if state == nil {
return nil
}
if state.turnTimingByID == nil {
state.turnTimingByID = make(map[string]*relayTurnTiming, 8)
}
timing, ok := state.turnTimingByID[responseID]
if !ok || timing == nil || timing.startAt.IsZero() {
startAt := state.consumePendingTurnStartedAt()
if startAt.IsZero() {
startAt = now
}
timing = &relayTurnTiming{startAt: startAt}
state.turnTimingByID[responseID] = timing
state.activeTurn = timing
return timing
}
return timing
}
func (s *relayState) setPendingTurnStartedAt(startedAt time.Time) {
if s == nil || startedAt.IsZero() {
return
}
startedAtCopy := startedAt
s.pendingTurnStart.Store(&startedAtCopy)
}
func (s *relayState) consumePendingTurnStartedAt() time.Time {
if s == nil {
return time.Time{}
}
startedAt := s.pendingTurnStart.Swap(nil)
if startedAt == nil {
return time.Time{}
}
return *startedAt
}
func openAIWSRelayDeleteTurnTiming(state *relayState, responseID string) (relayTurnTiming, bool) {
if state == nil || state.turnTimingByID == nil {
return relayTurnTiming{}, false
}
timing, ok := state.turnTimingByID[responseID]
if !ok || timing == nil {
return relayTurnTiming{}, false
}
delete(state.turnTimingByID, responseID)
if state.activeTurn == timing {
state.activeTurn = nil
}
return *timing, true
}
func openAIWSRelayCloneIntPtr(v *int) *int {
if v == nil {
return nil
}
cloned := *v
return &cloned
}
func parseUsageAndAccumulate(
state *relayState,
message []byte,
eventType string,
onParseFailure func(eventType string, usageRaw string),
) Usage {
if state == nil || len(message) == 0 || !shouldParseUsage(eventType) {
return Usage{}
}
usageResult := gjson.GetBytes(message, "response.usage")
if !usageResult.Exists() {
return Usage{}
}
usageRaw := strings.TrimSpace(usageResult.Raw)
if usageRaw == "" || !strings.HasPrefix(usageRaw, "{") {
recordUsageParseFailure()
if onParseFailure != nil {
onParseFailure(eventType, usageRaw)
}
return Usage{}
}
inputResult := gjson.GetBytes(message, "response.usage.input_tokens")
if !inputResult.Exists() {
inputResult = gjson.GetBytes(message, "response.usage.prompt_tokens")
}
outputResult := gjson.GetBytes(message, "response.usage.output_tokens")
if !outputResult.Exists() {
outputResult = gjson.GetBytes(message, "response.usage.completion_tokens")
}
cachedResult := gjson.GetBytes(message, "response.usage.input_tokens_details.cached_tokens")
if !cachedResult.Exists() {
cachedResult = gjson.GetBytes(message, "response.usage.prompt_tokens_details.cached_tokens")
}
imageTokens := usageResult.Get("output_tokens_details.image_tokens").Int()
if imageTokens == 0 {
imageTokens = usageResult.Get("completion_tokens_details.image_tokens").Int()
}
inputTokens, inputOK := parseUsageIntField(inputResult, true)
outputTokens, outputOK := parseUsageIntField(outputResult, true)
cachedTokens, cachedOK := parseUsageIntField(cachedResult, false)
if !inputOK || !outputOK || !cachedOK {
recordUsageParseFailure()
if onParseFailure != nil {
onParseFailure(eventType, usageRaw)
}
// 解析失败时不做部分字段累加,避免计费 usage 出现“半有效”状态。
return Usage{}
}
parsedUsage := Usage{
InputTokens: inputTokens,
OutputTokens: outputTokens,
CacheCreationInputTokens: openAICacheCreationTokensFromUsage(usageResult),
CacheReadInputTokens: cachedTokens,
ImageOutputTokens: int(imageTokens),
}
state.usage.InputTokens += parsedUsage.InputTokens
state.usage.OutputTokens += parsedUsage.OutputTokens
state.usage.CacheCreationInputTokens += parsedUsage.CacheCreationInputTokens
state.usage.CacheReadInputTokens += parsedUsage.CacheReadInputTokens
state.usage.ImageOutputTokens += parsedUsage.ImageOutputTokens
return parsedUsage
}
func parseUsageIntField(value gjson.Result, required bool) (int, bool) {
if !value.Exists() {
return 0, !required
}
if value.Type != gjson.Number {
return 0, false
}
return int(value.Int()), true
}
func openAICacheCreationTokensFromUsage(value gjson.Result) int {
for _, field := range []string{
"input_tokens_details.cache_write_tokens",
"prompt_tokens_details.cache_write_tokens",
"input_tokens_details.cache_creation_tokens",
"prompt_tokens_details.cache_creation_tokens",
} {
result := value.Get(field)
if result.Exists() {
return max(int(result.Int()), 0)
}
}
for _, field := range []string{
"cache_write_tokens",
"cache_creation_input_tokens",
"cache_write_input_tokens",
"cache_creation_tokens",
} {
if tokens := int(value.Get(field).Int()); tokens > 0 {
return tokens
}
}
return 0
}
func enrichResult(result *RelayResult, state *relayState, duration time.Duration) {
if result == nil {
return
}
result.Duration = duration
if state == nil {
return
}
result.RequestModel = state.currentRequestModel()
result.ResponseModel = state.lastResponseModel
result.ResponseModelConflict = state.responseConflict
result.Usage = state.usage
result.RequestID = state.lastResponseID
result.TerminalEventType = state.terminalEventType
result.FirstTokenMs = state.firstTokenMs
}
func (s *relayState) setRequestModel(model string) {
if s == nil || model == "" {
return
}
s.requestModelMu.Lock()
s.requestModel = model
s.requestModelMu.Unlock()
}
func (s *relayState) currentRequestModel() string {
if s == nil {
return ""
}
s.requestModelMu.RLock()
defer s.requestModelMu.RUnlock()
return s.requestModel
}
func isDisconnectError(err error) bool {
if err == nil {
return false
}
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) || errors.Is(err, context.Canceled) {
return true
}
switch coderws.CloseStatus(err) {
case coderws.StatusNormalClosure, coderws.StatusGoingAway, coderws.StatusNoStatusRcvd, coderws.StatusAbnormalClosure:
return true
}
message := strings.ToLower(strings.TrimSpace(err.Error()))
if message == "" {
return false
}
return strings.Contains(message, "failed to read frame header: eof") ||
strings.Contains(message, "unexpected eof") ||
strings.Contains(message, "use of closed network connection") ||
strings.Contains(message, "connection reset by peer") ||
strings.Contains(message, "broken pipe")
}
func isTerminalEvent(eventType string) bool {
switch eventType {
case "response.completed", "response.done", "response.failed", "response.incomplete", "response.cancelled", "response.canceled":
return true
default:
return false
}
}
func shouldParseUsage(eventType string) bool {
switch eventType {
case "response.completed", "response.done", "response.failed", "response.incomplete", "response.cancelled", "response.canceled":
return true
default:
return false
}
}
func isTokenEvent(eventType string) bool {
eventType = strings.TrimSpace(eventType)
return strings.HasSuffix(eventType, ".delta") ||
eventType == "response.output_text.done" ||
eventType == "response.function_call_arguments.done"
}
func minDuration(a, b time.Duration) time.Duration {
if a <= 0 {
return b
}
if b <= 0 {
return a
}
if a < b {
return a
}
return b
}
func waitRelayExit(exitCh <-chan relayExitSignal, timeout time.Duration) (relayExitSignal, bool) {
if timeout <= 0 {
timeout = 200 * time.Millisecond
}
select {
case sig := <-exitCh:
return sig, true
case <-time.After(timeout):
return relayExitSignal{}, false
}
}