| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704 |
- package service
- import (
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net"
- "net/http"
- "sort"
- "strings"
- "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
- coderws "github.com/coder/websocket"
- "github.com/gin-gonic/gin"
- "github.com/tidwall/gjson"
- "go.uber.org/zap"
- )
- func normalizeOpenAIWSLogValue(value string) string {
- trimmed := strings.TrimSpace(value)
- if trimmed == "" {
- return "-"
- }
- return openAIWSLogValueReplacer.Replace(trimmed)
- }
- func truncateOpenAIWSLogValue(value string, maxLen int) string {
- normalized := normalizeOpenAIWSLogValue(value)
- if normalized == "-" || maxLen <= 0 {
- return normalized
- }
- if len(normalized) <= maxLen {
- return normalized
- }
- return normalized[:maxLen] + "..."
- }
- func openAIWSHeaderValueForLog(headers http.Header, key string) string {
- if headers == nil {
- return "-"
- }
- return truncateOpenAIWSLogValue(headers.Get(key), openAIWSHeaderValueMaxLen)
- }
- func hasOpenAIWSHeader(headers http.Header, key string) bool {
- if headers == nil {
- return false
- }
- return strings.TrimSpace(headers.Get(key)) != ""
- }
- type openAIWSSessionHeaderResolution struct {
- SessionID string
- ConversationID string
- SessionSource string
- ConversationSource string
- }
- func resolveOpenAIWSSessionHeaders(c *gin.Context, promptCacheKey string) openAIWSSessionHeaderResolution {
- resolution := openAIWSSessionHeaderResolution{
- SessionSource: "none",
- ConversationSource: "none",
- }
- if c != nil && c.Request != nil {
- if sessionID := strings.TrimSpace(c.Request.Header.Get("session-id")); sessionID != "" {
- resolution.SessionID = sessionID
- resolution.SessionSource = "header_session-id"
- } else if sessionID := strings.TrimSpace(c.Request.Header.Get("session_id")); sessionID != "" {
- resolution.SessionID = sessionID
- resolution.SessionSource = "header_session_id"
- }
- if conversationID := strings.TrimSpace(c.Request.Header.Get("conversation_id")); conversationID != "" {
- resolution.ConversationID = conversationID
- resolution.ConversationSource = "header_conversation_id"
- if resolution.SessionID == "" {
- resolution.SessionID = conversationID
- resolution.SessionSource = "header_conversation_id"
- }
- }
- }
- cacheKey := strings.TrimSpace(promptCacheKey)
- if cacheKey != "" {
- if resolution.SessionID == "" {
- resolution.SessionID = cacheKey
- resolution.SessionSource = "prompt_cache_key"
- }
- }
- return resolution
- }
- func shouldLogOpenAIWSEvent(idx int, eventType string) bool {
- if idx <= openAIWSEventLogHeadLimit {
- return true
- }
- if openAIWSEventLogEveryN > 0 && idx%openAIWSEventLogEveryN == 0 {
- return true
- }
- if eventType == "error" || isOpenAIWSTerminalEvent(eventType) {
- return true
- }
- return false
- }
- func shouldLogOpenAIWSBufferedEvent(idx int) bool {
- if idx <= openAIWSBufferLogHeadLimit {
- return true
- }
- if openAIWSBufferLogEveryN > 0 && idx%openAIWSBufferLogEveryN == 0 {
- return true
- }
- return false
- }
- func openAIWSEventMayContainModel(eventType string) bool {
- switch eventType {
- case "response.created",
- "response.in_progress",
- "response.completed",
- "response.done",
- "response.failed",
- "response.incomplete",
- "response.cancelled",
- "response.canceled":
- return true
- default:
- trimmed := strings.TrimSpace(eventType)
- if trimmed == eventType {
- return false
- }
- switch trimmed {
- case "response.created",
- "response.in_progress",
- "response.completed",
- "response.done",
- "response.failed",
- "response.incomplete",
- "response.cancelled",
- "response.canceled":
- return true
- default:
- return false
- }
- }
- }
- func openAIWSEventMayContainToolCalls(eventType string) bool {
- eventType = strings.TrimSpace(eventType)
- if eventType == "" {
- return false
- }
- if strings.Contains(eventType, "function_call") || strings.Contains(eventType, "tool_call") {
- return true
- }
- switch eventType {
- case "response.output_item.added", "response.output_item.done", "response.completed", "response.done":
- return true
- default:
- return false
- }
- }
- func openAIWSEventShouldParseUsage(eventType string) bool {
- eventType = strings.TrimSpace(eventType)
- if eventType == "error" || openAIStreamEventTypeIsTerminal(eventType) {
- return true
- }
- return strings.HasPrefix(eventType, "response.") && !strings.HasSuffix(eventType, ".delta")
- }
- func openAIWSMessageShouldParseUsage(eventType string, message []byte) bool {
- return openAIWSEventShouldParseUsage(eventType) && bytes.Contains(message, []byte(`"usage"`))
- }
- func parseOpenAIWSEventEnvelope(message []byte) (eventType string, responseID string, response gjson.Result) {
- if len(message) == 0 {
- return "", "", gjson.Result{}
- }
- values := gjson.GetManyBytes(message, "type", "response.id", "id", "response")
- eventType = strings.TrimSpace(values[0].String())
- if id := strings.TrimSpace(values[1].String()); id != "" {
- responseID = id
- } else {
- responseID = strings.TrimSpace(values[2].String())
- }
- return eventType, responseID, values[3]
- }
- func openAIWSMessageLikelyContainsToolCalls(message []byte) bool {
- if len(message) == 0 {
- return false
- }
- return bytes.Contains(message, []byte(`"tool_calls"`)) ||
- bytes.Contains(message, []byte(`"tool_call"`)) ||
- bytes.Contains(message, []byte(`"function_call"`))
- }
- func parseOpenAIWSResponseUsageFromCompletedEvent(message []byte, usage *OpenAIUsage) {
- if usage == nil || len(message) == 0 || !bytes.Contains(message, []byte(`"usage"`)) {
- return
- }
- if parsedUsage, ok := extractOpenAIUsageFromJSONBytes(message); ok {
- if openAIStreamEventTypeIsTerminal(effectiveOpenAISSEEventType(message, "")) {
- if !openAIUsageHasTokens(&parsedUsage) && openAIUsageHasTokens(usage) {
- return
- }
- *usage = parsedUsage
- } else {
- mergeOpenAIUsageNonZero(usage, parsedUsage)
- }
- }
- }
- func parseOpenAIWSErrorEventFields(message []byte) (code string, errType string, errMessage string) {
- if len(message) == 0 {
- return "", "", ""
- }
- values := gjson.GetManyBytes(message, "error.code", "error.type", "error.message")
- return strings.TrimSpace(values[0].String()), strings.TrimSpace(values[1].String()), strings.TrimSpace(values[2].String())
- }
- func summarizeOpenAIWSErrorEventFieldsFromRaw(codeRaw, errTypeRaw, errMessageRaw string) (code string, errType string, errMessage string) {
- code = truncateOpenAIWSLogValue(codeRaw, openAIWSLogValueMaxLen)
- errType = truncateOpenAIWSLogValue(errTypeRaw, openAIWSLogValueMaxLen)
- errMessage = truncateOpenAIWSLogValue(errMessageRaw, openAIWSLogValueMaxLen)
- return code, errType, errMessage
- }
- func summarizeOpenAIWSErrorEventFields(message []byte) (code string, errType string, errMessage string) {
- if len(message) == 0 {
- return "-", "-", "-"
- }
- return summarizeOpenAIWSErrorEventFieldsFromRaw(parseOpenAIWSErrorEventFields(message))
- }
- func summarizeOpenAIWSPayloadKeySizes(payload map[string]any, topN int) string {
- if len(payload) == 0 {
- return "-"
- }
- type keySize struct {
- Key string
- Size int
- }
- sizes := make([]keySize, 0, len(payload))
- for key, value := range payload {
- size := estimateOpenAIWSPayloadValueSize(value, openAIWSPayloadSizeEstimateDepth)
- sizes = append(sizes, keySize{Key: key, Size: size})
- }
- sort.Slice(sizes, func(i, j int) bool {
- if sizes[i].Size == sizes[j].Size {
- return sizes[i].Key < sizes[j].Key
- }
- return sizes[i].Size > sizes[j].Size
- })
- if topN <= 0 || topN > len(sizes) {
- topN = len(sizes)
- }
- parts := make([]string, 0, topN)
- for idx := 0; idx < topN; idx++ {
- item := sizes[idx]
- parts = append(parts, fmt.Sprintf("%s:%d", item.Key, item.Size))
- }
- return strings.Join(parts, ",")
- }
- func estimateOpenAIWSPayloadValueSize(value any, depth int) int {
- if depth <= 0 {
- return -1
- }
- switch v := value.(type) {
- case nil:
- return 0
- case string:
- return len(v)
- case []byte:
- return len(v)
- case bool:
- return 1
- case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
- return 8
- case float32, float64:
- return 8
- case map[string]any:
- if len(v) == 0 {
- return 2
- }
- total := 2
- count := 0
- for key, item := range v {
- count++
- if count > openAIWSPayloadSizeEstimateMaxItems {
- return -1
- }
- itemSize := estimateOpenAIWSPayloadValueSize(item, depth-1)
- if itemSize < 0 {
- return -1
- }
- total += len(key) + itemSize + 3
- if total > openAIWSPayloadSizeEstimateMaxBytes {
- return -1
- }
- }
- return total
- case []any:
- if len(v) == 0 {
- return 2
- }
- total := 2
- limit := len(v)
- if limit > openAIWSPayloadSizeEstimateMaxItems {
- return -1
- }
- for i := 0; i < limit; i++ {
- itemSize := estimateOpenAIWSPayloadValueSize(v[i], depth-1)
- if itemSize < 0 {
- return -1
- }
- total += itemSize + 1
- if total > openAIWSPayloadSizeEstimateMaxBytes {
- return -1
- }
- }
- return total
- default:
- raw, err := json.Marshal(v)
- if err != nil {
- return -1
- }
- if len(raw) > openAIWSPayloadSizeEstimateMaxBytes {
- return -1
- }
- return len(raw)
- }
- }
- func openAIWSPayloadString(payload map[string]any, key string) string {
- if len(payload) == 0 {
- return ""
- }
- raw, ok := payload[key]
- if !ok {
- return ""
- }
- switch v := raw.(type) {
- case nil:
- return ""
- case string:
- return strings.TrimSpace(v)
- case []byte:
- return strings.TrimSpace(string(v))
- default:
- return ""
- }
- }
- func openAIWSPayloadStringFromRaw(payload []byte, key string) string {
- if len(payload) == 0 || strings.TrimSpace(key) == "" {
- return ""
- }
- return strings.TrimSpace(gjson.GetBytes(payload, key).String())
- }
- func openAIWSPayloadBoolFromRaw(payload []byte, key string, defaultValue bool) bool {
- if len(payload) == 0 || strings.TrimSpace(key) == "" {
- return defaultValue
- }
- value := gjson.GetBytes(payload, key)
- if !value.Exists() {
- return defaultValue
- }
- if value.Type != gjson.True && value.Type != gjson.False {
- return defaultValue
- }
- return value.Bool()
- }
- func openAIWSSessionHashesFromID(sessionID string) (string, string) {
- return deriveOpenAISessionHashes(sessionID)
- }
- func extractOpenAIWSImageURL(value any) string {
- switch v := value.(type) {
- case string:
- return strings.TrimSpace(v)
- case map[string]any:
- if raw, ok := v["url"].(string); ok {
- return strings.TrimSpace(raw)
- }
- }
- return ""
- }
- func summarizeOpenAIWSInput(input any) string {
- items, ok := input.([]any)
- if !ok || len(items) == 0 {
- return "-"
- }
- itemCount := len(items)
- textChars := 0
- imageDataURLs := 0
- imageDataURLChars := 0
- imageRemoteURLs := 0
- handleContentItem := func(contentItem map[string]any) {
- contentType, _ := contentItem["type"].(string)
- switch strings.TrimSpace(contentType) {
- case "input_text", "output_text", "text":
- if text, ok := contentItem["text"].(string); ok {
- textChars += len(text)
- }
- case "input_image":
- imageURL := extractOpenAIWSImageURL(contentItem["image_url"])
- if imageURL == "" {
- return
- }
- if strings.HasPrefix(strings.ToLower(imageURL), "data:image/") {
- imageDataURLs++
- imageDataURLChars += len(imageURL)
- return
- }
- imageRemoteURLs++
- }
- }
- handleInputItem := func(inputItem map[string]any) {
- if content, ok := inputItem["content"].([]any); ok {
- for _, rawContent := range content {
- contentItem, ok := rawContent.(map[string]any)
- if !ok {
- continue
- }
- handleContentItem(contentItem)
- }
- return
- }
- itemType, _ := inputItem["type"].(string)
- switch strings.TrimSpace(itemType) {
- case "input_text", "output_text", "text":
- if text, ok := inputItem["text"].(string); ok {
- textChars += len(text)
- }
- case "input_image":
- imageURL := extractOpenAIWSImageURL(inputItem["image_url"])
- if imageURL == "" {
- return
- }
- if strings.HasPrefix(strings.ToLower(imageURL), "data:image/") {
- imageDataURLs++
- imageDataURLChars += len(imageURL)
- return
- }
- imageRemoteURLs++
- }
- }
- for _, rawItem := range items {
- inputItem, ok := rawItem.(map[string]any)
- if !ok {
- continue
- }
- handleInputItem(inputItem)
- }
- return fmt.Sprintf(
- "items=%d,text_chars=%d,image_data_urls=%d,image_data_url_chars=%d,image_remote_urls=%d",
- itemCount,
- textChars,
- imageDataURLs,
- imageDataURLChars,
- imageRemoteURLs,
- )
- }
- func dropOpenAIWSPayloadKey(payload map[string]any, key string, removed *[]string) {
- if len(payload) == 0 || strings.TrimSpace(key) == "" {
- return
- }
- if _, exists := payload[key]; !exists {
- return
- }
- delete(payload, key)
- *removed = append(*removed, key)
- }
- // applyOpenAIWSRetryPayloadStrategy 在 WS 连续失败时仅移除无语义字段,
- // 避免重试成功却改变原始请求语义。
- // 注意:prompt_cache_key 不应在重试中移除;它常用于会话稳定标识(session_id 兜底)。
- func applyOpenAIWSRetryPayloadStrategy(payload map[string]any, attempt int) (strategy string, removedKeys []string) {
- if len(payload) == 0 {
- return "empty", nil
- }
- if attempt <= 1 {
- return "full", nil
- }
- removed := make([]string, 0, 2)
- if attempt >= 2 {
- dropOpenAIWSPayloadKey(payload, "include", &removed)
- }
- if len(removed) == 0 {
- return "full", nil
- }
- sort.Strings(removed)
- return "trim_optional_fields", removed
- }
- func logOpenAIWSModeInfo(format string, args ...any) {
- logger.LegacyPrintf("service.openai_gateway", "[OpenAI WS Mode][openai_ws_mode=true] "+format, args...)
- }
- func isOpenAIWSModeDebugEnabled() bool {
- return logger.L().Core().Enabled(zap.DebugLevel)
- }
- func logOpenAIWSModeDebug(format string, args ...any) {
- if !isOpenAIWSModeDebugEnabled() {
- return
- }
- logger.LegacyPrintf("service.openai_gateway", "[debug] [OpenAI WS Mode][openai_ws_mode=true] "+format, args...)
- }
- func logOpenAIWSBindResponseAccountWarn(groupID, accountID int64, responseID string, err error) {
- if err == nil {
- return
- }
- logger.L().Warn(
- "openai.ws_bind_response_account_failed",
- zap.Int64("group_id", groupID),
- zap.Int64("account_id", accountID),
- zap.String("response_id", truncateOpenAIWSLogValue(responseID, openAIWSIDValueMaxLen)),
- zap.Error(err),
- )
- }
- func summarizeOpenAIWSReadCloseError(err error) (status string, reason string) {
- if err == nil {
- return "-", "-"
- }
- statusCode := coderws.CloseStatus(err)
- if statusCode == -1 {
- return "-", "-"
- }
- closeStatus := fmt.Sprintf("%d(%s)", int(statusCode), statusCode.String())
- closeReason := "-"
- var closeErr coderws.CloseError
- if errors.As(err, &closeErr) {
- reasonText := strings.TrimSpace(closeErr.Reason)
- if reasonText != "" {
- closeReason = normalizeOpenAIWSLogValue(reasonText)
- }
- }
- return normalizeOpenAIWSLogValue(closeStatus), closeReason
- }
- func unwrapOpenAIWSDialBaseError(err error) error {
- if err == nil {
- return nil
- }
- var dialErr *openAIWSDialError
- if errors.As(err, &dialErr) && dialErr != nil && dialErr.Err != nil {
- return dialErr.Err
- }
- return err
- }
- func openAIWSDialRespHeaderForLog(err error, key string) string {
- var dialErr *openAIWSDialError
- if !errors.As(err, &dialErr) || dialErr == nil || dialErr.ResponseHeaders == nil {
- return "-"
- }
- return truncateOpenAIWSLogValue(dialErr.ResponseHeaders.Get(key), openAIWSHeaderValueMaxLen)
- }
- func classifyOpenAIWSDialError(err error) string {
- if err == nil {
- return "-"
- }
- baseErr := unwrapOpenAIWSDialBaseError(err)
- if baseErr == nil {
- return "-"
- }
- if errors.Is(baseErr, context.DeadlineExceeded) {
- return "ctx_deadline_exceeded"
- }
- if errors.Is(baseErr, context.Canceled) {
- return "ctx_canceled"
- }
- var netErr net.Error
- if errors.As(baseErr, &netErr) && netErr.Timeout() {
- return "net_timeout"
- }
- if status := coderws.CloseStatus(baseErr); status != -1 {
- return normalizeOpenAIWSLogValue(fmt.Sprintf("ws_close_%d", int(status)))
- }
- message := strings.ToLower(strings.TrimSpace(baseErr.Error()))
- switch {
- case strings.Contains(message, "handshake not finished"):
- return "handshake_not_finished"
- case strings.Contains(message, "bad handshake"):
- return "bad_handshake"
- case strings.Contains(message, "connection refused"):
- return "connection_refused"
- case strings.Contains(message, "no such host"):
- return "dns_not_found"
- case strings.Contains(message, "tls"):
- return "tls_error"
- case strings.Contains(message, "i/o timeout"):
- return "io_timeout"
- case strings.Contains(message, "context deadline exceeded"):
- return "ctx_deadline_exceeded"
- default:
- return "dial_error"
- }
- }
- func summarizeOpenAIWSDialError(err error) (
- statusCode int,
- dialClass string,
- closeStatus string,
- closeReason string,
- respServer string,
- respVia string,
- respCFRay string,
- respRequestID string,
- ) {
- dialClass = "-"
- closeStatus = "-"
- closeReason = "-"
- respServer = "-"
- respVia = "-"
- respCFRay = "-"
- respRequestID = "-"
- if err == nil {
- return
- }
- var dialErr *openAIWSDialError
- if errors.As(err, &dialErr) && dialErr != nil {
- statusCode = dialErr.StatusCode
- respServer = openAIWSDialRespHeaderForLog(err, "server")
- respVia = openAIWSDialRespHeaderForLog(err, "via")
- respCFRay = openAIWSDialRespHeaderForLog(err, "cf-ray")
- respRequestID = openAIWSDialRespHeaderForLog(err, "x-request-id")
- }
- dialClass = normalizeOpenAIWSLogValue(classifyOpenAIWSDialError(err))
- closeStatus, closeReason = summarizeOpenAIWSReadCloseError(unwrapOpenAIWSDialBaseError(err))
- return
- }
- func isOpenAIWSClientDisconnectError(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") ||
- strings.Contains(message, "an existing connection was forcibly closed by the remote host") ||
- strings.Contains(message, "an established connection was aborted")
- }
- func classifyOpenAIWSReadFallbackReason(err error) string {
- if err == nil {
- return "read_event"
- }
- switch coderws.CloseStatus(err) {
- case coderws.StatusPolicyViolation:
- return "policy_violation"
- case coderws.StatusMessageTooBig:
- return "message_too_big"
- default:
- return "read_event"
- }
- }
- func sortedKeys(m map[string]any) []string {
- if len(m) == 0 {
- return nil
- }
- keys := make([]string, 0, len(m))
- for k := range m {
- keys = append(keys, k)
- }
- sort.Strings(keys)
- return keys
- }
|