Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 

154 linhas
5.2 KiB

  1. package controller
  2. import (
  3. "bytes"
  4. "net/http"
  5. "net/http/httptest"
  6. "testing"
  7. "github.com/QuantumNous/new-api/common"
  8. "github.com/QuantumNous/new-api/model"
  9. "github.com/QuantumNous/new-api/setting/ratio_setting"
  10. "github.com/QuantumNous/new-api/types"
  11. "github.com/gin-gonic/gin"
  12. "github.com/glebarez/sqlite"
  13. "github.com/stretchr/testify/assert"
  14. "github.com/stretchr/testify/require"
  15. "gorm.io/gorm"
  16. )
  17. func setupModelPricingControllerTest(t *testing.T) *gin.Engine {
  18. t.Helper()
  19. gin.SetMode(gin.TestMode)
  20. db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
  21. require.NoError(t, err)
  22. require.NoError(t, db.AutoMigrate(&model.Option{}))
  23. sqlDB, err := db.DB()
  24. require.NoError(t, err)
  25. sqlDB.SetMaxOpenConns(1)
  26. origDB := model.DB
  27. origUsingSQLite := common.UsingSQLite
  28. origRedisEnabled := common.RedisEnabled
  29. model.DB = db
  30. common.UsingSQLite = true
  31. common.RedisEnabled = false
  32. model.InitOptionMap()
  33. require.NoError(t, ratio_setting.UpdateModelPricingRulesByJSONString(`{}`))
  34. require.NoError(t, ratio_setting.UpdateModelPriceByJSONString(`{"video-test":0.25}`))
  35. t.Cleanup(func() {
  36. model.DB = origDB
  37. common.UsingSQLite = origUsingSQLite
  38. common.RedisEnabled = origRedisEnabled
  39. require.NoError(t, ratio_setting.UpdateModelPricingRulesByJSONString(`{}`))
  40. require.NoError(t, ratio_setting.UpdateModelPriceByJSONString(`{}`))
  41. require.NoError(t, sqlDB.Close())
  42. })
  43. r := gin.New()
  44. r.GET("/api/option/model_pricing", GetModelPricingRules)
  45. r.GET("/api/option/model_pricing/*model", GetModelPricingRule)
  46. r.PUT("/api/option/model_pricing/*model", UpdateModelPricingRule)
  47. r.DELETE("/api/option/model_pricing/*model", DeleteModelPricingRule)
  48. return r
  49. }
  50. func TestModelPricingController_CRUD(t *testing.T) {
  51. r := setupModelPricingControllerTest(t)
  52. cfg := validControllerPricingConfig()
  53. w := performJSONRequest(t, r, http.MethodPut, "/api/option/model_pricing/video-test", cfg)
  54. require.Equal(t, http.StatusOK, w.Code)
  55. assert.True(t, responseSuccess(t, w.Body.Bytes()))
  56. w = performJSONRequest(t, r, http.MethodGet, "/api/option/model_pricing/video-test", nil)
  57. require.Equal(t, http.StatusOK, w.Code)
  58. assert.True(t, responseSuccess(t, w.Body.Bytes()))
  59. assert.Contains(t, w.Body.String(), `"billing_unit":"per_call"`)
  60. w = performJSONRequest(t, r, http.MethodGet, "/api/option/model_pricing", nil)
  61. require.Equal(t, http.StatusOK, w.Code)
  62. assert.True(t, responseSuccess(t, w.Body.Bytes()))
  63. assert.Contains(t, w.Body.String(), "video-test")
  64. w = performJSONRequest(t, r, http.MethodDelete, "/api/option/model_pricing/video-test", nil)
  65. require.Equal(t, http.StatusOK, w.Code)
  66. assert.True(t, responseSuccess(t, w.Body.Bytes()))
  67. w = performJSONRequest(t, r, http.MethodGet, "/api/option/model_pricing/video-test", nil)
  68. assert.Equal(t, http.StatusNotFound, w.Code)
  69. legacyPrice, ok := ratio_setting.GetModelPrice("video-test", false)
  70. require.True(t, ok)
  71. assert.Equal(t, 0.25, legacyPrice)
  72. }
  73. func TestModelPricingController_PutRejectsBadConfig(t *testing.T) {
  74. r := setupModelPricingControllerTest(t)
  75. cfg := validControllerPricingConfig()
  76. cfg.Table = nil
  77. w := performJSONRequest(t, r, http.MethodPut, "/api/option/model_pricing/video-test", cfg)
  78. assert.Equal(t, http.StatusBadRequest, w.Code)
  79. assert.Contains(t, w.Body.String(), "table must not be empty")
  80. }
  81. func TestModelPricingController_CRUDWithSlashModelName(t *testing.T) {
  82. r := setupModelPricingControllerTest(t)
  83. cfg := validControllerPricingConfig()
  84. modelName := "openai/gpt-4o"
  85. w := performJSONRequest(t, r, http.MethodPut, "/api/option/model_pricing/"+modelName, cfg)
  86. require.Equal(t, http.StatusOK, w.Code)
  87. require.NotNil(t, ratio_setting.GetPricingConfig(modelName))
  88. w = performJSONRequest(t, r, http.MethodGet, "/api/option/model_pricing/"+modelName, nil)
  89. require.Equal(t, http.StatusOK, w.Code)
  90. assert.Contains(t, w.Body.String(), `"billing_unit":"per_call"`)
  91. w = performJSONRequest(t, r, http.MethodDelete, "/api/option/model_pricing/"+modelName, nil)
  92. require.Equal(t, http.StatusOK, w.Code)
  93. assert.Nil(t, ratio_setting.GetPricingConfig(modelName))
  94. }
  95. func performJSONRequest(t *testing.T, r *gin.Engine, method, path string, body any) *httptest.ResponseRecorder {
  96. t.Helper()
  97. var payload []byte
  98. if body != nil {
  99. var err error
  100. payload, err = common.Marshal(body)
  101. require.NoError(t, err)
  102. }
  103. req := httptest.NewRequest(method, path, bytes.NewReader(payload))
  104. req.Header.Set("Content-Type", "application/json")
  105. w := httptest.NewRecorder()
  106. r.ServeHTTP(w, req)
  107. return w
  108. }
  109. func responseSuccess(t *testing.T, payload []byte) bool {
  110. t.Helper()
  111. var body struct {
  112. Success bool `json:"success"`
  113. }
  114. require.NoError(t, common.Unmarshal(payload, &body))
  115. return body.Success
  116. }
  117. func validControllerPricingConfig() types.PricingConfig {
  118. return types.PricingConfig{
  119. SchemaVersion: 1,
  120. Scope: types.PricingScopeModel,
  121. BillingUnit: types.BillingUnitPerCall,
  122. PreconsumeStrategy: types.PreconsumeStrategyExact,
  123. Dimensions: []types.PricingDimension{
  124. {Key: "resolution", Source: "request.resolution", Type: "string"},
  125. },
  126. Table: []types.PricingRow{
  127. {"resolution": "720P", "price": 0.1, "source": types.PricingRowSourceManual},
  128. },
  129. Fallback: types.PricingFallback{Strategy: types.PricingFallbackReject},
  130. }
  131. }