gateway_upstream_response.go 50 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499
  1. package service
  2. import (
  3. "bufio"
  4. "bytes"
  5. "context"
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "io"
  10. "net"
  11. "net/http"
  12. "strconv"
  13. "strings"
  14. "sync/atomic"
  15. "syscall"
  16. "time"
  17. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  18. "github.com/Wei-Shaw/sub2api/internal/util/responseheaders"
  19. "github.com/tidwall/gjson"
  20. "github.com/tidwall/sjson"
  21. "github.com/gin-gonic/gin"
  22. )
  23. // isClaudeCodeClient 判断请求是否来自真正的 Claude Code 客户端。
  24. // 判定条件:
  25. // 1. User-Agent 匹配 claude-cli/X.Y.Z(大小写不敏感)
  26. // 2. metadata.user_id 符合 Claude Code 格式(legacy 或 JSON 格式)
  27. //
  28. // 只检查 metadata.user_id 非空不够严格:第三方工具(opencode 等)可能伪造 UA
  29. // 并附带任意 metadata.user_id 字符串,从而绕过 mimicry。必须通过 ParseMetadataUserID
  30. // 验证格式才能确认是真正的 Claude Code 客户端。
  31. func isClaudeCodeClient(userAgent string, metadataUserID string) bool {
  32. if !claudeCliUserAgentRe.MatchString(userAgent) {
  33. return false
  34. }
  35. return ParseMetadataUserID(metadataUserID) != nil
  36. }
  37. func shouldUseClaudeCodeNoopDeltaKeepalive(userAgent string) bool {
  38. version := ExtractCLIVersion(userAgent)
  39. if version == "" {
  40. return false
  41. }
  42. return CompareVersions(version, claudeCodeNoopDeltaKeepaliveMinVersion) >= 0
  43. }
  44. func claudeCodeKeepaliveDeltaTypeForContentBlock(blockType string) string {
  45. switch blockType {
  46. case "text":
  47. return "text_delta"
  48. case "tool_use":
  49. return "input_json_delta"
  50. case "thinking":
  51. return "thinking_delta"
  52. default:
  53. return ""
  54. }
  55. }
  56. func claudeCodeKeepaliveFieldForDeltaType(deltaType string) string {
  57. switch deltaType {
  58. case "text_delta":
  59. return "text"
  60. case "input_json_delta":
  61. return "partial_json"
  62. case "thinking_delta":
  63. return "thinking"
  64. default:
  65. return ""
  66. }
  67. }
  68. func buildClaudeCodeNoopDeltaKeepalive(index int, deltaType string) (string, bool) {
  69. fieldName := claudeCodeKeepaliveFieldForDeltaType(deltaType)
  70. if fieldName == "" {
  71. return "", false
  72. }
  73. return fmt.Sprintf("event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":%d,\"delta\":{\"type\":\"%s\",\"%s\":\"\"}}\n\n", index, deltaType, fieldName), true
  74. }
  75. func sseEventIndex(event map[string]any) (int, bool) {
  76. switch v := event["index"].(type) {
  77. case float64:
  78. return int(v), true
  79. case int:
  80. return v, true
  81. case int64:
  82. return int(v), true
  83. case json.Number:
  84. i, err := v.Int64()
  85. if err != nil {
  86. return 0, false
  87. }
  88. return int(i), true
  89. default:
  90. return 0, false
  91. }
  92. }
  93. // shouldRectifySignatureError 统一判断是否应触发签名整流(strip thinking blocks 并重试)。
  94. // 根据账号类型检查对应的开关和匹配模式。
  95. //
  96. // mappedModel 用于按 thinking 协议族分流:passback-required (DeepSeek/Kimi/GLM 等) 上游
  97. // 的 400 不是签名缺失问题,retry 任何 thinking 变形都会破坏「原样回传」契约——直接透传
  98. // 错误给客户端。详见 thinking_protocol.go。
  99. func (s *GatewayService) shouldRectifySignatureError(ctx context.Context, account *Account, respBody []byte, mappedModel string) bool {
  100. if !ShouldRectifyThinkingSignatureError(mappedModel) {
  101. return false
  102. }
  103. if account.Type == AccountTypeAPIKey {
  104. // API Key 账号:独立开关,一次读取配置
  105. settings, err := s.settingService.GetRectifierSettings(ctx)
  106. if err != nil || !settings.Enabled || !settings.APIKeySignatureEnabled {
  107. return false
  108. }
  109. // 先检查内置模式(同 OAuth),再检查自定义关键词
  110. if s.isThinkingBlockSignatureError(respBody) {
  111. return true
  112. }
  113. return matchSignaturePatterns(respBody, settings.APIKeySignaturePatterns)
  114. }
  115. // OAuth/SetupToken/Upstream/Bedrock 等:保持原有行为(内置模式 + 原开关)
  116. return s.isThinkingBlockSignatureError(respBody) && s.settingService.IsSignatureRectifierEnabled(ctx)
  117. }
  118. // isSignatureErrorPattern 仅做模式匹配,不检查开关。
  119. // 用于已进入重试流程后的二阶段检测(此时开关已在首次调用时验证过)。
  120. func (s *GatewayService) isSignatureErrorPattern(ctx context.Context, account *Account, respBody []byte) bool {
  121. if s.isThinkingBlockSignatureError(respBody) {
  122. return true
  123. }
  124. if account.Type == AccountTypeAPIKey {
  125. settings, err := s.settingService.GetRectifierSettings(ctx)
  126. if err != nil {
  127. return false
  128. }
  129. return matchSignaturePatterns(respBody, settings.APIKeySignaturePatterns)
  130. }
  131. return false
  132. }
  133. // matchSignaturePatterns 检查响应体是否匹配自定义关键词列表(不区分大小写)。
  134. func matchSignaturePatterns(respBody []byte, patterns []string) bool {
  135. if len(patterns) == 0 {
  136. return false
  137. }
  138. bodyLower := strings.ToLower(string(respBody))
  139. for _, p := range patterns {
  140. p = strings.TrimSpace(p)
  141. if p == "" {
  142. continue
  143. }
  144. if strings.Contains(bodyLower, strings.ToLower(p)) {
  145. return true
  146. }
  147. }
  148. return false
  149. }
  150. // isThinkingBlockSignatureError 检测是否是thinking block相关错误
  151. // 这类错误可以通过过滤thinking blocks并重试来解决
  152. func (s *GatewayService) isThinkingBlockSignatureError(respBody []byte) bool {
  153. msg := strings.ToLower(strings.TrimSpace(extractUpstreamErrorMessage(respBody)))
  154. if msg == "" {
  155. return false
  156. }
  157. // 检测signature相关的错误(更宽松的匹配)
  158. // 例如: "Invalid `signature` in `thinking` block", "***.signature" 等
  159. if strings.Contains(msg, "signature") {
  160. return true
  161. }
  162. // 检测 thinking block 顺序/类型错误
  163. // 例如: "Expected `thinking` or `redacted_thinking`, but found `text`"
  164. if strings.Contains(msg, "expected") && (strings.Contains(msg, "thinking") || strings.Contains(msg, "redacted_thinking")) {
  165. logger.LegacyPrintf("service.gateway", "[SignatureCheck] Detected thinking block type error")
  166. return true
  167. }
  168. // 检测 thinking block 被修改的错误
  169. // 例如: "thinking or redacted_thinking blocks in the latest assistant message cannot be modified"
  170. if strings.Contains(msg, "cannot be modified") && (strings.Contains(msg, "thinking") || strings.Contains(msg, "redacted_thinking")) {
  171. logger.LegacyPrintf("service.gateway", "[SignatureCheck] Detected thinking block modification error")
  172. return true
  173. }
  174. // 检测空消息内容错误(可能是过滤 thinking blocks 后导致的,或客户端发送了空 text block)
  175. // 例如: "all messages must have non-empty content"
  176. // "messages: text content blocks must be non-empty"
  177. if strings.Contains(msg, "non-empty content") || strings.Contains(msg, "empty content") ||
  178. strings.Contains(msg, "content blocks must be non-empty") {
  179. logger.LegacyPrintf("service.gateway", "[SignatureCheck] Detected empty content error")
  180. return true
  181. }
  182. // 检测 thinking block 缺少 thinking 字段的错误(跨模型切换时常见:
  183. // 其他模型回过的 assistant 历史里有 type=thinking 但没有 thinking 文本,
  184. // 喂给开启 extended thinking 的 claude 时会被拒)
  185. // 例如: "messages.1.content.0.thinking: each thinking block must contain thinking"
  186. if strings.Contains(msg, "thinking block must contain") {
  187. logger.LegacyPrintf("service.gateway", "[SignatureCheck] Detected thinking block missing content error")
  188. return true
  189. }
  190. return false
  191. }
  192. func (s *GatewayService) shouldFailoverOn400(respBody []byte) bool {
  193. // 只对"可能是兼容性差异导致"的 400 允许切换,避免无意义重试。
  194. // 默认保守:无法识别则不切换。
  195. msg := strings.ToLower(strings.TrimSpace(extractUpstreamErrorMessage(respBody)))
  196. if msg == "" {
  197. return false
  198. }
  199. // 缺少/错误的 beta header:换账号/链路可能成功(尤其是混合调度时)。
  200. // 更精确匹配 beta 相关的兼容性问题,避免误触发切换。
  201. if strings.Contains(msg, "anthropic-beta") ||
  202. strings.Contains(msg, "beta feature") ||
  203. strings.Contains(msg, "requires beta") {
  204. return true
  205. }
  206. // thinking/tool streaming 等兼容性约束(常见于中间转换链路)
  207. if strings.Contains(msg, "thinking") || strings.Contains(msg, "thought_signature") || strings.Contains(msg, "signature") {
  208. return true
  209. }
  210. if strings.Contains(msg, "tool_use") || strings.Contains(msg, "tool_result") || strings.Contains(msg, "tools") {
  211. return true
  212. }
  213. return false
  214. }
  215. // sanitizeStreamError 返回不含网络地址的客户端可见错误描述。
  216. // 默认 (*net.OpError).Error() 会拼接 Source/Addr 字段,泄露内部 IP/端口与上游
  217. // 服务器地址(例如 "read tcp 10.0.0.1:54321->52.1.2.3:443: read: connection
  218. // reset by peer")。该函数只保留可识别的错误类别,原始 err 仍在调用点写入日志。
  219. func sanitizeStreamError(err error) string {
  220. if err == nil {
  221. return ""
  222. }
  223. switch {
  224. case errors.Is(err, io.ErrUnexpectedEOF):
  225. return "unexpected EOF"
  226. case errors.Is(err, io.EOF):
  227. return "EOF"
  228. case errors.Is(err, context.Canceled):
  229. return "canceled"
  230. case errors.Is(err, context.DeadlineExceeded):
  231. return "deadline exceeded"
  232. case errors.Is(err, syscall.ECONNRESET):
  233. return "connection reset by peer"
  234. case errors.Is(err, syscall.ECONNABORTED):
  235. return "connection aborted"
  236. case errors.Is(err, syscall.ETIMEDOUT):
  237. return "connection timed out"
  238. case errors.Is(err, syscall.EPIPE):
  239. return "broken pipe"
  240. case errors.Is(err, syscall.ECONNREFUSED):
  241. return "connection refused"
  242. }
  243. var netErr *net.OpError
  244. if errors.As(err, &netErr) {
  245. if netErr.Timeout() {
  246. if netErr.Op != "" {
  247. return netErr.Op + " timeout"
  248. }
  249. return "i/o timeout"
  250. }
  251. if netErr.Op != "" {
  252. return netErr.Op + " network error"
  253. }
  254. }
  255. return "upstream connection error"
  256. }
  257. // ExtractUpstreamErrorMessage 从上游响应体中提取错误消息
  258. // 支持 Claude 风格的错误格式:{"type":"error","error":{"type":"...","message":"..."}}
  259. func ExtractUpstreamErrorMessage(body []byte) string {
  260. return extractUpstreamErrorMessage(body)
  261. }
  262. // SanitizeUpstreamErrorMessage redacts sensitive query parameter values before
  263. // an upstream error message is recorded or returned to a client.
  264. func SanitizeUpstreamErrorMessage(message string) string {
  265. return sanitizeUpstreamErrorMessage(message)
  266. }
  267. func extractUpstreamErrorMessage(body []byte) string {
  268. // Claude 风格:{"type":"error","error":{"type":"...","message":"..."}}
  269. if m := gjson.GetBytes(body, "error.message").String(); strings.TrimSpace(m) != "" {
  270. inner := strings.TrimSpace(m)
  271. // 有些上游会把完整 JSON 作为字符串塞进 message
  272. if strings.HasPrefix(inner, "{") {
  273. if innerMsg := gjson.Get(inner, "error.message").String(); strings.TrimSpace(innerMsg) != "" {
  274. return innerMsg
  275. }
  276. }
  277. return m
  278. }
  279. // ChatGPT 内部 API 风格:{"detail":"..."}
  280. if d := gjson.GetBytes(body, "detail").String(); strings.TrimSpace(d) != "" {
  281. return d
  282. }
  283. // 兜底:尝试顶层 message
  284. return gjson.GetBytes(body, "message").String()
  285. }
  286. func extractUpstreamErrorCode(body []byte) string {
  287. if code := strings.TrimSpace(gjson.GetBytes(body, "error.code").String()); code != "" {
  288. return code
  289. }
  290. inner := strings.TrimSpace(gjson.GetBytes(body, "error.message").String())
  291. if !strings.HasPrefix(inner, "{") {
  292. return ""
  293. }
  294. if code := strings.TrimSpace(gjson.Get(inner, "error.code").String()); code != "" {
  295. return code
  296. }
  297. if lastBrace := strings.LastIndex(inner, "}"); lastBrace >= 0 {
  298. if code := strings.TrimSpace(gjson.Get(inner[:lastBrace+1], "error.code").String()); code != "" {
  299. return code
  300. }
  301. }
  302. return ""
  303. }
  304. func isCountTokensUnsupported404(statusCode int, body []byte) bool {
  305. if statusCode != http.StatusNotFound {
  306. return false
  307. }
  308. msg := strings.ToLower(strings.TrimSpace(extractUpstreamErrorMessage(body)))
  309. if msg == "" {
  310. return false
  311. }
  312. if strings.Contains(msg, "/v1/messages/count_tokens") {
  313. return true
  314. }
  315. return strings.Contains(msg, "count_tokens") && strings.Contains(msg, "not found")
  316. }
  317. func (s *GatewayService) readUpstreamErrorBody(resp *http.Response) ([]byte, error) {
  318. if resp == nil || resp.Body == nil {
  319. return nil, nil
  320. }
  321. limit := gatewayUpstreamErrorBodyReadLimit
  322. if s != nil && s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody && s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes > int(limit) {
  323. limit = int64(s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes)
  324. }
  325. return io.ReadAll(io.LimitReader(resp.Body, limit))
  326. }
  327. func (s *GatewayService) handleErrorResponse(ctx context.Context, resp *http.Response, c *gin.Context, account *Account, requestedModel ...string) (*ForwardResult, error) {
  328. // Upstream returned a non-success HTTP status; count Ollama Cloud activity.
  329. scheduleOllamaCloudUsageActivity(s.deferredService, account)
  330. body, readErr := s.readUpstreamErrorBody(resp)
  331. if readErr != nil {
  332. // 读取失败时 body 可能被截断,错误分类会基于不完整数据;记录日志以便排查,
  333. // 避免静默吞掉导致误判。
  334. logger.LegacyPrintf("service.gateway", "[Forward] Failed to fully read upstream error body: Account=%d(%s) Status=%d err=%v",
  335. account.ID, account.Name, resp.StatusCode, readErr)
  336. }
  337. // 调试日志:打印上游错误响应
  338. logger.LegacyPrintf("service.gateway", "[Forward] Upstream error (non-retryable): Account=%d(%s) Status=%d RequestID=%s Body=%s",
  339. account.ID, account.Name, resp.StatusCode, resp.Header.Get("x-request-id"), truncateString(string(body), 1000))
  340. upstreamMsg := strings.TrimSpace(extractUpstreamErrorMessage(body))
  341. upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg)
  342. // Print a compact upstream request fingerprint when we hit the Claude Code OAuth
  343. // credential scope error. This avoids requiring env-var tweaks in a fixed deploy.
  344. if isClaudeCodeCredentialScopeError(upstreamMsg) && c != nil {
  345. if v, ok := c.Get(claudeMimicDebugInfoKey); ok {
  346. if line, ok := v.(string); ok && strings.TrimSpace(line) != "" {
  347. logger.LegacyPrintf("service.gateway", "[ClaudeMimicDebugOnError] status=%d request_id=%s %s",
  348. resp.StatusCode,
  349. resp.Header.Get("x-request-id"),
  350. line,
  351. )
  352. }
  353. }
  354. }
  355. // Enrich Ops error logs with upstream status + message, and optionally a truncated body snippet.
  356. upstreamDetail := ""
  357. if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
  358. maxBytes := s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes
  359. if maxBytes <= 0 {
  360. maxBytes = 2048
  361. }
  362. upstreamDetail = truncateString(string(body), maxBytes)
  363. }
  364. setOpsUpstreamError(c, resp.StatusCode, upstreamMsg, upstreamDetail)
  365. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  366. ProxyID: opsUpstreamProxyID(account),
  367. ProxyName: opsUpstreamProxyName(account),
  368. Platform: account.Platform,
  369. AccountID: account.ID,
  370. UpstreamStatusCode: resp.StatusCode,
  371. UpstreamRequestID: resp.Header.Get("x-request-id"),
  372. Kind: "http_error",
  373. Message: upstreamMsg,
  374. Detail: upstreamDetail,
  375. })
  376. // 处理上游错误,标记账号状态
  377. shouldDisable := false
  378. if s.rateLimitService != nil {
  379. if len(requestedModel) > 0 {
  380. shouldDisable = s.rateLimitService.HandleUpstreamError(ctx, account, resp.StatusCode, resp.Header, body, requestedModel[0])
  381. } else {
  382. shouldDisable = s.rateLimitService.HandleUpstreamError(ctx, account, resp.StatusCode, resp.Header, body)
  383. }
  384. }
  385. if shouldDisable {
  386. return nil, &UpstreamFailoverError{StatusCode: resp.StatusCode, ResponseBody: body}
  387. }
  388. MarkResponseCommitted(c)
  389. // 记录上游错误响应体摘要便于排障(可选:由配置控制;不回显到客户端)
  390. if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
  391. logger.LegacyPrintf("service.gateway",
  392. "Upstream error %d (account=%d platform=%s type=%s): %s",
  393. resp.StatusCode,
  394. account.ID,
  395. account.Platform,
  396. account.Type,
  397. truncateForLog(body, s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes),
  398. )
  399. }
  400. // 非 failover 错误也支持错误透传规则匹配。
  401. if status, errType, errMsg, matched := applyErrorPassthroughRule(
  402. c,
  403. account.Platform,
  404. resp.StatusCode,
  405. body,
  406. http.StatusBadGateway,
  407. "upstream_error",
  408. "Upstream request failed",
  409. ); matched {
  410. c.JSON(status, gin.H{
  411. "type": "error",
  412. "error": gin.H{
  413. "type": errType,
  414. "message": errMsg,
  415. },
  416. })
  417. summary := upstreamMsg
  418. if summary == "" {
  419. summary = errMsg
  420. }
  421. if summary == "" {
  422. return nil, fmt.Errorf("upstream error: %d (passthrough rule matched)", resp.StatusCode)
  423. }
  424. return nil, fmt.Errorf("upstream error: %d (passthrough rule matched) message=%s", resp.StatusCode, summary)
  425. }
  426. // 根据状态码返回适当的自定义错误响应(不透传上游详细信息)
  427. var errType, errMsg string
  428. var statusCode int
  429. switch resp.StatusCode {
  430. case 400:
  431. c.Data(http.StatusBadRequest, "application/json", body)
  432. summary := upstreamMsg
  433. if summary == "" {
  434. summary = truncateForLog(body, 512)
  435. }
  436. if summary == "" {
  437. return nil, fmt.Errorf("upstream error: %d", resp.StatusCode)
  438. }
  439. return nil, fmt.Errorf("upstream error: %d message=%s", resp.StatusCode, summary)
  440. case 401:
  441. statusCode = http.StatusBadGateway
  442. errType = "upstream_error"
  443. errMsg = "Upstream authentication failed, please contact administrator"
  444. case 403:
  445. statusCode = http.StatusBadGateway
  446. errType = "upstream_error"
  447. errMsg = "Upstream access forbidden, please contact administrator"
  448. case 429:
  449. statusCode = http.StatusTooManyRequests
  450. errType = "rate_limit_error"
  451. errMsg = "Upstream rate limit exceeded, please retry later"
  452. case 529:
  453. statusCode = http.StatusServiceUnavailable
  454. errType = "overloaded_error"
  455. errMsg = "Upstream service overloaded, please retry later"
  456. case 500, 502, 503, 504:
  457. statusCode = http.StatusBadGateway
  458. errType = "upstream_error"
  459. errMsg = "Upstream service temporarily unavailable"
  460. default:
  461. statusCode = http.StatusBadGateway
  462. errType = "upstream_error"
  463. errMsg = "Upstream request failed"
  464. }
  465. // 返回自定义错误响应
  466. c.JSON(statusCode, gin.H{
  467. "type": "error",
  468. "error": gin.H{
  469. "type": errType,
  470. "message": errMsg,
  471. },
  472. })
  473. if upstreamMsg == "" {
  474. return nil, fmt.Errorf("upstream error: %d", resp.StatusCode)
  475. }
  476. return nil, fmt.Errorf("upstream error: %d message=%s", resp.StatusCode, upstreamMsg)
  477. }
  478. func (s *GatewayService) handleRetryExhaustedSideEffects(ctx context.Context, resp *http.Response, account *Account) {
  479. body, _ := s.readUpstreamErrorBody(resp)
  480. statusCode := resp.StatusCode
  481. // OAuth/Setup Token 账号的 403:标记账号异常
  482. if account.IsOAuth() && statusCode == 403 {
  483. s.rateLimitService.HandleUpstreamError(ctx, account, statusCode, resp.Header, body)
  484. logger.LegacyPrintf("service.gateway", "Account %d: marked as error after %d retries for status %d", account.ID, maxRetryAttempts, statusCode)
  485. } else {
  486. // API Key 未配置错误码:不标记账号状态
  487. logger.LegacyPrintf("service.gateway", "Account %d: upstream error %d after %d retries (not marking account)", account.ID, statusCode, maxRetryAttempts)
  488. }
  489. }
  490. func (s *GatewayService) handleFailoverSideEffects(ctx context.Context, resp *http.Response, account *Account, requestedModel ...string) {
  491. body, _ := s.readUpstreamErrorBody(resp)
  492. if len(requestedModel) > 0 {
  493. s.rateLimitService.HandleUpstreamError(ctx, account, resp.StatusCode, resp.Header, body, requestedModel[0])
  494. return
  495. }
  496. s.rateLimitService.HandleUpstreamError(ctx, account, resp.StatusCode, resp.Header, body)
  497. }
  498. // handleRetryExhaustedError 处理重试耗尽后的错误
  499. // OAuth 403:标记账号异常
  500. // API Key 未配置错误码:仅返回错误,不标记账号
  501. func (s *GatewayService) handleRetryExhaustedError(ctx context.Context, resp *http.Response, c *gin.Context, account *Account) (*ForwardResult, error) {
  502. MarkResponseCommitted(c)
  503. // Capture upstream error body before side-effects consume the stream.
  504. respBody, _ := s.readUpstreamErrorBody(resp)
  505. _ = resp.Body.Close()
  506. resp.Body = io.NopCloser(bytes.NewReader(respBody))
  507. s.handleRetryExhaustedSideEffects(ctx, resp, account)
  508. upstreamMsg := strings.TrimSpace(extractUpstreamErrorMessage(respBody))
  509. upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg)
  510. if isClaudeCodeCredentialScopeError(upstreamMsg) && c != nil {
  511. if v, ok := c.Get(claudeMimicDebugInfoKey); ok {
  512. if line, ok := v.(string); ok && strings.TrimSpace(line) != "" {
  513. logger.LegacyPrintf("service.gateway", "[ClaudeMimicDebugOnError] status=%d request_id=%s %s",
  514. resp.StatusCode,
  515. resp.Header.Get("x-request-id"),
  516. line,
  517. )
  518. }
  519. }
  520. }
  521. upstreamDetail := ""
  522. if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
  523. maxBytes := s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes
  524. if maxBytes <= 0 {
  525. maxBytes = 2048
  526. }
  527. upstreamDetail = truncateString(string(respBody), maxBytes)
  528. }
  529. setOpsUpstreamError(c, resp.StatusCode, upstreamMsg, upstreamDetail)
  530. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  531. ProxyID: opsUpstreamProxyID(account),
  532. ProxyName: opsUpstreamProxyName(account),
  533. Platform: account.Platform,
  534. AccountID: account.ID,
  535. UpstreamStatusCode: resp.StatusCode,
  536. UpstreamRequestID: resp.Header.Get("x-request-id"),
  537. Kind: "retry_exhausted",
  538. Message: upstreamMsg,
  539. Detail: upstreamDetail,
  540. })
  541. if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
  542. logger.LegacyPrintf("service.gateway",
  543. "Upstream error %d retries_exhausted (account=%d platform=%s type=%s): %s",
  544. resp.StatusCode,
  545. account.ID,
  546. account.Platform,
  547. account.Type,
  548. truncateForLog(respBody, s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes),
  549. )
  550. }
  551. if status, errType, errMsg, matched := applyErrorPassthroughRule(
  552. c,
  553. account.Platform,
  554. resp.StatusCode,
  555. respBody,
  556. http.StatusBadGateway,
  557. "upstream_error",
  558. "Upstream request failed after retries",
  559. ); matched {
  560. c.JSON(status, gin.H{
  561. "type": "error",
  562. "error": gin.H{
  563. "type": errType,
  564. "message": errMsg,
  565. },
  566. })
  567. summary := upstreamMsg
  568. if summary == "" {
  569. summary = errMsg
  570. }
  571. if summary == "" {
  572. return nil, fmt.Errorf("upstream error: %d (retries exhausted, passthrough rule matched)", resp.StatusCode)
  573. }
  574. return nil, fmt.Errorf("upstream error: %d (retries exhausted, passthrough rule matched) message=%s", resp.StatusCode, summary)
  575. }
  576. // 返回统一的重试耗尽错误响应
  577. c.JSON(http.StatusBadGateway, gin.H{
  578. "type": "error",
  579. "error": gin.H{
  580. "type": "upstream_error",
  581. "message": "Upstream request failed after retries",
  582. },
  583. })
  584. if upstreamMsg == "" {
  585. return nil, fmt.Errorf("upstream error: %d (retries exhausted)", resp.StatusCode)
  586. }
  587. return nil, fmt.Errorf("upstream error: %d (retries exhausted) message=%s", resp.StatusCode, upstreamMsg)
  588. }
  589. // streamingResult 流式响应结果
  590. type streamingResult struct {
  591. usage *ClaudeUsage
  592. firstTokenMs *int
  593. clientDisconnect bool // 客户端是否在流式传输过程中断开
  594. }
  595. // hasObservedTokens 报告流式过程中是否已观测到任何上游计量的 token。
  596. func (u *ClaudeUsage) hasObservedTokens() bool {
  597. if u == nil {
  598. return false
  599. }
  600. return u.InputTokens > 0 || u.OutputTokens > 0 ||
  601. u.CacheCreationInputTokens > 0 || u.CacheReadInputTokens > 0 ||
  602. u.CacheCreation5mTokens > 0 || u.CacheCreation1hTokens > 0 ||
  603. u.ImageOutputTokens > 0
  604. }
  605. // partialStreamUsageResult 在流式转发中途出错时,把已观测到 usage 的部分结果包装为
  606. // ForwardResult(与错误一起返回给 handler 记录)。上游一旦下发过 message_start,
  607. // input/cache token 就已计量,直接丢弃会让请求完全漏记漏计费(issue #5148)。
  608. // 无已观测 usage 时返回 nil。
  609. //
  610. // 不变式:UpstreamFailoverError 必须保持 result=nil——failover 重试成功后按成功请求
  611. // 计费,若同时返回部分 usage 会造成双重计费,此处显式拦截兜底。
  612. func partialStreamUsageResult(c *gin.Context, resp *http.Response, streamResult *streamingResult, model, upstreamModel string, startTime time.Time, err error) *ForwardResult {
  613. if streamResult == nil || !streamResult.usage.hasObservedTokens() {
  614. return nil
  615. }
  616. var failoverErr *UpstreamFailoverError
  617. if errors.As(err, &failoverErr) {
  618. return nil
  619. }
  620. return &ForwardResult{
  621. RequestID: resp.Header.Get("x-request-id"),
  622. UpstreamHeaders: resp.Header,
  623. Usage: *streamResult.usage,
  624. Model: model,
  625. UpstreamModel: upstreamModel,
  626. UpstreamResponseModel: observedUpstreamResponseModel(c),
  627. UpstreamResponseModelConflict: observedUpstreamResponseModelConflict(c),
  628. UpstreamResponseServiceTier: observedUpstreamResponseServiceTier(c),
  629. Stream: true,
  630. Duration: time.Since(startTime),
  631. FirstTokenMs: streamResult.firstTokenMs,
  632. ClientDisconnect: streamResult.clientDisconnect,
  633. }
  634. }
  635. func (s *GatewayService) handleStreamingResponse(ctx context.Context, resp *http.Response, c *gin.Context, account *Account, startTime time.Time, originalModel, mappedModel string, mimicClaudeCode bool) (*streamingResult, error) {
  636. observer := upstreamResponseModelObserverFromContext(c)
  637. if observer == nil {
  638. observer = beginUpstreamResponseModelObservation(c)
  639. }
  640. // 更新5h窗口状态
  641. s.rateLimitService.UpdateSessionWindow(ctx, account, resp.Header)
  642. if s.responseHeaderFilter != nil {
  643. responseheaders.WriteFilteredHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
  644. }
  645. // 设置SSE响应头
  646. c.Header("Content-Type", "text/event-stream")
  647. c.Header("Cache-Control", "no-cache")
  648. c.Header("Connection", "keep-alive")
  649. c.Header("X-Accel-Buffering", "no")
  650. // 透传其他响应头
  651. if v := resp.Header.Get("x-request-id"); v != "" {
  652. c.Header("x-request-id", v)
  653. }
  654. w := c.Writer
  655. flusher, ok := w.(http.Flusher)
  656. if !ok {
  657. return nil, errors.New("streaming not supported")
  658. }
  659. usage := &ClaudeUsage{}
  660. var firstTokenMs *int
  661. scanner := bufio.NewScanner(resp.Body)
  662. // 设置更大的buffer以处理长行
  663. maxLineSize := defaultMaxLineSize
  664. if s.cfg != nil && s.cfg.Gateway.MaxLineSize > 0 {
  665. maxLineSize = s.cfg.Gateway.MaxLineSize
  666. }
  667. scanBuf := getSSEScannerBuf64K()
  668. scanner.Buffer(scanBuf[:0], maxLineSize)
  669. type scanEvent struct {
  670. line string
  671. err error
  672. }
  673. // 独立 goroutine 读取上游,避免读取阻塞导致超时/keepalive无法处理
  674. events := make(chan scanEvent, 16)
  675. done := make(chan struct{})
  676. sendEvent := func(ev scanEvent) bool {
  677. select {
  678. case events <- ev:
  679. return true
  680. case <-done:
  681. return false
  682. }
  683. }
  684. var lastReadAt int64
  685. atomic.StoreInt64(&lastReadAt, time.Now().UnixNano())
  686. go func(scanBuf *sseScannerBuf64K) {
  687. defer putSSEScannerBuf64K(scanBuf)
  688. defer close(events)
  689. for scanner.Scan() {
  690. atomic.StoreInt64(&lastReadAt, time.Now().UnixNano())
  691. if !sendEvent(scanEvent{line: scanner.Text()}) {
  692. return
  693. }
  694. }
  695. if err := scanner.Err(); err != nil {
  696. _ = sendEvent(scanEvent{err: err})
  697. }
  698. }(scanBuf)
  699. defer close(done)
  700. streamInterval := time.Duration(0)
  701. if s.cfg != nil && s.cfg.Gateway.StreamDataIntervalTimeout > 0 {
  702. streamInterval = time.Duration(s.cfg.Gateway.StreamDataIntervalTimeout) * time.Second
  703. }
  704. // 仅监控上游数据间隔超时,避免下游写入阻塞导致误判
  705. var intervalTicker *time.Ticker
  706. if streamInterval > 0 {
  707. intervalTicker = time.NewTicker(streamInterval)
  708. defer intervalTicker.Stop()
  709. }
  710. var intervalCh <-chan time.Time
  711. if intervalTicker != nil {
  712. intervalCh = intervalTicker.C
  713. }
  714. // 下游 keepalive:防止代理/Cloudflare Tunnel 因连接空闲而断开
  715. keepaliveInterval := time.Duration(0)
  716. if s.cfg != nil && s.cfg.Gateway.StreamKeepaliveInterval > 0 {
  717. keepaliveInterval = time.Duration(s.cfg.Gateway.StreamKeepaliveInterval) * time.Second
  718. }
  719. var keepaliveTimer *time.Timer
  720. if keepaliveInterval > 0 {
  721. keepaliveTimer = time.NewTimer(keepaliveInterval)
  722. defer keepaliveTimer.Stop()
  723. }
  724. var keepaliveCh <-chan time.Time
  725. if keepaliveTimer != nil {
  726. keepaliveCh = keepaliveTimer.C
  727. }
  728. lastDataAt := time.Now()
  729. resetKeepaliveTimer := func() {
  730. if keepaliveTimer == nil {
  731. return
  732. }
  733. if !keepaliveTimer.Stop() {
  734. select {
  735. case <-keepaliveTimer.C:
  736. default:
  737. }
  738. }
  739. keepaliveTimer.Reset(keepaliveInterval)
  740. }
  741. // 仅发送一次错误事件,避免多次写入导致协议混乱(写失败时尽力通知客户端)。
  742. // 事件格式遵循 Anthropic SSE 标准:{"type":"error","error":{"type":<reason>,"message":<message>}}
  743. // 这样 Anthropic SDK / Claude Code 等客户端能按标准 error 类型解析,UI 能显示具体错误文案,
  744. // 服务端 ExtractUpstreamErrorMessage 也能从透传的 body 中提取 message。
  745. errorEventSent := false
  746. sendErrorEvent := func(reason, message string) {
  747. if errorEventSent {
  748. return
  749. }
  750. errorEventSent = true
  751. if message == "" {
  752. message = reason
  753. }
  754. body, err := json.Marshal(map[string]any{
  755. "type": "error",
  756. "error": map[string]string{
  757. "type": reason,
  758. "message": message,
  759. },
  760. })
  761. if err != nil {
  762. // json.Marshal 不可能在已知 string-only 输入上失败,保守 fallback
  763. body = []byte(fmt.Sprintf(`{"type":"error","error":{"type":%q,"message":%q}}`, reason, message))
  764. }
  765. _, _ = fmt.Fprintf(w, "event: error\ndata: %s\n\n", body)
  766. flusher.Flush()
  767. }
  768. needModelReplace := originalModel != mappedModel
  769. clientDisconnected := false // 客户端断开标志,断开后继续读取上游以获取完整usage
  770. sawTerminalEvent := false
  771. useNoopDeltaKeepalive := c != nil && c.Request != nil && shouldUseClaudeCodeNoopDeltaKeepalive(c.GetHeader("User-Agent"))
  772. noopDeltaKeepaliveBlockIndex := -1
  773. noopDeltaKeepaliveDeltaType := ""
  774. pendingEventLines := make([]string, 0, 4)
  775. processSSEEvent := func(lines []string) ([]string, string, *sseUsagePatch, error) {
  776. if len(lines) == 0 {
  777. return nil, "", nil, nil
  778. }
  779. eventName := ""
  780. dataLine := ""
  781. for _, line := range lines {
  782. trimmed := strings.TrimSpace(line)
  783. if strings.HasPrefix(trimmed, "event:") {
  784. eventName = strings.TrimSpace(strings.TrimPrefix(trimmed, "event:"))
  785. continue
  786. }
  787. if dataLine == "" && sseDataRe.MatchString(trimmed) {
  788. dataLine = sseDataRe.ReplaceAllString(trimmed, "")
  789. }
  790. }
  791. if eventName == "error" {
  792. return nil, dataLine, nil, &sseStreamErrorEventError{RawData: dataLine}
  793. }
  794. if dataLine == "" {
  795. return []string{strings.Join(lines, "\n") + "\n\n"}, "", nil, nil
  796. }
  797. if dataLine == "[DONE]" {
  798. sawTerminalEvent = true
  799. block := ""
  800. if eventName != "" {
  801. block = "event: " + eventName + "\n"
  802. }
  803. block += "data: " + dataLine + "\n\n"
  804. return []string{block}, dataLine, nil, nil
  805. }
  806. var event map[string]any
  807. if err := json.Unmarshal([]byte(dataLine), &event); err != nil {
  808. // JSON 解析失败,直接透传原始数据
  809. block := ""
  810. if eventName != "" {
  811. block = "event: " + eventName + "\n"
  812. }
  813. block += "data: " + dataLine + "\n\n"
  814. return []string{block}, dataLine, nil, nil
  815. }
  816. eventType, _ := event["type"].(string)
  817. observer.ObserveAnthropic([]byte(dataLine))
  818. if eventName == "" {
  819. eventName = eventType
  820. }
  821. eventChanged := false
  822. if useNoopDeltaKeepalive {
  823. switch eventType {
  824. case "content_block_start":
  825. if idx, ok := sseEventIndex(event); ok {
  826. noopDeltaKeepaliveBlockIndex = -1
  827. noopDeltaKeepaliveDeltaType = ""
  828. if contentBlock, ok := event["content_block"].(map[string]any); ok {
  829. blockType, _ := contentBlock["type"].(string)
  830. if deltaType := claudeCodeKeepaliveDeltaTypeForContentBlock(blockType); deltaType != "" {
  831. noopDeltaKeepaliveBlockIndex = idx
  832. noopDeltaKeepaliveDeltaType = deltaType
  833. }
  834. }
  835. }
  836. case "content_block_delta":
  837. if idx, ok := sseEventIndex(event); ok {
  838. if delta, ok := event["delta"].(map[string]any); ok {
  839. deltaType, _ := delta["type"].(string)
  840. if claudeCodeKeepaliveFieldForDeltaType(deltaType) != "" {
  841. noopDeltaKeepaliveBlockIndex = idx
  842. noopDeltaKeepaliveDeltaType = deltaType
  843. }
  844. }
  845. }
  846. case "content_block_stop":
  847. if idx, ok := sseEventIndex(event); ok && idx == noopDeltaKeepaliveBlockIndex {
  848. noopDeltaKeepaliveBlockIndex = -1
  849. noopDeltaKeepaliveDeltaType = ""
  850. }
  851. case "message_stop":
  852. noopDeltaKeepaliveBlockIndex = -1
  853. noopDeltaKeepaliveDeltaType = ""
  854. }
  855. }
  856. // 兼容 Kimi cached_tokens → cache_read_input_tokens
  857. if eventType == "message_start" {
  858. if msg, ok := event["message"].(map[string]any); ok {
  859. if u, ok := msg["usage"].(map[string]any); ok {
  860. eventChanged = reconcileCachedTokens(u) || eventChanged
  861. }
  862. }
  863. }
  864. if eventType == "message_delta" {
  865. if u, ok := event["usage"].(map[string]any); ok {
  866. eventChanged = reconcileCachedTokens(u) || eventChanged
  867. }
  868. }
  869. // Cache TTL Override: 重写 SSE 事件中的 cache_creation 分类。
  870. // 账号级设置优先;全局 1h 请求注入开启时,默认把 usage 计费归回 5m。
  871. if overrideTarget, ok := s.resolveCacheTTLUsageOverrideTarget(ctx, account); ok {
  872. if eventType == "message_start" {
  873. if msg, ok := event["message"].(map[string]any); ok {
  874. if u, ok := msg["usage"].(map[string]any); ok {
  875. eventChanged = rewriteCacheCreationJSON(u, overrideTarget) || eventChanged
  876. }
  877. }
  878. }
  879. if eventType == "message_delta" {
  880. if u, ok := event["usage"].(map[string]any); ok {
  881. eventChanged = rewriteCacheCreationJSON(u, overrideTarget) || eventChanged
  882. }
  883. }
  884. }
  885. if needModelReplace {
  886. if msg, ok := event["message"].(map[string]any); ok {
  887. if model, ok := msg["model"].(string); ok && model == mappedModel {
  888. msg["model"] = originalModel
  889. eventChanged = true
  890. }
  891. }
  892. }
  893. usagePatch := s.extractSSEUsagePatch(event)
  894. if anthropicStreamEventIsTerminal(eventName, dataLine) {
  895. sawTerminalEvent = true
  896. }
  897. if !eventChanged {
  898. block := ""
  899. if eventName != "" {
  900. block = "event: " + eventName + "\n"
  901. }
  902. block += "data: " + dataLine + "\n\n"
  903. return []string{block}, dataLine, usagePatch, nil
  904. }
  905. newData, err := json.Marshal(event)
  906. if err != nil {
  907. // 序列化失败,直接透传原始数据
  908. block := ""
  909. if eventName != "" {
  910. block = "event: " + eventName + "\n"
  911. }
  912. block += "data: " + dataLine + "\n\n"
  913. return []string{block}, dataLine, usagePatch, nil
  914. }
  915. block := ""
  916. if eventName != "" {
  917. block = "event: " + eventName + "\n"
  918. }
  919. block += "data: " + string(newData) + "\n\n"
  920. return []string{block}, string(newData), usagePatch, nil
  921. }
  922. for {
  923. select {
  924. case ev, ok := <-events:
  925. if !ok {
  926. // 上游完成,返回结果
  927. if !sawTerminalEvent {
  928. return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: clientDisconnected}, fmt.Errorf("stream usage incomplete: missing terminal event")
  929. }
  930. return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: clientDisconnected}, nil
  931. }
  932. if ev.err != nil {
  933. if sawTerminalEvent {
  934. return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: clientDisconnected}, nil
  935. }
  936. // 检测 context 取消(客户端断开会导致 context 取消,进而影响上游读取)
  937. if errors.Is(ev.err, context.Canceled) || errors.Is(ev.err, context.DeadlineExceeded) {
  938. return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: true}, fmt.Errorf("stream usage incomplete: %w", ev.err)
  939. }
  940. // 客户端已通过写入失败检测到断开,上游也出错了,返回已收集的 usage
  941. if clientDisconnected {
  942. return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: true}, fmt.Errorf("stream usage incomplete after disconnect: %w", ev.err)
  943. }
  944. // 客户端未断开,正常的错误处理
  945. if errors.Is(ev.err, bufio.ErrTooLong) {
  946. logger.LegacyPrintf("service.gateway", "SSE line too long: account=%d max_size=%d error=%v", account.ID, maxLineSize, ev.err)
  947. sendErrorEvent("response_too_large", fmt.Sprintf("upstream SSE line exceeded %d bytes", maxLineSize))
  948. return &streamingResult{usage: usage, firstTokenMs: firstTokenMs}, ev.err
  949. }
  950. // 上游中途读错误(unexpected EOF / connection reset 等,常见于 HTTP/2 GOAWAY):
  951. // 若尚未向客户端写过任何字节,包成 UpstreamFailoverError 让 handler 层走 failover/重试。
  952. // 已经开始写流时 SSE 协议无 resume,只能透传错误事件给客户端。
  953. // 注意:面向客户端的 disconnectMsg 必须用 sanitizeStreamError 剥离地址,
  954. // 默认 *net.OpError 的 Error() 会泄露内部 IP/端口和上游地址。完整 ev.err
  955. // 仅在下方 LegacyPrintf 内部日志中保留供运维诊断。
  956. disconnectMsg := "upstream stream disconnected: " + sanitizeStreamError(ev.err)
  957. if !c.Writer.Written() {
  958. logger.LegacyPrintf("service.gateway", "Upstream stream read error before any client output (account=%d), failing over: %v", account.ID, ev.err)
  959. body, _ := json.Marshal(map[string]any{
  960. "type": "error",
  961. "error": map[string]string{
  962. "type": "upstream_disconnected",
  963. "message": disconnectMsg,
  964. },
  965. })
  966. return nil, &UpstreamFailoverError{
  967. StatusCode: http.StatusBadGateway,
  968. ResponseBody: body,
  969. RetryableOnSameAccount: true,
  970. }
  971. }
  972. sendErrorEvent("stream_read_error", disconnectMsg)
  973. return &streamingResult{usage: usage, firstTokenMs: firstTokenMs}, fmt.Errorf("stream read error: %w", ev.err)
  974. }
  975. line := ev.line
  976. trimmed := strings.TrimSpace(line)
  977. if trimmed == "" {
  978. if len(pendingEventLines) == 0 {
  979. continue
  980. }
  981. outputBlocks, data, usagePatch, err := processSSEEvent(pendingEventLines)
  982. pendingEventLines = pendingEventLines[:0]
  983. if err != nil {
  984. if clientDisconnected {
  985. return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: true}, nil
  986. }
  987. return nil, err
  988. }
  989. for _, block := range outputBlocks {
  990. if !clientDisconnected {
  991. restored := reverseToolNamesIfPresent(c, []byte(block))
  992. if _, werr := fmt.Fprint(w, string(restored)); werr != nil {
  993. clientDisconnected = true
  994. logger.LegacyPrintf("service.gateway", "Client disconnected during streaming, continuing to drain upstream for billing")
  995. // 不 break:客户端断开后仍需继续合并本事件及后续事件的 usage,
  996. // 否则会漏计当前事件携带的 usage 导致少计费。后续写入由
  997. // clientDisconnected 守卫跳过。
  998. } else {
  999. flusher.Flush()
  1000. lastDataAt = time.Now()
  1001. resetKeepaliveTimer()
  1002. }
  1003. }
  1004. if data != "" {
  1005. if firstTokenMs == nil && data != "[DONE]" {
  1006. ms := int(time.Since(startTime).Milliseconds())
  1007. firstTokenMs = &ms
  1008. }
  1009. if usagePatch != nil {
  1010. mergeSSEUsagePatch(usage, usagePatch)
  1011. }
  1012. }
  1013. }
  1014. continue
  1015. }
  1016. pendingEventLines = append(pendingEventLines, line)
  1017. case <-intervalCh:
  1018. lastRead := time.Unix(0, atomic.LoadInt64(&lastReadAt))
  1019. if time.Since(lastRead) < streamInterval {
  1020. continue
  1021. }
  1022. if clientDisconnected {
  1023. return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: true}, fmt.Errorf("stream usage incomplete after timeout")
  1024. }
  1025. logger.LegacyPrintf("service.gateway", "Stream data interval timeout: account=%d model=%s interval=%s", account.ID, originalModel, streamInterval)
  1026. // 处理流超时,可能标记账户为临时不可调度或错误状态
  1027. if s.rateLimitService != nil {
  1028. s.rateLimitService.HandleStreamTimeout(ctx, account, originalModel)
  1029. }
  1030. sendErrorEvent("stream_timeout", fmt.Sprintf("upstream stream idle for %s", streamInterval))
  1031. return &streamingResult{usage: usage, firstTokenMs: firstTokenMs}, fmt.Errorf("stream data interval timeout")
  1032. case <-keepaliveCh:
  1033. if clientDisconnected {
  1034. continue
  1035. }
  1036. if time.Since(lastDataAt) < keepaliveInterval {
  1037. resetKeepaliveTimer()
  1038. continue
  1039. }
  1040. keepaliveBlock := "event: ping\ndata: {\"type\": \"ping\"}\n\n"
  1041. if useNoopDeltaKeepalive && noopDeltaKeepaliveBlockIndex >= 0 {
  1042. if block, ok := buildClaudeCodeNoopDeltaKeepalive(noopDeltaKeepaliveBlockIndex, noopDeltaKeepaliveDeltaType); ok {
  1043. keepaliveBlock = block
  1044. }
  1045. }
  1046. if _, werr := fmt.Fprint(w, keepaliveBlock); werr != nil {
  1047. clientDisconnected = true
  1048. logger.LegacyPrintf("service.gateway", "Client disconnected during keepalive ping, continuing to drain upstream for billing")
  1049. continue
  1050. }
  1051. flusher.Flush()
  1052. lastDataAt = time.Now()
  1053. resetKeepaliveTimer()
  1054. }
  1055. }
  1056. }
  1057. func (s *GatewayService) parseSSEUsage(data string, usage *ClaudeUsage) {
  1058. if usage == nil {
  1059. return
  1060. }
  1061. var event map[string]any
  1062. if err := json.Unmarshal([]byte(data), &event); err != nil {
  1063. return
  1064. }
  1065. if patch := s.extractSSEUsagePatch(event); patch != nil {
  1066. mergeSSEUsagePatch(usage, patch)
  1067. }
  1068. }
  1069. type sseUsagePatch struct {
  1070. inputTokens int
  1071. hasInputTokens bool
  1072. outputTokens int
  1073. hasOutputTokens bool
  1074. cacheCreationInputTokens int
  1075. hasCacheCreationInput bool
  1076. cacheReadInputTokens int
  1077. hasCacheReadInput bool
  1078. cacheCreation5mTokens int
  1079. hasCacheCreation5m bool
  1080. cacheCreation1hTokens int
  1081. hasCacheCreation1h bool
  1082. }
  1083. func (s *GatewayService) extractSSEUsagePatch(event map[string]any) *sseUsagePatch {
  1084. if len(event) == 0 {
  1085. return nil
  1086. }
  1087. eventType, _ := event["type"].(string)
  1088. switch eventType {
  1089. case "message_start":
  1090. msg, _ := event["message"].(map[string]any)
  1091. usageObj, _ := msg["usage"].(map[string]any)
  1092. if len(usageObj) == 0 {
  1093. return nil
  1094. }
  1095. patch := &sseUsagePatch{}
  1096. patch.hasInputTokens = true
  1097. if v, ok := parseSSEUsageInt(usageObj["input_tokens"]); ok {
  1098. patch.inputTokens = v
  1099. }
  1100. patch.hasCacheCreationInput = true
  1101. if v, ok := parseSSEUsageInt(usageObj["cache_creation_input_tokens"]); ok {
  1102. patch.cacheCreationInputTokens = v
  1103. }
  1104. patch.hasCacheReadInput = true
  1105. if v, ok := parseSSEUsageInt(usageObj["cache_read_input_tokens"]); ok {
  1106. patch.cacheReadInputTokens = v
  1107. }
  1108. if cc, ok := usageObj["cache_creation"].(map[string]any); ok {
  1109. if v, exists := parseSSEUsageInt(cc["ephemeral_5m_input_tokens"]); exists {
  1110. patch.cacheCreation5mTokens = v
  1111. patch.hasCacheCreation5m = true
  1112. }
  1113. if v, exists := parseSSEUsageInt(cc["ephemeral_1h_input_tokens"]); exists {
  1114. patch.cacheCreation1hTokens = v
  1115. patch.hasCacheCreation1h = true
  1116. }
  1117. }
  1118. return patch
  1119. case "message_delta":
  1120. usageObj, _ := event["usage"].(map[string]any)
  1121. if len(usageObj) == 0 {
  1122. return nil
  1123. }
  1124. patch := &sseUsagePatch{}
  1125. if v, ok := parseSSEUsageInt(usageObj["input_tokens"]); ok && v > 0 {
  1126. patch.inputTokens = v
  1127. patch.hasInputTokens = true
  1128. }
  1129. if v, ok := parseSSEUsageInt(usageObj["output_tokens"]); ok && v > 0 {
  1130. patch.outputTokens = v
  1131. patch.hasOutputTokens = true
  1132. }
  1133. if v, ok := parseSSEUsageInt(usageObj["cache_creation_input_tokens"]); ok && v > 0 {
  1134. patch.cacheCreationInputTokens = v
  1135. patch.hasCacheCreationInput = true
  1136. }
  1137. if v, ok := parseSSEUsageInt(usageObj["cache_read_input_tokens"]); ok && v > 0 {
  1138. patch.cacheReadInputTokens = v
  1139. patch.hasCacheReadInput = true
  1140. }
  1141. if cc, ok := usageObj["cache_creation"].(map[string]any); ok {
  1142. if v, exists := parseSSEUsageInt(cc["ephemeral_5m_input_tokens"]); exists {
  1143. patch.cacheCreation5mTokens = v
  1144. patch.hasCacheCreation5m = true
  1145. }
  1146. if v, exists := parseSSEUsageInt(cc["ephemeral_1h_input_tokens"]); exists {
  1147. patch.cacheCreation1hTokens = v
  1148. patch.hasCacheCreation1h = true
  1149. }
  1150. }
  1151. return patch
  1152. }
  1153. return nil
  1154. }
  1155. func mergeSSEUsagePatch(usage *ClaudeUsage, patch *sseUsagePatch) {
  1156. if usage == nil || patch == nil {
  1157. return
  1158. }
  1159. if patch.hasInputTokens {
  1160. usage.InputTokens = patch.inputTokens
  1161. }
  1162. if patch.hasCacheCreationInput {
  1163. usage.CacheCreationInputTokens = patch.cacheCreationInputTokens
  1164. }
  1165. if patch.hasCacheReadInput {
  1166. usage.CacheReadInputTokens = patch.cacheReadInputTokens
  1167. }
  1168. if patch.hasOutputTokens {
  1169. usage.OutputTokens = patch.outputTokens
  1170. }
  1171. if patch.hasCacheCreation5m {
  1172. usage.CacheCreation5mTokens = patch.cacheCreation5mTokens
  1173. }
  1174. if patch.hasCacheCreation1h {
  1175. usage.CacheCreation1hTokens = patch.cacheCreation1hTokens
  1176. }
  1177. }
  1178. func parseSSEUsageInt(value any) (int, bool) {
  1179. switch v := value.(type) {
  1180. case float64:
  1181. return int(v), true
  1182. case float32:
  1183. return int(v), true
  1184. case int:
  1185. return v, true
  1186. case int64:
  1187. return int(v), true
  1188. case int32:
  1189. return int(v), true
  1190. case json.Number:
  1191. if i, err := v.Int64(); err == nil {
  1192. return int(i), true
  1193. }
  1194. if f, err := v.Float64(); err == nil {
  1195. return int(f), true
  1196. }
  1197. case string:
  1198. if parsed, err := strconv.Atoi(strings.TrimSpace(v)); err == nil {
  1199. return parsed, true
  1200. }
  1201. }
  1202. return 0, false
  1203. }
  1204. // applyCacheTTLOverride 将所有 cache creation tokens 归入指定的 TTL 类型。
  1205. // target 为 "5m" 或 "1h"。返回 true 表示发生了变更。
  1206. func applyCacheTTLOverride(usage *ClaudeUsage, target string) bool {
  1207. // Fallback: 如果只有聚合字段但无 5m/1h 明细,将聚合字段归入 5m 默认类别
  1208. if usage.CacheCreation5mTokens == 0 && usage.CacheCreation1hTokens == 0 && usage.CacheCreationInputTokens > 0 {
  1209. usage.CacheCreation5mTokens = usage.CacheCreationInputTokens
  1210. }
  1211. total := usage.CacheCreation5mTokens + usage.CacheCreation1hTokens
  1212. if total == 0 {
  1213. return false
  1214. }
  1215. switch target {
  1216. case "1h":
  1217. if usage.CacheCreation1hTokens == total {
  1218. return false // 已经全是 1h
  1219. }
  1220. usage.CacheCreation1hTokens = total
  1221. usage.CacheCreation5mTokens = 0
  1222. default: // "5m"
  1223. if usage.CacheCreation5mTokens == total {
  1224. return false // 已经全是 5m
  1225. }
  1226. usage.CacheCreation5mTokens = total
  1227. usage.CacheCreation1hTokens = 0
  1228. }
  1229. return true
  1230. }
  1231. // rewriteCacheCreationJSON 在 JSON usage 对象中重写 cache_creation 嵌套对象的 TTL 分类。
  1232. // usageObj 是 usage JSON 对象(map[string]any)。
  1233. func rewriteCacheCreationJSON(usageObj map[string]any, target string) bool {
  1234. ccObj, ok := usageObj["cache_creation"].(map[string]any)
  1235. if !ok {
  1236. return false
  1237. }
  1238. v5m, _ := parseSSEUsageInt(ccObj["ephemeral_5m_input_tokens"])
  1239. v1h, _ := parseSSEUsageInt(ccObj["ephemeral_1h_input_tokens"])
  1240. total := v5m + v1h
  1241. if total == 0 {
  1242. return false
  1243. }
  1244. switch target {
  1245. case "1h":
  1246. if v1h == total {
  1247. return false
  1248. }
  1249. ccObj["ephemeral_1h_input_tokens"] = float64(total)
  1250. ccObj["ephemeral_5m_input_tokens"] = float64(0)
  1251. default: // "5m"
  1252. if v5m == total {
  1253. return false
  1254. }
  1255. ccObj["ephemeral_5m_input_tokens"] = float64(total)
  1256. ccObj["ephemeral_1h_input_tokens"] = float64(0)
  1257. }
  1258. return true
  1259. }
  1260. func (s *GatewayService) resolveCacheTTLUsageOverrideTarget(ctx context.Context, account *Account) (string, bool) {
  1261. if account == nil {
  1262. return "", false
  1263. }
  1264. if account.IsCacheTTLOverrideEnabled() {
  1265. return account.GetCacheTTLOverrideTarget(), true
  1266. }
  1267. if account.IsAnthropicOAuthOrSetupToken() && s != nil && s.settingService != nil && s.settingService.IsAnthropicCacheTTL1hInjectionEnabled(ctx) {
  1268. return cacheTTLTarget5m, true
  1269. }
  1270. return "", false
  1271. }
  1272. func (s *GatewayService) handleNonStreamingResponse(ctx context.Context, resp *http.Response, c *gin.Context, account *Account, originalModel, mappedModel string) (*ClaudeUsage, error) {
  1273. // 更新5h窗口状态
  1274. s.rateLimitService.UpdateSessionWindow(ctx, account, resp.Header)
  1275. body, err := ReadUpstreamResponseBody(resp.Body, s.cfg, c, anthropicTooLargeError)
  1276. if err != nil {
  1277. return nil, err
  1278. }
  1279. observer := upstreamResponseModelObserverFromContext(c)
  1280. if observer == nil {
  1281. observer = beginUpstreamResponseModelObservation(c)
  1282. }
  1283. observer.ObserveAnthropic(body)
  1284. // 解析usage
  1285. var response struct {
  1286. Usage ClaudeUsage `json:"usage"`
  1287. }
  1288. if err := json.Unmarshal(body, &response); err != nil {
  1289. if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
  1290. return nil, invalidNonStreamingJSONFailoverError(ctx, s.rateLimitService, resp, account, body, err, mappedModel)
  1291. }
  1292. return nil, fmt.Errorf("parse response: %w", err)
  1293. }
  1294. // 解析嵌套的 cache_creation 对象中的 5m/1h 明细
  1295. cc5m := gjson.GetBytes(body, "usage.cache_creation.ephemeral_5m_input_tokens")
  1296. cc1h := gjson.GetBytes(body, "usage.cache_creation.ephemeral_1h_input_tokens")
  1297. if cc5m.Exists() || cc1h.Exists() {
  1298. response.Usage.CacheCreation5mTokens = int(cc5m.Int())
  1299. response.Usage.CacheCreation1hTokens = int(cc1h.Int())
  1300. }
  1301. // 兼容 Kimi cached_tokens → cache_read_input_tokens
  1302. if response.Usage.CacheReadInputTokens == 0 {
  1303. cachedTokens := gjson.GetBytes(body, "usage.cached_tokens").Int()
  1304. if cachedTokens > 0 {
  1305. response.Usage.CacheReadInputTokens = int(cachedTokens)
  1306. if newBody, err := sjson.SetBytes(body, "usage.cache_read_input_tokens", cachedTokens); err == nil {
  1307. body = newBody
  1308. }
  1309. }
  1310. }
  1311. // Cache TTL Override: 重写 non-streaming 响应中的 cache_creation 分类。
  1312. // 账号级设置优先;全局 1h 请求注入开启时,默认把 usage 计费归回 5m。
  1313. if overrideTarget, ok := s.resolveCacheTTLUsageOverrideTarget(ctx, account); ok {
  1314. if applyCacheTTLOverride(&response.Usage, overrideTarget) {
  1315. // 同步更新 body JSON 中的嵌套 cache_creation 对象
  1316. if newBody, err := sjson.SetBytes(body, "usage.cache_creation.ephemeral_5m_input_tokens", response.Usage.CacheCreation5mTokens); err == nil {
  1317. body = newBody
  1318. }
  1319. if newBody, err := sjson.SetBytes(body, "usage.cache_creation.ephemeral_1h_input_tokens", response.Usage.CacheCreation1hTokens); err == nil {
  1320. body = newBody
  1321. }
  1322. }
  1323. }
  1324. // 如果有模型映射,替换响应中的model字段
  1325. if originalModel != mappedModel {
  1326. body = s.replaceModelInResponseBody(body, mappedModel, originalModel)
  1327. }
  1328. responseheaders.WriteFilteredHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
  1329. contentType := "application/json"
  1330. if s.cfg != nil && !s.cfg.Security.ResponseHeaders.Enabled {
  1331. if upstreamType := resp.Header.Get("Content-Type"); upstreamType != "" {
  1332. contentType = upstreamType
  1333. }
  1334. }
  1335. body = reverseToolNamesIfPresent(c, body)
  1336. // 写入响应
  1337. c.Data(resp.StatusCode, contentType, body)
  1338. return &response.Usage, nil
  1339. }
  1340. // replaceModelInResponseBody 替换响应体中的model字段
  1341. // 使用 gjson/sjson 精确替换,避免全量 JSON 反序列化
  1342. func (s *GatewayService) replaceModelInResponseBody(body []byte, fromModel, toModel string) []byte {
  1343. if m := gjson.GetBytes(body, "model"); m.Exists() && m.Str == fromModel {
  1344. newBody, err := sjson.SetBytes(body, "model", toModel)
  1345. if err != nil {
  1346. return body
  1347. }
  1348. return newBody
  1349. }
  1350. return body
  1351. }
  1352. // reconcileCachedTokens 兼容 Kimi 等上游:
  1353. // 将 OpenAI 风格的 cached_tokens 映射到 Claude 标准的 cache_read_input_tokens
  1354. func reconcileCachedTokens(usage map[string]any) bool {
  1355. if usage == nil {
  1356. return false
  1357. }
  1358. cacheRead, _ := usage["cache_read_input_tokens"].(float64)
  1359. if cacheRead > 0 {
  1360. return false // 已有标准字段,无需处理
  1361. }
  1362. cached, _ := usage["cached_tokens"].(float64)
  1363. if cached <= 0 {
  1364. return false
  1365. }
  1366. usage["cache_read_input_tokens"] = cached
  1367. return true
  1368. }