token_estimator.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. package service
  2. import (
  3. "math"
  4. "strings"
  5. "unicode"
  6. )
  7. // Provider 定义模型厂商大类
  8. type Provider string
  9. const (
  10. OpenAI Provider = "openai" // 代表 GPT-3.5, GPT-4, GPT-4o
  11. Gemini Provider = "gemini" // 代表 Gemini 1.0, 1.5 Pro/Flash
  12. Claude Provider = "claude" // 代表 Claude 3, 3.5 Sonnet
  13. Unknown Provider = "unknown" // 兜底默认
  14. )
  15. // multipliers 定义不同厂商的计费权重
  16. type multipliers struct {
  17. Word float64 // 英文单词 (每词)
  18. Number float64 // 数字 (每连续数字串)
  19. CJK float64 // 中日韩字符 (每字)
  20. Symbol float64 // 普通标点符号 (每个)
  21. MathSymbol float64 // 数学符号 (∑,∫,∂,√等,每个)
  22. URLDelim float64 // URL分隔符 (/,:,?,&,=,#,%) - tokenizer优化好
  23. AtSign float64 // @符号 - 导致单词切分,消耗较高
  24. Emoji float64 // Emoji表情 (每个)
  25. Newline float64 // 换行符/制表符 (每个)
  26. Space float64 // 空格 (每个)
  27. BasePad int // 基础起步消耗 (Start/End tokens)
  28. }
  29. var multipliersMap = map[Provider]multipliers{
  30. Gemini: {
  31. Word: 1.15, Number: 2.8, CJK: 0.68, Symbol: 0.38, MathSymbol: 1.05, URLDelim: 1.2, AtSign: 2.5, Emoji: 1.08, Newline: 1.15, Space: 0.2, BasePad: 0,
  32. },
  33. Claude: {
  34. Word: 1.13, Number: 1.63, CJK: 1.21, Symbol: 0.4, MathSymbol: 4.52, URLDelim: 1.26, AtSign: 2.82, Emoji: 2.6, Newline: 0.89, Space: 0.39, BasePad: 0,
  35. },
  36. OpenAI: {
  37. Word: 1.02, Number: 1.55, CJK: 0.85, Symbol: 0.4, MathSymbol: 2.68, URLDelim: 1.0, AtSign: 2.0, Emoji: 2.12, Newline: 0.5, Space: 0.42, BasePad: 0,
  38. },
  39. }
  40. // getMultipliers 根据厂商获取权重配置
  41. func getMultipliers(p Provider) multipliers {
  42. switch p {
  43. case Gemini:
  44. return multipliersMap[Gemini]
  45. case Claude:
  46. return multipliersMap[Claude]
  47. case OpenAI:
  48. return multipliersMap[OpenAI]
  49. default:
  50. // 默认兜底 (按 OpenAI 的算)
  51. return multipliersMap[OpenAI]
  52. }
  53. }
  54. // EstimateToken 计算 Token 数量
  55. func EstimateToken(provider Provider, text string) int {
  56. m := getMultipliers(provider)
  57. var count float64
  58. // 状态机变量
  59. type WordType int
  60. const (
  61. None WordType = iota
  62. Latin
  63. Number
  64. )
  65. currentWordType := None
  66. for _, r := range text {
  67. // 1. 处理空格和换行符
  68. if unicode.IsSpace(r) {
  69. currentWordType = None
  70. // 换行符和制表符使用Newline权重
  71. if r == '\n' || r == '\t' {
  72. count += m.Newline
  73. } else {
  74. // 普通空格使用Space权重
  75. count += m.Space
  76. }
  77. continue
  78. }
  79. // 2. 处理 CJK (中日韩) - 按字符计费
  80. if isCJK(r) {
  81. currentWordType = None
  82. count += m.CJK
  83. continue
  84. }
  85. // 3. 处理Emoji - 使用专门的Emoji权重
  86. if isEmoji(r) {
  87. currentWordType = None
  88. count += m.Emoji
  89. continue
  90. }
  91. // 4. 处理拉丁字母/数字 (英文单词)
  92. if isLatinOrNumber(r) {
  93. isNum := unicode.IsNumber(r)
  94. newType := Latin
  95. if isNum {
  96. newType = Number
  97. }
  98. // 如果之前不在单词中,或者类型发生变化(字母<->数字),则视为新token
  99. // 注意:对于OpenAI,通常"version 3.5"会切分,"abc123xyz"有时也会切分
  100. // 这里简单起见,字母和数字切换时增加权重
  101. if currentWordType == None || currentWordType != newType {
  102. if newType == Number {
  103. count += m.Number
  104. } else {
  105. count += m.Word
  106. }
  107. currentWordType = newType
  108. }
  109. // 单词中间的字符不额外计费
  110. continue
  111. }
  112. // 5. 处理标点符号/特殊字符 - 按类型使用不同权重
  113. currentWordType = None
  114. if isMathSymbol(r) {
  115. count += m.MathSymbol
  116. } else if r == '@' {
  117. count += m.AtSign
  118. } else if isURLDelim(r) {
  119. count += m.URLDelim
  120. } else {
  121. count += m.Symbol
  122. }
  123. }
  124. // 向上取整并加上基础 padding
  125. return int(math.Ceil(count)) + m.BasePad
  126. }
  127. // 辅助:判断是否为 CJK 字符
  128. func isCJK(r rune) bool {
  129. return unicode.Is(unicode.Han, r) ||
  130. (r >= 0x3040 && r <= 0x30FF) || // 日文
  131. (r >= 0xAC00 && r <= 0xD7A3) // 韩文
  132. }
  133. // 辅助:判断是否为单词主体 (字母或数字)
  134. func isLatinOrNumber(r rune) bool {
  135. return unicode.IsLetter(r) || unicode.IsNumber(r)
  136. }
  137. // 辅助:判断是否为Emoji字符
  138. func isEmoji(r rune) bool {
  139. // Emoji的Unicode范围
  140. // 基本范围:0x1F300-0x1F9FF (Emoticons, Symbols, Pictographs)
  141. // 补充范围:0x2600-0x26FF (Misc Symbols), 0x2700-0x27BF (Dingbats)
  142. // 表情符号:0x1F600-0x1F64F (Emoticons)
  143. // 其他:0x1F900-0x1F9FF (Supplemental Symbols and Pictographs)
  144. return (r >= 0x1F300 && r <= 0x1F9FF) ||
  145. (r >= 0x2600 && r <= 0x26FF) ||
  146. (r >= 0x2700 && r <= 0x27BF) ||
  147. (r >= 0x1F600 && r <= 0x1F64F) ||
  148. (r >= 0x1F900 && r <= 0x1F9FF) ||
  149. (r >= 0x1FA00 && r <= 0x1FAFF) // Symbols and Pictographs Extended-A
  150. }
  151. // 辅助:判断是否为数学符号
  152. func isMathSymbol(r rune) bool {
  153. // 数学运算符和符号
  154. // 基本数学符号:∑ ∫ ∂ √ ∞ ≤ ≥ ≠ ≈ ± × ÷
  155. // 上下标数字:² ³ ¹ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁰
  156. // 希腊字母等也常用于数学
  157. mathSymbols := "∑∫∂√∞≤≥≠≈±×÷∈∉∋∌⊂⊃⊆⊇∪∩∧∨¬∀∃∄∅∆∇∝∟∠∡∢°′″‴⁺⁻⁼⁽⁾ⁿ₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎²³¹⁴⁵⁶⁷⁸⁹⁰"
  158. for _, m := range mathSymbols {
  159. if r == m {
  160. return true
  161. }
  162. }
  163. // Mathematical Operators (U+2200–U+22FF)
  164. if r >= 0x2200 && r <= 0x22FF {
  165. return true
  166. }
  167. // Supplemental Mathematical Operators (U+2A00–U+2AFF)
  168. if r >= 0x2A00 && r <= 0x2AFF {
  169. return true
  170. }
  171. // Mathematical Alphanumeric Symbols (U+1D400–U+1D7FF)
  172. if r >= 0x1D400 && r <= 0x1D7FF {
  173. return true
  174. }
  175. return false
  176. }
  177. // 辅助:判断是否为URL分隔符(tokenizer对这些优化较好)
  178. func isURLDelim(r rune) bool {
  179. // URL中常见的分隔符,tokenizer通常优化处理
  180. urlDelims := "/:?&=;#%"
  181. for _, d := range urlDelims {
  182. if r == d {
  183. return true
  184. }
  185. }
  186. return false
  187. }
  188. func EstimateTokenByModel(model, text string) int {
  189. // strings.Contains(model, "gpt-4o")
  190. if text == "" {
  191. return 0
  192. }
  193. model = strings.ToLower(model)
  194. if strings.Contains(model, "gemini") {
  195. return EstimateToken(Gemini, text)
  196. } else if strings.Contains(model, "claude") {
  197. return EstimateToken(Claude, text)
  198. } else {
  199. return EstimateToken(OpenAI, text)
  200. }
  201. }