| @@ -138,6 +138,9 @@ func GetUserPricing(c *gin.Context) { | |||||
| if pricingData.PricingConfig != nil { | if pricingData.PricingConfig != nil { | ||||
| result["pricing_config"] = pricingData.PricingConfig | result["pricing_config"] = pricingData.PricingConfig | ||||
| } | } | ||||
| if len(pricingData.DisplayPricing) > 0 { | |||||
| result["display_pricing"] = pricingData.DisplayPricing | |||||
| } | |||||
| if pricingData.QuotaType == model.QuotaTypeByTokens { | if pricingData.QuotaType == model.QuotaTypeByTokens { | ||||
| if pricingData.PricingConfig != nil { | if pricingData.PricingConfig != nil { | ||||
| @@ -184,6 +187,9 @@ func respondOriginalPrice(c *gin.Context, pricingData *model.Pricing) { | |||||
| } | } | ||||
| result["original_price"] = pricingData.ModelPrice | result["original_price"] = pricingData.ModelPrice | ||||
| } | } | ||||
| if len(pricingData.DisplayPricing) > 0 { | |||||
| result["display_pricing"] = pricingData.DisplayPricing | |||||
| } | |||||
| c.JSON(200, result) | c.JSON(200, result) | ||||
| } | } | ||||
| @@ -9,8 +9,9 @@ import ( | |||||
| "github.com/QuantumNous/new-api/common" | "github.com/QuantumNous/new-api/common" | ||||
| "github.com/QuantumNous/new-api/model" | "github.com/QuantumNous/new-api/model" | ||||
| "github.com/QuantumNous/new-api/setting/ratio_setting" | "github.com/QuantumNous/new-api/setting/ratio_setting" | ||||
| "github.com/glebarez/sqlite" | |||||
| "github.com/QuantumNous/new-api/types" | |||||
| "github.com/gin-gonic/gin" | "github.com/gin-gonic/gin" | ||||
| "github.com/glebarez/sqlite" | |||||
| "github.com/stretchr/testify/assert" | "github.com/stretchr/testify/assert" | ||||
| "github.com/stretchr/testify/require" | "github.com/stretchr/testify/require" | ||||
| "gorm.io/gorm" | "gorm.io/gorm" | ||||
| @@ -152,6 +153,36 @@ func TestGetUserPricing_NotLoggedIn_PerCall(t *testing.T) { | |||||
| assert.Equal(t, float64(0.04), resp["original_price"]) | assert.Equal(t, float64(0.04), resp["original_price"]) | ||||
| } | } | ||||
| func TestGetUserPricingIncludesDisplayPricingWithoutChangingComputedPrice(t *testing.T) { | |||||
| setupPricingTestDB(t) | |||||
| modelName := "display-pricing-user-model" | |||||
| router := gin.New() | |||||
| router.GET("/api/pricing/user/*model", GetUserPricing) | |||||
| setPricingCache([]model.Pricing{ | |||||
| { | |||||
| ModelName: modelName, | |||||
| QuotaType: model.QuotaTypeByCall, | |||||
| ModelPrice: 0.5, | |||||
| EnableGroup: []string{"default"}, | |||||
| DisplayPricing: []types.ModelDisplayPricingItem{ | |||||
| {Specification: "768p-6s", Price: 0.33333, Unit: "second", SortOrder: 1, DiscountRate: 1}, | |||||
| }, | |||||
| }, | |||||
| }) | |||||
| w := httptest.NewRecorder() | |||||
| req, _ := http.NewRequest("GET", "/api/pricing/user/"+modelName, nil) | |||||
| router.ServeHTTP(w, req) | |||||
| var resp map[string]interface{} | |||||
| require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) | |||||
| require.Equal(t, http.StatusOK, w.Code) | |||||
| require.Contains(t, resp, "display_pricing") | |||||
| require.Equal(t, float64(0.5), resp["original_price"]) | |||||
| require.NotEqual(t, float64(0.33333), resp["original_price"]) | |||||
| } | |||||
| // TestGetUserPricing_LoggedIn_NoDiscount 已登录但无折扣(分组倍率=1,无个人倍率) | // TestGetUserPricing_LoggedIn_NoDiscount 已登录但无折扣(分组倍率=1,无个人倍率) | ||||
| func TestGetUserPricing_LoggedIn_NoDiscount(t *testing.T) { | func TestGetUserPricing_LoggedIn_NoDiscount(t *testing.T) { | ||||
| db := setupPricingTestDB(t) | db := setupPricingTestDB(t) | ||||
| @@ -226,8 +257,6 @@ func TestGetUserPricing_LoggedIn_GroupDiscount(t *testing.T) { | |||||
| assert.Equal(t, "8折", resp["discount"]) | assert.Equal(t, "8折", resp["discount"]) | ||||
| } | } | ||||
| // TestGetUserPricing_PerCall_WithDiscount 按次计费 + 折扣 | // TestGetUserPricing_PerCall_WithDiscount 按次计费 + 折扣 | ||||
| func TestGetUserPricing_PerCall_WithDiscount(t *testing.T) { | func TestGetUserPricing_PerCall_WithDiscount(t *testing.T) { | ||||
| db := setupPricingTestDB(t) | db := setupPricingTestDB(t) | ||||
| @@ -280,4 +309,3 @@ func TestFormatDiscount(t *testing.T) { | |||||
| assert.Equal(t, tt.expected, result, "ratio=%.2f", tt.ratio) | assert.Equal(t, tt.expected, result, "ratio=%.2f", tt.ratio) | ||||
| } | } | ||||
| } | } | ||||
| @@ -14,23 +14,24 @@ import ( | |||||
| ) | ) | ||||
| type Pricing struct { | type Pricing struct { | ||||
| ModelName string `json:"model_name"` | |||||
| Description string `json:"description,omitempty"` | |||||
| Icon string `json:"icon,omitempty"` | |||||
| Tags string `json:"tags,omitempty"` | |||||
| VendorID int `json:"vendor_id,omitempty"` | |||||
| QuotaType int `json:"quota_type"` | |||||
| ModelRatio float64 `json:"model_ratio"` | |||||
| ModelPrice float64 `json:"model_price"` | |||||
| OwnerBy string `json:"owner_by"` | |||||
| CompletionRatio float64 `json:"completion_ratio"` | |||||
| CacheRatio float64 `json:"cache_ratio"` | |||||
| CacheCreationRatio float64 `json:"cache_creation_ratio"` | |||||
| EnableGroup []string `json:"enable_groups"` | |||||
| SupportedEndpointTypes []constant.EndpointType `json:"supported_endpoint_types"` | |||||
| PricingVersion string `json:"pricing_version,omitempty"` | |||||
| Type int `json:"type"` | |||||
| PricingConfig *types.PricingConfig `json:"pricing_config,omitempty"` | |||||
| ModelName string `json:"model_name"` | |||||
| Description string `json:"description,omitempty"` | |||||
| Icon string `json:"icon,omitempty"` | |||||
| Tags string `json:"tags,omitempty"` | |||||
| VendorID int `json:"vendor_id,omitempty"` | |||||
| QuotaType int `json:"quota_type"` | |||||
| ModelRatio float64 `json:"model_ratio"` | |||||
| ModelPrice float64 `json:"model_price"` | |||||
| OwnerBy string `json:"owner_by"` | |||||
| CompletionRatio float64 `json:"completion_ratio"` | |||||
| CacheRatio float64 `json:"cache_ratio"` | |||||
| CacheCreationRatio float64 `json:"cache_creation_ratio"` | |||||
| EnableGroup []string `json:"enable_groups"` | |||||
| SupportedEndpointTypes []constant.EndpointType `json:"supported_endpoint_types"` | |||||
| PricingVersion string `json:"pricing_version,omitempty"` | |||||
| Type int `json:"type"` | |||||
| PricingConfig *types.PricingConfig `json:"pricing_config,omitempty"` | |||||
| DisplayPricing []types.ModelDisplayPricingItem `json:"display_pricing,omitempty"` | |||||
| } | } | ||||
| type PricingVendor struct { | type PricingVendor struct { | ||||
| @@ -340,6 +341,7 @@ func updatePricing() { | |||||
| } | } | ||||
| applyGlobalDefault(&pricing, model) | applyGlobalDefault(&pricing, model) | ||||
| applyDisplayPricing(&pricing, model) | |||||
| pricingMap = append(pricingMap, pricing) | pricingMap = append(pricingMap, pricing) | ||||
| } | } | ||||
| @@ -426,6 +428,13 @@ func applyMatrixPricingConfig(pricing *Pricing, model string) bool { | |||||
| return true | return true | ||||
| } | } | ||||
| func applyDisplayPricing(pricing *Pricing, model string) { | |||||
| items := ratio_setting.GetModelDisplayPricing(model) | |||||
| if len(items) > 0 { | |||||
| pricing.DisplayPricing = items | |||||
| } | |||||
| } | |||||
| func minPricingConfigPrice(cfg *types.PricingConfig) float64 { | func minPricingConfigPrice(cfg *types.PricingConfig) float64 { | ||||
| if cfg == nil || len(cfg.Table) == 0 { | if cfg == nil || len(cfg.Table) == 0 { | ||||
| return 0 | return 0 | ||||
| @@ -189,6 +189,41 @@ func TestGetPricingByModel_IncludesMatrixPricingConfig(t *testing.T) { | |||||
| require.Equal(t, []constant.EndpointType{constant.EndpointTypeDoubaoVideo}, pricing.SupportedEndpointTypes) | require.Equal(t, []constant.EndpointType{constant.EndpointTypeDoubaoVideo}, pricing.SupportedEndpointTypes) | ||||
| } | } | ||||
| func TestGetPricingByModelIncludesDisplayPricing(t *testing.T) { | |||||
| setupPricingTest(t) | |||||
| const displayModel = "display-pricing-model" | |||||
| require.NoError(t, ratio_setting.UpdateModelRatioByJSONString(`{"`+displayModel+`":10}`)) | |||||
| require.NoError(t, ratio_setting.SetModelDisplayPricing(displayModel, []types.ModelDisplayPricingItem{ | |||||
| {Specification: "768p-6s", Price: 0.33333, Unit: "second", SortOrder: 1}, | |||||
| })) | |||||
| require.NoError(t, (&Model{ | |||||
| ModelName: displayModel, | |||||
| Status: 1, | |||||
| }).Insert()) | |||||
| require.NoError(t, (&Channel{ | |||||
| Id: 1, | |||||
| Type: constant.ChannelTypeOpenAI, | |||||
| Key: "k", | |||||
| Name: "channel-a", | |||||
| Status: common.ChannelStatusEnabled, | |||||
| Models: displayModel, | |||||
| Group: "default", | |||||
| }).Insert()) | |||||
| RefreshPricing() | |||||
| pricing := GetPricingByModel(displayModel) | |||||
| require.NotNil(t, pricing) | |||||
| require.Len(t, pricing.DisplayPricing, 1) | |||||
| require.Equal(t, "768p-6s", pricing.DisplayPricing[0].Specification) | |||||
| require.Equal(t, QuotaTypeByTokens, pricing.QuotaType) | |||||
| require.Equal(t, 10.0, pricing.ModelRatio) | |||||
| require.Equal(t, 0.0, pricing.ModelPrice) | |||||
| require.Nil(t, pricing.PricingConfig) | |||||
| } | |||||
| func TestParseModelEndpointTypes_SupportsObjectAndArray(t *testing.T) { | func TestParseModelEndpointTypes_SupportsObjectAndArray(t *testing.T) { | ||||
| objectEndpoints := parseModelEndpointTypes(`{ | objectEndpoints := parseModelEndpointTypes(`{ | ||||
| "anthropic": {"path": "/v1/messages", "method": "POST"}, | "anthropic": {"path": "/v1/messages", "method": "POST"}, | ||||