| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624 |
- package service
- import (
- "bytes"
- "context"
- "errors"
- "fmt"
- "net/http"
- "strings"
- "github.com/Wei-Shaw/sub2api/internal/pkg/claude"
- "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
- "github.com/tidwall/gjson"
- "github.com/gin-gonic/gin"
- )
- // ForwardCountTokens 转发 count_tokens 请求到上游 API
- // 特点:不记录使用量、仅支持非流式响应
- func (s *GatewayService) ForwardCountTokens(ctx context.Context, c *gin.Context, account *Account, parsed *ParsedRequest) error {
- if parsed == nil {
- s.countTokensError(c, http.StatusBadRequest, "invalid_request_error", "Request body is empty")
- return fmt.Errorf("parse request: empty request")
- }
- if account != nil && account.IsAnthropicAPIKeyPassthroughEnabled() {
- passthroughBody := parsed.Body.Bytes()
- if reqModel := parsed.Model; reqModel != "" {
- if mappedModel := account.GetMappedModel(reqModel); mappedModel != reqModel {
- passthroughBody = s.replaceModelInBody(passthroughBody, mappedModel)
- logger.LegacyPrintf("service.gateway", "CountTokens passthrough model mapping: %s -> %s (account: %s)", reqModel, mappedModel, account.Name)
- }
- }
- return s.forwardCountTokensAnthropicAPIKeyPassthrough(ctx, c, account, passthroughBody)
- }
- // Bedrock 不支持 count_tokens 端点
- if account != nil && account.IsBedrock() {
- s.countTokensError(c, http.StatusNotFound, "not_found_error", "count_tokens endpoint is not supported for Bedrock")
- return nil
- }
- body := parsed.Body.Bytes()
- replaceBody := func(next []byte) error {
- if err := parsed.ReplaceBody(next); err != nil {
- return fmt.Errorf("rewrite count_tokens body: %w", err)
- }
- body = parsed.Body.Bytes()
- return nil
- }
- reqModel := parsed.Model
- // Pre-filter: strip empty text blocks to prevent upstream 400.
- if err := replaceBody(StripEmptyTextBlocks(body)); err != nil {
- return err
- }
- isClaudeCodeCT := IsClaudeCodeClient(ctx) || isClaudeCodeClient(c.GetHeader("User-Agent"), parsed.MetadataUserID)
- shouldMimicClaudeCode := account.IsOAuth() && !isClaudeCodeCT
- if shouldMimicClaudeCode {
- normalizeOpts := claudeOAuthNormalizeOptions{stripSystemCacheControl: true}
- var normalizedBody []byte
- normalizedBody, reqModel = normalizeClaudeOAuthRequestBody(body, reqModel, normalizeOpts)
- if err := replaceBody(normalizedBody); err != nil {
- return err
- }
- if err := replaceBody(s.rewriteMessageCacheControlIfEnabled(ctx, body)); err != nil {
- return err
- }
- if rw := buildToolNameRewriteFromBody(body); rw != nil {
- if err := replaceBody(applyToolNameRewriteToBody(body, rw)); err != nil {
- return err
- }
- } else {
- if err := replaceBody(applyToolsLastCacheBreakpoint(body)); err != nil {
- return err
- }
- }
- }
- // Antigravity 账户不支持 count_tokens,返回 404 让客户端 fallback 到本地估算。
- // 返回 nil 避免 handler 层记录为错误,也不设置 ops 上游错误上下文。
- if account.Platform == PlatformAntigravity {
- s.countTokensError(c, http.StatusNotFound, "not_found_error", "count_tokens endpoint is not supported for this platform")
- return nil
- }
- // 应用模型映射:
- // - APIKey 账号:使用账号级别的显式映射(如果配置),否则透传原始模型名
- // - OAuth/SetupToken 账号:使用 Anthropic 标准映射(短ID → 长ID)
- if reqModel != "" {
- mappedModel := reqModel
- mappingSource := ""
- if account.Type == AccountTypeAPIKey {
- mappedModel = account.GetMappedModel(reqModel)
- if mappedModel != reqModel {
- mappingSource = "account"
- }
- }
- if mappingSource == "" && account.Platform == PlatformAnthropic && account.Type != AccountTypeAPIKey {
- normalized := claude.NormalizeModelID(reqModel)
- if normalized != reqModel {
- mappedModel = normalized
- mappingSource = "prefix"
- }
- }
- if mappedModel != reqModel {
- originalReqModel := reqModel
- if err := replaceBody(s.replaceModelInBody(body, mappedModel)); err != nil {
- return err
- }
- reqModel = mappedModel
- parsed.Model = mappedModel
- logger.LegacyPrintf("service.gateway", "CountTokens model mapping applied: %s -> %s (account: %s, source=%s)", originalReqModel, mappedModel, account.Name, mappingSource)
- }
- }
- // 获取凭证
- token, tokenType, err := s.GetAccessToken(ctx, account)
- if err != nil {
- s.countTokensError(c, http.StatusBadGateway, "upstream_error", "Failed to get access token")
- return err
- }
- // 构建上游请求
- upstreamReq, wireBody, err := s.buildCountTokensRequest(ctx, c, account, body, token, tokenType, reqModel, shouldMimicClaudeCode)
- if err != nil {
- s.countTokensError(c, http.StatusInternalServerError, "api_error", "Failed to build request")
- return err
- }
- // 先记录首发 wire body;如果后面进入 400 retry,retry 会基于未签名的逻辑 body 重新构建。
- acceptedWireBody := wireBody
- // 获取代理URL(自定义 base URL 模式下,proxy 通过 buildCustomRelayURL 作为查询参数传递)
- proxyURL := ""
- if account.ProxyID != nil && account.Proxy != nil {
- if !account.IsCustomBaseURLEnabled() || account.GetCustomBaseURL() == "" {
- proxyURL = account.Proxy.URL()
- }
- }
- // 发送请求
- resp, err := s.httpUpstream.DoWithTLS(WithAccountTrafficRequest(upstreamReq, account), proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account))
- if err != nil {
- setOpsUpstreamError(c, 0, sanitizeUpstreamErrorMessage(err.Error()), "")
- s.countTokensError(c, http.StatusBadGateway, "upstream_error", "Request failed")
- return fmt.Errorf("upstream request failed: %w", err)
- }
- // 读取响应体
- countTokensTooLarge := func(c *gin.Context) {
- s.countTokensError(c, http.StatusBadGateway, "upstream_error", "Upstream response too large")
- }
- respBody, err := ReadUpstreamResponseBody(resp.Body, s.cfg, c, countTokensTooLarge)
- _ = resp.Body.Close()
- if err != nil {
- if !errors.Is(err, ErrUpstreamResponseBodyTooLarge) {
- s.countTokensError(c, http.StatusBadGateway, "upstream_error", "Failed to read response")
- }
- return err
- }
- // 检测 thinking block 签名错误(400)并重试一次(过滤 thinking blocks)
- if resp.StatusCode == 400 && s.shouldRectifySignatureError(ctx, account, respBody, reqModel) {
- logger.LegacyPrintf("service.gateway", "Account %d: detected thinking block signature error on count_tokens, retrying with filtered thinking blocks", account.ID)
- filteredBody := FilterThinkingBlocksForRetry(body, reqModel)
- retryReq, retryWireBody, buildErr := s.buildCountTokensRequest(ctx, c, account, filteredBody, token, tokenType, reqModel, shouldMimicClaudeCode)
- if buildErr == nil {
- retryResp, retryErr := s.httpUpstream.DoWithTLS(WithAccountTrafficRequest(retryReq, account), proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account))
- if retryErr == nil {
- if retryResp.StatusCode < 400 {
- // count_tokens 签名重试成功后记录最终 wire body,错误响应仍保留原 body 便于后续处理。
- acceptedWireBody = retryWireBody
- }
- resp = retryResp
- respBody, err = ReadUpstreamResponseBody(resp.Body, s.cfg, c, countTokensTooLarge)
- _ = resp.Body.Close()
- if err != nil {
- if !errors.Is(err, ErrUpstreamResponseBodyTooLarge) {
- s.countTokensError(c, http.StatusBadGateway, "upstream_error", "Failed to read response")
- }
- return err
- }
- }
- }
- }
- if resp.StatusCode < 400 && !bytes.Equal(acceptedWireBody, body) {
- // count_tokens 成功后再同步最终 wire body,避免 retry 从已签名 body 派生。
- if err := replaceBody(acceptedWireBody); err != nil {
- return err
- }
- }
- // 处理错误响应
- if resp.StatusCode >= 400 {
- // 标记账号状态(429/529等)
- s.rateLimitService.HandleUpstreamError(ctx, account, resp.StatusCode, resp.Header, respBody)
- upstreamMsg := strings.TrimSpace(extractUpstreamErrorMessage(respBody))
- upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg)
- upstreamDetail := ""
- if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
- maxBytes := s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes
- if maxBytes <= 0 {
- maxBytes = 2048
- }
- upstreamDetail = truncateString(string(respBody), maxBytes)
- }
- setOpsUpstreamError(c, resp.StatusCode, upstreamMsg, upstreamDetail)
- // 记录上游错误摘要便于排障(不回显请求内容)
- if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
- logger.LegacyPrintf("service.gateway",
- "count_tokens upstream error %d (account=%d platform=%s type=%s): %s",
- resp.StatusCode,
- account.ID,
- account.Platform,
- account.Type,
- truncateForLog(respBody, s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes),
- )
- }
- // 返回简化的错误响应
- errMsg := "Upstream request failed"
- switch resp.StatusCode {
- case 429:
- errMsg = "Rate limit exceeded"
- case 529:
- errMsg = "Service overloaded"
- }
- s.countTokensError(c, resp.StatusCode, "upstream_error", errMsg)
- if upstreamMsg == "" {
- return fmt.Errorf("upstream error: %d", resp.StatusCode)
- }
- return fmt.Errorf("upstream error: %d message=%s", resp.StatusCode, upstreamMsg)
- }
- // 透传成功响应
- c.Data(resp.StatusCode, "application/json", respBody)
- return nil
- }
- func (s *GatewayService) forwardCountTokensAnthropicAPIKeyPassthrough(ctx context.Context, c *gin.Context, account *Account, body []byte) error {
- token, tokenType, err := s.GetAccessToken(ctx, account)
- if err != nil {
- s.countTokensError(c, http.StatusBadGateway, "upstream_error", "Failed to get access token")
- return err
- }
- if tokenType != "apikey" {
- s.countTokensError(c, http.StatusBadGateway, "upstream_error", "Invalid account token type")
- return fmt.Errorf("anthropic api key passthrough requires apikey token, got: %s", tokenType)
- }
- upstreamReq, err := s.buildCountTokensRequestAnthropicAPIKeyPassthrough(ctx, c, account, body, token)
- if err != nil {
- s.countTokensError(c, http.StatusInternalServerError, "api_error", "Failed to build request")
- return err
- }
- proxyURL := ""
- if account.ProxyID != nil && account.Proxy != nil {
- proxyURL = account.Proxy.URL()
- }
- resp, err := s.httpUpstream.DoWithTLS(WithAccountTrafficRequest(upstreamReq, account), proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account))
- if err != nil {
- setOpsUpstreamError(c, 0, sanitizeUpstreamErrorMessage(err.Error()), "")
- appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
- ProxyID: opsUpstreamProxyID(account),
- ProxyName: opsUpstreamProxyName(account),
- Platform: account.Platform,
- AccountID: account.ID,
- AccountName: account.Name,
- UpstreamStatusCode: 0,
- UpstreamURL: safeUpstreamURL(upstreamReq.URL.String()),
- Passthrough: true,
- Kind: "request_error",
- Message: sanitizeUpstreamErrorMessage(err.Error()),
- })
- s.countTokensError(c, http.StatusBadGateway, "upstream_error", "Request failed")
- return fmt.Errorf("upstream request failed: %w", err)
- }
- countTokensTooLarge := func(c *gin.Context) {
- s.countTokensError(c, http.StatusBadGateway, "upstream_error", "Upstream response too large")
- }
- respBody, err := ReadUpstreamResponseBody(resp.Body, s.cfg, c, countTokensTooLarge)
- _ = resp.Body.Close()
- if err != nil {
- if !errors.Is(err, ErrUpstreamResponseBodyTooLarge) {
- s.countTokensError(c, http.StatusBadGateway, "upstream_error", "Failed to read response")
- }
- return err
- }
- if resp.StatusCode >= 400 {
- if s.rateLimitService != nil {
- s.rateLimitService.HandleUpstreamError(ctx, account, resp.StatusCode, resp.Header, respBody)
- }
- upstreamMsg := strings.TrimSpace(extractUpstreamErrorMessage(respBody))
- upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg)
- // 中转站不支持 count_tokens 端点时(404),返回 404 让客户端 fallback 到本地估算。
- // 仅在错误消息明确指向 count_tokens endpoint 不存在时生效,避免误吞其他 404(如错误 base_url)。
- // 返回 nil 避免 handler 层记录为错误,也不设置 ops 上游错误上下文。
- if isCountTokensUnsupported404(resp.StatusCode, respBody) {
- logger.LegacyPrintf("service.gateway",
- "[count_tokens] Upstream does not support count_tokens (404), returning 404: account=%d name=%s msg=%s",
- account.ID, account.Name, truncateString(upstreamMsg, 512))
- s.countTokensError(c, http.StatusNotFound, "not_found_error", "count_tokens endpoint is not supported by upstream")
- return nil
- }
- upstreamDetail := ""
- if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
- maxBytes := s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes
- if maxBytes <= 0 {
- maxBytes = 2048
- }
- upstreamDetail = truncateString(string(respBody), maxBytes)
- }
- setOpsUpstreamError(c, resp.StatusCode, upstreamMsg, upstreamDetail)
- 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: "http_error",
- Message: upstreamMsg,
- Detail: upstreamDetail,
- })
- errMsg := "Upstream request failed"
- switch resp.StatusCode {
- case 429:
- errMsg = "Rate limit exceeded"
- case 529:
- errMsg = "Service overloaded"
- }
- s.countTokensError(c, resp.StatusCode, "upstream_error", errMsg)
- if upstreamMsg == "" {
- return fmt.Errorf("upstream error: %d", resp.StatusCode)
- }
- return fmt.Errorf("upstream error: %d message=%s", resp.StatusCode, upstreamMsg)
- }
- writeAnthropicPassthroughResponseHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
- contentType := strings.TrimSpace(resp.Header.Get("Content-Type"))
- if contentType == "" {
- contentType = "application/json"
- }
- c.Data(resp.StatusCode, contentType, respBody)
- return nil
- }
- func (s *GatewayService) buildCountTokensRequestAnthropicAPIKeyPassthrough(
- ctx context.Context,
- c *gin.Context,
- account *Account,
- body []byte,
- token string,
- ) (*http.Request, error) {
- body = stripDeferredToolCacheControl(body)
- targetURL := claudeAPICountTokensURL
- baseURL := account.GetBaseURL()
- if baseURL != "" {
- validatedURL, err := s.validateUpstreamBaseURL(baseURL)
- if err != nil {
- return nil, err
- }
- targetURL = validatedURL + "/v1/messages/count_tokens?beta=true"
- }
- body = sanitizeCountTokensRequestBody(body)
- // 同 buildUpstreamRequestAnthropicAPIKeyPassthrough:能力维度 sanitize。
- 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
- }
- req, err := http.NewRequestWithContext(ctx, http.MethodPost, targetURL, bytes.NewReader(body))
- if err != nil {
- return 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 req.Header.Get("content-type") == "" {
- req.Header.Set("content-type", "application/json")
- }
- if req.Header.Get("anthropic-version") == "" {
- req.Header.Set("anthropic-version", "2023-06-01")
- }
- // 账号级请求头覆写(最终生效,覆盖上面所有来源的同名头)
- account.ApplyHeaderOverrides(req.Header)
- return req, nil
- }
- // buildCountTokensRequest 构建 count_tokens 上游请求
- func (s *GatewayService) buildCountTokensRequest(ctx context.Context, c *gin.Context, account *Account, body []byte, token, tokenType, modelID string, mimicClaudeCode bool) (*http.Request, []byte, error) {
- body = stripDeferredToolCacheControl(body)
- // 确定目标 URL
- targetURL := claudeAPICountTokensURL
- if account.Type == AccountTypeAPIKey {
- baseURL := account.GetBaseURL()
- if baseURL != "" {
- validatedURL, err := s.validateUpstreamBaseURL(baseURL)
- if err != nil {
- return nil, nil, err
- }
- targetURL = validatedURL + "/v1/messages/count_tokens?beta=true"
- }
- } else if account.IsCustomBaseURLEnabled() {
- customURL := account.GetCustomBaseURL()
- if customURL == "" {
- return nil, nil, fmt.Errorf("custom_base_url is enabled but not configured for account %d", account.ID)
- }
- validatedURL, err := s.validateUpstreamBaseURL(customURL)
- if err != nil {
- return nil, nil, err
- }
- targetURL = s.buildCustomRelayURL(validatedURL, "/v1/messages/count_tokens", account)
- }
- clientHeaders := http.Header{}
- if c != nil && c.Request != nil {
- clientHeaders = c.Request.Header
- }
- // OAuth 账号:应用统一指纹和重写 userID(受设置开关控制)
- // 如果启用了会话ID伪装,会在重写后替换 session 部分为固定值
- ctEnableFP, ctEnableMPT := true, false
- if s.settingService != nil {
- ctEnableFP, ctEnableMPT, _ = s.settingService.GetGatewayForwardingSettings(ctx)
- }
- var ctFingerprint *Fingerprint
- if account.IsOAuth() && s.identityService != nil {
- fp, err := s.identityService.GetOrCreateFingerprint(ctx, account.ID, clientHeaders)
- if err == nil {
- ctFingerprint = fp
- if !ctEnableMPT {
- accountUUID := account.GetExtraString("account_uuid")
- if accountUUID != "" && fp.ClientID != "" {
- if newBody, err := s.identityService.RewriteUserIDWithMasking(ctx, body, account, accountUUID, fp.ClientID, fp.UserAgent); err == nil && len(newBody) > 0 {
- body = newBody
- }
- }
- }
- }
- }
- // Disabled fingerprint unification does not disable forced mimicry headers.
- var billingFingerprint *Fingerprint
- if ctEnableFP {
- billingFingerprint = ctFingerprint
- }
- if billingUA := effectiveBillingUserAgent(tokenType, mimicClaudeCode, billingFingerprint); billingUA != "" {
- body = syncBillingHeaderVersion(body, billingUA)
- }
- // === 计算最终 anthropic-beta header(先于 body sanitize 与 CCH 签名)===
- // 顺序约束同 buildUpstreamRequest。
- ctEffectiveDropSet := mergeDropSets(s.getBetaPolicyFilterSet(ctx, c, account, modelID))
- finalBetaHeader, finalBetaShouldSet := s.computeFinalCountTokensAnthropicBeta(
- tokenType, mimicClaudeCode, modelID, clientHeaders, body, ctEffectiveDropSet,
- )
- // 账号覆写了 anthropic-beta 时,覆写值即最终上游值:净化以覆写值为准
- if beta, ok := account.HeaderOverrideValue("anthropic-beta"); ok {
- finalBetaHeader, finalBetaShouldSet = beta, true
- }
- // 能力维度 body sanitize:与最终 anthropic-beta header 对称
- if sanitized, changed := sanitizeAnthropicBodyForBetaTokens(body, finalBetaHeader); changed {
- body = sanitized
- }
- body = sanitizeCountTokensRequestBody(body)
- req, err := http.NewRequestWithContext(ctx, "POST", targetURL, bytes.NewReader(body))
- if err != nil {
- return nil, nil, err
- }
- // 设置认证头(保持原始大小写)
- if tokenType == "oauth" {
- setHeaderRaw(req.Header, "authorization", "Bearer "+token)
- } else {
- // Ollama Cloud Anthropic 兼容端点按实际 base_url 强制 Bearer(同上方
- // targetURL 的 base 取值),其余保持 extra/default 行为。
- setAnthropicAPIKeyAuthHeader(req.Header, account, token, account.GetBaseURL())
- }
- // 白名单透传 headers(恢复真实 wire casing)
- for key, values := range clientHeaders {
- lowerKey := strings.ToLower(key)
- if allowedHeaders[lowerKey] {
- wireKey := resolveWireCasing(key)
- for _, v := range values {
- addHeaderRaw(req.Header, wireKey, v)
- }
- }
- }
- // OAuth 账号:应用指纹到请求头(受设置开关控制)
- if ctEnableFP && ctFingerprint != nil {
- s.identityService.ApplyFingerprint(req, ctFingerprint)
- }
- // 确保必要的 headers 存在(保持原始大小写)
- 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")
- }
- if tokenType == "oauth" {
- applyClaudeOAuthHeaderDefaults(req)
- }
- // OAuth + mimic Claude Code:强制注入 CLI 指纹 header
- if tokenType == "oauth" && mimicClaudeCode {
- applyClaudeCodeMimicHeaders(req, false)
- }
- // 写入最终 anthropic-beta header(Del 一次避免白名单透传值残留)
- deleteHeaderAllForms(req.Header, "anthropic-beta")
- if finalBetaShouldSet {
- setHeaderRaw(req.Header, "anthropic-beta", finalBetaHeader)
- }
- // 同步 X-Claude-Code-Session-Id 头:取 body 中已处理的 metadata.user_id 的 session_id 覆盖
- if sessionHeader := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id"); sessionHeader != "" {
- if uid := gjson.GetBytes(body, "metadata.user_id").String(); uid != "" {
- if parsed := ParseMetadataUserID(uid); parsed != nil {
- setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", parsed.SessionID)
- }
- }
- }
- // 账号级请求头覆写(仅 anthropic/openai api_key 账号启用时生效;OAuth 路径 no-op)
- account.ApplyHeaderOverrides(req.Header)
- if c != nil && tokenType == "oauth" {
- c.Set(claudeMimicDebugInfoKey, buildClaudeMimicDebugLine(req, body, account, tokenType, mimicClaudeCode))
- }
- if s.debugClaudeMimicEnabled() {
- logClaudeMimicDebug(req, body, account, tokenType, mimicClaudeCode)
- }
- return req, body, nil
- }
- func sanitizeCountTokensRequestBody(body []byte) []byte {
- out := body
- for _, path := range []string{
- "temperature",
- "top_p",
- "top_k",
- "stream",
- "stop_sequences",
- "stop",
- // Anthropic's /v1/messages/count_tokens accepts request-input fields only.
- // max_tokens is a generation parameter; OAuth mimicry may inject it to
- // resemble Claude Code messages requests, so it must never reach this endpoint.
- "max_tokens",
- } {
- if gjson.GetBytes(out, path).Exists() {
- if next, ok := deleteJSONPathBytes(out, path); ok {
- out = next
- }
- }
- }
- return out
- }
- // countTokensError 返回 count_tokens 错误响应
- func (s *GatewayService) countTokensError(c *gin.Context, status int, errType, message string) {
- c.JSON(status, gin.H{
- "type": "error",
- "error": gin.H{
- "type": errType,
- "message": message,
- },
- })
- }
|