Files
CaIon eb169e74d7 feat(pricing): adjust synced upstream prices by the site exchange rate
Upstream price sync saved an upstream's price as it was, so a price
quoted in another currency had to be corrected by hand afterwards. Each
selected model now has a price adjustment: keep the source price, or
divide or multiply it by the site exchange rate. "Set selected prices"
applies one choice to every selected model. Fixed prices, ratios and the
prices inside billing expressions are adjusted; an expression that
cannot be adjusted automatically has to keep its source price or be
edited by hand. The confirmation dialog shows the adjustment, the final
saved price and the price in the site currency, with the exchange rate
in use.

"Show models with matching prices" also lists upstream prices that
already match the local ones, so they can be selected and adjusted too.
2026-10-22 16:23:19 +08:00

152 lines
9.0 KiB
Go

package controller
import (
"testing"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/relaykit/dto"
"github.com/QuantumNous/new-api/setting/billing_setting"
"github.com/QuantumNous/new-api/setting/config"
"github.com/QuantumNous/new-api/setting/ratio_setting"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"net/http"
"net/http/httptest"
)
func TestPricingSyncExpressionPriority(t *testing.T) {
expression := `tier("base", p * 2 + c * 8 + cr * 0)`
cases := []struct {
name string
local map[string]any
source map[string]any
wantFields []string
}{
{"equal expressions suppress stale ratios", map[string]any{"billing_mode": map[string]string{"m": "tiered_expr"}, "billing_expr": map[string]string{"m": expression}}, map[string]any{"billing_mode": map[string]string{"m": "tiered_expr"}, "billing_expr": map[string]string{"m": expression}, "model_ratio": map[string]float64{"m": 3}, "model_price": map[string]float64{"m": 2}}, nil},
{"local expression excludes legacy-only source", map[string]any{"billing_mode": map[string]string{"m": "tiered_expr"}, "billing_expr": map[string]string{"m": expression}}, map[string]any{"model_ratio": map[string]float64{"m": 3}, "completion_ratio": map[string]float64{"m": 2}}, nil},
{"expression imports without legacy conflicts", map[string]any{"model_ratio": map[string]float64{"m": 1}}, map[string]any{"billing_mode": map[string]string{"m": "tiered_expr"}, "billing_expr": map[string]string{"m": expression}, "model_ratio": map[string]float64{"m": 3}, "model_price": map[string]float64{"m": 2}}, []string{"billing_mode", "billing_expr"}},
{"inactive expression follows explicit ratio mode", map[string]any{"model_ratio": map[string]float64{"m": 1}}, map[string]any{"billing_mode": map[string]string{"m": "ratio"}, "billing_expr": map[string]string{"m": expression}, "model_ratio": map[string]float64{"m": 3}}, []string{"model_ratio"}},
{"empty active expression never imports a false free price", map[string]any{}, map[string]any{"billing_mode": map[string]string{"m": "tiered_expr"}, "billing_expr": map[string]string{"m": " "}, "model_ratio": map[string]float64{"m": 0}}, nil},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
diff := buildDifferences(tt.local, []struct {
name string
data map[string]any
}{{"source", tt.source}})
fields := make([]string, 0, len(diff["m"]))
for field := range diff["m"] {
fields = append(fields, field)
}
assert.ElementsMatch(t, tt.wantFields, fields)
})
}
}
func TestRatioConfigExportsEffectiveExpressions(t *testing.T) {
before := config.GlobalConfig.ExportAllConfigs()
expose := ratio_setting.IsExposeRatioEnabled()
t.Cleanup(func() {
config.UpdateConfigFromMap(config.GlobalConfig.Get("billing_setting"), map[string]string{"billing_mode": before["billing_setting.billing_mode"], "billing_expr": before["billing_setting.billing_expr"]})
ratio_setting.SetExposeRatioEnabled(expose)
})
config.UpdateConfigFromMap(config.GlobalConfig.Get("billing_setting"), map[string]string{"billing_mode": `{"sync-export":"tiered_expr"}`, "billing_expr": `{"sync-export":"tier(\"base\", p * 2)"}`})
ratio_setting.SetExposeRatioEnabled(true)
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
c.Request = httptest.NewRequest(http.MethodGet, "/api/ratio_config", nil)
GetRatioConfig(c)
var response struct {
Success bool
Data map[string]any
}
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
require.True(t, response.Success)
assert.Equal(t, billing_setting.BillingModeTieredExpr, valueMap(response.Data["billing_mode"])["sync-export"])
assert.Equal(t, `tier("base", p * 2)`, valueMap(response.Data["billing_expr"])["sync-export"])
}
func TestPricingSyncCompleteSourcesAndArrayFormats(t *testing.T) {
before := config.GlobalConfig.ExportAllConfigs()
oldRatios, oldCompletion := ratio_setting.ModelRatio2JSONString(), ratio_setting.CompletionRatio2JSONString()
t.Cleanup(func() {
config.UpdateConfigFromMap(config.GlobalConfig.Get("billing_setting"), map[string]string{"billing_mode": before["billing_setting.billing_mode"], "billing_expr": before["billing_setting.billing_expr"]})
require.NoError(t, ratio_setting.UpdateModelRatioByJSONString(oldRatios))
require.NoError(t, ratio_setting.UpdateCompletionRatioByJSONString(oldCompletion))
})
require.NoError(t, ratio_setting.UpdateModelRatioByJSONString(`{"sync-token":1}`))
require.NoError(t, ratio_setting.UpdateCompletionRatioByJSONString(`{"sync-token":2}`))
expression := `len <= 200000 ? tier("short", p * 2 + c * 8 + cr * 0) : tier("long", p * 4 + c * 12)`
expressions, err := common.Marshal(map[string]string{"sync-already": expression})
require.NoError(t, err)
config.UpdateConfigFromMap(config.GlobalConfig.Get("billing_setting"), map[string]string{"billing_mode": `{"sync-already":"tiered_expr"}`, "billing_expr": string(expressions)})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var data any
if r.URL.Path == "/ratio_config" {
data = map[string]any{
"billing_mode": map[string]string{"sync-already": "tiered_expr", "sync-expression": "tiered_expr"},
"billing_expr": map[string]string{"sync-already": expression, "sync-expression": expression},
"model_ratio": map[string]float64{"sync-already": 9, "sync-expression": 9, "sync-token": 1},
"model_price": map[string]float64{"sync-expression": 4},
"completion_ratio": map[string]float64{"sync-token": 4},
"cache_ratio": map[string]float64{"sync-token": 0},
}
} else {
data = []map[string]any{
{"model_name": "sync-already", "model_ratio": 5, "completion_ratio": 3},
{"model_name": "sync-expression", "model_ratio": 2, "model_price": 1},
{"model_name": "sync-array-expression", "billing_mode": "tiered_expr", "billing_expr": expression, "quota_type": 1, "model_price": 0},
{"model_name": "sync-unpriced"},
{"model_name": "sync-invalid-expression", "billing_mode": "tiered_expr", "billing_expr": "", "model_ratio": 0},
{"model_name": "sync-free", "model_ratio": 0, "completion_ratio": 0},
}
}
encoded, err := common.Marshal(map[string]any{"success": true, "data": data})
require.NoError(t, err)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(encoded)
}))
defer server.Close()
var response struct {
Success bool
Data struct {
Differences map[string]map[string]dto.DifferenceItem
Prices map[string]struct {
Current map[string]any
Upstreams map[string]map[string]any
}
TestResults []dto.TestResult `json:"test_results"`
}
}
body := map[string]any{"upstreams": []map[string]any{
{"id": 1, "name": "Expressions", "base_url": server.URL, "endpoint": "/ratio_config"},
{"id": 2, "name": "Legacy", "base_url": server.URL, "endpoint": "/pricing"},
}}
recorder := modelManagementRequest(t, FetchUpstreamRatios, http.MethodPost, "/api/channel/fetch_upstream_ratios", body, &response)
require.True(t, response.Success, recorder.Body.String())
require.Len(t, response.Data.TestResults, 2)
for _, result := range response.Data.TestResults {
require.Equal(t, "success", result.Status, result.Error)
}
assert.NotContains(t, response.Data.Differences, "sync-already")
assert.NotContains(t, response.Data.Differences, "sync-unpriced")
assert.NotContains(t, response.Data.Differences, "sync-invalid-expression")
assert.Equal(t, map[string]any{"billing_mode": "tiered_expr", "billing_expr": expression}, response.Data.Prices["sync-expression"].Upstreams["Expressions(1)"])
assert.NotContains(t, response.Data.Prices["sync-expression"].Upstreams, "Legacy(2)")
assert.Equal(t, map[string]any{"billing_mode": "tiered_expr", "billing_expr": expression}, response.Data.Prices["sync-array-expression"].Upstreams["Legacy(2)"])
assert.Equal(t, float64(1), response.Data.Prices["sync-token"].Upstreams["Expressions(1)"]["model_ratio"], "unchanged base prices are included for a complete price preview")
assert.Equal(t, float64(4), response.Data.Prices["sync-token"].Upstreams["Expressions(1)"]["completion_ratio"])
assert.Equal(t, float64(0), response.Data.Prices["sync-token"].Upstreams["Expressions(1)"]["cache_ratio"])
assert.Equal(t, float64(0), response.Data.Prices["sync-free"].Upstreams["Legacy(2)"]["model_ratio"])
assert.NotContains(t, response.Data.Prices, "sync-already")
body["include_unchanged"] = true
recorder = modelManagementRequest(t, FetchUpstreamRatios, http.MethodPost, "/api/ratio_sync/fetch", body, &response)
require.True(t, response.Success, recorder.Body.String())
assert.Equal(t, map[string]any{"billing_mode": "tiered_expr", "billing_expr": expression}, response.Data.Prices["sync-already"].Upstreams["Expressions(1)"])
assert.NotContains(t, response.Data.Prices["sync-already"].Upstreams, "Legacy(2)")
assert.NotContains(t, response.Data.Prices, "sync-unpriced")
assert.NotContains(t, response.Data.Prices, "sync-invalid-expression")
assert.NotContains(t, response.Data.Differences, "sync-already")
}