You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

203 lines
7.8 KiB

  1. package ratio_setting
  2. import (
  3. "testing"
  4. "github.com/QuantumNous/new-api/types"
  5. "github.com/stretchr/testify/require"
  6. )
  7. func validPricingConfig() *types.PricingConfig {
  8. return &types.PricingConfig{
  9. SchemaVersion: types.PricingSchemaVersion,
  10. Scope: types.PricingScopeModel,
  11. Dimensions: []types.PricingDimension{
  12. {Key: "resolution", Source: "request.resolution", Type: "string", Default: nil},
  13. {Key: "duration", Source: "request.duration", Type: "number", Default: nil},
  14. },
  15. Table: []types.PricingRow{
  16. {"resolution": "720P", "duration": float64(5), "price": float64(0.10), "source": types.PricingRowSourceManual},
  17. {"resolution": "720P", "duration": float64(10), "price": float64(0.20), "source": types.PricingRowSourceGenerated},
  18. },
  19. Fallback: types.PricingFallback{Strategy: types.PricingFallbackReject},
  20. }
  21. }
  22. func TestValidatePricingConfigValidDefaults(t *testing.T) {
  23. cfg := validPricingConfig()
  24. require.NoError(t, ValidatePricingConfig("hailuo-test", cfg))
  25. require.Equal(t, types.BillingUnitPerCall, cfg.BillingUnit)
  26. require.Equal(t, types.PreconsumeStrategyExact, cfg.PreconsumeStrategy)
  27. }
  28. func TestValidatePricingConfigSchemaRules(t *testing.T) {
  29. tests := []struct {
  30. name string
  31. mutate func(*types.PricingConfig)
  32. wantErr string
  33. }{
  34. {"schema_version", func(c *types.PricingConfig) { c.SchemaVersion = 2 }, "schema_version"},
  35. {"scope", func(c *types.PricingConfig) { c.Scope = "channel" }, "scope"},
  36. {"dimensions", func(c *types.PricingConfig) { c.Dimensions = nil }, "dimensions"},
  37. {"table", func(c *types.PricingConfig) { c.Table = nil }, "table"},
  38. {"fallback", func(c *types.PricingConfig) { c.Fallback.Strategy = "nearest" }, "fallback.strategy"},
  39. {"default_price", func(c *types.PricingConfig) {
  40. c.Fallback = types.PricingFallback{Strategy: types.PricingFallbackDefault, DefaultPrice: -1}
  41. }, "default_price"},
  42. {"per_call_strategy", func(c *types.PricingConfig) {
  43. c.BillingUnit = types.BillingUnitPerCall
  44. c.PreconsumeStrategy = types.PreconsumeStrategyMinimum
  45. }, "per_call"},
  46. {"usage_strategy", func(c *types.PricingConfig) {
  47. c.BillingUnit = types.BillingUnitPer1MTokens
  48. c.PreconsumeStrategy = types.PreconsumeStrategyExact
  49. }, "per_1m_tokens"},
  50. {"usage_allowlist", func(c *types.PricingConfig) {
  51. c.BillingUnit = types.BillingUnitPer1MTokens
  52. c.PreconsumeStrategy = types.PreconsumeStrategyMinimum
  53. }, "does not support"},
  54. }
  55. for _, tt := range tests {
  56. t.Run(tt.name, func(t *testing.T) {
  57. cfg := validPricingConfig()
  58. tt.mutate(cfg)
  59. err := ValidatePricingConfig("hailuo-test", cfg)
  60. require.Error(t, err)
  61. require.Contains(t, err.Error(), tt.wantErr)
  62. })
  63. }
  64. }
  65. func TestValidatePricingConfigUsageAllowlist(t *testing.T) {
  66. cfg := validPricingConfig()
  67. cfg.BillingUnit = types.BillingUnitPer1MTokens
  68. cfg.PreconsumeStrategy = types.PreconsumeStrategyMinimum
  69. require.NoError(t, ValidatePricingConfig("seedance-2", cfg))
  70. }
  71. func TestValidatePricingConfigUsageAllowlistKlingAipingModels(t *testing.T) {
  72. models := []string{
  73. "kling-v1",
  74. "kling-v1-6",
  75. "kling-v2-6",
  76. "kling-v3",
  77. "kling-video-o1",
  78. "kling-v3-omni",
  79. }
  80. for _, model := range models {
  81. t.Run(model, func(t *testing.T) {
  82. cfg := validPricingConfig()
  83. cfg.BillingUnit = types.BillingUnitPer1MTokens
  84. cfg.PreconsumeStrategy = types.PreconsumeStrategyMinimum
  85. require.NoError(t, ValidatePricingConfig(model, cfg))
  86. })
  87. }
  88. }
  89. func TestValidatePricingConfigAllowsDerivedVideoInput(t *testing.T) {
  90. cfg := &types.PricingConfig{
  91. SchemaVersion: types.PricingSchemaVersion,
  92. Scope: types.PricingScopeModel,
  93. BillingUnit: types.BillingUnitPer1MTokens,
  94. PreconsumeStrategy: types.PreconsumeStrategyMinimum,
  95. Dimensions: []types.PricingDimension{
  96. {Key: "video_input", Source: "derived.video_input", Type: "boolean", Default: nil},
  97. },
  98. Table: []types.PricingRow{
  99. {"video_input": false, "price": float64(46), "source": types.PricingRowSourceManual},
  100. {"video_input": true, "price": float64(28), "source": types.PricingRowSourceManual},
  101. },
  102. Fallback: types.PricingFallback{Strategy: types.PricingFallbackReject},
  103. }
  104. require.NoError(t, ValidatePricingConfig("seedance-2", cfg))
  105. }
  106. func TestValidatePricingConfigDimensionRules(t *testing.T) {
  107. tests := []struct {
  108. name string
  109. dim types.PricingDimension
  110. wantErr string
  111. }{
  112. {"bad_key", types.PricingDimension{Key: "Bad", Source: "request.x", Type: "string", Default: nil}, "invalid"},
  113. {"reserved", types.PricingDimension{Key: "price", Source: "request.x", Type: "string", Default: nil}, "reserved"},
  114. {"bad_type", types.PricingDimension{Key: "quality", Source: "request.x", Type: "object", Default: nil}, "type"},
  115. {"bad_source", types.PricingDimension{Key: "quality", Source: "response.usage.total_tokens", Type: "number", Default: nil}, "source"},
  116. {"optional_default_missing", types.PricingDimension{Key: "quality", Source: "request.quality", Type: "string", Optional: true, Default: nil}, "default required"},
  117. {"optional_default_type", types.PricingDimension{Key: "quality", Source: "request.quality", Type: "number", Optional: true, Default: "fast"}, "default type"},
  118. {"required_default", types.PricingDimension{Key: "quality", Source: "request.quality", Type: "string", Default: "fast"}, "default must be null"},
  119. }
  120. for _, tt := range tests {
  121. t.Run(tt.name, func(t *testing.T) {
  122. cfg := validPricingConfig()
  123. cfg.Dimensions = []types.PricingDimension{tt.dim}
  124. cfg.Table = []types.PricingRow{{tt.dim.Key: "x", "price": float64(1), "source": types.PricingRowSourceManual}}
  125. if tt.dim.Type == "number" {
  126. cfg.Table[0][tt.dim.Key] = float64(1)
  127. }
  128. err := ValidatePricingConfig("hailuo-test", cfg)
  129. require.Error(t, err)
  130. require.Contains(t, err.Error(), tt.wantErr)
  131. })
  132. }
  133. }
  134. func TestValidatePricingConfigRowRules(t *testing.T) {
  135. tests := []struct {
  136. name string
  137. mutate func(*types.PricingConfig)
  138. wantErr string
  139. }{
  140. {"missing_dim", func(c *types.PricingConfig) { delete(c.Table[0], "duration") }, "missing dimension"},
  141. {"extra_field", func(c *types.PricingConfig) { c.Table[0]["price_formula"] = "x" }, "unexpected field"},
  142. {"bad_dim_type", func(c *types.PricingConfig) { c.Table[0]["duration"] = "five" }, "value type"},
  143. {"bad_price", func(c *types.PricingConfig) { c.Table[0]["price"] = -1.0 }, "price"},
  144. {"bad_source", func(c *types.PricingConfig) { c.Table[0]["source"] = "api" }, "source"},
  145. {"duplicate", func(c *types.PricingConfig) {
  146. c.Table = append(c.Table, types.PricingRow(types.CloneMapAny(c.Table[0])))
  147. }, "duplicate"},
  148. {"ambiguous", func(c *types.PricingConfig) {
  149. c.Dimensions = []types.PricingDimension{
  150. {Key: "res", Source: "request.res", Type: "string", Default: nil},
  151. {Key: "dur", Source: "request.dur", Type: "number", Default: nil},
  152. }
  153. c.Table = []types.PricingRow{
  154. {"res": "*", "dur": float64(5), "price": float64(0.10), "source": types.PricingRowSourceManual},
  155. {"res": "720P", "dur": "*", "price": float64(0.12), "source": types.PricingRowSourceManual},
  156. }
  157. }, "ambiguous"},
  158. }
  159. for _, tt := range tests {
  160. t.Run(tt.name, func(t *testing.T) {
  161. cfg := validPricingConfig()
  162. tt.mutate(cfg)
  163. err := ValidatePricingConfig("hailuo-test", cfg)
  164. require.Error(t, err)
  165. require.Contains(t, err.Error(), tt.wantErr)
  166. })
  167. }
  168. }
  169. func TestModelPricingRulesRoundTripAndClone(t *testing.T) {
  170. require.NoError(t, UpdateModelPricingRulesByJSONString(`{}`))
  171. cfg := validPricingConfig()
  172. require.NoError(t, SetPricingConfig("hailuo-test", cfg))
  173. cfg.Table[0]["price"] = float64(999)
  174. got := GetPricingConfig("hailuo-test")
  175. require.NotNil(t, got)
  176. require.Equal(t, float64(0.10), got.Table[0]["price"])
  177. jsonStr := ModelPricingRules2JSONString()
  178. require.Contains(t, jsonStr, "hailuo-test")
  179. require.NoError(t, UpdateModelPricingRulesByJSONString(jsonStr))
  180. require.NotNil(t, GetPricingConfig("hailuo-test"))
  181. require.NoError(t, DeletePricingConfig("hailuo-test"))
  182. require.Nil(t, GetPricingConfig("hailuo-test"))
  183. }