token_counter.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. package service
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "image"
  7. _ "image/gif"
  8. _ "image/jpeg"
  9. _ "image/png"
  10. "log"
  11. "math"
  12. "path/filepath"
  13. "strings"
  14. "sync"
  15. "unicode/utf8"
  16. "github.com/QuantumNous/new-api/common"
  17. "github.com/QuantumNous/new-api/constant"
  18. "github.com/QuantumNous/new-api/dto"
  19. relaycommon "github.com/QuantumNous/new-api/relay/common"
  20. constant2 "github.com/QuantumNous/new-api/relay/constant"
  21. "github.com/QuantumNous/new-api/types"
  22. "github.com/gin-gonic/gin"
  23. "github.com/tiktoken-go/tokenizer"
  24. "github.com/tiktoken-go/tokenizer/codec"
  25. )
  26. // tokenEncoderMap won't grow after initialization
  27. var defaultTokenEncoder tokenizer.Codec
  28. // tokenEncoderMap is used to store token encoders for different models
  29. var tokenEncoderMap = make(map[string]tokenizer.Codec)
  30. // tokenEncoderMutex protects tokenEncoderMap for concurrent access
  31. var tokenEncoderMutex sync.RWMutex
  32. func InitTokenEncoders() {
  33. common.SysLog("initializing token encoders")
  34. defaultTokenEncoder = codec.NewCl100kBase()
  35. common.SysLog("token encoders initialized")
  36. }
  37. func getTokenEncoder(model string) tokenizer.Codec {
  38. // First, try to get the encoder from cache with read lock
  39. tokenEncoderMutex.RLock()
  40. if encoder, exists := tokenEncoderMap[model]; exists {
  41. tokenEncoderMutex.RUnlock()
  42. return encoder
  43. }
  44. tokenEncoderMutex.RUnlock()
  45. // If not in cache, create new encoder with write lock
  46. tokenEncoderMutex.Lock()
  47. defer tokenEncoderMutex.Unlock()
  48. // Double-check if another goroutine already created the encoder
  49. if encoder, exists := tokenEncoderMap[model]; exists {
  50. return encoder
  51. }
  52. // Create new encoder
  53. modelCodec, err := tokenizer.ForModel(tokenizer.Model(model))
  54. if err != nil {
  55. // Cache the default encoder for this model to avoid repeated failures
  56. tokenEncoderMap[model] = defaultTokenEncoder
  57. return defaultTokenEncoder
  58. }
  59. // Cache the new encoder
  60. tokenEncoderMap[model] = modelCodec
  61. return modelCodec
  62. }
  63. func getTokenNum(tokenEncoder tokenizer.Codec, text string) int {
  64. if text == "" {
  65. return 0
  66. }
  67. tkm, _ := tokenEncoder.Count(text)
  68. return tkm
  69. }
  70. func getImageToken(fileMeta *types.FileMeta, model string, stream bool) (int, error) {
  71. if fileMeta == nil {
  72. return 0, fmt.Errorf("image_url_is_nil")
  73. }
  74. // Defaults for 4o/4.1/4.5 family unless overridden below
  75. baseTokens := 85
  76. tileTokens := 170
  77. // Model classification
  78. lowerModel := strings.ToLower(model)
  79. // Special cases from existing behavior
  80. if strings.HasPrefix(lowerModel, "glm-4") {
  81. return 1047, nil
  82. }
  83. // Patch-based models (32x32 patches, capped at 1536, with multiplier)
  84. isPatchBased := false
  85. multiplier := 1.0
  86. switch {
  87. case strings.Contains(lowerModel, "gpt-4.1-mini"):
  88. isPatchBased = true
  89. multiplier = 1.62
  90. case strings.Contains(lowerModel, "gpt-4.1-nano"):
  91. isPatchBased = true
  92. multiplier = 2.46
  93. case strings.HasPrefix(lowerModel, "o4-mini"):
  94. isPatchBased = true
  95. multiplier = 1.72
  96. case strings.HasPrefix(lowerModel, "gpt-5-mini"):
  97. isPatchBased = true
  98. multiplier = 1.62
  99. case strings.HasPrefix(lowerModel, "gpt-5-nano"):
  100. isPatchBased = true
  101. multiplier = 2.46
  102. }
  103. // Tile-based model tokens and bases per doc
  104. if !isPatchBased {
  105. if strings.HasPrefix(lowerModel, "gpt-4o-mini") {
  106. baseTokens = 2833
  107. tileTokens = 5667
  108. } else if strings.HasPrefix(lowerModel, "gpt-5-chat-latest") || (strings.HasPrefix(lowerModel, "gpt-5") && !strings.Contains(lowerModel, "mini") && !strings.Contains(lowerModel, "nano")) {
  109. baseTokens = 70
  110. tileTokens = 140
  111. } else if strings.HasPrefix(lowerModel, "o1") || strings.HasPrefix(lowerModel, "o3") || strings.HasPrefix(lowerModel, "o1-pro") {
  112. baseTokens = 75
  113. tileTokens = 150
  114. } else if strings.Contains(lowerModel, "computer-use-preview") {
  115. baseTokens = 65
  116. tileTokens = 129
  117. } else if strings.Contains(lowerModel, "4.1") || strings.Contains(lowerModel, "4o") || strings.Contains(lowerModel, "4.5") {
  118. baseTokens = 85
  119. tileTokens = 170
  120. }
  121. }
  122. // Respect existing feature flags/short-circuits
  123. if fileMeta.Detail == "low" && !isPatchBased {
  124. return baseTokens, nil
  125. }
  126. if !constant.GetMediaTokenNotStream && !stream {
  127. return 3 * baseTokens, nil
  128. }
  129. // Normalize detail
  130. if fileMeta.Detail == "auto" || fileMeta.Detail == "" {
  131. fileMeta.Detail = "high"
  132. }
  133. // Whether to count image tokens at all
  134. if !constant.GetMediaToken {
  135. return 3 * baseTokens, nil
  136. }
  137. // Decode image to get dimensions
  138. var config image.Config
  139. var err error
  140. var format string
  141. var b64str string
  142. if fileMeta.ParsedData != nil {
  143. config, format, b64str, err = DecodeBase64ImageData(fileMeta.ParsedData.Base64Data)
  144. } else {
  145. if strings.HasPrefix(fileMeta.OriginData, "http") {
  146. config, format, err = DecodeUrlImageData(fileMeta.OriginData)
  147. } else {
  148. common.SysLog(fmt.Sprintf("decoding image"))
  149. config, format, b64str, err = DecodeBase64ImageData(fileMeta.OriginData)
  150. }
  151. fileMeta.MimeType = format
  152. }
  153. if err != nil {
  154. return 0, err
  155. }
  156. if config.Width == 0 || config.Height == 0 {
  157. // not an image
  158. if format != "" && b64str != "" {
  159. // file type
  160. return 3 * baseTokens, nil
  161. }
  162. return 0, errors.New(fmt.Sprintf("fail to decode base64 config: %s", fileMeta.OriginData))
  163. }
  164. width := config.Width
  165. height := config.Height
  166. log.Printf("format: %s, width: %d, height: %d", format, width, height)
  167. if isPatchBased {
  168. // 32x32 patch-based calculation with 1536 cap and model multiplier
  169. ceilDiv := func(a, b int) int { return (a + b - 1) / b }
  170. rawPatchesW := ceilDiv(width, 32)
  171. rawPatchesH := ceilDiv(height, 32)
  172. rawPatches := rawPatchesW * rawPatchesH
  173. if rawPatches > 1536 {
  174. // scale down
  175. area := float64(width * height)
  176. r := math.Sqrt(float64(32*32*1536) / area)
  177. wScaled := float64(width) * r
  178. hScaled := float64(height) * r
  179. // adjust to fit whole number of patches after scaling
  180. adjW := math.Floor(wScaled/32.0) / (wScaled / 32.0)
  181. adjH := math.Floor(hScaled/32.0) / (hScaled / 32.0)
  182. adj := math.Min(adjW, adjH)
  183. if !math.IsNaN(adj) && adj > 0 {
  184. r = r * adj
  185. }
  186. wScaled = float64(width) * r
  187. hScaled = float64(height) * r
  188. patchesW := math.Ceil(wScaled / 32.0)
  189. patchesH := math.Ceil(hScaled / 32.0)
  190. imageTokens := int(patchesW * patchesH)
  191. if imageTokens > 1536 {
  192. imageTokens = 1536
  193. }
  194. return int(math.Round(float64(imageTokens) * multiplier)), nil
  195. }
  196. // below cap
  197. imageTokens := rawPatches
  198. return int(math.Round(float64(imageTokens) * multiplier)), nil
  199. }
  200. // Tile-based calculation for 4o/4.1/4.5/o1/o3/etc.
  201. // Step 1: fit within 2048x2048 square
  202. maxSide := math.Max(float64(width), float64(height))
  203. fitScale := 1.0
  204. if maxSide > 2048 {
  205. fitScale = maxSide / 2048.0
  206. }
  207. fitW := int(math.Round(float64(width) / fitScale))
  208. fitH := int(math.Round(float64(height) / fitScale))
  209. // Step 2: scale so that shortest side is exactly 768
  210. minSide := math.Min(float64(fitW), float64(fitH))
  211. if minSide == 0 {
  212. return baseTokens, nil
  213. }
  214. shortScale := 768.0 / minSide
  215. finalW := int(math.Round(float64(fitW) * shortScale))
  216. finalH := int(math.Round(float64(fitH) * shortScale))
  217. // Count 512px tiles
  218. tilesW := (finalW + 512 - 1) / 512
  219. tilesH := (finalH + 512 - 1) / 512
  220. tiles := tilesW * tilesH
  221. if common.DebugEnabled {
  222. log.Printf("scaled to: %dx%d, tiles: %d", finalW, finalH, tiles)
  223. }
  224. return tiles*tileTokens + baseTokens, nil
  225. }
  226. func CountRequestToken(c *gin.Context, meta *types.TokenCountMeta, info *relaycommon.RelayInfo) (int, error) {
  227. if meta == nil {
  228. return 0, errors.New("token count meta is nil")
  229. }
  230. if !constant.GetMediaToken {
  231. return 0, nil
  232. }
  233. if !constant.GetMediaTokenNotStream && !info.IsStream {
  234. return 0, nil
  235. }
  236. if info.RelayFormat == types.RelayFormatOpenAIRealtime {
  237. return 0, nil
  238. }
  239. if info.RelayMode == constant2.RelayModeAudioTranscription || info.RelayMode == constant2.RelayModeAudioTranslation {
  240. multiForm, err := common.ParseMultipartFormReusable(c)
  241. if err != nil {
  242. return 0, fmt.Errorf("error parsing multipart form: %v", err)
  243. }
  244. fileHeaders := multiForm.File["file"]
  245. totalAudioToken := 0
  246. for _, fileHeader := range fileHeaders {
  247. file, err := fileHeader.Open()
  248. if err != nil {
  249. return 0, fmt.Errorf("error opening audio file: %v", err)
  250. }
  251. defer file.Close()
  252. // get ext and io.seeker
  253. ext := filepath.Ext(fileHeader.Filename)
  254. duration, err := common.GetAudioDuration(c.Request.Context(), file, ext)
  255. if err != nil {
  256. return 0, fmt.Errorf("error getting audio duration: %v", err)
  257. }
  258. // 一分钟 1000 token,与 $price / minute 对齐
  259. totalAudioToken += int(math.Round(math.Ceil(duration) / 60.0 * 1000))
  260. }
  261. return totalAudioToken, nil
  262. }
  263. model := common.GetContextKeyString(c, constant.ContextKeyOriginalModel)
  264. tkm := 0
  265. if meta.TokenType == types.TokenTypeTextNumber {
  266. tkm += utf8.RuneCountInString(meta.CombineText)
  267. } else {
  268. tkm += CountTextToken(meta.CombineText, model)
  269. }
  270. if info.RelayFormat == types.RelayFormatOpenAI {
  271. tkm += meta.ToolsCount * 8
  272. tkm += meta.MessagesCount * 3 // 每条消息的格式化token数量
  273. tkm += meta.NameCount * 3
  274. tkm += 3
  275. }
  276. shouldFetchFiles := true
  277. if info.RelayFormat == types.RelayFormatGemini {
  278. shouldFetchFiles = false
  279. }
  280. if shouldFetchFiles {
  281. for _, file := range meta.Files {
  282. if strings.HasPrefix(file.OriginData, "http") {
  283. mineType, err := GetFileTypeFromUrl(c, file.OriginData, "token_counter")
  284. if err != nil {
  285. return 0, fmt.Errorf("error getting file base64 from url: %v", err)
  286. }
  287. if strings.HasPrefix(mineType, "image/") {
  288. file.FileType = types.FileTypeImage
  289. } else if strings.HasPrefix(mineType, "video/") {
  290. file.FileType = types.FileTypeVideo
  291. } else if strings.HasPrefix(mineType, "audio/") {
  292. file.FileType = types.FileTypeAudio
  293. } else {
  294. file.FileType = types.FileTypeFile
  295. }
  296. file.MimeType = mineType
  297. } else if strings.HasPrefix(file.OriginData, "data:") {
  298. // get mime type from base64 header
  299. parts := strings.SplitN(file.OriginData, ",", 2)
  300. if len(parts) >= 1 {
  301. header := parts[0]
  302. // Extract mime type from "data:mime/type;base64" format
  303. if strings.Contains(header, ":") && strings.Contains(header, ";") {
  304. mimeStart := strings.Index(header, ":") + 1
  305. mimeEnd := strings.Index(header, ";")
  306. if mimeStart < mimeEnd {
  307. mineType := header[mimeStart:mimeEnd]
  308. if strings.HasPrefix(mineType, "image/") {
  309. file.FileType = types.FileTypeImage
  310. } else if strings.HasPrefix(mineType, "video/") {
  311. file.FileType = types.FileTypeVideo
  312. } else if strings.HasPrefix(mineType, "audio/") {
  313. file.FileType = types.FileTypeAudio
  314. } else {
  315. file.FileType = types.FileTypeFile
  316. }
  317. file.MimeType = mineType
  318. }
  319. }
  320. }
  321. }
  322. }
  323. }
  324. for i, file := range meta.Files {
  325. switch file.FileType {
  326. case types.FileTypeImage:
  327. if info.RelayFormat == types.RelayFormatGemini {
  328. tkm += 256
  329. } else {
  330. token, err := getImageToken(file, model, info.IsStream)
  331. if err != nil {
  332. return 0, fmt.Errorf("error counting image token, media index[%d], original data[%s], err: %v", i, file.OriginData, err)
  333. }
  334. tkm += token
  335. }
  336. case types.FileTypeAudio:
  337. tkm += 256
  338. case types.FileTypeVideo:
  339. tkm += 4096 * 2
  340. case types.FileTypeFile:
  341. tkm += 4096
  342. default:
  343. tkm += 4096 // Default case for unknown file types
  344. }
  345. }
  346. common.SetContextKey(c, constant.ContextKeyPromptTokens, tkm)
  347. return tkm, nil
  348. }
  349. func CountTokenClaudeRequest(request dto.ClaudeRequest, model string) (int, error) {
  350. tkm := 0
  351. // Count tokens in messages
  352. msgTokens, err := CountTokenClaudeMessages(request.Messages, model, request.Stream)
  353. if err != nil {
  354. return 0, err
  355. }
  356. tkm += msgTokens
  357. // Count tokens in system message
  358. if request.System != "" {
  359. systemTokens := CountTokenInput(request.System, model)
  360. tkm += systemTokens
  361. }
  362. if request.Tools != nil {
  363. // check is array
  364. if tools, ok := request.Tools.([]any); ok {
  365. if len(tools) > 0 {
  366. parsedTools, err1 := common.Any2Type[[]dto.Tool](request.Tools)
  367. if err1 != nil {
  368. return 0, fmt.Errorf("tools: Input should be a valid list: %v", err)
  369. }
  370. toolTokens, err2 := CountTokenClaudeTools(parsedTools, model)
  371. if err2 != nil {
  372. return 0, fmt.Errorf("tools: %v", err)
  373. }
  374. tkm += toolTokens
  375. }
  376. } else {
  377. return 0, errors.New("tools: Input should be a valid list")
  378. }
  379. }
  380. return tkm, nil
  381. }
  382. func CountTokenClaudeMessages(messages []dto.ClaudeMessage, model string, stream bool) (int, error) {
  383. tokenEncoder := getTokenEncoder(model)
  384. tokenNum := 0
  385. for _, message := range messages {
  386. // Count tokens for role
  387. tokenNum += getTokenNum(tokenEncoder, message.Role)
  388. if message.IsStringContent() {
  389. tokenNum += getTokenNum(tokenEncoder, message.GetStringContent())
  390. } else {
  391. content, err := message.ParseContent()
  392. if err != nil {
  393. return 0, err
  394. }
  395. for _, mediaMessage := range content {
  396. switch mediaMessage.Type {
  397. case "text":
  398. tokenNum += getTokenNum(tokenEncoder, mediaMessage.GetText())
  399. case "image":
  400. //imageTokenNum, err := getClaudeImageToken(mediaMsg.Source, model, stream)
  401. //if err != nil {
  402. // return 0, err
  403. //}
  404. tokenNum += 1000
  405. case "tool_use":
  406. if mediaMessage.Input != nil {
  407. tokenNum += getTokenNum(tokenEncoder, mediaMessage.Name)
  408. inputJSON, _ := json.Marshal(mediaMessage.Input)
  409. tokenNum += getTokenNum(tokenEncoder, string(inputJSON))
  410. }
  411. case "tool_result":
  412. if mediaMessage.Content != nil {
  413. contentJSON, _ := json.Marshal(mediaMessage.Content)
  414. tokenNum += getTokenNum(tokenEncoder, string(contentJSON))
  415. }
  416. }
  417. }
  418. }
  419. }
  420. // Add a constant for message formatting (this may need adjustment based on Claude's exact formatting)
  421. tokenNum += len(messages) * 2 // Assuming 2 tokens per message for formatting
  422. return tokenNum, nil
  423. }
  424. func CountTokenClaudeTools(tools []dto.Tool, model string) (int, error) {
  425. tokenEncoder := getTokenEncoder(model)
  426. tokenNum := 0
  427. for _, tool := range tools {
  428. tokenNum += getTokenNum(tokenEncoder, tool.Name)
  429. tokenNum += getTokenNum(tokenEncoder, tool.Description)
  430. schemaJSON, err := json.Marshal(tool.InputSchema)
  431. if err != nil {
  432. return 0, errors.New(fmt.Sprintf("marshal_tool_schema_fail: %s", err.Error()))
  433. }
  434. tokenNum += getTokenNum(tokenEncoder, string(schemaJSON))
  435. }
  436. // Add a constant for tool formatting (this may need adjustment based on Claude's exact formatting)
  437. tokenNum += len(tools) * 3 // Assuming 3 tokens per tool for formatting
  438. return tokenNum, nil
  439. }
  440. func CountTokenRealtime(info *relaycommon.RelayInfo, request dto.RealtimeEvent, model string) (int, int, error) {
  441. audioToken := 0
  442. textToken := 0
  443. switch request.Type {
  444. case dto.RealtimeEventTypeSessionUpdate:
  445. if request.Session != nil {
  446. msgTokens := CountTextToken(request.Session.Instructions, model)
  447. textToken += msgTokens
  448. }
  449. case dto.RealtimeEventResponseAudioDelta:
  450. // count audio token
  451. atk, err := CountAudioTokenOutput(request.Delta, info.OutputAudioFormat)
  452. if err != nil {
  453. return 0, 0, fmt.Errorf("error counting audio token: %v", err)
  454. }
  455. audioToken += atk
  456. case dto.RealtimeEventResponseAudioTranscriptionDelta, dto.RealtimeEventResponseFunctionCallArgumentsDelta:
  457. // count text token
  458. tkm := CountTextToken(request.Delta, model)
  459. textToken += tkm
  460. case dto.RealtimeEventInputAudioBufferAppend:
  461. // count audio token
  462. atk, err := CountAudioTokenInput(request.Audio, info.InputAudioFormat)
  463. if err != nil {
  464. return 0, 0, fmt.Errorf("error counting audio token: %v", err)
  465. }
  466. audioToken += atk
  467. case dto.RealtimeEventConversationItemCreated:
  468. if request.Item != nil {
  469. switch request.Item.Type {
  470. case "message":
  471. for _, content := range request.Item.Content {
  472. if content.Type == "input_text" {
  473. tokens := CountTextToken(content.Text, model)
  474. textToken += tokens
  475. }
  476. }
  477. }
  478. }
  479. case dto.RealtimeEventTypeResponseDone:
  480. // count tools token
  481. if !info.IsFirstRequest {
  482. if info.RealtimeTools != nil && len(info.RealtimeTools) > 0 {
  483. for _, tool := range info.RealtimeTools {
  484. toolTokens := CountTokenInput(tool, model)
  485. textToken += 8
  486. textToken += toolTokens
  487. }
  488. }
  489. }
  490. }
  491. return textToken, audioToken, nil
  492. }
  493. func CountTokenInput(input any, model string) int {
  494. switch v := input.(type) {
  495. case string:
  496. return CountTextToken(v, model)
  497. case []string:
  498. text := ""
  499. for _, s := range v {
  500. text += s
  501. }
  502. return CountTextToken(text, model)
  503. case []interface{}:
  504. text := ""
  505. for _, item := range v {
  506. text += fmt.Sprintf("%v", item)
  507. }
  508. return CountTextToken(text, model)
  509. }
  510. return CountTokenInput(fmt.Sprintf("%v", input), model)
  511. }
  512. func CountTokenStreamChoices(messages []dto.ChatCompletionsStreamResponseChoice, model string) int {
  513. tokens := 0
  514. for _, message := range messages {
  515. tkm := CountTokenInput(message.Delta.GetContentString(), model)
  516. tokens += tkm
  517. if message.Delta.ToolCalls != nil {
  518. for _, tool := range message.Delta.ToolCalls {
  519. tkm := CountTokenInput(tool.Function.Name, model)
  520. tokens += tkm
  521. tkm = CountTokenInput(tool.Function.Arguments, model)
  522. tokens += tkm
  523. }
  524. }
  525. }
  526. return tokens
  527. }
  528. func CountTTSToken(text string, model string) int {
  529. if strings.HasPrefix(model, "tts") {
  530. return utf8.RuneCountInString(text)
  531. } else {
  532. return CountTextToken(text, model)
  533. }
  534. }
  535. func CountAudioTokenInput(audioBase64 string, audioFormat string) (int, error) {
  536. if audioBase64 == "" {
  537. return 0, nil
  538. }
  539. duration, err := parseAudio(audioBase64, audioFormat)
  540. if err != nil {
  541. return 0, err
  542. }
  543. return int(duration / 60 * 100 / 0.06), nil
  544. }
  545. func CountAudioTokenOutput(audioBase64 string, audioFormat string) (int, error) {
  546. if audioBase64 == "" {
  547. return 0, nil
  548. }
  549. duration, err := parseAudio(audioBase64, audioFormat)
  550. if err != nil {
  551. return 0, err
  552. }
  553. return int(duration / 60 * 200 / 0.24), nil
  554. }
  555. //func CountAudioToken(sec float64, audioType string) {
  556. // if audioType == "input" {
  557. //
  558. // }
  559. //}
  560. // CountTextToken 统计文本的token数量,仅当文本包含敏感词,返回错误,同时返回token数量
  561. func CountTextToken(text string, model string) int {
  562. if text == "" {
  563. return 0
  564. }
  565. tokenEncoder := getTokenEncoder(model)
  566. return getTokenNum(tokenEncoder, text)
  567. }