gateway_anthropic_passthrough.go 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918
  1. package service
  2. // 本文件由 gateway_service.go 纯移动拆分而来:Anthropic APIKey 直通
  3. // (passthrough)转发路径及其流式/非流式响应与 usage 解析。仅做代码搬迁,
  4. // 无任何行为变更。
  5. import (
  6. "bufio"
  7. "bytes"
  8. "context"
  9. "encoding/json"
  10. "errors"
  11. "fmt"
  12. "io"
  13. "net/http"
  14. "strings"
  15. "sync/atomic"
  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. type anthropicPassthroughForwardInput struct {
  24. Body []byte
  25. Parsed *ParsedRequest
  26. RequestModel string
  27. OriginalModel string
  28. RequestStream bool
  29. StartTime time.Time
  30. }
  31. func (s *GatewayService) forwardAnthropicAPIKeyPassthrough(
  32. ctx context.Context,
  33. c *gin.Context,
  34. account *Account,
  35. body []byte,
  36. reqModel string,
  37. originalModel string,
  38. reqStream bool,
  39. startTime time.Time,
  40. ) (*ForwardResult, error) {
  41. return s.forwardAnthropicAPIKeyPassthroughWithInput(ctx, c, account, anthropicPassthroughForwardInput{
  42. Body: body,
  43. RequestModel: reqModel,
  44. OriginalModel: originalModel,
  45. RequestStream: reqStream,
  46. StartTime: startTime,
  47. })
  48. }
  49. func (s *GatewayService) forwardAnthropicAPIKeyPassthroughWithInput(
  50. ctx context.Context,
  51. c *gin.Context,
  52. account *Account,
  53. input anthropicPassthroughForwardInput,
  54. ) (*ForwardResult, error) {
  55. token, tokenType, err := s.GetAccessToken(ctx, account)
  56. if err != nil {
  57. return nil, err
  58. }
  59. if tokenType != "apikey" {
  60. return nil, fmt.Errorf("anthropic api key passthrough requires apikey token, got: %s", tokenType)
  61. }
  62. proxyURL := ""
  63. if account.ProxyID != nil && account.Proxy != nil {
  64. proxyURL = account.Proxy.URL()
  65. }
  66. logger.LegacyPrintf("service.gateway", "[Anthropic 自动透传] 命中 API Key 透传分支: account=%d name=%s model=%s stream=%v",
  67. account.ID, account.Name, input.RequestModel, input.RequestStream)
  68. if c != nil {
  69. c.Set("anthropic_passthrough", true)
  70. }
  71. // Pre-filter: strip empty text blocks (including nested in tool_result) to prevent upstream 400.
  72. input.Body = StripEmptyTextBlocks(input.Body)
  73. // Pre-filter: strip web-search history blocks the upstream cannot accept
  74. // (emulation-synthesized ones always; genuine ones additionally for
  75. // passback-required third-party upstreams such as GLM/Kimi/DeepSeek,
  76. // which reject server_tool_use with 400). input.RequestModel 已是映射后的模型 ID。
  77. input.Body = FilterWebSearchHistoryBlocks(input.Body, input.RequestModel)
  78. if input.Parsed != nil {
  79. // 透传分支也会改写实际 wire body,成功 usage hash 依赖这里同步当前 body。
  80. if err := input.Parsed.ReplaceBody(input.Body); err != nil {
  81. return nil, err
  82. }
  83. }
  84. var resp *http.Response
  85. retryStart := time.Now()
  86. for attempt := 1; attempt <= maxRetryAttempts; attempt++ {
  87. upstreamCtx, releaseUpstreamCtx := detachStreamUpstreamContext(ctx, input.RequestStream)
  88. upstreamReq, wireBody, err := s.buildUpstreamRequestAnthropicAPIKeyPassthrough(upstreamCtx, c, account, input.Body, token)
  89. releaseUpstreamCtx()
  90. if err != nil {
  91. return nil, err
  92. }
  93. if input.Parsed != nil && !bytes.Equal(wireBody, input.Body) {
  94. // build 阶段会按 beta 能力清理 body,发送前同步到 ParsedRequest 当前视图。
  95. if err := input.Parsed.ReplaceBody(wireBody); err != nil {
  96. return nil, err
  97. }
  98. input.Body = input.Parsed.Body.Bytes()
  99. }
  100. resp, err = s.httpUpstream.DoWithTLS(WithAccountTrafficRequest(upstreamReq, account), proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account))
  101. if err != nil {
  102. if resp != nil && resp.Body != nil {
  103. _ = resp.Body.Close()
  104. }
  105. return nil, s.handleUpstreamTransportError(ctx, c, account, err, OpsUpstreamErrorEvent{
  106. UpstreamURL: safeUpstreamURL(upstreamReq.URL.String()),
  107. Passthrough: true,
  108. })
  109. }
  110. // 透传分支禁止 400 请求体降级重试(该重试会改写请求体)
  111. if resp.StatusCode >= 400 && resp.StatusCode != 400 && s.shouldRetryUpstreamError(account, resp.StatusCode) {
  112. if attempt < maxRetryAttempts {
  113. elapsed := time.Since(retryStart)
  114. if elapsed >= maxRetryElapsed {
  115. break
  116. }
  117. delay := retryBackoffDelay(attempt)
  118. remaining := maxRetryElapsed - elapsed
  119. if delay > remaining {
  120. delay = remaining
  121. }
  122. if delay <= 0 {
  123. break
  124. }
  125. respBody, _ := s.readUpstreamErrorBody(resp)
  126. _ = resp.Body.Close()
  127. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  128. ProxyID: opsUpstreamProxyID(account),
  129. ProxyName: opsUpstreamProxyName(account),
  130. Platform: account.Platform,
  131. AccountID: account.ID,
  132. AccountName: account.Name,
  133. UpstreamStatusCode: resp.StatusCode,
  134. UpstreamRequestID: resp.Header.Get("x-request-id"),
  135. UpstreamURL: safeUpstreamURL(upstreamReq.URL.String()),
  136. Passthrough: true,
  137. Kind: "retry",
  138. Message: extractUpstreamErrorMessage(respBody),
  139. Detail: func() string {
  140. if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
  141. return truncateString(string(respBody), s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes)
  142. }
  143. return ""
  144. }(),
  145. })
  146. logger.LegacyPrintf("service.gateway", "Anthropic passthrough account %d: upstream error %d, retry %d/%d after %v (elapsed=%v/%v)",
  147. account.ID, resp.StatusCode, attempt, maxRetryAttempts, delay, elapsed, maxRetryElapsed)
  148. if err := sleepWithContext(ctx, delay); err != nil {
  149. return nil, err
  150. }
  151. continue
  152. }
  153. break
  154. }
  155. break
  156. }
  157. if resp == nil || resp.Body == nil {
  158. return nil, errors.New("upstream request failed: empty response")
  159. }
  160. defer func() { _ = resp.Body.Close() }()
  161. if resp.StatusCode >= 400 && s.shouldRetryUpstreamError(account, resp.StatusCode) {
  162. if s.shouldFailoverUpstreamError(resp.StatusCode) {
  163. respBody, _ := s.readUpstreamErrorBody(resp)
  164. _ = resp.Body.Close()
  165. resp.Body = io.NopCloser(bytes.NewReader(respBody))
  166. logger.LegacyPrintf("service.gateway", "[Anthropic Passthrough] Upstream error (retry exhausted, failover): Account=%d(%s) Status=%d RequestID=%s Body=%s",
  167. account.ID, account.Name, resp.StatusCode, resp.Header.Get("x-request-id"), truncateString(string(respBody), 1000))
  168. s.handleRetryExhaustedSideEffects(ctx, resp, account)
  169. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  170. ProxyID: opsUpstreamProxyID(account),
  171. ProxyName: opsUpstreamProxyName(account),
  172. Platform: account.Platform,
  173. AccountID: account.ID,
  174. AccountName: account.Name,
  175. UpstreamStatusCode: resp.StatusCode,
  176. UpstreamRequestID: resp.Header.Get("x-request-id"),
  177. Passthrough: true,
  178. Kind: "retry_exhausted_failover",
  179. Message: extractUpstreamErrorMessage(respBody),
  180. Detail: func() string {
  181. if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
  182. return truncateString(string(respBody), s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes)
  183. }
  184. return ""
  185. }(),
  186. })
  187. return nil, &UpstreamFailoverError{
  188. StatusCode: resp.StatusCode,
  189. ResponseBody: respBody,
  190. RetryableOnSameAccount: account.IsPoolMode() && account.IsPoolModeRetryableStatus(resp.StatusCode),
  191. }
  192. }
  193. return s.handleRetryExhaustedError(ctx, resp, c, account)
  194. }
  195. if resp.StatusCode >= 400 && s.shouldFailoverUpstreamError(resp.StatusCode) {
  196. respBody, _ := s.readUpstreamErrorBody(resp)
  197. _ = resp.Body.Close()
  198. resp.Body = io.NopCloser(bytes.NewReader(respBody))
  199. logger.LegacyPrintf("service.gateway", "[Anthropic Passthrough] Upstream error (failover): Account=%d(%s) Status=%d RequestID=%s Body=%s",
  200. account.ID, account.Name, resp.StatusCode, resp.Header.Get("x-request-id"), truncateString(string(respBody), 1000))
  201. s.handleFailoverSideEffects(ctx, resp, account, input.RequestModel)
  202. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  203. ProxyID: opsUpstreamProxyID(account),
  204. ProxyName: opsUpstreamProxyName(account),
  205. Platform: account.Platform,
  206. AccountID: account.ID,
  207. AccountName: account.Name,
  208. UpstreamStatusCode: resp.StatusCode,
  209. UpstreamRequestID: resp.Header.Get("x-request-id"),
  210. Passthrough: true,
  211. Kind: "failover",
  212. Message: extractUpstreamErrorMessage(respBody),
  213. Detail: func() string {
  214. if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
  215. return truncateString(string(respBody), s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes)
  216. }
  217. return ""
  218. }(),
  219. })
  220. return nil, &UpstreamFailoverError{
  221. StatusCode: resp.StatusCode,
  222. ResponseBody: respBody,
  223. RetryableOnSameAccount: account.IsPoolMode() && account.IsPoolModeRetryableStatus(resp.StatusCode),
  224. }
  225. }
  226. if resp.StatusCode >= 400 {
  227. return s.handleErrorResponse(ctx, resp, c, account, input.RequestModel)
  228. }
  229. var usage *ClaudeUsage
  230. var firstTokenMs *int
  231. var clientDisconnect bool
  232. if input.RequestStream {
  233. streamResult, err := s.handleStreamingResponseAnthropicAPIKeyPassthrough(ctx, resp, c, account, input.StartTime, input.RequestModel)
  234. if err != nil {
  235. // 流中断时保留已观测到的 usage 与错误一起返回,避免上游已计量的请求
  236. // 完全漏记漏计费(issue #5148)。
  237. if partial := partialStreamUsageResult(c, resp, streamResult, input.OriginalModel, input.RequestModel, input.StartTime, err); partial != nil {
  238. return partial, err
  239. }
  240. return nil, err
  241. }
  242. usage = streamResult.usage
  243. firstTokenMs = streamResult.firstTokenMs
  244. clientDisconnect = streamResult.clientDisconnect
  245. } else {
  246. usage, err = s.handleNonStreamingResponseAnthropicAPIKeyPassthrough(ctx, resp, c, account)
  247. if err != nil {
  248. return nil, err
  249. }
  250. }
  251. if usage == nil {
  252. usage = &ClaudeUsage{}
  253. }
  254. return &ForwardResult{
  255. RequestID: resp.Header.Get("x-request-id"),
  256. UpstreamHeaders: resp.Header,
  257. Usage: *usage,
  258. Model: input.OriginalModel,
  259. UpstreamModel: input.RequestModel,
  260. UpstreamResponseModel: observedUpstreamResponseModel(c),
  261. UpstreamResponseModelConflict: observedUpstreamResponseModelConflict(c),
  262. UpstreamResponseServiceTier: observedUpstreamResponseServiceTier(c),
  263. Stream: input.RequestStream,
  264. Duration: time.Since(input.StartTime),
  265. FirstTokenMs: firstTokenMs,
  266. ClientDisconnect: clientDisconnect,
  267. }, nil
  268. }
  269. func (s *GatewayService) buildUpstreamRequestAnthropicAPIKeyPassthrough(
  270. ctx context.Context,
  271. c *gin.Context,
  272. account *Account,
  273. body []byte,
  274. token string,
  275. ) (*http.Request, []byte, error) {
  276. body = stripDeferredToolCacheControl(body)
  277. targetURL := claudeAPIURL
  278. baseURL := account.GetBaseURL()
  279. if baseURL != "" {
  280. validatedURL, err := s.validateUpstreamBaseURL(baseURL)
  281. if err != nil {
  282. return nil, nil, err
  283. }
  284. targetURL = validatedURL + "/v1/messages?beta=true"
  285. }
  286. // 能力维度 body sanitize:透传路径上 anthropic-beta header 原样透传客户端值,
  287. // 依此决定是否保留 body 中的 context_management。避免“客户端 body 带字段但
  288. // header 忘记带 beta token”的客户端 bug 在透传场景下让上游 400。
  289. clientBeta := ""
  290. if c != nil && c.Request != nil {
  291. clientBeta = getHeaderRaw(c.Request.Header, "anthropic-beta")
  292. }
  293. // 账号覆写了 anthropic-beta 时,覆写值即最终上游值:净化以覆写值为准
  294. if beta, ok := account.HeaderOverrideValue("anthropic-beta"); ok {
  295. clientBeta = beta
  296. }
  297. if sanitized, changed := sanitizeAnthropicBodyForBetaTokens(body, clientBeta); changed {
  298. body = sanitized
  299. }
  300. // Ollama Cloud DeepSeek 出站 max_tokens clamp:判定与上方 targetURL 的
  301. // base 取值同源(GetBaseURL),详见 helper 注释。
  302. body = clampOllamaCloudAnthropicMessagesMaxTokens(account, account.GetBaseURL(), body)
  303. req, err := http.NewRequestWithContext(ctx, http.MethodPost, targetURL, bytes.NewReader(body))
  304. if err != nil {
  305. return nil, nil, err
  306. }
  307. if c != nil && c.Request != nil {
  308. for key, values := range c.Request.Header {
  309. lowerKey := strings.ToLower(strings.TrimSpace(key))
  310. if !allowedHeaders[lowerKey] {
  311. continue
  312. }
  313. wireKey := resolveWireCasing(key)
  314. for _, v := range values {
  315. addHeaderRaw(req.Header, wireKey, v)
  316. }
  317. }
  318. }
  319. // 覆盖入站鉴权残留,并注入上游认证
  320. req.Header.Del("authorization")
  321. req.Header.Del("x-api-key")
  322. req.Header.Del("x-goog-api-key")
  323. req.Header.Del("cookie")
  324. // Ollama Cloud Anthropic 兼容端点按实际 base_url 强制 Bearer(同上方
  325. // targetURL 的 base 取值),其余保持 extra/default 行为。
  326. setAnthropicAPIKeyAuthHeader(req.Header, account, token, account.GetBaseURL())
  327. if getHeaderRaw(req.Header, "content-type") == "" {
  328. setHeaderRaw(req.Header, "content-type", "application/json")
  329. }
  330. if getHeaderRaw(req.Header, "anthropic-version") == "" {
  331. setHeaderRaw(req.Header, "anthropic-version", "2023-06-01")
  332. }
  333. // 账号级请求头覆写(最终生效,覆盖上面所有来源的同名头)
  334. account.ApplyHeaderOverrides(req.Header)
  335. return req, body, nil
  336. }
  337. func (s *GatewayService) handleStreamingResponseAnthropicAPIKeyPassthrough(
  338. ctx context.Context,
  339. resp *http.Response,
  340. c *gin.Context,
  341. account *Account,
  342. startTime time.Time,
  343. model string,
  344. ) (*streamingResult, error) {
  345. observer := upstreamResponseModelObserverFromContext(c)
  346. if observer == nil {
  347. observer = beginUpstreamResponseModelObservation(c)
  348. }
  349. if s.rateLimitService != nil {
  350. s.rateLimitService.UpdateSessionWindow(ctx, account, resp.Header)
  351. }
  352. writeAnthropicPassthroughResponseHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
  353. contentType := strings.TrimSpace(resp.Header.Get("Content-Type"))
  354. if contentType == "" {
  355. contentType = "text/event-stream"
  356. }
  357. c.Header("Content-Type", contentType)
  358. if c.Writer.Header().Get("Cache-Control") == "" {
  359. c.Header("Cache-Control", "no-cache")
  360. }
  361. if c.Writer.Header().Get("Connection") == "" {
  362. c.Header("Connection", "keep-alive")
  363. }
  364. c.Header("X-Accel-Buffering", "no")
  365. if v := resp.Header.Get("x-request-id"); v != "" {
  366. c.Header("x-request-id", v)
  367. }
  368. w := c.Writer
  369. flusher, ok := w.(http.Flusher)
  370. if !ok {
  371. return nil, errors.New("streaming not supported")
  372. }
  373. usage := &ClaudeUsage{}
  374. var firstTokenMs *int
  375. clientDisconnected := false
  376. sawTerminalEvent := false
  377. scanner := bufio.NewScanner(resp.Body)
  378. maxLineSize := defaultMaxLineSize
  379. if s.cfg != nil && s.cfg.Gateway.MaxLineSize > 0 {
  380. maxLineSize = s.cfg.Gateway.MaxLineSize
  381. }
  382. scanBuf := getSSEScannerBuf64K()
  383. scanner.Buffer(scanBuf[:0], maxLineSize)
  384. type scanEvent struct {
  385. line string
  386. err error
  387. }
  388. events := make(chan scanEvent, 16)
  389. done := make(chan struct{})
  390. sendEvent := func(ev scanEvent) bool {
  391. select {
  392. case events <- ev:
  393. return true
  394. case <-done:
  395. return false
  396. }
  397. }
  398. var lastReadAt int64
  399. atomic.StoreInt64(&lastReadAt, time.Now().UnixNano())
  400. go func(scanBuf *sseScannerBuf64K) {
  401. defer putSSEScannerBuf64K(scanBuf)
  402. defer close(events)
  403. for scanner.Scan() {
  404. atomic.StoreInt64(&lastReadAt, time.Now().UnixNano())
  405. if !sendEvent(scanEvent{line: scanner.Text()}) {
  406. return
  407. }
  408. }
  409. if err := scanner.Err(); err != nil {
  410. _ = sendEvent(scanEvent{err: err})
  411. }
  412. }(scanBuf)
  413. defer close(done)
  414. streamInterval := time.Duration(0)
  415. if s.cfg != nil && s.cfg.Gateway.StreamDataIntervalTimeout > 0 {
  416. streamInterval = time.Duration(s.cfg.Gateway.StreamDataIntervalTimeout) * time.Second
  417. }
  418. var intervalTicker *time.Ticker
  419. if streamInterval > 0 {
  420. intervalTicker = time.NewTicker(streamInterval)
  421. defer intervalTicker.Stop()
  422. }
  423. var intervalCh <-chan time.Time
  424. if intervalTicker != nil {
  425. intervalCh = intervalTicker.C
  426. }
  427. keepaliveInterval := time.Duration(0)
  428. if s.cfg != nil && s.cfg.Gateway.StreamKeepaliveInterval > 0 {
  429. keepaliveInterval = time.Duration(s.cfg.Gateway.StreamKeepaliveInterval) * time.Second
  430. }
  431. var keepaliveTimer *time.Timer
  432. if keepaliveInterval > 0 {
  433. keepaliveTimer = time.NewTimer(keepaliveInterval)
  434. defer keepaliveTimer.Stop()
  435. }
  436. var keepaliveCh <-chan time.Time
  437. if keepaliveTimer != nil {
  438. keepaliveCh = keepaliveTimer.C
  439. }
  440. lastDataAt := time.Now()
  441. resetKeepaliveTimer := func() {
  442. if keepaliveTimer == nil {
  443. return
  444. }
  445. if !keepaliveTimer.Stop() {
  446. select {
  447. case <-keepaliveTimer.C:
  448. default:
  449. }
  450. }
  451. keepaliveTimer.Reset(keepaliveInterval)
  452. }
  453. inPartialEvent := false
  454. for {
  455. select {
  456. case ev, ok := <-events:
  457. if !ok {
  458. if !clientDisconnected {
  459. // 兜底补刷,确保最后一个未以空行结尾的事件也能及时送达客户端。
  460. flusher.Flush()
  461. }
  462. if !sawTerminalEvent {
  463. if clientDisconnected && streamInterval > 0 {
  464. lastRead := time.Unix(0, atomic.LoadInt64(&lastReadAt))
  465. if time.Since(lastRead) >= streamInterval {
  466. return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: true}, fmt.Errorf("stream usage incomplete after timeout")
  467. }
  468. }
  469. return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: clientDisconnected}, fmt.Errorf("stream usage incomplete: missing terminal event")
  470. }
  471. return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: clientDisconnected}, nil
  472. }
  473. if ev.err != nil {
  474. if sawTerminalEvent {
  475. return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: clientDisconnected}, nil
  476. }
  477. if clientDisconnected {
  478. return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: true}, fmt.Errorf("stream usage incomplete after disconnect: %w", ev.err)
  479. }
  480. if errors.Is(ev.err, context.Canceled) || errors.Is(ev.err, context.DeadlineExceeded) {
  481. return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: true}, fmt.Errorf("stream usage incomplete: %w", ev.err)
  482. }
  483. if errors.Is(ev.err, bufio.ErrTooLong) {
  484. logger.LegacyPrintf("service.gateway", "[Anthropic passthrough] SSE line too long: account=%d max_size=%d error=%v", account.ID, maxLineSize, ev.err)
  485. return &streamingResult{usage: usage, firstTokenMs: firstTokenMs}, ev.err
  486. }
  487. return &streamingResult{usage: usage, firstTokenMs: firstTokenMs}, fmt.Errorf("stream read error: %w", ev.err)
  488. }
  489. line := ev.line
  490. if data, ok := extractAnthropicSSEDataLine(line); ok {
  491. trimmed := strings.TrimSpace(data)
  492. observer.ObserveAnthropic([]byte(trimmed))
  493. if anthropicStreamEventIsTerminal("", trimmed) {
  494. sawTerminalEvent = true
  495. }
  496. if firstTokenMs == nil && trimmed != "" && trimmed != "[DONE]" {
  497. ms := int(time.Since(startTime).Milliseconds())
  498. firstTokenMs = &ms
  499. }
  500. parseSSEUsagePassthrough(data, usage)
  501. } else {
  502. trimmed := strings.TrimSpace(line)
  503. if strings.HasPrefix(trimmed, "event:") && anthropicStreamEventIsTerminal(strings.TrimSpace(strings.TrimPrefix(trimmed, "event:")), "") {
  504. sawTerminalEvent = true
  505. }
  506. }
  507. if !clientDisconnected {
  508. restored := string(reverseToolNamesIfPresent(c, []byte(line)))
  509. if _, err := io.WriteString(w, restored); err != nil {
  510. clientDisconnected = true
  511. logger.LegacyPrintf("service.gateway", "[Anthropic passthrough] Client disconnected during streaming, continue draining upstream for usage: account=%d", account.ID)
  512. } else if _, err := io.WriteString(w, "\n"); err != nil {
  513. clientDisconnected = true
  514. logger.LegacyPrintf("service.gateway", "[Anthropic passthrough] Client disconnected during streaming, continue draining upstream for usage: account=%d", account.ID)
  515. } else if line == "" {
  516. // 按 SSE 事件边界刷出,减少每行 flush 带来的 syscall 开销。
  517. flusher.Flush()
  518. lastDataAt = time.Now()
  519. resetKeepaliveTimer()
  520. inPartialEvent = false
  521. } else {
  522. inPartialEvent = true
  523. }
  524. }
  525. case <-intervalCh:
  526. lastRead := time.Unix(0, atomic.LoadInt64(&lastReadAt))
  527. if time.Since(lastRead) < streamInterval {
  528. continue
  529. }
  530. if clientDisconnected {
  531. return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: true}, fmt.Errorf("stream usage incomplete after timeout")
  532. }
  533. logger.LegacyPrintf("service.gateway", "[Anthropic passthrough] Stream data interval timeout: account=%d model=%s interval=%s", account.ID, model, streamInterval)
  534. if s.rateLimitService != nil {
  535. s.rateLimitService.HandleStreamTimeout(ctx, account, model)
  536. }
  537. return &streamingResult{usage: usage, firstTokenMs: firstTokenMs}, fmt.Errorf("stream data interval timeout")
  538. case <-keepaliveCh:
  539. if clientDisconnected {
  540. continue
  541. }
  542. if inPartialEvent {
  543. resetKeepaliveTimer()
  544. continue
  545. }
  546. if time.Since(lastDataAt) < keepaliveInterval {
  547. resetKeepaliveTimer()
  548. continue
  549. }
  550. if _, err := fmt.Fprint(w, "event: ping\ndata: {\"type\": \"ping\"}\n\n"); err != nil {
  551. clientDisconnected = true
  552. logger.LegacyPrintf("service.gateway", "[Anthropic passthrough] Client disconnected during keepalive ping, continue draining upstream for usage: account=%d", account.ID)
  553. continue
  554. }
  555. flusher.Flush()
  556. lastDataAt = time.Now()
  557. resetKeepaliveTimer()
  558. }
  559. }
  560. }
  561. func extractAnthropicSSEDataLine(line string) (string, bool) {
  562. if !strings.HasPrefix(line, "data:") {
  563. return "", false
  564. }
  565. start := len("data:")
  566. for start < len(line) {
  567. if line[start] != ' ' && line[start] != '\t' {
  568. break
  569. }
  570. start++
  571. }
  572. return line[start:], true
  573. }
  574. // parseSSEUsagePassthrough 从 Anthropic SSE data 行提取 usage(包级函数:
  575. // Anthropic 平台 passthrough 与国产供应商原生 Anthropic 直通共用)。
  576. func parseSSEUsagePassthrough(data string, usage *ClaudeUsage) {
  577. if usage == nil || data == "" || data == "[DONE]" {
  578. return
  579. }
  580. parsed := gjson.Parse(data)
  581. switch parsed.Get("type").String() {
  582. case "message_start":
  583. msgUsage := parsed.Get("message.usage")
  584. if msgUsage.Exists() {
  585. usage.InputTokens = int(msgUsage.Get("input_tokens").Int())
  586. usage.CacheCreationInputTokens = int(msgUsage.Get("cache_creation_input_tokens").Int())
  587. usage.CacheReadInputTokens = int(msgUsage.Get("cache_read_input_tokens").Int())
  588. // 保持与通用解析一致:message_start 允许覆盖 5m/1h 明细(包括 0)。
  589. cc5m := msgUsage.Get("cache_creation.ephemeral_5m_input_tokens")
  590. cc1h := msgUsage.Get("cache_creation.ephemeral_1h_input_tokens")
  591. if cc5m.Exists() || cc1h.Exists() {
  592. usage.CacheCreation5mTokens = int(cc5m.Int())
  593. usage.CacheCreation1hTokens = int(cc1h.Int())
  594. }
  595. }
  596. case "message_delta":
  597. deltaUsage := parsed.Get("usage")
  598. if deltaUsage.Exists() {
  599. if v := deltaUsage.Get("input_tokens").Int(); v > 0 {
  600. usage.InputTokens = int(v)
  601. }
  602. if v := deltaUsage.Get("output_tokens").Int(); v > 0 {
  603. usage.OutputTokens = int(v)
  604. }
  605. if v := deltaUsage.Get("cache_creation_input_tokens").Int(); v > 0 {
  606. usage.CacheCreationInputTokens = int(v)
  607. }
  608. if v := deltaUsage.Get("cache_read_input_tokens").Int(); v > 0 {
  609. usage.CacheReadInputTokens = int(v)
  610. }
  611. cc5m := deltaUsage.Get("cache_creation.ephemeral_5m_input_tokens")
  612. cc1h := deltaUsage.Get("cache_creation.ephemeral_1h_input_tokens")
  613. if cc5m.Exists() {
  614. usage.CacheCreation5mTokens = int(cc5m.Int())
  615. }
  616. if cc1h.Exists() {
  617. usage.CacheCreation1hTokens = int(cc1h.Int())
  618. }
  619. }
  620. }
  621. if usage.CacheReadInputTokens == 0 {
  622. if cached := parsed.Get("message.usage.cached_tokens").Int(); cached > 0 {
  623. usage.CacheReadInputTokens = int(cached)
  624. }
  625. if cached := parsed.Get("usage.cached_tokens").Int(); usage.CacheReadInputTokens == 0 && cached > 0 {
  626. usage.CacheReadInputTokens = int(cached)
  627. }
  628. }
  629. if usage.CacheCreationInputTokens == 0 {
  630. cc5m := parsed.Get("message.usage.cache_creation.ephemeral_5m_input_tokens").Int()
  631. cc1h := parsed.Get("message.usage.cache_creation.ephemeral_1h_input_tokens").Int()
  632. if cc5m == 0 && cc1h == 0 {
  633. cc5m = parsed.Get("usage.cache_creation.ephemeral_5m_input_tokens").Int()
  634. cc1h = parsed.Get("usage.cache_creation.ephemeral_1h_input_tokens").Int()
  635. }
  636. total := cc5m + cc1h
  637. if total > 0 {
  638. usage.CacheCreationInputTokens = int(total)
  639. }
  640. }
  641. // Kimi's Anthropic-compatible stream uses input_tokens with two meanings:
  642. // message_start reports total prompt input, while message_delta reports only
  643. // uncached input. prompt_tokens remains the total in both events. Normalize
  644. // to ClaudeUsage's mutually-exclusive buckets so downstream billing does not
  645. // subtract cache tokens from an already-uncached value.
  646. usageNode := parsed.Get("usage")
  647. if parsed.Get("type").String() == "message_start" {
  648. usageNode = parsed.Get("message.usage")
  649. }
  650. normalizeAnthropicCompatiblePromptUsage(usageNode, usage)
  651. }
  652. // normalizeAnthropicCompatiblePromptUsage converts provider-native OpenAI-style
  653. // prompt/cache fields into Claude's mutually-exclusive usage buckets. Native
  654. // Anthropic responses do not expose these aliases and are left alone.
  655. func normalizeAnthropicCompatiblePromptUsage(usageNode gjson.Result, usage *ClaudeUsage) bool {
  656. if usage == nil || !usageNode.Exists() {
  657. return false
  658. }
  659. promptTokens := usageNode.Get("prompt_tokens")
  660. promptCacheHitTokens := usageNode.Get("prompt_cache_hit_tokens")
  661. promptCacheMissTokens := usageNode.Get("prompt_cache_miss_tokens")
  662. if (!promptTokens.Exists() || promptTokens.Int() <= 0) &&
  663. !promptCacheHitTokens.Exists() && !promptCacheMissTokens.Exists() {
  664. return false
  665. }
  666. cacheReadTokens := usage.CacheReadInputTokens
  667. if v := usageNode.Get("cache_read_input_tokens"); v.Exists() {
  668. cacheReadTokens = int(v.Int())
  669. }
  670. if cacheReadTokens == 0 {
  671. if v := usageNode.Get("cached_tokens"); v.Exists() {
  672. cacheReadTokens = int(v.Int())
  673. }
  674. }
  675. if cacheReadTokens == 0 {
  676. if v := usageNode.Get("prompt_tokens_details.cached_tokens"); v.Exists() {
  677. cacheReadTokens = int(v.Int())
  678. }
  679. }
  680. if cacheReadTokens == 0 && promptCacheHitTokens.Exists() {
  681. cacheReadTokens = max(int(promptCacheHitTokens.Int()), 0)
  682. }
  683. cacheCreationTokens := usage.CacheCreationInputTokens
  684. if v := usageNode.Get("cache_creation_input_tokens"); v.Exists() {
  685. cacheCreationTokens = int(v.Int())
  686. }
  687. if cacheCreationTokens == 0 {
  688. cc5m := usageNode.Get("cache_creation.ephemeral_5m_input_tokens").Int()
  689. cc1h := usageNode.Get("cache_creation.ephemeral_1h_input_tokens").Int()
  690. if cc5m > 0 || cc1h > 0 {
  691. cacheCreationTokens = int(cc5m + cc1h)
  692. }
  693. }
  694. if promptCacheMissTokens.Exists() {
  695. usage.InputTokens = max(int(promptCacheMissTokens.Int()), 0)
  696. } else {
  697. usage.InputTokens = max(int(promptTokens.Int())-cacheReadTokens-cacheCreationTokens, 0)
  698. }
  699. usage.CacheReadInputTokens = cacheReadTokens
  700. usage.CacheCreationInputTokens = cacheCreationTokens
  701. return true
  702. }
  703. func parseClaudeUsageFromResponseBody(body []byte) *ClaudeUsage {
  704. usage := &ClaudeUsage{}
  705. if len(body) == 0 {
  706. return usage
  707. }
  708. parsed := gjson.ParseBytes(body)
  709. usageNode := parsed.Get("usage")
  710. if !usageNode.Exists() {
  711. return usage
  712. }
  713. usage.InputTokens = int(usageNode.Get("input_tokens").Int())
  714. usage.OutputTokens = int(usageNode.Get("output_tokens").Int())
  715. usage.CacheCreationInputTokens = int(usageNode.Get("cache_creation_input_tokens").Int())
  716. usage.CacheReadInputTokens = int(usageNode.Get("cache_read_input_tokens").Int())
  717. cc5m := usageNode.Get("cache_creation.ephemeral_5m_input_tokens").Int()
  718. cc1h := usageNode.Get("cache_creation.ephemeral_1h_input_tokens").Int()
  719. if cc5m > 0 || cc1h > 0 {
  720. usage.CacheCreation5mTokens = int(cc5m)
  721. usage.CacheCreation1hTokens = int(cc1h)
  722. }
  723. if usage.CacheCreationInputTokens == 0 && (cc5m > 0 || cc1h > 0) {
  724. usage.CacheCreationInputTokens = int(cc5m + cc1h)
  725. }
  726. if usage.CacheReadInputTokens == 0 {
  727. if cached := usageNode.Get("cached_tokens").Int(); cached > 0 {
  728. usage.CacheReadInputTokens = int(cached)
  729. }
  730. }
  731. normalizeAnthropicCompatiblePromptUsage(usageNode, usage)
  732. return usage
  733. }
  734. // invalidNonStreamingJSONFailoverError 把"上游 2xx 返回非 JSON body"归一为
  735. // failover 错误(包级函数:Anthropic 平台 passthrough 与国产供应商原生
  736. // Anthropic 直通共用)。
  737. func invalidNonStreamingJSONFailoverError(
  738. ctx context.Context,
  739. rateLimitService *RateLimitService,
  740. resp *http.Response,
  741. account *Account,
  742. body []byte,
  743. parseErr error,
  744. requestedModel ...string,
  745. ) error {
  746. const statusCode = http.StatusBadGateway
  747. accountID := int64(0)
  748. accountName := ""
  749. retryableOnSameAccount := false
  750. if account != nil {
  751. accountID = account.ID
  752. accountName = account.Name
  753. retryableOnSameAccount = account.IsPoolMode() && account.IsPoolModeRetryableStatus(statusCode)
  754. }
  755. logger.LegacyPrintf(
  756. "service.gateway",
  757. "Account %d(%s): upstream returned non-JSON 2xx response, attempting failover: status=%d request_id=%s error=%v",
  758. accountID,
  759. accountName,
  760. resp.StatusCode,
  761. resp.Header.Get("x-request-id"),
  762. parseErr,
  763. )
  764. if rateLimitService != nil && account != nil {
  765. if len(requestedModel) > 0 {
  766. rateLimitService.HandleUpstreamError(ctx, account, statusCode, resp.Header, body, requestedModel[0])
  767. } else {
  768. rateLimitService.HandleUpstreamError(ctx, account, statusCode, resp.Header, body)
  769. }
  770. }
  771. return &UpstreamFailoverError{
  772. StatusCode: statusCode,
  773. ResponseBody: body,
  774. ResponseHeaders: resp.Header,
  775. RetryableOnSameAccount: retryableOnSameAccount,
  776. }
  777. }
  778. func (s *GatewayService) handleNonStreamingResponseAnthropicAPIKeyPassthrough(
  779. ctx context.Context,
  780. resp *http.Response,
  781. c *gin.Context,
  782. account *Account,
  783. ) (*ClaudeUsage, error) {
  784. if s.rateLimitService != nil {
  785. s.rateLimitService.UpdateSessionWindow(ctx, account, resp.Header)
  786. }
  787. body, err := ReadUpstreamResponseBody(resp.Body, s.cfg, c, anthropicTooLargeError)
  788. if err != nil {
  789. return nil, err
  790. }
  791. observer := upstreamResponseModelObserverFromContext(c)
  792. if observer == nil {
  793. observer = beginUpstreamResponseModelObservation(c)
  794. }
  795. observer.ObserveAnthropic(body)
  796. if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
  797. var raw json.RawMessage
  798. if err := json.Unmarshal(body, &raw); err != nil {
  799. return nil, invalidNonStreamingJSONFailoverError(ctx, s.rateLimitService, resp, account, body, err)
  800. }
  801. }
  802. usage := parseClaudeUsageFromResponseBody(body)
  803. if IsForceCacheBilling(ctx) && usage.InputTokens > 0 {
  804. body, err = classifyAnthropicResponseInputAsCacheRead(body, usage)
  805. if err != nil {
  806. return nil, err
  807. }
  808. }
  809. writeAnthropicPassthroughResponseHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
  810. contentType := strings.TrimSpace(resp.Header.Get("Content-Type"))
  811. if contentType == "" {
  812. contentType = "application/json"
  813. }
  814. body = reverseToolNamesIfPresent(c, body)
  815. c.Data(resp.StatusCode, contentType, body)
  816. return usage, nil
  817. }
  818. func classifyAnthropicResponseInputAsCacheRead(body []byte, usage *ClaudeUsage) ([]byte, error) {
  819. classified, err := sjson.SetBytes(body, "usage.input_tokens", 0)
  820. if err != nil {
  821. return nil, fmt.Errorf("classify forced cache billing input tokens: %w", err)
  822. }
  823. classified, err = sjson.SetBytes(classified, "usage.cache_read_input_tokens", usage.CacheReadInputTokens+usage.InputTokens)
  824. if err != nil {
  825. return nil, fmt.Errorf("classify forced cache billing cache read tokens: %w", err)
  826. }
  827. return classified, nil
  828. }
  829. func writeAnthropicPassthroughResponseHeaders(dst http.Header, src http.Header, filter *responseheaders.CompiledHeaderFilter) {
  830. if dst == nil || src == nil {
  831. return
  832. }
  833. if filter != nil {
  834. responseheaders.WriteFilteredHeaders(dst, src, filter)
  835. return
  836. }
  837. if v := strings.TrimSpace(src.Get("Content-Type")); v != "" {
  838. dst.Set("Content-Type", v)
  839. }
  840. if v := strings.TrimSpace(src.Get("x-request-id")); v != "" {
  841. dst.Set("x-request-id", v)
  842. }
  843. }