openai_gateway_messages_anthropic_native.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. package service
  2. // 国产供应商(kimi/zhipu/deepseek)原生 Anthropic 端点直通路径。
  3. //
  4. // 当账号 credentials["api_protocol"] = "anthropic" 时,入站 /v1/messages 请求
  5. // 不再做 Anthropic→CC→Anthropic 双重转换,而是零转换直通供应商的官方
  6. // Anthropic 兼容端点(如 https://open.bigmodel.cn/api/anthropic/v1/messages),
  7. // 适配 Claude Code 等原生 Anthropic 客户端。转发骨架以
  8. // gateway_anthropic_passthrough.go 的 APIKey 透传为模板(字节级 SSE 中继 +
  9. // usage 解析),错误/failover 语义对齐 OpenAI 网关其他路径
  10. // (failoverOpenAIUpstreamHTTPError / handleAnthropicErrorResponse)。
  11. import (
  12. "bufio"
  13. "bytes"
  14. "context"
  15. "encoding/json"
  16. "errors"
  17. "fmt"
  18. "io"
  19. "net/http"
  20. "strings"
  21. "sync/atomic"
  22. "time"
  23. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  24. "github.com/gin-gonic/gin"
  25. "github.com/tidwall/gjson"
  26. "github.com/tidwall/sjson"
  27. )
  28. // forwardAnthropicViaNativeAnthropicEndpoint 将 Anthropic Messages 请求零转换
  29. // 直通到国产供应商的原生 Anthropic 端点。仅做模型名映射与少量 body 清洗
  30. // (空文本块 / web-search 历史块),协议本身不转换。
  31. func (s *OpenAIGatewayService) forwardAnthropicViaNativeAnthropicEndpoint(
  32. ctx context.Context,
  33. c *gin.Context,
  34. account *Account,
  35. body []byte,
  36. defaultMappedModel string,
  37. ) (*OpenAIForwardResult, error) {
  38. startTime := time.Now()
  39. originalModel := strings.TrimSpace(gjson.GetBytes(body, "model").String())
  40. if originalModel == "" {
  41. writeAnthropicError(c, http.StatusBadRequest, "invalid_request_error", "model is required")
  42. return nil, fmt.Errorf("missing model in request")
  43. }
  44. clientStream := gjson.GetBytes(body, "stream").Bool()
  45. billingModel := resolveOpenAIForwardModel(account, originalModel, defaultMappedModel)
  46. upstreamModel := normalizeOpenAIModelForUpstream(account, billingModel)
  47. if upstreamModel != originalModel {
  48. rewritten, err := sjson.SetBytes(body, "model", upstreamModel)
  49. if err != nil {
  50. return nil, fmt.Errorf("rewrite model: %w", err)
  51. }
  52. body = rewritten
  53. }
  54. if normalized, changed := NormalizeGLM53AnthropicThinking(body, upstreamModel); changed {
  55. body = normalized
  56. }
  57. // 记录客户端请求的推理强度:优先 Claude 协议的 output_config.effort;
  58. // 缺失且 thinking 已启用时,按国产 passback-required 模型兜底为 high
  59. // (对齐 Anthropic 网关 gateway_handler 的记录语义,避免该路径长期落 NULL)。
  60. requestedReasoningEffort := NormalizeClaudeOutputEffort(gjson.GetBytes(body, "output_config.effort").String())
  61. reasoningEffort := ApplyThinkingEnabledFallback(
  62. requestedReasoningEffort,
  63. body,
  64. billingModel,
  65. )
  66. // 与 Anthropic 平台 passthrough 相同的 pre-filter:剥离空文本块与上游
  67. // 无法接受的 web-search 历史块(GLM/Kimi/DeepSeek 对 server_tool_use 400)。
  68. body = StripEmptyTextBlocks(body)
  69. body = FilterWebSearchHistoryBlocks(body, upstreamModel)
  70. logger.LegacyPrintf("service.gateway", "[CN Anthropic 直通] account=%d(%s) platform=%s model=%s upstream=%s stream=%v",
  71. account.ID, account.Name, account.Platform, originalModel, upstreamModel, clientStream)
  72. apiKey := strings.TrimSpace(account.GetOpenAIProtocolAPIKey())
  73. if apiKey == "" {
  74. return nil, fmt.Errorf("account %d missing api_key", account.ID)
  75. }
  76. targetURL, err := s.nativeAnthropicTargetURL(account)
  77. if err != nil {
  78. return nil, err
  79. }
  80. proxyURL := ""
  81. if account.ProxyID != nil && account.Proxy != nil {
  82. proxyURL = account.Proxy.URL()
  83. }
  84. upstreamCtx, releaseUpstreamCtx := detachStreamUpstreamContext(ctx, clientStream)
  85. upstreamReq, _, err := s.buildNativeAnthropicUpstreamRequest(upstreamCtx, c, account, body, apiKey, targetURL)
  86. releaseUpstreamCtx()
  87. if err != nil {
  88. return nil, err
  89. }
  90. resp, err := s.doOpenAIUpstream(upstreamReq, proxyURL, account)
  91. if err != nil {
  92. return nil, s.handleOpenAIUpstreamTransportError(ctx, c, account, err, true)
  93. }
  94. defer func() { _ = resp.Body.Close() }()
  95. if resp.StatusCode >= 400 {
  96. respBody, upstreamMsg := s.readOpenAIUpstreamError(resp)
  97. if foErr := s.failoverOpenAIUpstreamHTTPError(ctx, c, account, resp, respBody, upstreamMsg, upstreamModel); foErr != nil {
  98. return nil, foErr
  99. }
  100. // 非 failover 错误:经共享 compat handler 以 Anthropic 格式回写
  101. // (透传规则、ops 记录、cyber_policy 与 CC 回退路径一致)。
  102. return s.handleAnthropicErrorResponse(resp, c, account, billingModel)
  103. }
  104. if clientStream {
  105. return s.handleNativeAnthropicStreamingResponse(ctx, resp, c, account, originalModel, billingModel, upstreamModel, reasoningEffort, startTime)
  106. }
  107. return s.handleNativeAnthropicBufferedResponse(ctx, resp, c, account, originalModel, billingModel, upstreamModel, reasoningEffort, startTime)
  108. }
  109. // nativeAnthropicTargetURL 组装国产供应商原生 Anthropic messages 端点。
  110. // 第三方端点保持朴素路径,不附加 ?beta=true。
  111. func (s *OpenAIGatewayService) nativeAnthropicTargetURL(account *Account) (string, error) {
  112. baseURL := strings.TrimSpace(account.GetAnthropicProtocolBaseURL())
  113. if baseURL == "" {
  114. return "", fmt.Errorf("account %d has no anthropic protocol base url", account.ID)
  115. }
  116. validatedURL, err := s.validateUpstreamBaseURL(baseURL)
  117. if err != nil {
  118. return "", fmt.Errorf("invalid base_url: %w", err)
  119. }
  120. return strings.TrimRight(validatedURL, "/") + "/v1/messages", nil
  121. }
  122. func (s *OpenAIGatewayService) buildNativeAnthropicUpstreamRequest(
  123. ctx context.Context,
  124. c *gin.Context,
  125. account *Account,
  126. body []byte,
  127. apiKey string,
  128. targetURL string,
  129. ) (*http.Request, []byte, error) {
  130. // 能力维度 body sanitize:与 Anthropic 平台 passthrough 相同,按 beta
  131. // header 决定是否保留 body 中的 beta 能力字段,避免客户端"body 带字段但
  132. // header 忘带 token"的 bug 让第三方上游 400。
  133. clientBeta := ""
  134. if c != nil && c.Request != nil {
  135. clientBeta = getHeaderRaw(c.Request.Header, "anthropic-beta")
  136. }
  137. if beta, ok := account.HeaderOverrideValue("anthropic-beta"); ok {
  138. clientBeta = beta
  139. }
  140. if sanitized, changed := sanitizeAnthropicBodyForBetaTokens(body, clientBeta); changed {
  141. body = sanitized
  142. }
  143. // Ollama Cloud DeepSeek 出站 max_tokens clamp:判定与 nativeAnthropicTargetURL
  144. // 的 base 取值同源(GetAnthropicProtocolBaseURL,adaptive 时是 Anthropic 协议
  145. // 地址而非 CC/Responses 地址),详见 helper 注释。
  146. body = clampOllamaCloudAnthropicMessagesMaxTokens(account, account.GetAnthropicProtocolBaseURL(), body)
  147. req, err := http.NewRequestWithContext(ctx, http.MethodPost, targetURL, bytes.NewReader(body))
  148. if err != nil {
  149. return nil, nil, err
  150. }
  151. if c != nil && c.Request != nil {
  152. for key, values := range c.Request.Header {
  153. lowerKey := strings.ToLower(strings.TrimSpace(key))
  154. if !allowedHeaders[lowerKey] {
  155. continue
  156. }
  157. wireKey := resolveWireCasing(key)
  158. for _, v := range values {
  159. addHeaderRaw(req.Header, wireKey, v)
  160. }
  161. }
  162. }
  163. // 覆盖入站鉴权残留,注入上游认证(默认 x-api-key;可经 extra
  164. // anthropic_apikey_auth_scheme 切换 Authorization: Bearer;Ollama Cloud
  165. // 上游按实际 base_url 强制 Bearer,与 nativeAnthropicTargetURL 同源)。
  166. req.Header.Del("authorization")
  167. req.Header.Del("x-api-key")
  168. req.Header.Del("x-goog-api-key")
  169. req.Header.Del("cookie")
  170. setAnthropicAPIKeyAuthHeader(req.Header, account, apiKey, account.GetAnthropicProtocolBaseURL())
  171. if getHeaderRaw(req.Header, "content-type") == "" {
  172. setHeaderRaw(req.Header, "content-type", "application/json")
  173. }
  174. if getHeaderRaw(req.Header, "anthropic-version") == "" {
  175. setHeaderRaw(req.Header, "anthropic-version", "2023-06-01")
  176. }
  177. // 账号级请求头覆写(最终生效,覆盖上面所有来源的同名头)
  178. account.ApplyHeaderOverrides(req.Header)
  179. return req, body, nil
  180. }
  181. // handleNativeAnthropicBufferedResponse 处理非流式原生 Anthropic 响应:
  182. // 校验 JSON、解析 usage、透传响应头后原样回写(仅工具名反向还原)。
  183. func (s *OpenAIGatewayService) handleNativeAnthropicBufferedResponse(
  184. ctx context.Context,
  185. resp *http.Response,
  186. c *gin.Context,
  187. account *Account,
  188. originalModel string,
  189. billingModel string,
  190. upstreamModel string,
  191. reasoningEffort *string,
  192. startTime time.Time,
  193. ) (*OpenAIForwardResult, error) {
  194. if s.rateLimitService != nil {
  195. s.rateLimitService.UpdateSessionWindow(ctx, account, resp.Header)
  196. }
  197. body, err := ReadUpstreamResponseBody(resp.Body, s.cfg, c, anthropicTooLargeError)
  198. if err != nil {
  199. return nil, err
  200. }
  201. observer := upstreamResponseModelObserverFromContext(c)
  202. if observer == nil {
  203. observer = beginUpstreamResponseModelObservation(c)
  204. }
  205. observer.ObserveAnthropic(body)
  206. var raw json.RawMessage
  207. if err := json.Unmarshal(body, &raw); err != nil {
  208. return nil, invalidNonStreamingJSONFailoverError(ctx, s.rateLimitService, resp, account, body, err, billingModel)
  209. }
  210. usage := parseClaudeUsageFromResponseBody(body)
  211. if IsForceCacheBilling(ctx) && usage.InputTokens > 0 {
  212. body, err = classifyAnthropicResponseInputAsCacheRead(body, usage)
  213. if err != nil {
  214. return nil, err
  215. }
  216. }
  217. writeAnthropicPassthroughResponseHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
  218. contentType := strings.TrimSpace(resp.Header.Get("Content-Type"))
  219. if contentType == "" {
  220. contentType = "application/json"
  221. }
  222. body = reverseToolNamesIfPresent(c, body)
  223. c.Data(resp.StatusCode, contentType, body)
  224. return &OpenAIForwardResult{
  225. RequestID: resp.Header.Get("x-request-id"),
  226. UpstreamHeaders: resp.Header,
  227. Usage: claudeUsageToOpenAIUsage(usage),
  228. Model: originalModel,
  229. BillingModel: billingModel,
  230. UpstreamModel: upstreamModel,
  231. UpstreamEndpoint: "/v1/messages",
  232. Stream: false,
  233. ReasoningEffort: reasoningEffort,
  234. Duration: time.Since(startTime),
  235. }, nil
  236. }
  237. // handleNativeAnthropicStreamingResponse 处理流式原生 Anthropic 响应:
  238. // 字节级 SSE 中继(逐行透传、按事件边界 flush),同时解析 usage。
  239. // 骨架与 handleStreamingResponseAnthropicAPIKeyPassthrough 一致。
  240. func (s *OpenAIGatewayService) handleNativeAnthropicStreamingResponse(
  241. ctx context.Context,
  242. resp *http.Response,
  243. c *gin.Context,
  244. account *Account,
  245. originalModel string,
  246. billingModel string,
  247. upstreamModel string,
  248. reasoningEffort *string,
  249. startTime time.Time,
  250. ) (*OpenAIForwardResult, error) {
  251. observer := upstreamResponseModelObserverFromContext(c)
  252. if observer == nil {
  253. observer = beginUpstreamResponseModelObservation(c)
  254. }
  255. if s.rateLimitService != nil {
  256. s.rateLimitService.UpdateSessionWindow(ctx, account, resp.Header)
  257. }
  258. writeAnthropicPassthroughResponseHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
  259. contentType := strings.TrimSpace(resp.Header.Get("Content-Type"))
  260. if contentType == "" {
  261. contentType = "text/event-stream"
  262. }
  263. c.Header("Content-Type", contentType)
  264. if c.Writer.Header().Get("Cache-Control") == "" {
  265. c.Header("Cache-Control", "no-cache")
  266. }
  267. if c.Writer.Header().Get("Connection") == "" {
  268. c.Header("Connection", "keep-alive")
  269. }
  270. c.Header("X-Accel-Buffering", "no")
  271. if v := resp.Header.Get("x-request-id"); v != "" {
  272. c.Header("x-request-id", v)
  273. }
  274. w := c.Writer
  275. flusher, ok := w.(http.Flusher)
  276. if !ok {
  277. return nil, errors.New("streaming not supported")
  278. }
  279. usage := &ClaudeUsage{}
  280. var firstTokenMs *int
  281. clientDisconnected := false
  282. sawTerminalEvent := false
  283. scanner := bufio.NewScanner(resp.Body)
  284. maxLineSize := defaultMaxLineSize
  285. if s.cfg != nil && s.cfg.Gateway.MaxLineSize > 0 {
  286. maxLineSize = s.cfg.Gateway.MaxLineSize
  287. }
  288. scanBuf := getSSEScannerBuf64K()
  289. scanner.Buffer(scanBuf[:0], maxLineSize)
  290. type scanEvent struct {
  291. line string
  292. err error
  293. }
  294. events := make(chan scanEvent, 16)
  295. done := make(chan struct{})
  296. sendEvent := func(ev scanEvent) bool {
  297. select {
  298. case events <- ev:
  299. return true
  300. case <-done:
  301. return false
  302. }
  303. }
  304. var lastReadAt int64
  305. atomic.StoreInt64(&lastReadAt, time.Now().UnixNano())
  306. go func(scanBuf *sseScannerBuf64K) {
  307. defer putSSEScannerBuf64K(scanBuf)
  308. defer close(events)
  309. for scanner.Scan() {
  310. atomic.StoreInt64(&lastReadAt, time.Now().UnixNano())
  311. if !sendEvent(scanEvent{line: scanner.Text()}) {
  312. return
  313. }
  314. }
  315. if err := scanner.Err(); err != nil {
  316. _ = sendEvent(scanEvent{err: err})
  317. }
  318. }(scanBuf)
  319. defer close(done)
  320. streamInterval := time.Duration(0)
  321. if s.cfg != nil && s.cfg.Gateway.StreamDataIntervalTimeout > 0 {
  322. streamInterval = time.Duration(s.cfg.Gateway.StreamDataIntervalTimeout) * time.Second
  323. }
  324. var intervalTicker *time.Ticker
  325. if streamInterval > 0 {
  326. intervalTicker = time.NewTicker(streamInterval)
  327. defer intervalTicker.Stop()
  328. }
  329. var intervalCh <-chan time.Time
  330. if intervalTicker != nil {
  331. intervalCh = intervalTicker.C
  332. }
  333. keepaliveInterval := time.Duration(0)
  334. if s.cfg != nil && s.cfg.Gateway.StreamKeepaliveInterval > 0 {
  335. keepaliveInterval = time.Duration(s.cfg.Gateway.StreamKeepaliveInterval) * time.Second
  336. }
  337. var keepaliveTimer *time.Timer
  338. if keepaliveInterval > 0 {
  339. keepaliveTimer = time.NewTimer(keepaliveInterval)
  340. defer keepaliveTimer.Stop()
  341. }
  342. var keepaliveCh <-chan time.Time
  343. if keepaliveTimer != nil {
  344. keepaliveCh = keepaliveTimer.C
  345. }
  346. lastDataAt := time.Now()
  347. resetKeepaliveTimer := func() {
  348. if keepaliveTimer == nil {
  349. return
  350. }
  351. if !keepaliveTimer.Stop() {
  352. select {
  353. case <-keepaliveTimer.C:
  354. default:
  355. }
  356. }
  357. keepaliveTimer.Reset(keepaliveInterval)
  358. }
  359. inPartialEvent := false
  360. for {
  361. select {
  362. case ev, ok := <-events:
  363. if !ok {
  364. if !clientDisconnected {
  365. flusher.Flush()
  366. }
  367. if !sawTerminalEvent {
  368. return s.nativeAnthropicStreamResult(c, resp, usage, firstTokenMs, clientDisconnected, originalModel, billingModel, upstreamModel, reasoningEffort, startTime),
  369. fmt.Errorf("stream usage incomplete: missing terminal event")
  370. }
  371. return s.nativeAnthropicStreamResult(c, resp, usage, firstTokenMs, clientDisconnected, originalModel, billingModel, upstreamModel, reasoningEffort, startTime), nil
  372. }
  373. if ev.err != nil {
  374. if sawTerminalEvent {
  375. return s.nativeAnthropicStreamResult(c, resp, usage, firstTokenMs, clientDisconnected, originalModel, billingModel, upstreamModel, reasoningEffort, startTime), nil
  376. }
  377. if clientDisconnected {
  378. return s.nativeAnthropicStreamResult(c, resp, usage, firstTokenMs, clientDisconnected, originalModel, billingModel, upstreamModel, reasoningEffort, startTime),
  379. fmt.Errorf("stream usage incomplete after disconnect: %w", ev.err)
  380. }
  381. if errors.Is(ev.err, context.Canceled) || errors.Is(ev.err, context.DeadlineExceeded) {
  382. return s.nativeAnthropicStreamResult(c, resp, usage, firstTokenMs, clientDisconnected, originalModel, billingModel, upstreamModel, reasoningEffort, startTime),
  383. fmt.Errorf("stream usage incomplete: %w", ev.err)
  384. }
  385. if errors.Is(ev.err, bufio.ErrTooLong) {
  386. logger.LegacyPrintf("service.gateway", "[CN Anthropic 直通] SSE line too long: account=%d max_size=%d error=%v", account.ID, maxLineSize, ev.err)
  387. return s.nativeAnthropicStreamResult(c, resp, usage, firstTokenMs, clientDisconnected, originalModel, billingModel, upstreamModel, reasoningEffort, startTime), ev.err
  388. }
  389. return s.nativeAnthropicStreamResult(c, resp, usage, firstTokenMs, clientDisconnected, originalModel, billingModel, upstreamModel, reasoningEffort, startTime),
  390. fmt.Errorf("stream read error: %w", ev.err)
  391. }
  392. line := ev.line
  393. if data, ok := extractAnthropicSSEDataLine(line); ok {
  394. trimmed := strings.TrimSpace(data)
  395. observer.ObserveAnthropic([]byte(trimmed))
  396. if anthropicStreamEventIsTerminal("", trimmed) {
  397. sawTerminalEvent = true
  398. }
  399. if firstTokenMs == nil && trimmed != "" && trimmed != "[DONE]" {
  400. ms := int(time.Since(startTime).Milliseconds())
  401. firstTokenMs = &ms
  402. }
  403. parseSSEUsagePassthrough(data, usage)
  404. } else {
  405. trimmed := strings.TrimSpace(line)
  406. if strings.HasPrefix(trimmed, "event:") && anthropicStreamEventIsTerminal(strings.TrimSpace(strings.TrimPrefix(trimmed, "event:")), "") {
  407. sawTerminalEvent = true
  408. }
  409. }
  410. if !clientDisconnected {
  411. restored := string(reverseToolNamesIfPresent(c, []byte(line)))
  412. if _, err := io.WriteString(w, restored); err != nil {
  413. clientDisconnected = true
  414. logger.LegacyPrintf("service.gateway", "[CN Anthropic 直通] Client disconnected during streaming, continue draining upstream for usage: account=%d", account.ID)
  415. } else if _, err := io.WriteString(w, "\n"); err != nil {
  416. clientDisconnected = true
  417. logger.LegacyPrintf("service.gateway", "[CN Anthropic 直通] Client disconnected during streaming, continue draining upstream for usage: account=%d", account.ID)
  418. } else if line == "" {
  419. // 按 SSE 事件边界刷出,减少每行 flush 带来的 syscall 开销。
  420. flusher.Flush()
  421. lastDataAt = time.Now()
  422. resetKeepaliveTimer()
  423. inPartialEvent = false
  424. } else {
  425. inPartialEvent = true
  426. }
  427. }
  428. case <-intervalCh:
  429. lastRead := time.Unix(0, atomic.LoadInt64(&lastReadAt))
  430. if time.Since(lastRead) < streamInterval {
  431. continue
  432. }
  433. if clientDisconnected {
  434. return s.nativeAnthropicStreamResult(c, resp, usage, firstTokenMs, clientDisconnected, originalModel, billingModel, upstreamModel, reasoningEffort, startTime),
  435. fmt.Errorf("stream usage incomplete after timeout")
  436. }
  437. logger.LegacyPrintf("service.gateway", "[CN Anthropic 直通] Stream data interval timeout: account=%d model=%s interval=%s", account.ID, upstreamModel, streamInterval)
  438. if s.rateLimitService != nil {
  439. s.rateLimitService.HandleStreamTimeout(ctx, account, upstreamModel)
  440. }
  441. return s.nativeAnthropicStreamResult(c, resp, usage, firstTokenMs, clientDisconnected, originalModel, billingModel, upstreamModel, reasoningEffort, startTime),
  442. fmt.Errorf("stream data interval timeout")
  443. case <-keepaliveCh:
  444. if clientDisconnected {
  445. continue
  446. }
  447. if inPartialEvent {
  448. resetKeepaliveTimer()
  449. continue
  450. }
  451. if time.Since(lastDataAt) < keepaliveInterval {
  452. resetKeepaliveTimer()
  453. continue
  454. }
  455. if _, err := fmt.Fprint(w, "event: ping\ndata: {\"type\": \"ping\"}\n\n"); err != nil {
  456. clientDisconnected = true
  457. logger.LegacyPrintf("service.gateway", "[CN Anthropic 直通] Client disconnected during keepalive ping, continue draining upstream for usage: account=%d", account.ID)
  458. continue
  459. }
  460. flusher.Flush()
  461. lastDataAt = time.Now()
  462. resetKeepaliveTimer()
  463. }
  464. }
  465. }
  466. // nativeAnthropicStreamResult 组装流式直通结果;流中断时同样返回已观测到的
  467. // usage 与错误一起带出,避免上游已计量的请求漏记漏计费(对齐 issue #5148 语义)。
  468. func (s *OpenAIGatewayService) nativeAnthropicStreamResult(
  469. c *gin.Context,
  470. resp *http.Response,
  471. usage *ClaudeUsage,
  472. firstTokenMs *int,
  473. clientDisconnect bool,
  474. originalModel string,
  475. billingModel string,
  476. upstreamModel string,
  477. reasoningEffort *string,
  478. startTime time.Time,
  479. ) *OpenAIForwardResult {
  480. if usage == nil {
  481. usage = &ClaudeUsage{}
  482. }
  483. return &OpenAIForwardResult{
  484. RequestID: resp.Header.Get("x-request-id"),
  485. UpstreamHeaders: resp.Header,
  486. Usage: claudeUsageToOpenAIUsage(usage),
  487. Model: originalModel,
  488. BillingModel: billingModel,
  489. UpstreamModel: upstreamModel,
  490. UpstreamEndpoint: "/v1/messages",
  491. Stream: true,
  492. ReasoningEffort: reasoningEffort,
  493. Duration: time.Since(startTime),
  494. FirstTokenMs: firstTokenMs,
  495. ClientDisconnect: clientDisconnect,
  496. }
  497. }
  498. // claudeUsageToOpenAIUsage 把 Anthropic 格式 usage 映射到 OpenAI 网关统一的
  499. // 用量结构。Anthropic 的 input_tokens 不含缓存读写,而 OpenAI 网关内部
  500. // 约定 InputTokens 是包含缓存明细的总输入;这里必须先合并,RecordUsage
  501. // 才能准确拆回互斥的计费桶。
  502. func claudeUsageToOpenAIUsage(u *ClaudeUsage) OpenAIUsage {
  503. if u == nil {
  504. return OpenAIUsage{}
  505. }
  506. return OpenAIUsage{
  507. InputTokens: u.InputTokens + u.CacheCreationInputTokens + u.CacheReadInputTokens,
  508. OutputTokens: u.OutputTokens,
  509. CacheCreationInputTokens: u.CacheCreationInputTokens,
  510. CacheReadInputTokens: u.CacheReadInputTokens,
  511. }
  512. }