| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918 |
- package service
- // 本文件由 gateway_service.go 纯移动拆分而来:Anthropic APIKey 直通
- // (passthrough)转发路径及其流式/非流式响应与 usage 解析。仅做代码搬迁,
- // 无任何行为变更。
- import (
- "bufio"
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net/http"
- "strings"
- "sync/atomic"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
- "github.com/Wei-Shaw/sub2api/internal/util/responseheaders"
- "github.com/tidwall/gjson"
- "github.com/tidwall/sjson"
- "github.com/gin-gonic/gin"
- )
- type anthropicPassthroughForwardInput struct {
- Body []byte
- Parsed *ParsedRequest
- RequestModel string
- OriginalModel string
- RequestStream bool
- StartTime time.Time
- }
- func (s *GatewayService) forwardAnthropicAPIKeyPassthrough(
- ctx context.Context,
- c *gin.Context,
- account *Account,
- body []byte,
- reqModel string,
- originalModel string,
- reqStream bool,
- startTime time.Time,
- ) (*ForwardResult, error) {
- return s.forwardAnthropicAPIKeyPassthroughWithInput(ctx, c, account, anthropicPassthroughForwardInput{
- Body: body,
- RequestModel: reqModel,
- OriginalModel: originalModel,
- RequestStream: reqStream,
- StartTime: startTime,
- })
- }
- func (s *GatewayService) forwardAnthropicAPIKeyPassthroughWithInput(
- ctx context.Context,
- c *gin.Context,
- account *Account,
- input anthropicPassthroughForwardInput,
- ) (*ForwardResult, error) {
- token, tokenType, err := s.GetAccessToken(ctx, account)
- if err != nil {
- return nil, err
- }
- if tokenType != "apikey" {
- return nil, fmt.Errorf("anthropic api key passthrough requires apikey token, got: %s", tokenType)
- }
- proxyURL := ""
- if account.ProxyID != nil && account.Proxy != nil {
- proxyURL = account.Proxy.URL()
- }
- logger.LegacyPrintf("service.gateway", "[Anthropic 自动透传] 命中 API Key 透传分支: account=%d name=%s model=%s stream=%v",
- account.ID, account.Name, input.RequestModel, input.RequestStream)
- if c != nil {
- c.Set("anthropic_passthrough", true)
- }
- // Pre-filter: strip empty text blocks (including nested in tool_result) to prevent upstream 400.
- input.Body = StripEmptyTextBlocks(input.Body)
- // Pre-filter: strip web-search history blocks the upstream cannot accept
- // (emulation-synthesized ones always; genuine ones additionally for
- // passback-required third-party upstreams such as GLM/Kimi/DeepSeek,
- // which reject server_tool_use with 400). input.RequestModel 已是映射后的模型 ID。
- input.Body = FilterWebSearchHistoryBlocks(input.Body, input.RequestModel)
- if input.Parsed != nil {
- // 透传分支也会改写实际 wire body,成功 usage hash 依赖这里同步当前 body。
- if err := input.Parsed.ReplaceBody(input.Body); err != nil {
- return nil, err
- }
- }
- var resp *http.Response
- retryStart := time.Now()
- for attempt := 1; attempt <= maxRetryAttempts; attempt++ {
- upstreamCtx, releaseUpstreamCtx := detachStreamUpstreamContext(ctx, input.RequestStream)
- upstreamReq, wireBody, err := s.buildUpstreamRequestAnthropicAPIKeyPassthrough(upstreamCtx, c, account, input.Body, token)
- releaseUpstreamCtx()
- if err != nil {
- return nil, err
- }
- if input.Parsed != nil && !bytes.Equal(wireBody, input.Body) {
- // build 阶段会按 beta 能力清理 body,发送前同步到 ParsedRequest 当前视图。
- if err := input.Parsed.ReplaceBody(wireBody); err != nil {
- return nil, err
- }
- input.Body = input.Parsed.Body.Bytes()
- }
- resp, err = s.httpUpstream.DoWithTLS(WithAccountTrafficRequest(upstreamReq, account), proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account))
- if err != nil {
- if resp != nil && resp.Body != nil {
- _ = resp.Body.Close()
- }
- return nil, s.handleUpstreamTransportError(ctx, c, account, err, OpsUpstreamErrorEvent{
- UpstreamURL: safeUpstreamURL(upstreamReq.URL.String()),
- Passthrough: true,
- })
- }
- // 透传分支禁止 400 请求体降级重试(该重试会改写请求体)
- if resp.StatusCode >= 400 && resp.StatusCode != 400 && s.shouldRetryUpstreamError(account, resp.StatusCode) {
- if attempt < maxRetryAttempts {
- elapsed := time.Since(retryStart)
- if elapsed >= maxRetryElapsed {
- break
- }
- delay := retryBackoffDelay(attempt)
- remaining := maxRetryElapsed - elapsed
- if delay > remaining {
- delay = remaining
- }
- if delay <= 0 {
- break
- }
- respBody, _ := s.readUpstreamErrorBody(resp)
- _ = resp.Body.Close()
- appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
- ProxyID: opsUpstreamProxyID(account),
- ProxyName: opsUpstreamProxyName(account),
- Platform: account.Platform,
- AccountID: account.ID,
- AccountName: account.Name,
- UpstreamStatusCode: resp.StatusCode,
- UpstreamRequestID: resp.Header.Get("x-request-id"),
- UpstreamURL: safeUpstreamURL(upstreamReq.URL.String()),
- Passthrough: true,
- Kind: "retry",
- Message: extractUpstreamErrorMessage(respBody),
- Detail: func() string {
- if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
- return truncateString(string(respBody), s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes)
- }
- return ""
- }(),
- })
- logger.LegacyPrintf("service.gateway", "Anthropic passthrough account %d: upstream error %d, retry %d/%d after %v (elapsed=%v/%v)",
- account.ID, resp.StatusCode, attempt, maxRetryAttempts, delay, elapsed, maxRetryElapsed)
- if err := sleepWithContext(ctx, delay); err != nil {
- return nil, err
- }
- continue
- }
- break
- }
- break
- }
- if resp == nil || resp.Body == nil {
- return nil, errors.New("upstream request failed: empty response")
- }
- defer func() { _ = resp.Body.Close() }()
- if resp.StatusCode >= 400 && s.shouldRetryUpstreamError(account, resp.StatusCode) {
- if s.shouldFailoverUpstreamError(resp.StatusCode) {
- respBody, _ := s.readUpstreamErrorBody(resp)
- _ = resp.Body.Close()
- resp.Body = io.NopCloser(bytes.NewReader(respBody))
- logger.LegacyPrintf("service.gateway", "[Anthropic Passthrough] Upstream error (retry exhausted, failover): Account=%d(%s) Status=%d RequestID=%s Body=%s",
- account.ID, account.Name, resp.StatusCode, resp.Header.Get("x-request-id"), truncateString(string(respBody), 1000))
- s.handleRetryExhaustedSideEffects(ctx, resp, account)
- appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
- ProxyID: opsUpstreamProxyID(account),
- ProxyName: opsUpstreamProxyName(account),
- Platform: account.Platform,
- AccountID: account.ID,
- AccountName: account.Name,
- UpstreamStatusCode: resp.StatusCode,
- UpstreamRequestID: resp.Header.Get("x-request-id"),
- Passthrough: true,
- Kind: "retry_exhausted_failover",
- Message: extractUpstreamErrorMessage(respBody),
- Detail: func() string {
- if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
- return truncateString(string(respBody), s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes)
- }
- return ""
- }(),
- })
- return nil, &UpstreamFailoverError{
- StatusCode: resp.StatusCode,
- ResponseBody: respBody,
- RetryableOnSameAccount: account.IsPoolMode() && account.IsPoolModeRetryableStatus(resp.StatusCode),
- }
- }
- return s.handleRetryExhaustedError(ctx, resp, c, account)
- }
- if resp.StatusCode >= 400 && s.shouldFailoverUpstreamError(resp.StatusCode) {
- respBody, _ := s.readUpstreamErrorBody(resp)
- _ = resp.Body.Close()
- resp.Body = io.NopCloser(bytes.NewReader(respBody))
- logger.LegacyPrintf("service.gateway", "[Anthropic Passthrough] Upstream error (failover): Account=%d(%s) Status=%d RequestID=%s Body=%s",
- account.ID, account.Name, resp.StatusCode, resp.Header.Get("x-request-id"), truncateString(string(respBody), 1000))
- s.handleFailoverSideEffects(ctx, resp, account, input.RequestModel)
- appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
- ProxyID: opsUpstreamProxyID(account),
- ProxyName: opsUpstreamProxyName(account),
- Platform: account.Platform,
- AccountID: account.ID,
- AccountName: account.Name,
- UpstreamStatusCode: resp.StatusCode,
- UpstreamRequestID: resp.Header.Get("x-request-id"),
- Passthrough: true,
- Kind: "failover",
- Message: extractUpstreamErrorMessage(respBody),
- Detail: func() string {
- if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
- return truncateString(string(respBody), s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes)
- }
- return ""
- }(),
- })
- return nil, &UpstreamFailoverError{
- StatusCode: resp.StatusCode,
- ResponseBody: respBody,
- RetryableOnSameAccount: account.IsPoolMode() && account.IsPoolModeRetryableStatus(resp.StatusCode),
- }
- }
- if resp.StatusCode >= 400 {
- return s.handleErrorResponse(ctx, resp, c, account, input.RequestModel)
- }
- var usage *ClaudeUsage
- var firstTokenMs *int
- var clientDisconnect bool
- if input.RequestStream {
- streamResult, err := s.handleStreamingResponseAnthropicAPIKeyPassthrough(ctx, resp, c, account, input.StartTime, input.RequestModel)
- if err != nil {
- // 流中断时保留已观测到的 usage 与错误一起返回,避免上游已计量的请求
- // 完全漏记漏计费(issue #5148)。
- if partial := partialStreamUsageResult(c, resp, streamResult, input.OriginalModel, input.RequestModel, input.StartTime, err); partial != nil {
- return partial, err
- }
- return nil, err
- }
- usage = streamResult.usage
- firstTokenMs = streamResult.firstTokenMs
- clientDisconnect = streamResult.clientDisconnect
- } else {
- usage, err = s.handleNonStreamingResponseAnthropicAPIKeyPassthrough(ctx, resp, c, account)
- if err != nil {
- return nil, err
- }
- }
- if usage == nil {
- usage = &ClaudeUsage{}
- }
- return &ForwardResult{
- RequestID: resp.Header.Get("x-request-id"),
- UpstreamHeaders: resp.Header,
- Usage: *usage,
- Model: input.OriginalModel,
- UpstreamModel: input.RequestModel,
- UpstreamResponseModel: observedUpstreamResponseModel(c),
- UpstreamResponseModelConflict: observedUpstreamResponseModelConflict(c),
- UpstreamResponseServiceTier: observedUpstreamResponseServiceTier(c),
- Stream: input.RequestStream,
- Duration: time.Since(input.StartTime),
- FirstTokenMs: firstTokenMs,
- ClientDisconnect: clientDisconnect,
- }, nil
- }
- func (s *GatewayService) buildUpstreamRequestAnthropicAPIKeyPassthrough(
- ctx context.Context,
- c *gin.Context,
- account *Account,
- body []byte,
- token string,
- ) (*http.Request, []byte, error) {
- body = stripDeferredToolCacheControl(body)
- targetURL := claudeAPIURL
- baseURL := account.GetBaseURL()
- if baseURL != "" {
- validatedURL, err := s.validateUpstreamBaseURL(baseURL)
- if err != nil {
- return nil, nil, err
- }
- targetURL = validatedURL + "/v1/messages?beta=true"
- }
- // 能力维度 body sanitize:透传路径上 anthropic-beta header 原样透传客户端值,
- // 依此决定是否保留 body 中的 context_management。避免“客户端 body 带字段但
- // header 忘记带 beta token”的客户端 bug 在透传场景下让上游 400。
- clientBeta := ""
- if c != nil && c.Request != nil {
- clientBeta = getHeaderRaw(c.Request.Header, "anthropic-beta")
- }
- // 账号覆写了 anthropic-beta 时,覆写值即最终上游值:净化以覆写值为准
- if beta, ok := account.HeaderOverrideValue("anthropic-beta"); ok {
- clientBeta = beta
- }
- if sanitized, changed := sanitizeAnthropicBodyForBetaTokens(body, clientBeta); changed {
- body = sanitized
- }
- // Ollama Cloud DeepSeek 出站 max_tokens clamp:判定与上方 targetURL 的
- // base 取值同源(GetBaseURL),详见 helper 注释。
- body = clampOllamaCloudAnthropicMessagesMaxTokens(account, account.GetBaseURL(), body)
- req, err := http.NewRequestWithContext(ctx, http.MethodPost, targetURL, bytes.NewReader(body))
- if err != nil {
- return nil, nil, err
- }
- if c != nil && c.Request != nil {
- for key, values := range c.Request.Header {
- lowerKey := strings.ToLower(strings.TrimSpace(key))
- if !allowedHeaders[lowerKey] {
- continue
- }
- wireKey := resolveWireCasing(key)
- for _, v := range values {
- addHeaderRaw(req.Header, wireKey, v)
- }
- }
- }
- // 覆盖入站鉴权残留,并注入上游认证
- req.Header.Del("authorization")
- req.Header.Del("x-api-key")
- req.Header.Del("x-goog-api-key")
- req.Header.Del("cookie")
- // Ollama Cloud Anthropic 兼容端点按实际 base_url 强制 Bearer(同上方
- // targetURL 的 base 取值),其余保持 extra/default 行为。
- setAnthropicAPIKeyAuthHeader(req.Header, account, token, account.GetBaseURL())
- if getHeaderRaw(req.Header, "content-type") == "" {
- setHeaderRaw(req.Header, "content-type", "application/json")
- }
- if getHeaderRaw(req.Header, "anthropic-version") == "" {
- setHeaderRaw(req.Header, "anthropic-version", "2023-06-01")
- }
- // 账号级请求头覆写(最终生效,覆盖上面所有来源的同名头)
- account.ApplyHeaderOverrides(req.Header)
- return req, body, nil
- }
- func (s *GatewayService) handleStreamingResponseAnthropicAPIKeyPassthrough(
- ctx context.Context,
- resp *http.Response,
- c *gin.Context,
- account *Account,
- startTime time.Time,
- model string,
- ) (*streamingResult, error) {
- observer := upstreamResponseModelObserverFromContext(c)
- if observer == nil {
- observer = beginUpstreamResponseModelObservation(c)
- }
- if s.rateLimitService != nil {
- s.rateLimitService.UpdateSessionWindow(ctx, account, resp.Header)
- }
- writeAnthropicPassthroughResponseHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
- contentType := strings.TrimSpace(resp.Header.Get("Content-Type"))
- if contentType == "" {
- contentType = "text/event-stream"
- }
- c.Header("Content-Type", contentType)
- if c.Writer.Header().Get("Cache-Control") == "" {
- c.Header("Cache-Control", "no-cache")
- }
- if c.Writer.Header().Get("Connection") == "" {
- c.Header("Connection", "keep-alive")
- }
- c.Header("X-Accel-Buffering", "no")
- if v := resp.Header.Get("x-request-id"); v != "" {
- c.Header("x-request-id", v)
- }
- w := c.Writer
- flusher, ok := w.(http.Flusher)
- if !ok {
- return nil, errors.New("streaming not supported")
- }
- usage := &ClaudeUsage{}
- var firstTokenMs *int
- clientDisconnected := false
- sawTerminalEvent := false
- scanner := bufio.NewScanner(resp.Body)
- maxLineSize := defaultMaxLineSize
- if s.cfg != nil && s.cfg.Gateway.MaxLineSize > 0 {
- maxLineSize = s.cfg.Gateway.MaxLineSize
- }
- scanBuf := getSSEScannerBuf64K()
- scanner.Buffer(scanBuf[:0], maxLineSize)
- type scanEvent struct {
- line string
- err error
- }
- events := make(chan scanEvent, 16)
- done := make(chan struct{})
- sendEvent := func(ev scanEvent) bool {
- select {
- case events <- ev:
- return true
- case <-done:
- return false
- }
- }
- var lastReadAt int64
- atomic.StoreInt64(&lastReadAt, time.Now().UnixNano())
- go func(scanBuf *sseScannerBuf64K) {
- defer putSSEScannerBuf64K(scanBuf)
- defer close(events)
- for scanner.Scan() {
- atomic.StoreInt64(&lastReadAt, time.Now().UnixNano())
- if !sendEvent(scanEvent{line: scanner.Text()}) {
- return
- }
- }
- if err := scanner.Err(); err != nil {
- _ = sendEvent(scanEvent{err: err})
- }
- }(scanBuf)
- defer close(done)
- streamInterval := time.Duration(0)
- if s.cfg != nil && s.cfg.Gateway.StreamDataIntervalTimeout > 0 {
- streamInterval = time.Duration(s.cfg.Gateway.StreamDataIntervalTimeout) * time.Second
- }
- var intervalTicker *time.Ticker
- if streamInterval > 0 {
- intervalTicker = time.NewTicker(streamInterval)
- defer intervalTicker.Stop()
- }
- var intervalCh <-chan time.Time
- if intervalTicker != nil {
- intervalCh = intervalTicker.C
- }
- keepaliveInterval := time.Duration(0)
- if s.cfg != nil && s.cfg.Gateway.StreamKeepaliveInterval > 0 {
- keepaliveInterval = time.Duration(s.cfg.Gateway.StreamKeepaliveInterval) * time.Second
- }
- var keepaliveTimer *time.Timer
- if keepaliveInterval > 0 {
- keepaliveTimer = time.NewTimer(keepaliveInterval)
- defer keepaliveTimer.Stop()
- }
- var keepaliveCh <-chan time.Time
- if keepaliveTimer != nil {
- keepaliveCh = keepaliveTimer.C
- }
- lastDataAt := time.Now()
- resetKeepaliveTimer := func() {
- if keepaliveTimer == nil {
- return
- }
- if !keepaliveTimer.Stop() {
- select {
- case <-keepaliveTimer.C:
- default:
- }
- }
- keepaliveTimer.Reset(keepaliveInterval)
- }
- inPartialEvent := false
- for {
- select {
- case ev, ok := <-events:
- if !ok {
- if !clientDisconnected {
- // 兜底补刷,确保最后一个未以空行结尾的事件也能及时送达客户端。
- flusher.Flush()
- }
- if !sawTerminalEvent {
- if clientDisconnected && streamInterval > 0 {
- lastRead := time.Unix(0, atomic.LoadInt64(&lastReadAt))
- if time.Since(lastRead) >= streamInterval {
- return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: true}, fmt.Errorf("stream usage incomplete after timeout")
- }
- }
- return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: clientDisconnected}, fmt.Errorf("stream usage incomplete: missing terminal event")
- }
- return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: clientDisconnected}, nil
- }
- if ev.err != nil {
- if sawTerminalEvent {
- return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: clientDisconnected}, nil
- }
- if clientDisconnected {
- return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: true}, fmt.Errorf("stream usage incomplete after disconnect: %w", ev.err)
- }
- if errors.Is(ev.err, context.Canceled) || errors.Is(ev.err, context.DeadlineExceeded) {
- return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: true}, fmt.Errorf("stream usage incomplete: %w", ev.err)
- }
- if errors.Is(ev.err, bufio.ErrTooLong) {
- logger.LegacyPrintf("service.gateway", "[Anthropic passthrough] SSE line too long: account=%d max_size=%d error=%v", account.ID, maxLineSize, ev.err)
- return &streamingResult{usage: usage, firstTokenMs: firstTokenMs}, ev.err
- }
- return &streamingResult{usage: usage, firstTokenMs: firstTokenMs}, fmt.Errorf("stream read error: %w", ev.err)
- }
- line := ev.line
- if data, ok := extractAnthropicSSEDataLine(line); ok {
- trimmed := strings.TrimSpace(data)
- observer.ObserveAnthropic([]byte(trimmed))
- if anthropicStreamEventIsTerminal("", trimmed) {
- sawTerminalEvent = true
- }
- if firstTokenMs == nil && trimmed != "" && trimmed != "[DONE]" {
- ms := int(time.Since(startTime).Milliseconds())
- firstTokenMs = &ms
- }
- parseSSEUsagePassthrough(data, usage)
- } else {
- trimmed := strings.TrimSpace(line)
- if strings.HasPrefix(trimmed, "event:") && anthropicStreamEventIsTerminal(strings.TrimSpace(strings.TrimPrefix(trimmed, "event:")), "") {
- sawTerminalEvent = true
- }
- }
- if !clientDisconnected {
- restored := string(reverseToolNamesIfPresent(c, []byte(line)))
- if _, err := io.WriteString(w, restored); err != nil {
- clientDisconnected = true
- logger.LegacyPrintf("service.gateway", "[Anthropic passthrough] Client disconnected during streaming, continue draining upstream for usage: account=%d", account.ID)
- } else if _, err := io.WriteString(w, "\n"); err != nil {
- clientDisconnected = true
- logger.LegacyPrintf("service.gateway", "[Anthropic passthrough] Client disconnected during streaming, continue draining upstream for usage: account=%d", account.ID)
- } else if line == "" {
- // 按 SSE 事件边界刷出,减少每行 flush 带来的 syscall 开销。
- flusher.Flush()
- lastDataAt = time.Now()
- resetKeepaliveTimer()
- inPartialEvent = false
- } else {
- inPartialEvent = true
- }
- }
- case <-intervalCh:
- lastRead := time.Unix(0, atomic.LoadInt64(&lastReadAt))
- if time.Since(lastRead) < streamInterval {
- continue
- }
- if clientDisconnected {
- return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: true}, fmt.Errorf("stream usage incomplete after timeout")
- }
- logger.LegacyPrintf("service.gateway", "[Anthropic passthrough] Stream data interval timeout: account=%d model=%s interval=%s", account.ID, model, streamInterval)
- if s.rateLimitService != nil {
- s.rateLimitService.HandleStreamTimeout(ctx, account, model)
- }
- return &streamingResult{usage: usage, firstTokenMs: firstTokenMs}, fmt.Errorf("stream data interval timeout")
- case <-keepaliveCh:
- if clientDisconnected {
- continue
- }
- if inPartialEvent {
- resetKeepaliveTimer()
- continue
- }
- if time.Since(lastDataAt) < keepaliveInterval {
- resetKeepaliveTimer()
- continue
- }
- if _, err := fmt.Fprint(w, "event: ping\ndata: {\"type\": \"ping\"}\n\n"); err != nil {
- clientDisconnected = true
- logger.LegacyPrintf("service.gateway", "[Anthropic passthrough] Client disconnected during keepalive ping, continue draining upstream for usage: account=%d", account.ID)
- continue
- }
- flusher.Flush()
- lastDataAt = time.Now()
- resetKeepaliveTimer()
- }
- }
- }
- func extractAnthropicSSEDataLine(line string) (string, bool) {
- if !strings.HasPrefix(line, "data:") {
- return "", false
- }
- start := len("data:")
- for start < len(line) {
- if line[start] != ' ' && line[start] != '\t' {
- break
- }
- start++
- }
- return line[start:], true
- }
- // parseSSEUsagePassthrough 从 Anthropic SSE data 行提取 usage(包级函数:
- // Anthropic 平台 passthrough 与国产供应商原生 Anthropic 直通共用)。
- func parseSSEUsagePassthrough(data string, usage *ClaudeUsage) {
- if usage == nil || data == "" || data == "[DONE]" {
- return
- }
- parsed := gjson.Parse(data)
- switch parsed.Get("type").String() {
- case "message_start":
- msgUsage := parsed.Get("message.usage")
- if msgUsage.Exists() {
- usage.InputTokens = int(msgUsage.Get("input_tokens").Int())
- usage.CacheCreationInputTokens = int(msgUsage.Get("cache_creation_input_tokens").Int())
- usage.CacheReadInputTokens = int(msgUsage.Get("cache_read_input_tokens").Int())
- // 保持与通用解析一致:message_start 允许覆盖 5m/1h 明细(包括 0)。
- cc5m := msgUsage.Get("cache_creation.ephemeral_5m_input_tokens")
- cc1h := msgUsage.Get("cache_creation.ephemeral_1h_input_tokens")
- if cc5m.Exists() || cc1h.Exists() {
- usage.CacheCreation5mTokens = int(cc5m.Int())
- usage.CacheCreation1hTokens = int(cc1h.Int())
- }
- }
- case "message_delta":
- deltaUsage := parsed.Get("usage")
- if deltaUsage.Exists() {
- if v := deltaUsage.Get("input_tokens").Int(); v > 0 {
- usage.InputTokens = int(v)
- }
- if v := deltaUsage.Get("output_tokens").Int(); v > 0 {
- usage.OutputTokens = int(v)
- }
- if v := deltaUsage.Get("cache_creation_input_tokens").Int(); v > 0 {
- usage.CacheCreationInputTokens = int(v)
- }
- if v := deltaUsage.Get("cache_read_input_tokens").Int(); v > 0 {
- usage.CacheReadInputTokens = int(v)
- }
- cc5m := deltaUsage.Get("cache_creation.ephemeral_5m_input_tokens")
- cc1h := deltaUsage.Get("cache_creation.ephemeral_1h_input_tokens")
- if cc5m.Exists() {
- usage.CacheCreation5mTokens = int(cc5m.Int())
- }
- if cc1h.Exists() {
- usage.CacheCreation1hTokens = int(cc1h.Int())
- }
- }
- }
- if usage.CacheReadInputTokens == 0 {
- if cached := parsed.Get("message.usage.cached_tokens").Int(); cached > 0 {
- usage.CacheReadInputTokens = int(cached)
- }
- if cached := parsed.Get("usage.cached_tokens").Int(); usage.CacheReadInputTokens == 0 && cached > 0 {
- usage.CacheReadInputTokens = int(cached)
- }
- }
- if usage.CacheCreationInputTokens == 0 {
- cc5m := parsed.Get("message.usage.cache_creation.ephemeral_5m_input_tokens").Int()
- cc1h := parsed.Get("message.usage.cache_creation.ephemeral_1h_input_tokens").Int()
- if cc5m == 0 && cc1h == 0 {
- cc5m = parsed.Get("usage.cache_creation.ephemeral_5m_input_tokens").Int()
- cc1h = parsed.Get("usage.cache_creation.ephemeral_1h_input_tokens").Int()
- }
- total := cc5m + cc1h
- if total > 0 {
- usage.CacheCreationInputTokens = int(total)
- }
- }
- // Kimi's Anthropic-compatible stream uses input_tokens with two meanings:
- // message_start reports total prompt input, while message_delta reports only
- // uncached input. prompt_tokens remains the total in both events. Normalize
- // to ClaudeUsage's mutually-exclusive buckets so downstream billing does not
- // subtract cache tokens from an already-uncached value.
- usageNode := parsed.Get("usage")
- if parsed.Get("type").String() == "message_start" {
- usageNode = parsed.Get("message.usage")
- }
- normalizeAnthropicCompatiblePromptUsage(usageNode, usage)
- }
- // normalizeAnthropicCompatiblePromptUsage converts provider-native OpenAI-style
- // prompt/cache fields into Claude's mutually-exclusive usage buckets. Native
- // Anthropic responses do not expose these aliases and are left alone.
- func normalizeAnthropicCompatiblePromptUsage(usageNode gjson.Result, usage *ClaudeUsage) bool {
- if usage == nil || !usageNode.Exists() {
- return false
- }
- promptTokens := usageNode.Get("prompt_tokens")
- promptCacheHitTokens := usageNode.Get("prompt_cache_hit_tokens")
- promptCacheMissTokens := usageNode.Get("prompt_cache_miss_tokens")
- if (!promptTokens.Exists() || promptTokens.Int() <= 0) &&
- !promptCacheHitTokens.Exists() && !promptCacheMissTokens.Exists() {
- return false
- }
- cacheReadTokens := usage.CacheReadInputTokens
- if v := usageNode.Get("cache_read_input_tokens"); v.Exists() {
- cacheReadTokens = int(v.Int())
- }
- if cacheReadTokens == 0 {
- if v := usageNode.Get("cached_tokens"); v.Exists() {
- cacheReadTokens = int(v.Int())
- }
- }
- if cacheReadTokens == 0 {
- if v := usageNode.Get("prompt_tokens_details.cached_tokens"); v.Exists() {
- cacheReadTokens = int(v.Int())
- }
- }
- if cacheReadTokens == 0 && promptCacheHitTokens.Exists() {
- cacheReadTokens = max(int(promptCacheHitTokens.Int()), 0)
- }
- cacheCreationTokens := usage.CacheCreationInputTokens
- if v := usageNode.Get("cache_creation_input_tokens"); v.Exists() {
- cacheCreationTokens = int(v.Int())
- }
- if cacheCreationTokens == 0 {
- cc5m := usageNode.Get("cache_creation.ephemeral_5m_input_tokens").Int()
- cc1h := usageNode.Get("cache_creation.ephemeral_1h_input_tokens").Int()
- if cc5m > 0 || cc1h > 0 {
- cacheCreationTokens = int(cc5m + cc1h)
- }
- }
- if promptCacheMissTokens.Exists() {
- usage.InputTokens = max(int(promptCacheMissTokens.Int()), 0)
- } else {
- usage.InputTokens = max(int(promptTokens.Int())-cacheReadTokens-cacheCreationTokens, 0)
- }
- usage.CacheReadInputTokens = cacheReadTokens
- usage.CacheCreationInputTokens = cacheCreationTokens
- return true
- }
- func parseClaudeUsageFromResponseBody(body []byte) *ClaudeUsage {
- usage := &ClaudeUsage{}
- if len(body) == 0 {
- return usage
- }
- parsed := gjson.ParseBytes(body)
- usageNode := parsed.Get("usage")
- if !usageNode.Exists() {
- return usage
- }
- usage.InputTokens = int(usageNode.Get("input_tokens").Int())
- usage.OutputTokens = int(usageNode.Get("output_tokens").Int())
- usage.CacheCreationInputTokens = int(usageNode.Get("cache_creation_input_tokens").Int())
- usage.CacheReadInputTokens = int(usageNode.Get("cache_read_input_tokens").Int())
- cc5m := usageNode.Get("cache_creation.ephemeral_5m_input_tokens").Int()
- cc1h := usageNode.Get("cache_creation.ephemeral_1h_input_tokens").Int()
- if cc5m > 0 || cc1h > 0 {
- usage.CacheCreation5mTokens = int(cc5m)
- usage.CacheCreation1hTokens = int(cc1h)
- }
- if usage.CacheCreationInputTokens == 0 && (cc5m > 0 || cc1h > 0) {
- usage.CacheCreationInputTokens = int(cc5m + cc1h)
- }
- if usage.CacheReadInputTokens == 0 {
- if cached := usageNode.Get("cached_tokens").Int(); cached > 0 {
- usage.CacheReadInputTokens = int(cached)
- }
- }
- normalizeAnthropicCompatiblePromptUsage(usageNode, usage)
- return usage
- }
- // invalidNonStreamingJSONFailoverError 把"上游 2xx 返回非 JSON body"归一为
- // failover 错误(包级函数:Anthropic 平台 passthrough 与国产供应商原生
- // Anthropic 直通共用)。
- func invalidNonStreamingJSONFailoverError(
- ctx context.Context,
- rateLimitService *RateLimitService,
- resp *http.Response,
- account *Account,
- body []byte,
- parseErr error,
- requestedModel ...string,
- ) error {
- const statusCode = http.StatusBadGateway
- accountID := int64(0)
- accountName := ""
- retryableOnSameAccount := false
- if account != nil {
- accountID = account.ID
- accountName = account.Name
- retryableOnSameAccount = account.IsPoolMode() && account.IsPoolModeRetryableStatus(statusCode)
- }
- logger.LegacyPrintf(
- "service.gateway",
- "Account %d(%s): upstream returned non-JSON 2xx response, attempting failover: status=%d request_id=%s error=%v",
- accountID,
- accountName,
- resp.StatusCode,
- resp.Header.Get("x-request-id"),
- parseErr,
- )
- if rateLimitService != nil && account != nil {
- if len(requestedModel) > 0 {
- rateLimitService.HandleUpstreamError(ctx, account, statusCode, resp.Header, body, requestedModel[0])
- } else {
- rateLimitService.HandleUpstreamError(ctx, account, statusCode, resp.Header, body)
- }
- }
- return &UpstreamFailoverError{
- StatusCode: statusCode,
- ResponseBody: body,
- ResponseHeaders: resp.Header,
- RetryableOnSameAccount: retryableOnSameAccount,
- }
- }
- func (s *GatewayService) handleNonStreamingResponseAnthropicAPIKeyPassthrough(
- ctx context.Context,
- resp *http.Response,
- c *gin.Context,
- account *Account,
- ) (*ClaudeUsage, error) {
- if s.rateLimitService != nil {
- s.rateLimitService.UpdateSessionWindow(ctx, account, resp.Header)
- }
- body, err := ReadUpstreamResponseBody(resp.Body, s.cfg, c, anthropicTooLargeError)
- if err != nil {
- return nil, err
- }
- observer := upstreamResponseModelObserverFromContext(c)
- if observer == nil {
- observer = beginUpstreamResponseModelObservation(c)
- }
- observer.ObserveAnthropic(body)
- if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
- var raw json.RawMessage
- if err := json.Unmarshal(body, &raw); err != nil {
- return nil, invalidNonStreamingJSONFailoverError(ctx, s.rateLimitService, resp, account, body, err)
- }
- }
- usage := parseClaudeUsageFromResponseBody(body)
- if IsForceCacheBilling(ctx) && usage.InputTokens > 0 {
- body, err = classifyAnthropicResponseInputAsCacheRead(body, usage)
- if err != nil {
- return nil, err
- }
- }
- writeAnthropicPassthroughResponseHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
- contentType := strings.TrimSpace(resp.Header.Get("Content-Type"))
- if contentType == "" {
- contentType = "application/json"
- }
- body = reverseToolNamesIfPresent(c, body)
- c.Data(resp.StatusCode, contentType, body)
- return usage, nil
- }
- func classifyAnthropicResponseInputAsCacheRead(body []byte, usage *ClaudeUsage) ([]byte, error) {
- classified, err := sjson.SetBytes(body, "usage.input_tokens", 0)
- if err != nil {
- return nil, fmt.Errorf("classify forced cache billing input tokens: %w", err)
- }
- classified, err = sjson.SetBytes(classified, "usage.cache_read_input_tokens", usage.CacheReadInputTokens+usage.InputTokens)
- if err != nil {
- return nil, fmt.Errorf("classify forced cache billing cache read tokens: %w", err)
- }
- return classified, nil
- }
- func writeAnthropicPassthroughResponseHeaders(dst http.Header, src http.Header, filter *responseheaders.CompiledHeaderFilter) {
- if dst == nil || src == nil {
- return
- }
- if filter != nil {
- responseheaders.WriteFilteredHeaders(dst, src, filter)
- return
- }
- if v := strings.TrimSpace(src.Get("Content-Type")); v != "" {
- dst.Set("Content-Type", v)
- }
- if v := strings.TrimSpace(src.Get("x-request-id")); v != "" {
- dst.Set("x-request-id", v)
- }
- }
|