gateway_count_tokens.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  1. package service
  2. import (
  3. "bytes"
  4. "context"
  5. "errors"
  6. "fmt"
  7. "net/http"
  8. "strings"
  9. "github.com/Wei-Shaw/sub2api/internal/pkg/claude"
  10. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  11. "github.com/tidwall/gjson"
  12. "github.com/gin-gonic/gin"
  13. )
  14. // ForwardCountTokens 转发 count_tokens 请求到上游 API
  15. // 特点:不记录使用量、仅支持非流式响应
  16. func (s *GatewayService) ForwardCountTokens(ctx context.Context, c *gin.Context, account *Account, parsed *ParsedRequest) error {
  17. if parsed == nil {
  18. s.countTokensError(c, http.StatusBadRequest, "invalid_request_error", "Request body is empty")
  19. return fmt.Errorf("parse request: empty request")
  20. }
  21. if account != nil && account.IsAnthropicAPIKeyPassthroughEnabled() {
  22. passthroughBody := parsed.Body.Bytes()
  23. if reqModel := parsed.Model; reqModel != "" {
  24. if mappedModel := account.GetMappedModel(reqModel); mappedModel != reqModel {
  25. passthroughBody = s.replaceModelInBody(passthroughBody, mappedModel)
  26. logger.LegacyPrintf("service.gateway", "CountTokens passthrough model mapping: %s -> %s (account: %s)", reqModel, mappedModel, account.Name)
  27. }
  28. }
  29. return s.forwardCountTokensAnthropicAPIKeyPassthrough(ctx, c, account, passthroughBody)
  30. }
  31. // Bedrock 不支持 count_tokens 端点
  32. if account != nil && account.IsBedrock() {
  33. s.countTokensError(c, http.StatusNotFound, "not_found_error", "count_tokens endpoint is not supported for Bedrock")
  34. return nil
  35. }
  36. body := parsed.Body.Bytes()
  37. replaceBody := func(next []byte) error {
  38. if err := parsed.ReplaceBody(next); err != nil {
  39. return fmt.Errorf("rewrite count_tokens body: %w", err)
  40. }
  41. body = parsed.Body.Bytes()
  42. return nil
  43. }
  44. reqModel := parsed.Model
  45. // Pre-filter: strip empty text blocks to prevent upstream 400.
  46. if err := replaceBody(StripEmptyTextBlocks(body)); err != nil {
  47. return err
  48. }
  49. isClaudeCodeCT := IsClaudeCodeClient(ctx) || isClaudeCodeClient(c.GetHeader("User-Agent"), parsed.MetadataUserID)
  50. shouldMimicClaudeCode := account.IsOAuth() && !isClaudeCodeCT
  51. if shouldMimicClaudeCode {
  52. normalizeOpts := claudeOAuthNormalizeOptions{stripSystemCacheControl: true}
  53. var normalizedBody []byte
  54. normalizedBody, reqModel = normalizeClaudeOAuthRequestBody(body, reqModel, normalizeOpts)
  55. if err := replaceBody(normalizedBody); err != nil {
  56. return err
  57. }
  58. if err := replaceBody(s.rewriteMessageCacheControlIfEnabled(ctx, body)); err != nil {
  59. return err
  60. }
  61. if rw := buildToolNameRewriteFromBody(body); rw != nil {
  62. if err := replaceBody(applyToolNameRewriteToBody(body, rw)); err != nil {
  63. return err
  64. }
  65. } else {
  66. if err := replaceBody(applyToolsLastCacheBreakpoint(body)); err != nil {
  67. return err
  68. }
  69. }
  70. }
  71. // Antigravity 账户不支持 count_tokens,返回 404 让客户端 fallback 到本地估算。
  72. // 返回 nil 避免 handler 层记录为错误,也不设置 ops 上游错误上下文。
  73. if account.Platform == PlatformAntigravity {
  74. s.countTokensError(c, http.StatusNotFound, "not_found_error", "count_tokens endpoint is not supported for this platform")
  75. return nil
  76. }
  77. // 应用模型映射:
  78. // - APIKey 账号:使用账号级别的显式映射(如果配置),否则透传原始模型名
  79. // - OAuth/SetupToken 账号:使用 Anthropic 标准映射(短ID → 长ID)
  80. if reqModel != "" {
  81. mappedModel := reqModel
  82. mappingSource := ""
  83. if account.Type == AccountTypeAPIKey {
  84. mappedModel = account.GetMappedModel(reqModel)
  85. if mappedModel != reqModel {
  86. mappingSource = "account"
  87. }
  88. }
  89. if mappingSource == "" && account.Platform == PlatformAnthropic && account.Type != AccountTypeAPIKey {
  90. normalized := claude.NormalizeModelID(reqModel)
  91. if normalized != reqModel {
  92. mappedModel = normalized
  93. mappingSource = "prefix"
  94. }
  95. }
  96. if mappedModel != reqModel {
  97. originalReqModel := reqModel
  98. if err := replaceBody(s.replaceModelInBody(body, mappedModel)); err != nil {
  99. return err
  100. }
  101. reqModel = mappedModel
  102. parsed.Model = mappedModel
  103. logger.LegacyPrintf("service.gateway", "CountTokens model mapping applied: %s -> %s (account: %s, source=%s)", originalReqModel, mappedModel, account.Name, mappingSource)
  104. }
  105. }
  106. // 获取凭证
  107. token, tokenType, err := s.GetAccessToken(ctx, account)
  108. if err != nil {
  109. s.countTokensError(c, http.StatusBadGateway, "upstream_error", "Failed to get access token")
  110. return err
  111. }
  112. // 构建上游请求
  113. upstreamReq, wireBody, err := s.buildCountTokensRequest(ctx, c, account, body, token, tokenType, reqModel, shouldMimicClaudeCode)
  114. if err != nil {
  115. s.countTokensError(c, http.StatusInternalServerError, "api_error", "Failed to build request")
  116. return err
  117. }
  118. // 先记录首发 wire body;如果后面进入 400 retry,retry 会基于未签名的逻辑 body 重新构建。
  119. acceptedWireBody := wireBody
  120. // 获取代理URL(自定义 base URL 模式下,proxy 通过 buildCustomRelayURL 作为查询参数传递)
  121. proxyURL := ""
  122. if account.ProxyID != nil && account.Proxy != nil {
  123. if !account.IsCustomBaseURLEnabled() || account.GetCustomBaseURL() == "" {
  124. proxyURL = account.Proxy.URL()
  125. }
  126. }
  127. // 发送请求
  128. resp, err := s.httpUpstream.DoWithTLS(WithAccountTrafficRequest(upstreamReq, account), proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account))
  129. if err != nil {
  130. setOpsUpstreamError(c, 0, sanitizeUpstreamErrorMessage(err.Error()), "")
  131. s.countTokensError(c, http.StatusBadGateway, "upstream_error", "Request failed")
  132. return fmt.Errorf("upstream request failed: %w", err)
  133. }
  134. // 读取响应体
  135. countTokensTooLarge := func(c *gin.Context) {
  136. s.countTokensError(c, http.StatusBadGateway, "upstream_error", "Upstream response too large")
  137. }
  138. respBody, err := ReadUpstreamResponseBody(resp.Body, s.cfg, c, countTokensTooLarge)
  139. _ = resp.Body.Close()
  140. if err != nil {
  141. if !errors.Is(err, ErrUpstreamResponseBodyTooLarge) {
  142. s.countTokensError(c, http.StatusBadGateway, "upstream_error", "Failed to read response")
  143. }
  144. return err
  145. }
  146. // 检测 thinking block 签名错误(400)并重试一次(过滤 thinking blocks)
  147. if resp.StatusCode == 400 && s.shouldRectifySignatureError(ctx, account, respBody, reqModel) {
  148. logger.LegacyPrintf("service.gateway", "Account %d: detected thinking block signature error on count_tokens, retrying with filtered thinking blocks", account.ID)
  149. filteredBody := FilterThinkingBlocksForRetry(body, reqModel)
  150. retryReq, retryWireBody, buildErr := s.buildCountTokensRequest(ctx, c, account, filteredBody, token, tokenType, reqModel, shouldMimicClaudeCode)
  151. if buildErr == nil {
  152. retryResp, retryErr := s.httpUpstream.DoWithTLS(WithAccountTrafficRequest(retryReq, account), proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account))
  153. if retryErr == nil {
  154. if retryResp.StatusCode < 400 {
  155. // count_tokens 签名重试成功后记录最终 wire body,错误响应仍保留原 body 便于后续处理。
  156. acceptedWireBody = retryWireBody
  157. }
  158. resp = retryResp
  159. respBody, err = ReadUpstreamResponseBody(resp.Body, s.cfg, c, countTokensTooLarge)
  160. _ = resp.Body.Close()
  161. if err != nil {
  162. if !errors.Is(err, ErrUpstreamResponseBodyTooLarge) {
  163. s.countTokensError(c, http.StatusBadGateway, "upstream_error", "Failed to read response")
  164. }
  165. return err
  166. }
  167. }
  168. }
  169. }
  170. if resp.StatusCode < 400 && !bytes.Equal(acceptedWireBody, body) {
  171. // count_tokens 成功后再同步最终 wire body,避免 retry 从已签名 body 派生。
  172. if err := replaceBody(acceptedWireBody); err != nil {
  173. return err
  174. }
  175. }
  176. // 处理错误响应
  177. if resp.StatusCode >= 400 {
  178. // 标记账号状态(429/529等)
  179. s.rateLimitService.HandleUpstreamError(ctx, account, resp.StatusCode, resp.Header, respBody)
  180. upstreamMsg := strings.TrimSpace(extractUpstreamErrorMessage(respBody))
  181. upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg)
  182. upstreamDetail := ""
  183. if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
  184. maxBytes := s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes
  185. if maxBytes <= 0 {
  186. maxBytes = 2048
  187. }
  188. upstreamDetail = truncateString(string(respBody), maxBytes)
  189. }
  190. setOpsUpstreamError(c, resp.StatusCode, upstreamMsg, upstreamDetail)
  191. // 记录上游错误摘要便于排障(不回显请求内容)
  192. if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
  193. logger.LegacyPrintf("service.gateway",
  194. "count_tokens upstream error %d (account=%d platform=%s type=%s): %s",
  195. resp.StatusCode,
  196. account.ID,
  197. account.Platform,
  198. account.Type,
  199. truncateForLog(respBody, s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes),
  200. )
  201. }
  202. // 返回简化的错误响应
  203. errMsg := "Upstream request failed"
  204. switch resp.StatusCode {
  205. case 429:
  206. errMsg = "Rate limit exceeded"
  207. case 529:
  208. errMsg = "Service overloaded"
  209. }
  210. s.countTokensError(c, resp.StatusCode, "upstream_error", errMsg)
  211. if upstreamMsg == "" {
  212. return fmt.Errorf("upstream error: %d", resp.StatusCode)
  213. }
  214. return fmt.Errorf("upstream error: %d message=%s", resp.StatusCode, upstreamMsg)
  215. }
  216. // 透传成功响应
  217. c.Data(resp.StatusCode, "application/json", respBody)
  218. return nil
  219. }
  220. func (s *GatewayService) forwardCountTokensAnthropicAPIKeyPassthrough(ctx context.Context, c *gin.Context, account *Account, body []byte) error {
  221. token, tokenType, err := s.GetAccessToken(ctx, account)
  222. if err != nil {
  223. s.countTokensError(c, http.StatusBadGateway, "upstream_error", "Failed to get access token")
  224. return err
  225. }
  226. if tokenType != "apikey" {
  227. s.countTokensError(c, http.StatusBadGateway, "upstream_error", "Invalid account token type")
  228. return fmt.Errorf("anthropic api key passthrough requires apikey token, got: %s", tokenType)
  229. }
  230. upstreamReq, err := s.buildCountTokensRequestAnthropicAPIKeyPassthrough(ctx, c, account, body, token)
  231. if err != nil {
  232. s.countTokensError(c, http.StatusInternalServerError, "api_error", "Failed to build request")
  233. return err
  234. }
  235. proxyURL := ""
  236. if account.ProxyID != nil && account.Proxy != nil {
  237. proxyURL = account.Proxy.URL()
  238. }
  239. resp, err := s.httpUpstream.DoWithTLS(WithAccountTrafficRequest(upstreamReq, account), proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account))
  240. if err != nil {
  241. setOpsUpstreamError(c, 0, sanitizeUpstreamErrorMessage(err.Error()), "")
  242. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  243. ProxyID: opsUpstreamProxyID(account),
  244. ProxyName: opsUpstreamProxyName(account),
  245. Platform: account.Platform,
  246. AccountID: account.ID,
  247. AccountName: account.Name,
  248. UpstreamStatusCode: 0,
  249. UpstreamURL: safeUpstreamURL(upstreamReq.URL.String()),
  250. Passthrough: true,
  251. Kind: "request_error",
  252. Message: sanitizeUpstreamErrorMessage(err.Error()),
  253. })
  254. s.countTokensError(c, http.StatusBadGateway, "upstream_error", "Request failed")
  255. return fmt.Errorf("upstream request failed: %w", err)
  256. }
  257. countTokensTooLarge := func(c *gin.Context) {
  258. s.countTokensError(c, http.StatusBadGateway, "upstream_error", "Upstream response too large")
  259. }
  260. respBody, err := ReadUpstreamResponseBody(resp.Body, s.cfg, c, countTokensTooLarge)
  261. _ = resp.Body.Close()
  262. if err != nil {
  263. if !errors.Is(err, ErrUpstreamResponseBodyTooLarge) {
  264. s.countTokensError(c, http.StatusBadGateway, "upstream_error", "Failed to read response")
  265. }
  266. return err
  267. }
  268. if resp.StatusCode >= 400 {
  269. if s.rateLimitService != nil {
  270. s.rateLimitService.HandleUpstreamError(ctx, account, resp.StatusCode, resp.Header, respBody)
  271. }
  272. upstreamMsg := strings.TrimSpace(extractUpstreamErrorMessage(respBody))
  273. upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg)
  274. // 中转站不支持 count_tokens 端点时(404),返回 404 让客户端 fallback 到本地估算。
  275. // 仅在错误消息明确指向 count_tokens endpoint 不存在时生效,避免误吞其他 404(如错误 base_url)。
  276. // 返回 nil 避免 handler 层记录为错误,也不设置 ops 上游错误上下文。
  277. if isCountTokensUnsupported404(resp.StatusCode, respBody) {
  278. logger.LegacyPrintf("service.gateway",
  279. "[count_tokens] Upstream does not support count_tokens (404), returning 404: account=%d name=%s msg=%s",
  280. account.ID, account.Name, truncateString(upstreamMsg, 512))
  281. s.countTokensError(c, http.StatusNotFound, "not_found_error", "count_tokens endpoint is not supported by upstream")
  282. return nil
  283. }
  284. upstreamDetail := ""
  285. if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
  286. maxBytes := s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes
  287. if maxBytes <= 0 {
  288. maxBytes = 2048
  289. }
  290. upstreamDetail = truncateString(string(respBody), maxBytes)
  291. }
  292. setOpsUpstreamError(c, resp.StatusCode, upstreamMsg, upstreamDetail)
  293. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  294. ProxyID: opsUpstreamProxyID(account),
  295. ProxyName: opsUpstreamProxyName(account),
  296. Platform: account.Platform,
  297. AccountID: account.ID,
  298. AccountName: account.Name,
  299. UpstreamStatusCode: resp.StatusCode,
  300. UpstreamRequestID: resp.Header.Get("x-request-id"),
  301. UpstreamURL: safeUpstreamURL(upstreamReq.URL.String()),
  302. Passthrough: true,
  303. Kind: "http_error",
  304. Message: upstreamMsg,
  305. Detail: upstreamDetail,
  306. })
  307. errMsg := "Upstream request failed"
  308. switch resp.StatusCode {
  309. case 429:
  310. errMsg = "Rate limit exceeded"
  311. case 529:
  312. errMsg = "Service overloaded"
  313. }
  314. s.countTokensError(c, resp.StatusCode, "upstream_error", errMsg)
  315. if upstreamMsg == "" {
  316. return fmt.Errorf("upstream error: %d", resp.StatusCode)
  317. }
  318. return fmt.Errorf("upstream error: %d message=%s", resp.StatusCode, upstreamMsg)
  319. }
  320. writeAnthropicPassthroughResponseHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
  321. contentType := strings.TrimSpace(resp.Header.Get("Content-Type"))
  322. if contentType == "" {
  323. contentType = "application/json"
  324. }
  325. c.Data(resp.StatusCode, contentType, respBody)
  326. return nil
  327. }
  328. func (s *GatewayService) buildCountTokensRequestAnthropicAPIKeyPassthrough(
  329. ctx context.Context,
  330. c *gin.Context,
  331. account *Account,
  332. body []byte,
  333. token string,
  334. ) (*http.Request, error) {
  335. body = stripDeferredToolCacheControl(body)
  336. targetURL := claudeAPICountTokensURL
  337. baseURL := account.GetBaseURL()
  338. if baseURL != "" {
  339. validatedURL, err := s.validateUpstreamBaseURL(baseURL)
  340. if err != nil {
  341. return nil, err
  342. }
  343. targetURL = validatedURL + "/v1/messages/count_tokens?beta=true"
  344. }
  345. body = sanitizeCountTokensRequestBody(body)
  346. // 同 buildUpstreamRequestAnthropicAPIKeyPassthrough:能力维度 sanitize。
  347. clientBeta := ""
  348. if c != nil && c.Request != nil {
  349. clientBeta = getHeaderRaw(c.Request.Header, "anthropic-beta")
  350. }
  351. // 账号覆写了 anthropic-beta 时,覆写值即最终上游值:净化以覆写值为准
  352. if beta, ok := account.HeaderOverrideValue("anthropic-beta"); ok {
  353. clientBeta = beta
  354. }
  355. if sanitized, changed := sanitizeAnthropicBodyForBetaTokens(body, clientBeta); changed {
  356. body = sanitized
  357. }
  358. req, err := http.NewRequestWithContext(ctx, http.MethodPost, targetURL, bytes.NewReader(body))
  359. if err != nil {
  360. return nil, err
  361. }
  362. if c != nil && c.Request != nil {
  363. for key, values := range c.Request.Header {
  364. lowerKey := strings.ToLower(strings.TrimSpace(key))
  365. if !allowedHeaders[lowerKey] {
  366. continue
  367. }
  368. wireKey := resolveWireCasing(key)
  369. for _, v := range values {
  370. addHeaderRaw(req.Header, wireKey, v)
  371. }
  372. }
  373. }
  374. req.Header.Del("authorization")
  375. req.Header.Del("x-api-key")
  376. req.Header.Del("x-goog-api-key")
  377. req.Header.Del("cookie")
  378. // Ollama Cloud Anthropic 兼容端点按实际 base_url 强制 Bearer(同上方
  379. // targetURL 的 base 取值),其余保持 extra/default 行为。
  380. setAnthropicAPIKeyAuthHeader(req.Header, account, token, account.GetBaseURL())
  381. if req.Header.Get("content-type") == "" {
  382. req.Header.Set("content-type", "application/json")
  383. }
  384. if req.Header.Get("anthropic-version") == "" {
  385. req.Header.Set("anthropic-version", "2023-06-01")
  386. }
  387. // 账号级请求头覆写(最终生效,覆盖上面所有来源的同名头)
  388. account.ApplyHeaderOverrides(req.Header)
  389. return req, nil
  390. }
  391. // buildCountTokensRequest 构建 count_tokens 上游请求
  392. func (s *GatewayService) buildCountTokensRequest(ctx context.Context, c *gin.Context, account *Account, body []byte, token, tokenType, modelID string, mimicClaudeCode bool) (*http.Request, []byte, error) {
  393. body = stripDeferredToolCacheControl(body)
  394. // 确定目标 URL
  395. targetURL := claudeAPICountTokensURL
  396. if account.Type == AccountTypeAPIKey {
  397. baseURL := account.GetBaseURL()
  398. if baseURL != "" {
  399. validatedURL, err := s.validateUpstreamBaseURL(baseURL)
  400. if err != nil {
  401. return nil, nil, err
  402. }
  403. targetURL = validatedURL + "/v1/messages/count_tokens?beta=true"
  404. }
  405. } else if account.IsCustomBaseURLEnabled() {
  406. customURL := account.GetCustomBaseURL()
  407. if customURL == "" {
  408. return nil, nil, fmt.Errorf("custom_base_url is enabled but not configured for account %d", account.ID)
  409. }
  410. validatedURL, err := s.validateUpstreamBaseURL(customURL)
  411. if err != nil {
  412. return nil, nil, err
  413. }
  414. targetURL = s.buildCustomRelayURL(validatedURL, "/v1/messages/count_tokens", account)
  415. }
  416. clientHeaders := http.Header{}
  417. if c != nil && c.Request != nil {
  418. clientHeaders = c.Request.Header
  419. }
  420. // OAuth 账号:应用统一指纹和重写 userID(受设置开关控制)
  421. // 如果启用了会话ID伪装,会在重写后替换 session 部分为固定值
  422. ctEnableFP, ctEnableMPT := true, false
  423. if s.settingService != nil {
  424. ctEnableFP, ctEnableMPT, _ = s.settingService.GetGatewayForwardingSettings(ctx)
  425. }
  426. var ctFingerprint *Fingerprint
  427. if account.IsOAuth() && s.identityService != nil {
  428. fp, err := s.identityService.GetOrCreateFingerprint(ctx, account.ID, clientHeaders)
  429. if err == nil {
  430. ctFingerprint = fp
  431. if !ctEnableMPT {
  432. accountUUID := account.GetExtraString("account_uuid")
  433. if accountUUID != "" && fp.ClientID != "" {
  434. if newBody, err := s.identityService.RewriteUserIDWithMasking(ctx, body, account, accountUUID, fp.ClientID, fp.UserAgent); err == nil && len(newBody) > 0 {
  435. body = newBody
  436. }
  437. }
  438. }
  439. }
  440. }
  441. // Disabled fingerprint unification does not disable forced mimicry headers.
  442. var billingFingerprint *Fingerprint
  443. if ctEnableFP {
  444. billingFingerprint = ctFingerprint
  445. }
  446. if billingUA := effectiveBillingUserAgent(tokenType, mimicClaudeCode, billingFingerprint); billingUA != "" {
  447. body = syncBillingHeaderVersion(body, billingUA)
  448. }
  449. // === 计算最终 anthropic-beta header(先于 body sanitize 与 CCH 签名)===
  450. // 顺序约束同 buildUpstreamRequest。
  451. ctEffectiveDropSet := mergeDropSets(s.getBetaPolicyFilterSet(ctx, c, account, modelID))
  452. finalBetaHeader, finalBetaShouldSet := s.computeFinalCountTokensAnthropicBeta(
  453. tokenType, mimicClaudeCode, modelID, clientHeaders, body, ctEffectiveDropSet,
  454. )
  455. // 账号覆写了 anthropic-beta 时,覆写值即最终上游值:净化以覆写值为准
  456. if beta, ok := account.HeaderOverrideValue("anthropic-beta"); ok {
  457. finalBetaHeader, finalBetaShouldSet = beta, true
  458. }
  459. // 能力维度 body sanitize:与最终 anthropic-beta header 对称
  460. if sanitized, changed := sanitizeAnthropicBodyForBetaTokens(body, finalBetaHeader); changed {
  461. body = sanitized
  462. }
  463. body = sanitizeCountTokensRequestBody(body)
  464. req, err := http.NewRequestWithContext(ctx, "POST", targetURL, bytes.NewReader(body))
  465. if err != nil {
  466. return nil, nil, err
  467. }
  468. // 设置认证头(保持原始大小写)
  469. if tokenType == "oauth" {
  470. setHeaderRaw(req.Header, "authorization", "Bearer "+token)
  471. } else {
  472. // Ollama Cloud Anthropic 兼容端点按实际 base_url 强制 Bearer(同上方
  473. // targetURL 的 base 取值),其余保持 extra/default 行为。
  474. setAnthropicAPIKeyAuthHeader(req.Header, account, token, account.GetBaseURL())
  475. }
  476. // 白名单透传 headers(恢复真实 wire casing)
  477. for key, values := range clientHeaders {
  478. lowerKey := strings.ToLower(key)
  479. if allowedHeaders[lowerKey] {
  480. wireKey := resolveWireCasing(key)
  481. for _, v := range values {
  482. addHeaderRaw(req.Header, wireKey, v)
  483. }
  484. }
  485. }
  486. // OAuth 账号:应用指纹到请求头(受设置开关控制)
  487. if ctEnableFP && ctFingerprint != nil {
  488. s.identityService.ApplyFingerprint(req, ctFingerprint)
  489. }
  490. // 确保必要的 headers 存在(保持原始大小写)
  491. if getHeaderRaw(req.Header, "content-type") == "" {
  492. setHeaderRaw(req.Header, "content-type", "application/json")
  493. }
  494. if getHeaderRaw(req.Header, "anthropic-version") == "" {
  495. setHeaderRaw(req.Header, "anthropic-version", "2023-06-01")
  496. }
  497. if tokenType == "oauth" {
  498. applyClaudeOAuthHeaderDefaults(req)
  499. }
  500. // OAuth + mimic Claude Code:强制注入 CLI 指纹 header
  501. if tokenType == "oauth" && mimicClaudeCode {
  502. applyClaudeCodeMimicHeaders(req, false)
  503. }
  504. // 写入最终 anthropic-beta header(Del 一次避免白名单透传值残留)
  505. deleteHeaderAllForms(req.Header, "anthropic-beta")
  506. if finalBetaShouldSet {
  507. setHeaderRaw(req.Header, "anthropic-beta", finalBetaHeader)
  508. }
  509. // 同步 X-Claude-Code-Session-Id 头:取 body 中已处理的 metadata.user_id 的 session_id 覆盖
  510. if sessionHeader := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id"); sessionHeader != "" {
  511. if uid := gjson.GetBytes(body, "metadata.user_id").String(); uid != "" {
  512. if parsed := ParseMetadataUserID(uid); parsed != nil {
  513. setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", parsed.SessionID)
  514. }
  515. }
  516. }
  517. // 账号级请求头覆写(仅 anthropic/openai api_key 账号启用时生效;OAuth 路径 no-op)
  518. account.ApplyHeaderOverrides(req.Header)
  519. if c != nil && tokenType == "oauth" {
  520. c.Set(claudeMimicDebugInfoKey, buildClaudeMimicDebugLine(req, body, account, tokenType, mimicClaudeCode))
  521. }
  522. if s.debugClaudeMimicEnabled() {
  523. logClaudeMimicDebug(req, body, account, tokenType, mimicClaudeCode)
  524. }
  525. return req, body, nil
  526. }
  527. func sanitizeCountTokensRequestBody(body []byte) []byte {
  528. out := body
  529. for _, path := range []string{
  530. "temperature",
  531. "top_p",
  532. "top_k",
  533. "stream",
  534. "stop_sequences",
  535. "stop",
  536. // Anthropic's /v1/messages/count_tokens accepts request-input fields only.
  537. // max_tokens is a generation parameter; OAuth mimicry may inject it to
  538. // resemble Claude Code messages requests, so it must never reach this endpoint.
  539. "max_tokens",
  540. } {
  541. if gjson.GetBytes(out, path).Exists() {
  542. if next, ok := deleteJSONPathBytes(out, path); ok {
  543. out = next
  544. }
  545. }
  546. }
  547. return out
  548. }
  549. // countTokensError 返回 count_tokens 错误响应
  550. func (s *GatewayService) countTokensError(c *gin.Context, status int, errType, message string) {
  551. c.JSON(status, gin.H{
  552. "type": "error",
  553. "error": gin.H{
  554. "type": errType,
  555. "message": message,
  556. },
  557. })
  558. }