openai_ws_forwarder_logutil.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704
  1. package service
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "net"
  10. "net/http"
  11. "sort"
  12. "strings"
  13. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  14. coderws "github.com/coder/websocket"
  15. "github.com/gin-gonic/gin"
  16. "github.com/tidwall/gjson"
  17. "go.uber.org/zap"
  18. )
  19. func normalizeOpenAIWSLogValue(value string) string {
  20. trimmed := strings.TrimSpace(value)
  21. if trimmed == "" {
  22. return "-"
  23. }
  24. return openAIWSLogValueReplacer.Replace(trimmed)
  25. }
  26. func truncateOpenAIWSLogValue(value string, maxLen int) string {
  27. normalized := normalizeOpenAIWSLogValue(value)
  28. if normalized == "-" || maxLen <= 0 {
  29. return normalized
  30. }
  31. if len(normalized) <= maxLen {
  32. return normalized
  33. }
  34. return normalized[:maxLen] + "..."
  35. }
  36. func openAIWSHeaderValueForLog(headers http.Header, key string) string {
  37. if headers == nil {
  38. return "-"
  39. }
  40. return truncateOpenAIWSLogValue(headers.Get(key), openAIWSHeaderValueMaxLen)
  41. }
  42. func hasOpenAIWSHeader(headers http.Header, key string) bool {
  43. if headers == nil {
  44. return false
  45. }
  46. return strings.TrimSpace(headers.Get(key)) != ""
  47. }
  48. type openAIWSSessionHeaderResolution struct {
  49. SessionID string
  50. ConversationID string
  51. SessionSource string
  52. ConversationSource string
  53. }
  54. func resolveOpenAIWSSessionHeaders(c *gin.Context, promptCacheKey string) openAIWSSessionHeaderResolution {
  55. resolution := openAIWSSessionHeaderResolution{
  56. SessionSource: "none",
  57. ConversationSource: "none",
  58. }
  59. if c != nil && c.Request != nil {
  60. if sessionID := strings.TrimSpace(c.Request.Header.Get("session-id")); sessionID != "" {
  61. resolution.SessionID = sessionID
  62. resolution.SessionSource = "header_session-id"
  63. } else if sessionID := strings.TrimSpace(c.Request.Header.Get("session_id")); sessionID != "" {
  64. resolution.SessionID = sessionID
  65. resolution.SessionSource = "header_session_id"
  66. }
  67. if conversationID := strings.TrimSpace(c.Request.Header.Get("conversation_id")); conversationID != "" {
  68. resolution.ConversationID = conversationID
  69. resolution.ConversationSource = "header_conversation_id"
  70. if resolution.SessionID == "" {
  71. resolution.SessionID = conversationID
  72. resolution.SessionSource = "header_conversation_id"
  73. }
  74. }
  75. }
  76. cacheKey := strings.TrimSpace(promptCacheKey)
  77. if cacheKey != "" {
  78. if resolution.SessionID == "" {
  79. resolution.SessionID = cacheKey
  80. resolution.SessionSource = "prompt_cache_key"
  81. }
  82. }
  83. return resolution
  84. }
  85. func shouldLogOpenAIWSEvent(idx int, eventType string) bool {
  86. if idx <= openAIWSEventLogHeadLimit {
  87. return true
  88. }
  89. if openAIWSEventLogEveryN > 0 && idx%openAIWSEventLogEveryN == 0 {
  90. return true
  91. }
  92. if eventType == "error" || isOpenAIWSTerminalEvent(eventType) {
  93. return true
  94. }
  95. return false
  96. }
  97. func shouldLogOpenAIWSBufferedEvent(idx int) bool {
  98. if idx <= openAIWSBufferLogHeadLimit {
  99. return true
  100. }
  101. if openAIWSBufferLogEveryN > 0 && idx%openAIWSBufferLogEveryN == 0 {
  102. return true
  103. }
  104. return false
  105. }
  106. func openAIWSEventMayContainModel(eventType string) bool {
  107. switch eventType {
  108. case "response.created",
  109. "response.in_progress",
  110. "response.completed",
  111. "response.done",
  112. "response.failed",
  113. "response.incomplete",
  114. "response.cancelled",
  115. "response.canceled":
  116. return true
  117. default:
  118. trimmed := strings.TrimSpace(eventType)
  119. if trimmed == eventType {
  120. return false
  121. }
  122. switch trimmed {
  123. case "response.created",
  124. "response.in_progress",
  125. "response.completed",
  126. "response.done",
  127. "response.failed",
  128. "response.incomplete",
  129. "response.cancelled",
  130. "response.canceled":
  131. return true
  132. default:
  133. return false
  134. }
  135. }
  136. }
  137. func openAIWSEventMayContainToolCalls(eventType string) bool {
  138. eventType = strings.TrimSpace(eventType)
  139. if eventType == "" {
  140. return false
  141. }
  142. if strings.Contains(eventType, "function_call") || strings.Contains(eventType, "tool_call") {
  143. return true
  144. }
  145. switch eventType {
  146. case "response.output_item.added", "response.output_item.done", "response.completed", "response.done":
  147. return true
  148. default:
  149. return false
  150. }
  151. }
  152. func openAIWSEventShouldParseUsage(eventType string) bool {
  153. eventType = strings.TrimSpace(eventType)
  154. if eventType == "error" || openAIStreamEventTypeIsTerminal(eventType) {
  155. return true
  156. }
  157. return strings.HasPrefix(eventType, "response.") && !strings.HasSuffix(eventType, ".delta")
  158. }
  159. func openAIWSMessageShouldParseUsage(eventType string, message []byte) bool {
  160. return openAIWSEventShouldParseUsage(eventType) && bytes.Contains(message, []byte(`"usage"`))
  161. }
  162. func parseOpenAIWSEventEnvelope(message []byte) (eventType string, responseID string, response gjson.Result) {
  163. if len(message) == 0 {
  164. return "", "", gjson.Result{}
  165. }
  166. values := gjson.GetManyBytes(message, "type", "response.id", "id", "response")
  167. eventType = strings.TrimSpace(values[0].String())
  168. if id := strings.TrimSpace(values[1].String()); id != "" {
  169. responseID = id
  170. } else {
  171. responseID = strings.TrimSpace(values[2].String())
  172. }
  173. return eventType, responseID, values[3]
  174. }
  175. func openAIWSMessageLikelyContainsToolCalls(message []byte) bool {
  176. if len(message) == 0 {
  177. return false
  178. }
  179. return bytes.Contains(message, []byte(`"tool_calls"`)) ||
  180. bytes.Contains(message, []byte(`"tool_call"`)) ||
  181. bytes.Contains(message, []byte(`"function_call"`))
  182. }
  183. func parseOpenAIWSResponseUsageFromCompletedEvent(message []byte, usage *OpenAIUsage) {
  184. if usage == nil || len(message) == 0 || !bytes.Contains(message, []byte(`"usage"`)) {
  185. return
  186. }
  187. if parsedUsage, ok := extractOpenAIUsageFromJSONBytes(message); ok {
  188. if openAIStreamEventTypeIsTerminal(effectiveOpenAISSEEventType(message, "")) {
  189. if !openAIUsageHasTokens(&parsedUsage) && openAIUsageHasTokens(usage) {
  190. return
  191. }
  192. *usage = parsedUsage
  193. } else {
  194. mergeOpenAIUsageNonZero(usage, parsedUsage)
  195. }
  196. }
  197. }
  198. func parseOpenAIWSErrorEventFields(message []byte) (code string, errType string, errMessage string) {
  199. if len(message) == 0 {
  200. return "", "", ""
  201. }
  202. values := gjson.GetManyBytes(message, "error.code", "error.type", "error.message")
  203. return strings.TrimSpace(values[0].String()), strings.TrimSpace(values[1].String()), strings.TrimSpace(values[2].String())
  204. }
  205. func summarizeOpenAIWSErrorEventFieldsFromRaw(codeRaw, errTypeRaw, errMessageRaw string) (code string, errType string, errMessage string) {
  206. code = truncateOpenAIWSLogValue(codeRaw, openAIWSLogValueMaxLen)
  207. errType = truncateOpenAIWSLogValue(errTypeRaw, openAIWSLogValueMaxLen)
  208. errMessage = truncateOpenAIWSLogValue(errMessageRaw, openAIWSLogValueMaxLen)
  209. return code, errType, errMessage
  210. }
  211. func summarizeOpenAIWSErrorEventFields(message []byte) (code string, errType string, errMessage string) {
  212. if len(message) == 0 {
  213. return "-", "-", "-"
  214. }
  215. return summarizeOpenAIWSErrorEventFieldsFromRaw(parseOpenAIWSErrorEventFields(message))
  216. }
  217. func summarizeOpenAIWSPayloadKeySizes(payload map[string]any, topN int) string {
  218. if len(payload) == 0 {
  219. return "-"
  220. }
  221. type keySize struct {
  222. Key string
  223. Size int
  224. }
  225. sizes := make([]keySize, 0, len(payload))
  226. for key, value := range payload {
  227. size := estimateOpenAIWSPayloadValueSize(value, openAIWSPayloadSizeEstimateDepth)
  228. sizes = append(sizes, keySize{Key: key, Size: size})
  229. }
  230. sort.Slice(sizes, func(i, j int) bool {
  231. if sizes[i].Size == sizes[j].Size {
  232. return sizes[i].Key < sizes[j].Key
  233. }
  234. return sizes[i].Size > sizes[j].Size
  235. })
  236. if topN <= 0 || topN > len(sizes) {
  237. topN = len(sizes)
  238. }
  239. parts := make([]string, 0, topN)
  240. for idx := 0; idx < topN; idx++ {
  241. item := sizes[idx]
  242. parts = append(parts, fmt.Sprintf("%s:%d", item.Key, item.Size))
  243. }
  244. return strings.Join(parts, ",")
  245. }
  246. func estimateOpenAIWSPayloadValueSize(value any, depth int) int {
  247. if depth <= 0 {
  248. return -1
  249. }
  250. switch v := value.(type) {
  251. case nil:
  252. return 0
  253. case string:
  254. return len(v)
  255. case []byte:
  256. return len(v)
  257. case bool:
  258. return 1
  259. case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
  260. return 8
  261. case float32, float64:
  262. return 8
  263. case map[string]any:
  264. if len(v) == 0 {
  265. return 2
  266. }
  267. total := 2
  268. count := 0
  269. for key, item := range v {
  270. count++
  271. if count > openAIWSPayloadSizeEstimateMaxItems {
  272. return -1
  273. }
  274. itemSize := estimateOpenAIWSPayloadValueSize(item, depth-1)
  275. if itemSize < 0 {
  276. return -1
  277. }
  278. total += len(key) + itemSize + 3
  279. if total > openAIWSPayloadSizeEstimateMaxBytes {
  280. return -1
  281. }
  282. }
  283. return total
  284. case []any:
  285. if len(v) == 0 {
  286. return 2
  287. }
  288. total := 2
  289. limit := len(v)
  290. if limit > openAIWSPayloadSizeEstimateMaxItems {
  291. return -1
  292. }
  293. for i := 0; i < limit; i++ {
  294. itemSize := estimateOpenAIWSPayloadValueSize(v[i], depth-1)
  295. if itemSize < 0 {
  296. return -1
  297. }
  298. total += itemSize + 1
  299. if total > openAIWSPayloadSizeEstimateMaxBytes {
  300. return -1
  301. }
  302. }
  303. return total
  304. default:
  305. raw, err := json.Marshal(v)
  306. if err != nil {
  307. return -1
  308. }
  309. if len(raw) > openAIWSPayloadSizeEstimateMaxBytes {
  310. return -1
  311. }
  312. return len(raw)
  313. }
  314. }
  315. func openAIWSPayloadString(payload map[string]any, key string) string {
  316. if len(payload) == 0 {
  317. return ""
  318. }
  319. raw, ok := payload[key]
  320. if !ok {
  321. return ""
  322. }
  323. switch v := raw.(type) {
  324. case nil:
  325. return ""
  326. case string:
  327. return strings.TrimSpace(v)
  328. case []byte:
  329. return strings.TrimSpace(string(v))
  330. default:
  331. return ""
  332. }
  333. }
  334. func openAIWSPayloadStringFromRaw(payload []byte, key string) string {
  335. if len(payload) == 0 || strings.TrimSpace(key) == "" {
  336. return ""
  337. }
  338. return strings.TrimSpace(gjson.GetBytes(payload, key).String())
  339. }
  340. func openAIWSPayloadBoolFromRaw(payload []byte, key string, defaultValue bool) bool {
  341. if len(payload) == 0 || strings.TrimSpace(key) == "" {
  342. return defaultValue
  343. }
  344. value := gjson.GetBytes(payload, key)
  345. if !value.Exists() {
  346. return defaultValue
  347. }
  348. if value.Type != gjson.True && value.Type != gjson.False {
  349. return defaultValue
  350. }
  351. return value.Bool()
  352. }
  353. func openAIWSSessionHashesFromID(sessionID string) (string, string) {
  354. return deriveOpenAISessionHashes(sessionID)
  355. }
  356. func extractOpenAIWSImageURL(value any) string {
  357. switch v := value.(type) {
  358. case string:
  359. return strings.TrimSpace(v)
  360. case map[string]any:
  361. if raw, ok := v["url"].(string); ok {
  362. return strings.TrimSpace(raw)
  363. }
  364. }
  365. return ""
  366. }
  367. func summarizeOpenAIWSInput(input any) string {
  368. items, ok := input.([]any)
  369. if !ok || len(items) == 0 {
  370. return "-"
  371. }
  372. itemCount := len(items)
  373. textChars := 0
  374. imageDataURLs := 0
  375. imageDataURLChars := 0
  376. imageRemoteURLs := 0
  377. handleContentItem := func(contentItem map[string]any) {
  378. contentType, _ := contentItem["type"].(string)
  379. switch strings.TrimSpace(contentType) {
  380. case "input_text", "output_text", "text":
  381. if text, ok := contentItem["text"].(string); ok {
  382. textChars += len(text)
  383. }
  384. case "input_image":
  385. imageURL := extractOpenAIWSImageURL(contentItem["image_url"])
  386. if imageURL == "" {
  387. return
  388. }
  389. if strings.HasPrefix(strings.ToLower(imageURL), "data:image/") {
  390. imageDataURLs++
  391. imageDataURLChars += len(imageURL)
  392. return
  393. }
  394. imageRemoteURLs++
  395. }
  396. }
  397. handleInputItem := func(inputItem map[string]any) {
  398. if content, ok := inputItem["content"].([]any); ok {
  399. for _, rawContent := range content {
  400. contentItem, ok := rawContent.(map[string]any)
  401. if !ok {
  402. continue
  403. }
  404. handleContentItem(contentItem)
  405. }
  406. return
  407. }
  408. itemType, _ := inputItem["type"].(string)
  409. switch strings.TrimSpace(itemType) {
  410. case "input_text", "output_text", "text":
  411. if text, ok := inputItem["text"].(string); ok {
  412. textChars += len(text)
  413. }
  414. case "input_image":
  415. imageURL := extractOpenAIWSImageURL(inputItem["image_url"])
  416. if imageURL == "" {
  417. return
  418. }
  419. if strings.HasPrefix(strings.ToLower(imageURL), "data:image/") {
  420. imageDataURLs++
  421. imageDataURLChars += len(imageURL)
  422. return
  423. }
  424. imageRemoteURLs++
  425. }
  426. }
  427. for _, rawItem := range items {
  428. inputItem, ok := rawItem.(map[string]any)
  429. if !ok {
  430. continue
  431. }
  432. handleInputItem(inputItem)
  433. }
  434. return fmt.Sprintf(
  435. "items=%d,text_chars=%d,image_data_urls=%d,image_data_url_chars=%d,image_remote_urls=%d",
  436. itemCount,
  437. textChars,
  438. imageDataURLs,
  439. imageDataURLChars,
  440. imageRemoteURLs,
  441. )
  442. }
  443. func dropOpenAIWSPayloadKey(payload map[string]any, key string, removed *[]string) {
  444. if len(payload) == 0 || strings.TrimSpace(key) == "" {
  445. return
  446. }
  447. if _, exists := payload[key]; !exists {
  448. return
  449. }
  450. delete(payload, key)
  451. *removed = append(*removed, key)
  452. }
  453. // applyOpenAIWSRetryPayloadStrategy 在 WS 连续失败时仅移除无语义字段,
  454. // 避免重试成功却改变原始请求语义。
  455. // 注意:prompt_cache_key 不应在重试中移除;它常用于会话稳定标识(session_id 兜底)。
  456. func applyOpenAIWSRetryPayloadStrategy(payload map[string]any, attempt int) (strategy string, removedKeys []string) {
  457. if len(payload) == 0 {
  458. return "empty", nil
  459. }
  460. if attempt <= 1 {
  461. return "full", nil
  462. }
  463. removed := make([]string, 0, 2)
  464. if attempt >= 2 {
  465. dropOpenAIWSPayloadKey(payload, "include", &removed)
  466. }
  467. if len(removed) == 0 {
  468. return "full", nil
  469. }
  470. sort.Strings(removed)
  471. return "trim_optional_fields", removed
  472. }
  473. func logOpenAIWSModeInfo(format string, args ...any) {
  474. logger.LegacyPrintf("service.openai_gateway", "[OpenAI WS Mode][openai_ws_mode=true] "+format, args...)
  475. }
  476. func isOpenAIWSModeDebugEnabled() bool {
  477. return logger.L().Core().Enabled(zap.DebugLevel)
  478. }
  479. func logOpenAIWSModeDebug(format string, args ...any) {
  480. if !isOpenAIWSModeDebugEnabled() {
  481. return
  482. }
  483. logger.LegacyPrintf("service.openai_gateway", "[debug] [OpenAI WS Mode][openai_ws_mode=true] "+format, args...)
  484. }
  485. func logOpenAIWSBindResponseAccountWarn(groupID, accountID int64, responseID string, err error) {
  486. if err == nil {
  487. return
  488. }
  489. logger.L().Warn(
  490. "openai.ws_bind_response_account_failed",
  491. zap.Int64("group_id", groupID),
  492. zap.Int64("account_id", accountID),
  493. zap.String("response_id", truncateOpenAIWSLogValue(responseID, openAIWSIDValueMaxLen)),
  494. zap.Error(err),
  495. )
  496. }
  497. func summarizeOpenAIWSReadCloseError(err error) (status string, reason string) {
  498. if err == nil {
  499. return "-", "-"
  500. }
  501. statusCode := coderws.CloseStatus(err)
  502. if statusCode == -1 {
  503. return "-", "-"
  504. }
  505. closeStatus := fmt.Sprintf("%d(%s)", int(statusCode), statusCode.String())
  506. closeReason := "-"
  507. var closeErr coderws.CloseError
  508. if errors.As(err, &closeErr) {
  509. reasonText := strings.TrimSpace(closeErr.Reason)
  510. if reasonText != "" {
  511. closeReason = normalizeOpenAIWSLogValue(reasonText)
  512. }
  513. }
  514. return normalizeOpenAIWSLogValue(closeStatus), closeReason
  515. }
  516. func unwrapOpenAIWSDialBaseError(err error) error {
  517. if err == nil {
  518. return nil
  519. }
  520. var dialErr *openAIWSDialError
  521. if errors.As(err, &dialErr) && dialErr != nil && dialErr.Err != nil {
  522. return dialErr.Err
  523. }
  524. return err
  525. }
  526. func openAIWSDialRespHeaderForLog(err error, key string) string {
  527. var dialErr *openAIWSDialError
  528. if !errors.As(err, &dialErr) || dialErr == nil || dialErr.ResponseHeaders == nil {
  529. return "-"
  530. }
  531. return truncateOpenAIWSLogValue(dialErr.ResponseHeaders.Get(key), openAIWSHeaderValueMaxLen)
  532. }
  533. func classifyOpenAIWSDialError(err error) string {
  534. if err == nil {
  535. return "-"
  536. }
  537. baseErr := unwrapOpenAIWSDialBaseError(err)
  538. if baseErr == nil {
  539. return "-"
  540. }
  541. if errors.Is(baseErr, context.DeadlineExceeded) {
  542. return "ctx_deadline_exceeded"
  543. }
  544. if errors.Is(baseErr, context.Canceled) {
  545. return "ctx_canceled"
  546. }
  547. var netErr net.Error
  548. if errors.As(baseErr, &netErr) && netErr.Timeout() {
  549. return "net_timeout"
  550. }
  551. if status := coderws.CloseStatus(baseErr); status != -1 {
  552. return normalizeOpenAIWSLogValue(fmt.Sprintf("ws_close_%d", int(status)))
  553. }
  554. message := strings.ToLower(strings.TrimSpace(baseErr.Error()))
  555. switch {
  556. case strings.Contains(message, "handshake not finished"):
  557. return "handshake_not_finished"
  558. case strings.Contains(message, "bad handshake"):
  559. return "bad_handshake"
  560. case strings.Contains(message, "connection refused"):
  561. return "connection_refused"
  562. case strings.Contains(message, "no such host"):
  563. return "dns_not_found"
  564. case strings.Contains(message, "tls"):
  565. return "tls_error"
  566. case strings.Contains(message, "i/o timeout"):
  567. return "io_timeout"
  568. case strings.Contains(message, "context deadline exceeded"):
  569. return "ctx_deadline_exceeded"
  570. default:
  571. return "dial_error"
  572. }
  573. }
  574. func summarizeOpenAIWSDialError(err error) (
  575. statusCode int,
  576. dialClass string,
  577. closeStatus string,
  578. closeReason string,
  579. respServer string,
  580. respVia string,
  581. respCFRay string,
  582. respRequestID string,
  583. ) {
  584. dialClass = "-"
  585. closeStatus = "-"
  586. closeReason = "-"
  587. respServer = "-"
  588. respVia = "-"
  589. respCFRay = "-"
  590. respRequestID = "-"
  591. if err == nil {
  592. return
  593. }
  594. var dialErr *openAIWSDialError
  595. if errors.As(err, &dialErr) && dialErr != nil {
  596. statusCode = dialErr.StatusCode
  597. respServer = openAIWSDialRespHeaderForLog(err, "server")
  598. respVia = openAIWSDialRespHeaderForLog(err, "via")
  599. respCFRay = openAIWSDialRespHeaderForLog(err, "cf-ray")
  600. respRequestID = openAIWSDialRespHeaderForLog(err, "x-request-id")
  601. }
  602. dialClass = normalizeOpenAIWSLogValue(classifyOpenAIWSDialError(err))
  603. closeStatus, closeReason = summarizeOpenAIWSReadCloseError(unwrapOpenAIWSDialBaseError(err))
  604. return
  605. }
  606. func isOpenAIWSClientDisconnectError(err error) bool {
  607. if err == nil {
  608. return false
  609. }
  610. if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) || errors.Is(err, context.Canceled) {
  611. return true
  612. }
  613. switch coderws.CloseStatus(err) {
  614. case coderws.StatusNormalClosure, coderws.StatusGoingAway, coderws.StatusNoStatusRcvd, coderws.StatusAbnormalClosure:
  615. return true
  616. }
  617. message := strings.ToLower(strings.TrimSpace(err.Error()))
  618. if message == "" {
  619. return false
  620. }
  621. return strings.Contains(message, "failed to read frame header: eof") ||
  622. strings.Contains(message, "unexpected eof") ||
  623. strings.Contains(message, "use of closed network connection") ||
  624. strings.Contains(message, "connection reset by peer") ||
  625. strings.Contains(message, "broken pipe") ||
  626. strings.Contains(message, "an existing connection was forcibly closed by the remote host") ||
  627. strings.Contains(message, "an established connection was aborted")
  628. }
  629. func classifyOpenAIWSReadFallbackReason(err error) string {
  630. if err == nil {
  631. return "read_event"
  632. }
  633. switch coderws.CloseStatus(err) {
  634. case coderws.StatusPolicyViolation:
  635. return "policy_violation"
  636. case coderws.StatusMessageTooBig:
  637. return "message_too_big"
  638. default:
  639. return "read_event"
  640. }
  641. }
  642. func sortedKeys(m map[string]any) []string {
  643. if len(m) == 0 {
  644. return nil
  645. }
  646. keys := make([]string, 0, len(m))
  647. for k := range m {
  648. keys = append(keys, k)
  649. }
  650. sort.Strings(keys)
  651. return keys
  652. }