gateway_billing_header.go 2.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
  1. package service
  2. import (
  3. "fmt"
  4. "regexp"
  5. "strings"
  6. "github.com/Wei-Shaw/sub2api/internal/pkg/claude"
  7. "github.com/tidwall/gjson"
  8. "github.com/tidwall/sjson"
  9. )
  10. // ccVersionInBillingRe matches the semver part of cc_version (X.Y.Z).
  11. var ccVersionInBillingRe = regexp.MustCompile(`cc_version=\d+\.\d+\.\d+`)
  12. var ccVersionWithFingerprintInBillingRe = regexp.MustCompile(`cc_version=\d+\.\d+\.\d+\.[0-9a-fA-F]{3}\b`)
  13. // OAuth mimicry forces the built-in User-Agent after applying account fingerprints.
  14. func effectiveBillingUserAgent(tokenType string, mimicClaudeCode bool, fingerprint *Fingerprint) string {
  15. if tokenType == "oauth" && mimicClaudeCode {
  16. return claude.DefaultHeaders["User-Agent"]
  17. }
  18. if fingerprint == nil {
  19. return ""
  20. }
  21. return fingerprint.UserAgent
  22. }
  23. // syncBillingHeaderVersion rewrites cc_version in x-anthropic-billing-header
  24. // system text blocks to match the version extracted from userAgent.
  25. // Recompute any recognized fingerprint suffix because its input includes the version.
  26. // Only touches system array blocks whose text starts with "x-anthropic-billing-header".
  27. func syncBillingHeaderVersion(body []byte, userAgent string) []byte {
  28. version := ExtractCLIVersion(userAgent)
  29. if version == "" {
  30. return body
  31. }
  32. systemResult := gjson.GetBytes(body, "system")
  33. if !systemResult.Exists() || !systemResult.IsArray() {
  34. return body
  35. }
  36. replacement := "cc_version=" + version
  37. idx := 0
  38. systemResult.ForEach(func(_, item gjson.Result) bool {
  39. text := item.Get("text")
  40. if text.Exists() && text.Type == gjson.String &&
  41. strings.HasPrefix(text.String(), "x-anthropic-billing-header") {
  42. fingerprintedReplacement := replacement + "." + computeClaudeCodeFingerprint(body, version)
  43. newText := ccVersionWithFingerprintInBillingRe.ReplaceAllString(text.String(), fingerprintedReplacement)
  44. newText = ccVersionInBillingRe.ReplaceAllString(newText, replacement)
  45. if newText != text.String() {
  46. if updated, err := sjson.SetBytes(body, fmt.Sprintf("system.%d.text", idx), newText); err == nil {
  47. body = updated
  48. }
  49. }
  50. }
  51. idx++
  52. return true
  53. })
  54. return body
  55. }