token_counter.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  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. // Whether to count image tokens at all
  127. if !constant.GetMediaToken {
  128. return 3 * baseTokens, nil
  129. }
  130. if !constant.GetMediaTokenNotStream && !stream {
  131. return 3 * baseTokens, nil
  132. }
  133. // Normalize detail
  134. if fileMeta.Detail == "auto" || fileMeta.Detail == "" {
  135. fileMeta.Detail = "high"
  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. // 是否统计token
  228. if !constant.CountToken {
  229. return 0, nil
  230. }
  231. if meta == nil {
  232. return 0, errors.New("token count meta is nil")
  233. }
  234. if info.RelayFormat == types.RelayFormatOpenAIRealtime {
  235. return 0, nil
  236. }
  237. if info.RelayMode == constant2.RelayModeAudioTranscription || info.RelayMode == constant2.RelayModeAudioTranslation {
  238. multiForm, err := common.ParseMultipartFormReusable(c)
  239. if err != nil {
  240. return 0, fmt.Errorf("error parsing multipart form: %v", err)
  241. }
  242. fileHeaders := multiForm.File["file"]
  243. totalAudioToken := 0
  244. for _, fileHeader := range fileHeaders {
  245. file, err := fileHeader.Open()
  246. if err != nil {
  247. return 0, fmt.Errorf("error opening audio file: %v", err)
  248. }
  249. defer file.Close()
  250. // get ext and io.seeker
  251. ext := filepath.Ext(fileHeader.Filename)
  252. duration, err := common.GetAudioDuration(c.Request.Context(), file, ext)
  253. if err != nil {
  254. return 0, fmt.Errorf("error getting audio duration: %v", err)
  255. }
  256. // 一分钟 1000 token,与 $price / minute 对齐
  257. totalAudioToken += int(math.Round(math.Ceil(duration) / 60.0 * 1000))
  258. }
  259. return totalAudioToken, nil
  260. }
  261. model := common.GetContextKeyString(c, constant.ContextKeyOriginalModel)
  262. tkm := 0
  263. if meta.TokenType == types.TokenTypeTextNumber {
  264. tkm += utf8.RuneCountInString(meta.CombineText)
  265. } else {
  266. tkm += CountTextToken(meta.CombineText, model)
  267. }
  268. if info.RelayFormat == types.RelayFormatOpenAI {
  269. tkm += meta.ToolsCount * 8
  270. tkm += meta.MessagesCount * 3 // 每条消息的格式化token数量
  271. tkm += meta.NameCount * 3
  272. tkm += 3
  273. }
  274. shouldFetchFiles := true
  275. if info.RelayFormat == types.RelayFormatGemini {
  276. shouldFetchFiles = false
  277. }
  278. // 是否本地计算媒体token数量
  279. if !constant.GetMediaToken {
  280. shouldFetchFiles = false
  281. }
  282. // 是否在非流模式下本地计算媒体token数量
  283. if !constant.GetMediaTokenNotStream && !info.IsStream {
  284. shouldFetchFiles = false
  285. }
  286. for _, file := range meta.Files {
  287. if strings.HasPrefix(file.OriginData, "http") {
  288. if shouldFetchFiles {
  289. mineType, err := GetFileTypeFromUrl(c, file.OriginData, "token_counter")
  290. if err != nil {
  291. return 0, fmt.Errorf("error getting file base64 from url: %v", err)
  292. }
  293. if strings.HasPrefix(mineType, "image/") {
  294. file.FileType = types.FileTypeImage
  295. } else if strings.HasPrefix(mineType, "video/") {
  296. file.FileType = types.FileTypeVideo
  297. } else if strings.HasPrefix(mineType, "audio/") {
  298. file.FileType = types.FileTypeAudio
  299. } else {
  300. file.FileType = types.FileTypeFile
  301. }
  302. file.MimeType = mineType
  303. }
  304. } else if strings.HasPrefix(file.OriginData, "data:") {
  305. // get mime type from base64 header
  306. parts := strings.SplitN(file.OriginData, ",", 2)
  307. if len(parts) >= 1 {
  308. header := parts[0]
  309. // Extract mime type from "data:mime/type;base64" format
  310. if strings.Contains(header, ":") && strings.Contains(header, ";") {
  311. mimeStart := strings.Index(header, ":") + 1
  312. mimeEnd := strings.Index(header, ";")
  313. if mimeStart < mimeEnd {
  314. mineType := header[mimeStart:mimeEnd]
  315. if strings.HasPrefix(mineType, "image/") {
  316. file.FileType = types.FileTypeImage
  317. } else if strings.HasPrefix(mineType, "video/") {
  318. file.FileType = types.FileTypeVideo
  319. } else if strings.HasPrefix(mineType, "audio/") {
  320. file.FileType = types.FileTypeAudio
  321. } else {
  322. file.FileType = types.FileTypeFile
  323. }
  324. file.MimeType = mineType
  325. }
  326. }
  327. }
  328. }
  329. }
  330. for i, file := range meta.Files {
  331. switch file.FileType {
  332. case types.FileTypeImage:
  333. if info.RelayFormat == types.RelayFormatGemini {
  334. tkm += 520 // gemini per input image tokens
  335. } else {
  336. token, err := getImageToken(file, model, info.IsStream)
  337. if err != nil {
  338. return 0, fmt.Errorf("error counting image token, media index[%d], original data[%s], err: %v", i, file.OriginData, err)
  339. }
  340. tkm += token
  341. }
  342. case types.FileTypeAudio:
  343. tkm += 256
  344. case types.FileTypeVideo:
  345. tkm += 4096 * 2
  346. case types.FileTypeFile:
  347. tkm += 4096
  348. default:
  349. tkm += 4096 // Default case for unknown file types
  350. }
  351. }
  352. common.SetContextKey(c, constant.ContextKeyPromptTokens, tkm)
  353. return tkm, nil
  354. }
  355. func CountTokenClaudeRequest(request dto.ClaudeRequest, model string) (int, error) {
  356. tkm := 0
  357. // Count tokens in messages
  358. msgTokens, err := CountTokenClaudeMessages(request.Messages, model, request.Stream)
  359. if err != nil {
  360. return 0, err
  361. }
  362. tkm += msgTokens
  363. // Count tokens in system message
  364. if request.System != "" {
  365. systemTokens := CountTokenInput(request.System, model)
  366. tkm += systemTokens
  367. }
  368. if request.Tools != nil {
  369. // check is array
  370. if tools, ok := request.Tools.([]any); ok {
  371. if len(tools) > 0 {
  372. parsedTools, err1 := common.Any2Type[[]dto.Tool](request.Tools)
  373. if err1 != nil {
  374. return 0, fmt.Errorf("tools: Input should be a valid list: %v", err)
  375. }
  376. toolTokens, err2 := CountTokenClaudeTools(parsedTools, model)
  377. if err2 != nil {
  378. return 0, fmt.Errorf("tools: %v", err)
  379. }
  380. tkm += toolTokens
  381. }
  382. } else {
  383. return 0, errors.New("tools: Input should be a valid list")
  384. }
  385. }
  386. return tkm, nil
  387. }
  388. func CountTokenClaudeMessages(messages []dto.ClaudeMessage, model string, stream bool) (int, error) {
  389. tokenEncoder := getTokenEncoder(model)
  390. tokenNum := 0
  391. for _, message := range messages {
  392. // Count tokens for role
  393. tokenNum += getTokenNum(tokenEncoder, message.Role)
  394. if message.IsStringContent() {
  395. tokenNum += getTokenNum(tokenEncoder, message.GetStringContent())
  396. } else {
  397. content, err := message.ParseContent()
  398. if err != nil {
  399. return 0, err
  400. }
  401. for _, mediaMessage := range content {
  402. switch mediaMessage.Type {
  403. case "text":
  404. tokenNum += getTokenNum(tokenEncoder, mediaMessage.GetText())
  405. case "image":
  406. //imageTokenNum, err := getClaudeImageToken(mediaMsg.Source, model, stream)
  407. //if err != nil {
  408. // return 0, err
  409. //}
  410. tokenNum += 1000
  411. case "tool_use":
  412. if mediaMessage.Input != nil {
  413. tokenNum += getTokenNum(tokenEncoder, mediaMessage.Name)
  414. inputJSON, _ := json.Marshal(mediaMessage.Input)
  415. tokenNum += getTokenNum(tokenEncoder, string(inputJSON))
  416. }
  417. case "tool_result":
  418. if mediaMessage.Content != nil {
  419. contentJSON, _ := json.Marshal(mediaMessage.Content)
  420. tokenNum += getTokenNum(tokenEncoder, string(contentJSON))
  421. }
  422. }
  423. }
  424. }
  425. }
  426. // Add a constant for message formatting (this may need adjustment based on Claude's exact formatting)
  427. tokenNum += len(messages) * 2 // Assuming 2 tokens per message for formatting
  428. return tokenNum, nil
  429. }
  430. func CountTokenClaudeTools(tools []dto.Tool, model string) (int, error) {
  431. tokenEncoder := getTokenEncoder(model)
  432. tokenNum := 0
  433. for _, tool := range tools {
  434. tokenNum += getTokenNum(tokenEncoder, tool.Name)
  435. tokenNum += getTokenNum(tokenEncoder, tool.Description)
  436. schemaJSON, err := json.Marshal(tool.InputSchema)
  437. if err != nil {
  438. return 0, errors.New(fmt.Sprintf("marshal_tool_schema_fail: %s", err.Error()))
  439. }
  440. tokenNum += getTokenNum(tokenEncoder, string(schemaJSON))
  441. }
  442. // Add a constant for tool formatting (this may need adjustment based on Claude's exact formatting)
  443. tokenNum += len(tools) * 3 // Assuming 3 tokens per tool for formatting
  444. return tokenNum, nil
  445. }
  446. func CountTokenRealtime(info *relaycommon.RelayInfo, request dto.RealtimeEvent, model string) (int, int, error) {
  447. audioToken := 0
  448. textToken := 0
  449. switch request.Type {
  450. case dto.RealtimeEventTypeSessionUpdate:
  451. if request.Session != nil {
  452. msgTokens := CountTextToken(request.Session.Instructions, model)
  453. textToken += msgTokens
  454. }
  455. case dto.RealtimeEventResponseAudioDelta:
  456. // count audio token
  457. atk, err := CountAudioTokenOutput(request.Delta, info.OutputAudioFormat)
  458. if err != nil {
  459. return 0, 0, fmt.Errorf("error counting audio token: %v", err)
  460. }
  461. audioToken += atk
  462. case dto.RealtimeEventResponseAudioTranscriptionDelta, dto.RealtimeEventResponseFunctionCallArgumentsDelta:
  463. // count text token
  464. tkm := CountTextToken(request.Delta, model)
  465. textToken += tkm
  466. case dto.RealtimeEventInputAudioBufferAppend:
  467. // count audio token
  468. atk, err := CountAudioTokenInput(request.Audio, info.InputAudioFormat)
  469. if err != nil {
  470. return 0, 0, fmt.Errorf("error counting audio token: %v", err)
  471. }
  472. audioToken += atk
  473. case dto.RealtimeEventConversationItemCreated:
  474. if request.Item != nil {
  475. switch request.Item.Type {
  476. case "message":
  477. for _, content := range request.Item.Content {
  478. if content.Type == "input_text" {
  479. tokens := CountTextToken(content.Text, model)
  480. textToken += tokens
  481. }
  482. }
  483. }
  484. }
  485. case dto.RealtimeEventTypeResponseDone:
  486. // count tools token
  487. if !info.IsFirstRequest {
  488. if info.RealtimeTools != nil && len(info.RealtimeTools) > 0 {
  489. for _, tool := range info.RealtimeTools {
  490. toolTokens := CountTokenInput(tool, model)
  491. textToken += 8
  492. textToken += toolTokens
  493. }
  494. }
  495. }
  496. }
  497. return textToken, audioToken, nil
  498. }
  499. func CountTokenInput(input any, model string) int {
  500. switch v := input.(type) {
  501. case string:
  502. return CountTextToken(v, model)
  503. case []string:
  504. text := ""
  505. for _, s := range v {
  506. text += s
  507. }
  508. return CountTextToken(text, model)
  509. case []interface{}:
  510. text := ""
  511. for _, item := range v {
  512. text += fmt.Sprintf("%v", item)
  513. }
  514. return CountTextToken(text, model)
  515. }
  516. return CountTokenInput(fmt.Sprintf("%v", input), model)
  517. }
  518. func CountTokenStreamChoices(messages []dto.ChatCompletionsStreamResponseChoice, model string) int {
  519. tokens := 0
  520. for _, message := range messages {
  521. tkm := CountTokenInput(message.Delta.GetContentString(), model)
  522. tokens += tkm
  523. if message.Delta.ToolCalls != nil {
  524. for _, tool := range message.Delta.ToolCalls {
  525. tkm := CountTokenInput(tool.Function.Name, model)
  526. tokens += tkm
  527. tkm = CountTokenInput(tool.Function.Arguments, model)
  528. tokens += tkm
  529. }
  530. }
  531. }
  532. return tokens
  533. }
  534. func CountTTSToken(text string, model string) int {
  535. if strings.HasPrefix(model, "tts") {
  536. return utf8.RuneCountInString(text)
  537. } else {
  538. return CountTextToken(text, model)
  539. }
  540. }
  541. func CountAudioTokenInput(audioBase64 string, audioFormat string) (int, error) {
  542. if audioBase64 == "" {
  543. return 0, nil
  544. }
  545. duration, err := parseAudio(audioBase64, audioFormat)
  546. if err != nil {
  547. return 0, err
  548. }
  549. return int(duration / 60 * 100 / 0.06), nil
  550. }
  551. func CountAudioTokenOutput(audioBase64 string, audioFormat string) (int, error) {
  552. if audioBase64 == "" {
  553. return 0, nil
  554. }
  555. duration, err := parseAudio(audioBase64, audioFormat)
  556. if err != nil {
  557. return 0, err
  558. }
  559. return int(duration / 60 * 200 / 0.24), nil
  560. }
  561. //func CountAudioToken(sec float64, audioType string) {
  562. // if audioType == "input" {
  563. //
  564. // }
  565. //}
  566. // CountTextToken 统计文本的token数量,仅当文本包含敏感词,返回错误,同时返回token数量
  567. func CountTextToken(text string, model string) int {
  568. if text == "" {
  569. return 0
  570. }
  571. tokenEncoder := getTokenEncoder(model)
  572. return getTokenNum(tokenEncoder, text)
  573. }