price.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. package helper
  2. import (
  3. "fmt"
  4. "strings"
  5. "github.com/QuantumNous/new-api/common"
  6. "github.com/QuantumNous/new-api/logger"
  7. "github.com/QuantumNous/new-api/model"
  8. "github.com/QuantumNous/new-api/pkg/billingexpr"
  9. relaycommon "github.com/QuantumNous/new-api/relay/common"
  10. "github.com/QuantumNous/new-api/setting/billing_setting"
  11. "github.com/QuantumNous/new-api/setting/operation_setting"
  12. "github.com/QuantumNous/new-api/setting/ratio_setting"
  13. "github.com/QuantumNous/new-api/types"
  14. "github.com/gin-gonic/gin"
  15. )
  16. func modelPriceNotConfiguredError(modelName string, userId int) error {
  17. if model.IsAdmin(userId) {
  18. return fmt.Errorf(
  19. "模型 %s 的价格未配置。请前往「系统设置 → 运营设置」开启自用模式,或在「系统设置 → 分组与模型定价设置」中为该模型配置价格;"+
  20. "Model %s price not configured. Go to System Settings → Operation Settings to enable self-use mode, or configure the model price in System Settings → Group & Model Pricing.",
  21. modelName, modelName,
  22. )
  23. }
  24. return fmt.Errorf(
  25. "模型 %s 的价格尚未由管理员配置,暂时无法使用,请联系站点管理员开启该模型;"+
  26. "Model %s has not been priced by the administrator yet. Please contact the site administrator to enable this model.",
  27. modelName, modelName,
  28. )
  29. }
  30. // https://docs.claude.com/en/docs/build-with-claude/prompt-caching#1-hour-cache-duration
  31. const claudeCacheCreation1hMultiplier = 6 / 3.75
  32. // HandleGroupRatio checks for "auto_group" in the context and updates the group ratio and relayInfo.UsingGroup if present
  33. func HandleGroupRatio(ctx *gin.Context, relayInfo *relaycommon.RelayInfo) types.GroupRatioInfo {
  34. groupRatioInfo := types.GroupRatioInfo{
  35. GroupRatio: 1.0, // default ratio
  36. GroupSpecialRatio: -1,
  37. }
  38. // check auto group
  39. autoGroup, exists := ctx.Get("auto_group")
  40. if exists {
  41. logger.LogDebug(ctx, fmt.Sprintf("final group: %s", autoGroup))
  42. relayInfo.UsingGroup = autoGroup.(string)
  43. }
  44. // check user group special ratio
  45. userGroupRatio, ok := ratio_setting.GetGroupGroupRatio(relayInfo.UserGroup, relayInfo.UsingGroup)
  46. if ok {
  47. // user group special ratio
  48. groupRatioInfo.GroupSpecialRatio = userGroupRatio
  49. groupRatioInfo.GroupRatio = userGroupRatio
  50. groupRatioInfo.HasSpecialRatio = true
  51. } else {
  52. // normal group ratio
  53. groupRatioInfo.GroupRatio = ratio_setting.GetGroupRatio(relayInfo.UsingGroup)
  54. }
  55. return groupRatioInfo
  56. }
  57. func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens int, meta *types.TokenCountMeta) (types.PriceData, error) {
  58. modelPrice, usePrice := ratio_setting.GetModelPrice(info.OriginModelName, false)
  59. groupRatioInfo := HandleGroupRatio(c, info)
  60. // Check if this model uses tiered_expr billing
  61. if billing_setting.GetBillingMode(info.OriginModelName) == billing_setting.BillingModeTieredExpr {
  62. return modelPriceHelperTiered(c, info, promptTokens, meta, groupRatioInfo)
  63. }
  64. var preConsumedQuota int
  65. var modelRatio float64
  66. var completionRatio float64
  67. var cacheRatio float64
  68. var imageRatio float64
  69. var cacheCreationRatio float64
  70. var cacheCreationRatio5m float64
  71. var cacheCreationRatio1h float64
  72. var audioRatio float64
  73. var audioCompletionRatio float64
  74. var freeModel bool
  75. if !usePrice {
  76. preConsumedTokens := common.Max(promptTokens, common.PreConsumedQuota)
  77. if meta.MaxTokens != 0 {
  78. preConsumedTokens += meta.MaxTokens
  79. }
  80. var success bool
  81. var matchName string
  82. modelRatio, success, matchName = ratio_setting.GetModelRatio(info.OriginModelName)
  83. if !success {
  84. acceptUnsetRatio := false
  85. if info.UserSetting.AcceptUnsetRatioModel {
  86. acceptUnsetRatio = true
  87. }
  88. if !acceptUnsetRatio {
  89. return types.PriceData{}, modelPriceNotConfiguredError(matchName, info.UserId)
  90. }
  91. }
  92. completionRatio = ratio_setting.GetCompletionRatio(info.OriginModelName)
  93. cacheRatio, _ = ratio_setting.GetCacheRatio(info.OriginModelName)
  94. cacheCreationRatio, _ = ratio_setting.GetCreateCacheRatio(info.OriginModelName)
  95. cacheCreationRatio5m = cacheCreationRatio
  96. // 固定1h和5min缓存写入价格的比例
  97. cacheCreationRatio1h = cacheCreationRatio * claudeCacheCreation1hMultiplier
  98. imageRatio, _ = ratio_setting.GetImageRatio(info.OriginModelName)
  99. audioRatio = ratio_setting.GetAudioRatio(info.OriginModelName)
  100. audioCompletionRatio = ratio_setting.GetAudioCompletionRatio(info.OriginModelName)
  101. ratio := modelRatio * groupRatioInfo.GroupRatio
  102. preConsumedQuota = int(float64(preConsumedTokens) * ratio)
  103. } else {
  104. if meta.ImagePriceRatio != 0 {
  105. modelPrice = modelPrice * meta.ImagePriceRatio
  106. }
  107. preConsumedQuota = int(modelPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio)
  108. }
  109. // check if free model pre-consume is disabled
  110. if !operation_setting.GetQuotaSetting().EnableFreeModelPreConsume {
  111. // if model price or ratio is 0, do not pre-consume quota
  112. if groupRatioInfo.GroupRatio == 0 {
  113. preConsumedQuota = 0
  114. freeModel = true
  115. } else if usePrice {
  116. if modelPrice == 0 {
  117. preConsumedQuota = 0
  118. freeModel = true
  119. }
  120. } else {
  121. if modelRatio == 0 {
  122. preConsumedQuota = 0
  123. freeModel = true
  124. }
  125. }
  126. }
  127. priceData := types.PriceData{
  128. FreeModel: freeModel,
  129. ModelPrice: modelPrice,
  130. ModelRatio: modelRatio,
  131. CompletionRatio: completionRatio,
  132. GroupRatioInfo: groupRatioInfo,
  133. UsePrice: usePrice,
  134. CacheRatio: cacheRatio,
  135. ImageRatio: imageRatio,
  136. AudioRatio: audioRatio,
  137. AudioCompletionRatio: audioCompletionRatio,
  138. CacheCreationRatio: cacheCreationRatio,
  139. CacheCreation5mRatio: cacheCreationRatio5m,
  140. CacheCreation1hRatio: cacheCreationRatio1h,
  141. QuotaToPreConsume: preConsumedQuota,
  142. }
  143. if common.DebugEnabled {
  144. println(fmt.Sprintf("model_price_helper result: %s", priceData.ToSetting()))
  145. }
  146. info.PriceData = priceData
  147. return priceData, nil
  148. }
  149. // ModelPriceHelperPerCall 按次/按量计费的 PriceHelper (MJ、Task)
  150. func ModelPriceHelperPerCall(c *gin.Context, info *relaycommon.RelayInfo) (types.PriceData, error) {
  151. groupRatioInfo := HandleGroupRatio(c, info)
  152. modelPrice, success := ratio_setting.GetModelPrice(info.OriginModelName, true)
  153. usePrice := success
  154. var modelRatio float64
  155. if !success {
  156. defaultPrice, ok := ratio_setting.GetDefaultModelPriceMap()[info.OriginModelName]
  157. if ok {
  158. modelPrice = defaultPrice
  159. usePrice = true
  160. } else {
  161. var ratioSuccess bool
  162. var matchName string
  163. modelRatio, ratioSuccess, matchName = ratio_setting.GetModelRatio(info.OriginModelName)
  164. acceptUnsetRatio := false
  165. if info.UserSetting.AcceptUnsetRatioModel {
  166. acceptUnsetRatio = true
  167. }
  168. if !ratioSuccess && !acceptUnsetRatio {
  169. return types.PriceData{}, modelPriceNotConfiguredError(matchName, info.UserId)
  170. }
  171. }
  172. }
  173. var quota int
  174. freeModel := false
  175. if usePrice {
  176. quota = int(modelPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio)
  177. if !operation_setting.GetQuotaSetting().EnableFreeModelPreConsume {
  178. if groupRatioInfo.GroupRatio == 0 || modelPrice == 0 {
  179. quota = 0
  180. freeModel = true
  181. }
  182. }
  183. } else {
  184. // 按量计费:以模型倍率的一半作为预扣额度
  185. quota = int(modelRatio / 2 * common.QuotaPerUnit * groupRatioInfo.GroupRatio)
  186. modelPrice = -1
  187. if !operation_setting.GetQuotaSetting().EnableFreeModelPreConsume {
  188. if groupRatioInfo.GroupRatio == 0 || modelRatio == 0 {
  189. quota = 0
  190. freeModel = true
  191. }
  192. }
  193. }
  194. priceData := types.PriceData{
  195. FreeModel: freeModel,
  196. ModelPrice: modelPrice,
  197. ModelRatio: modelRatio,
  198. UsePrice: usePrice,
  199. Quota: quota,
  200. GroupRatioInfo: groupRatioInfo,
  201. }
  202. return priceData, nil
  203. }
  204. func HasModelBillingConfig(modelName string) bool {
  205. if _, ok := ratio_setting.GetModelPrice(modelName, false); ok {
  206. return true
  207. }
  208. if _, ok, _ := ratio_setting.GetModelRatio(modelName); ok {
  209. return true
  210. }
  211. if billing_setting.GetBillingMode(modelName) != billing_setting.BillingModeTieredExpr {
  212. return false
  213. }
  214. expr, ok := billing_setting.GetBillingExpr(modelName)
  215. return ok && strings.TrimSpace(expr) != ""
  216. }
  217. func modelPriceHelperTiered(c *gin.Context, info *relaycommon.RelayInfo, promptTokens int, meta *types.TokenCountMeta, groupRatioInfo types.GroupRatioInfo) (types.PriceData, error) {
  218. exprStr, ok := billing_setting.GetBillingExpr(info.OriginModelName)
  219. if !ok {
  220. return types.PriceData{}, fmt.Errorf("model %s is configured as tiered_expr but has no billing expression", info.OriginModelName)
  221. }
  222. estimatedCompletionTokens := 0
  223. if meta.MaxTokens != 0 {
  224. estimatedCompletionTokens = meta.MaxTokens
  225. }
  226. requestInput, err := ResolveIncomingBillingExprRequestInput(c, info)
  227. if err != nil {
  228. return types.PriceData{}, err
  229. }
  230. rawCost, trace, err := billingexpr.RunExprWithRequest(exprStr, billingexpr.TokenParams{
  231. P: float64(promptTokens),
  232. C: float64(estimatedCompletionTokens),
  233. Len: float64(promptTokens),
  234. }, requestInput)
  235. if err != nil {
  236. return types.PriceData{}, fmt.Errorf("model %s tiered expr run failed: %w", info.OriginModelName, err)
  237. }
  238. // Expression coefficients are $/1M tokens prices; convert to quota the same way per-call billing does.
  239. quotaBeforeGroup := rawCost / 1_000_000 * common.QuotaPerUnit
  240. preConsumedQuota := billingexpr.QuotaRound(quotaBeforeGroup * groupRatioInfo.GroupRatio)
  241. freeModel := false
  242. if !operation_setting.GetQuotaSetting().EnableFreeModelPreConsume {
  243. if groupRatioInfo.GroupRatio == 0 {
  244. preConsumedQuota = 0
  245. freeModel = true
  246. }
  247. }
  248. exprHash := billingexpr.ExprHashString(exprStr)
  249. snapshot := &billingexpr.BillingSnapshot{
  250. BillingMode: billing_setting.BillingModeTieredExpr,
  251. ModelName: info.OriginModelName,
  252. ExprString: exprStr,
  253. ExprHash: exprHash,
  254. GroupRatio: groupRatioInfo.GroupRatio,
  255. EstimatedPromptTokens: promptTokens,
  256. EstimatedCompletionTokens: estimatedCompletionTokens,
  257. EstimatedQuotaBeforeGroup: quotaBeforeGroup,
  258. EstimatedQuotaAfterGroup: preConsumedQuota,
  259. EstimatedTier: trace.MatchedTier,
  260. QuotaPerUnit: common.QuotaPerUnit,
  261. ExprVersion: billingexpr.ExprVersion(exprStr),
  262. }
  263. info.TieredBillingSnapshot = snapshot
  264. info.BillingRequestInput = &requestInput
  265. priceData := types.PriceData{
  266. FreeModel: freeModel,
  267. GroupRatioInfo: groupRatioInfo,
  268. QuotaToPreConsume: preConsumedQuota,
  269. }
  270. if common.DebugEnabled {
  271. println(fmt.Sprintf("model_price_helper_tiered result: model=%s preConsume=%d quotaBeforeGroup=%.2f groupRatio=%.2f tier=%s", info.OriginModelName, preConsumedQuota, quotaBeforeGroup, groupRatioInfo.GroupRatio, trace.MatchedTier))
  272. }
  273. info.PriceData = priceData
  274. return priceData, nil
  275. }