Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 

288 строки
8.4 KiB

  1. package helper
  2. import (
  3. "fmt"
  4. "net/http"
  5. "github.com/QuantumNous/new-api/common"
  6. "github.com/QuantumNous/new-api/dto"
  7. "github.com/QuantumNous/new-api/types"
  8. )
  9. type pricingLookupResult struct {
  10. Mode types.PricingMode
  11. PriceUSD float64
  12. Snapshot map[string]any
  13. }
  14. func lookupPricingConfig(cfg *types.PricingConfig, resolved *ResolvedDimensionValues) (*pricingLookupResult, *dto.TaskError) {
  15. if cfg == nil {
  16. return nil, localPricingTaskError("pricing config is required", "pricing_config_required")
  17. }
  18. var best types.PricingRow
  19. bestWildcardCount := len(cfg.Dimensions) + 1
  20. bestCount := 0
  21. for _, row := range cfg.Table {
  22. wildcards, ok := rowMatchesDimensions(cfg.Dimensions, row, resolved.Values)
  23. if !ok {
  24. continue
  25. }
  26. if wildcards < bestWildcardCount {
  27. best = row
  28. bestWildcardCount = wildcards
  29. bestCount = 1
  30. continue
  31. }
  32. if wildcards == bestWildcardCount {
  33. bestCount++
  34. }
  35. }
  36. if best != nil && bestCount == 1 {
  37. price, _ := pricingNumberValue(best["price"])
  38. return &pricingLookupResult{
  39. Mode: types.PricingModeMatched,
  40. PriceUSD: price,
  41. Snapshot: buildPricingSnapshot(cfg, resolved, price, types.PricingModeMatched, ""),
  42. }, nil
  43. }
  44. if bestCount > 1 {
  45. return fallbackPricingConfig(cfg, resolved)
  46. }
  47. return fallbackPricingConfig(cfg, resolved)
  48. }
  49. func buildMatchedDecision(cfg *types.PricingConfig, result *pricingLookupResult, group types.GroupRatioInfo) *types.PricingDecision {
  50. return buildMatrixDecision(cfg, result, group)
  51. }
  52. func buildFallbackDecision(cfg *types.PricingConfig, result *pricingLookupResult, group types.GroupRatioInfo) *types.PricingDecision {
  53. return buildMatrixDecision(cfg, result, group)
  54. }
  55. func buildLegacyDecision(modelPrice float64, group types.GroupRatioInfo, free bool, quota int) *types.PricingDecision {
  56. return &types.PricingDecision{
  57. Mode: types.PricingModeLegacy,
  58. PriceUSD: modelPrice,
  59. Quota: quota,
  60. GroupRatioInfo: group,
  61. FreeModel: free,
  62. LegacyModelPrice: modelPrice,
  63. UsePrice: true,
  64. PerCallBilling: true,
  65. Finalized: true,
  66. }
  67. }
  68. func buildRemixMatrixDecision(origin *types.OriginPricingSnapshot, group types.GroupRatioInfo) (*types.PricingDecision, *dto.TaskError) {
  69. if origin == nil {
  70. return nil, localPricingTaskError("origin pricing snapshot is required", "invalid_pricing_snapshot")
  71. }
  72. billingUnit := origin.BillingUnit
  73. if billingUnit == "" {
  74. billingUnit = types.BillingUnitPerCall
  75. }
  76. result := &pricingLookupResult{
  77. Mode: types.PricingModeRemix,
  78. PriceUSD: origin.PriceUSD,
  79. Snapshot: types.CloneMapAny(origin.Snapshot),
  80. }
  81. cfg := &types.PricingConfig{BillingUnit: billingUnit}
  82. decision := buildMatrixDecision(cfg, result, group)
  83. decision.Mode = types.PricingModeRemix
  84. decision.TokenUnitPriceUSD = origin.TokenUnitPriceUSD
  85. decision.OtherRatios = types.CloneRatios(origin.OtherRatios)
  86. decision.PerCallBilling = origin.PerCallBilling
  87. if billingUnit == types.BillingUnitPer1MTokens {
  88. decision.PriceUSD = 0
  89. decision.Quota = minimumUsagePreconsumeQuota(origin.TokenUnitPriceUSD, group.GroupRatio)
  90. }
  91. return decision, nil
  92. }
  93. func buildRemixLegacyDecision(origin *types.OriginPricingSnapshot, group types.GroupRatioInfo) (*types.PricingDecision, *dto.TaskError) {
  94. if origin == nil {
  95. return nil, localPricingTaskError("origin legacy pricing snapshot is required", "invalid_legacy_pricing_snapshot")
  96. }
  97. quota := int(origin.PriceUSD * common.QuotaPerUnit * group.GroupRatio)
  98. return &types.PricingDecision{
  99. Mode: types.PricingModeRemix,
  100. PriceUSD: origin.PriceUSD,
  101. Quota: quota,
  102. GroupRatioInfo: group,
  103. LegacyModelPrice: origin.PriceUSD,
  104. OtherRatios: types.CloneRatios(origin.OtherRatios),
  105. UsePrice: true,
  106. PerCallBilling: origin.PerCallBilling,
  107. Finalized: true,
  108. }, nil
  109. }
  110. func buildMatrixDecision(cfg *types.PricingConfig, result *pricingLookupResult, group types.GroupRatioInfo) *types.PricingDecision {
  111. billingUnit := cfg.BillingUnit
  112. if billingUnit == "" {
  113. billingUnit = types.BillingUnitPerCall
  114. }
  115. decision := &types.PricingDecision{
  116. Mode: result.Mode,
  117. BillingMode: types.BillingModeMatrix,
  118. BillingUnit: billingUnit,
  119. GroupRatioInfo: group,
  120. Snapshot: types.CloneMapAny(result.Snapshot),
  121. UsePrice: true,
  122. Finalized: true,
  123. }
  124. if billingUnit == types.BillingUnitPer1MTokens {
  125. decision.TokenUnitPriceUSD = result.PriceUSD
  126. decision.Quota = minimumUsagePreconsumeQuota(result.PriceUSD, group.GroupRatio)
  127. decision.PerCallBilling = false
  128. return decision
  129. }
  130. decision.PriceUSD = result.PriceUSD
  131. decision.Quota = int(result.PriceUSD * common.QuotaPerUnit * group.GroupRatio)
  132. decision.PerCallBilling = true
  133. if group.GroupRatio == 0 || result.PriceUSD == 0 {
  134. decision.FreeModel = true
  135. decision.Quota = 0
  136. }
  137. return decision
  138. }
  139. func fallbackPricingConfig(cfg *types.PricingConfig, resolved *ResolvedDimensionValues) (*pricingLookupResult, *dto.TaskError) {
  140. switch cfg.Fallback.Strategy {
  141. case types.PricingFallbackReject:
  142. return nil, localPricingTaskError("pricing dimensions did not match any row", "pricing_no_match")
  143. case types.PricingFallbackMax:
  144. price := maxPricingTablePrice(cfg.Table)
  145. return &pricingLookupResult{
  146. Mode: types.PricingModeFallbackMax,
  147. PriceUSD: price,
  148. Snapshot: buildPricingSnapshot(cfg, resolved, price, types.PricingModeFallbackMax, cfg.Fallback.Strategy),
  149. }, nil
  150. case types.PricingFallbackDefault:
  151. price := cfg.Fallback.DefaultPrice
  152. return &pricingLookupResult{
  153. Mode: types.PricingModeFallbackDefault,
  154. PriceUSD: price,
  155. Snapshot: buildPricingSnapshot(cfg, resolved, price, types.PricingModeFallbackDefault, cfg.Fallback.Strategy),
  156. }, nil
  157. default:
  158. return nil, localPricingTaskError(fmt.Sprintf("unsupported fallback strategy %q", cfg.Fallback.Strategy), "invalid_pricing_config")
  159. }
  160. }
  161. func buildPricingSnapshot(cfg *types.PricingConfig, resolved *ResolvedDimensionValues, price float64, mode types.PricingMode, fallbackStrategy string) map[string]any {
  162. snapshot := make(map[string]any, len(cfg.Dimensions)+4)
  163. for _, dim := range cfg.Dimensions {
  164. snapshot[dim.Key] = resolved.Values[dim.Key]
  165. }
  166. snapshot["price"] = price
  167. snapshot["billing_unit"] = cfg.BillingUnit
  168. snapshot["pricing_mode"] = string(mode)
  169. if fallbackStrategy != "" {
  170. snapshot["fallback_strategy"] = fallbackStrategy
  171. }
  172. return snapshot
  173. }
  174. func rowMatchesDimensions(dimensions []types.PricingDimension, row types.PricingRow, values map[string]any) (int, bool) {
  175. wildcards := 0
  176. for _, dim := range dimensions {
  177. rv := row[dim.Key]
  178. if s, ok := rv.(string); ok && s == "*" {
  179. wildcards++
  180. continue
  181. }
  182. if !samePricingLiteral(rv, values[dim.Key]) {
  183. return 0, false
  184. }
  185. }
  186. return wildcards, true
  187. }
  188. func maxPricingTablePrice(rows []types.PricingRow) float64 {
  189. var max float64
  190. for i, row := range rows {
  191. price, ok := pricingNumberValue(row["price"])
  192. if !ok {
  193. continue
  194. }
  195. if i == 0 || price > max {
  196. max = price
  197. }
  198. }
  199. return max
  200. }
  201. func minimumUsagePreconsumeQuota(tokenUnitPriceUSD float64, groupRatio float64) int {
  202. if tokenUnitPriceUSD == 0 || groupRatio == 0 {
  203. return 0
  204. }
  205. if common.PreConsumedQuota <= 0 {
  206. return 0
  207. }
  208. quota := int(float64(common.PreConsumedQuota) * tokenUnitPriceUSD / 1_000_000 * common.QuotaPerUnit * groupRatio)
  209. if quota <= 0 {
  210. return 1
  211. }
  212. return quota
  213. }
  214. func priceDataFromDecision(decision *types.PricingDecision) types.PriceData {
  215. if decision == nil {
  216. return types.PriceData{}
  217. }
  218. return types.PriceData{
  219. FreeModel: decision.FreeModel,
  220. ModelPrice: decision.PriceUSD,
  221. Quota: decision.Quota,
  222. GroupRatioInfo: decision.GroupRatioInfo,
  223. OtherRatios: types.CloneRatios(decision.OtherRatios),
  224. UsePrice: decision.UsePrice,
  225. PricingDecision: decision,
  226. }
  227. }
  228. func pricingNumberValue(v any) (float64, bool) {
  229. switch n := v.(type) {
  230. case float64:
  231. return n, true
  232. case float32:
  233. return float64(n), true
  234. case int:
  235. return float64(n), true
  236. case int64:
  237. return float64(n), true
  238. case int32:
  239. return float64(n), true
  240. default:
  241. return 0, false
  242. }
  243. }
  244. func samePricingLiteral(a, b any) bool {
  245. af, aok := pricingNumberValue(a)
  246. bf, bok := pricingNumberValue(b)
  247. if aok && bok {
  248. return af == bf
  249. }
  250. return fmt.Sprintf("%v", a) == fmt.Sprintf("%v", b)
  251. }
  252. func localPricingTaskError(message string, code string) *dto.TaskError {
  253. err := fmt.Errorf("%s", message)
  254. return &dto.TaskError{
  255. Code: code,
  256. Message: message,
  257. StatusCode: http.StatusBadRequest,
  258. LocalError: true,
  259. Error: err,
  260. }
  261. }