feat: implement tiered billing expression evaluation and related functionality
- Added support for tiered billing expressions in the billing system. - Introduced new types and functions for handling billing expressions, including caching and execution. - Updated existing billing logic to accommodate tiered billing scenarios. - Enhanced request handling to support incoming billing expression requests. - Added tests for tiered billing functionality to ensure correctness.
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
package billing_setting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/pkg/billingexpr"
|
||||
)
|
||||
|
||||
var (
|
||||
mu sync.RWMutex
|
||||
|
||||
// model -> "ratio" | "tiered_expr"
|
||||
billingModeMap = make(map[string]string)
|
||||
|
||||
// model -> expr string (authored by frontend, stored directly)
|
||||
billingExprMap = make(map[string]string)
|
||||
)
|
||||
|
||||
const (
|
||||
BillingModeRatio = "ratio"
|
||||
BillingModeTieredExpr = "tiered_expr"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Read accessors (hot path, must be fast)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func GetBillingMode(model string) string {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
if mode, ok := billingModeMap[model]; ok {
|
||||
return mode
|
||||
}
|
||||
return BillingModeRatio
|
||||
}
|
||||
|
||||
func GetBillingExpr(model string) (string, bool) {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
expr, ok := billingExprMap[model]
|
||||
return expr, ok
|
||||
}
|
||||
|
||||
func UpdateBillingModeByJSONString(jsonStr string) error {
|
||||
var m map[string]string
|
||||
if err := common.Unmarshal([]byte(jsonStr), &m); err != nil {
|
||||
return fmt.Errorf("parse ModelBillingMode: %w", err)
|
||||
}
|
||||
for k, v := range m {
|
||||
if v != BillingModeRatio && v != BillingModeTieredExpr {
|
||||
return fmt.Errorf("invalid billing mode %q for model %q", v, k)
|
||||
}
|
||||
}
|
||||
mu.Lock()
|
||||
billingModeMap = m
|
||||
mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateBillingExprByJSONString(jsonStr string) error {
|
||||
var m map[string]string
|
||||
if err := common.Unmarshal([]byte(jsonStr), &m); err != nil {
|
||||
return fmt.Errorf("parse ModelBillingExpr: %w", err)
|
||||
}
|
||||
for model, exprStr := range m {
|
||||
if _, err := billingexpr.CompileFromCache(exprStr); err != nil {
|
||||
return fmt.Errorf("model %q: %w", model, err)
|
||||
}
|
||||
if err := smokeTestExpr(exprStr); err != nil {
|
||||
return fmt.Errorf("model %q smoke test: %w", model, err)
|
||||
}
|
||||
}
|
||||
mu.Lock()
|
||||
billingExprMap = m
|
||||
mu.Unlock()
|
||||
billingexpr.InvalidateCache()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JSON serializers (for OptionMap / API response)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func BillingMode2JSONString() string {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
b, err := common.Marshal(billingModeMap)
|
||||
if err != nil {
|
||||
return "{}"
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func BillingExpr2JSONString() string {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
b, err := common.Marshal(billingExprMap)
|
||||
if err != nil {
|
||||
return "{}"
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func smokeTestExpr(exprStr string) error {
|
||||
vectors := []billingexpr.TokenParams{
|
||||
{P: 0, C: 0},
|
||||
{P: 1000, C: 1000},
|
||||
{P: 100000, C: 100000},
|
||||
{P: 1000000, C: 1000000},
|
||||
}
|
||||
requests := []billingexpr.RequestInput{
|
||||
{},
|
||||
{
|
||||
Headers: map[string]string{
|
||||
"anthropic-beta": "fast-mode-2026-02-01",
|
||||
},
|
||||
Body: []byte(`{"service_tier":"fast","stream_options":{"include_usage":true},"messages":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]}`),
|
||||
},
|
||||
}
|
||||
|
||||
for _, v := range vectors {
|
||||
for _, request := range requests {
|
||||
result, _, err := billingexpr.RunExprWithRequest(exprStr, v, request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("vector {p=%g, c=%g}: run failed: %w", v.P, v.C, err)
|
||||
}
|
||||
if result < 0 {
|
||||
return fmt.Errorf("vector {p=%g, c=%g}: result %f < 0", v.P, v.C, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package model_setting
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClaudeSettingsWriteHeadersMergesConfiguredValuesIntoSingleHeader(t *testing.T) {
|
||||
settings := &ClaudeSettings{
|
||||
HeadersSettings: map[string]map[string][]string{
|
||||
"claude-3-7-sonnet-20250219-thinking": {
|
||||
"anthropic-beta": {
|
||||
"token-efficient-tools-2025-02-19",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
headers := http.Header{}
|
||||
headers.Set("anthropic-beta", "output-128k-2025-02-19")
|
||||
|
||||
settings.WriteHeaders("claude-3-7-sonnet-20250219-thinking", &headers)
|
||||
|
||||
got := headers.Values("anthropic-beta")
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("expected a single merged header value, got %v", got)
|
||||
}
|
||||
expected := "output-128k-2025-02-19,token-efficient-tools-2025-02-19"
|
||||
if got[0] != expected {
|
||||
t.Fatalf("expected merged header %q, got %q", expected, got[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeSettingsWriteHeadersDeduplicatesAcrossCommaSeparatedAndRepeatedValues(t *testing.T) {
|
||||
settings := &ClaudeSettings{
|
||||
HeadersSettings: map[string]map[string][]string{
|
||||
"claude-3-7-sonnet-20250219-thinking": {
|
||||
"anthropic-beta": {
|
||||
"token-efficient-tools-2025-02-19",
|
||||
"computer-use-2025-01-24",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
headers := http.Header{}
|
||||
headers.Add("anthropic-beta", "output-128k-2025-02-19, token-efficient-tools-2025-02-19")
|
||||
headers.Add("anthropic-beta", "token-efficient-tools-2025-02-19")
|
||||
|
||||
settings.WriteHeaders("claude-3-7-sonnet-20250219-thinking", &headers)
|
||||
|
||||
got := headers.Values("anthropic-beta")
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("expected duplicate values to collapse into one header, got %v", got)
|
||||
}
|
||||
expected := "output-128k-2025-02-19,token-efficient-tools-2025-02-19,computer-use-2025-01-24"
|
||||
if got[0] != expected {
|
||||
t.Fatalf("expected deduplicated merged header %q, got %q", expected, got[0])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user