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

293 строки
8.6 KiB

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