Files
easyai-ai-gateway/apps/api/internal/runner/pricing_v2_test.go
T
chengcheng 5114686c35 feat(billing): 统一计价与候选冻结估算
新增 effective-pricing-v2、9 位十进制定点计算、显式免费校验和结构化缺价错误。估价与生产预处理覆盖全部可用候选,并按最大候选费用冻结;规则优先级为平台模型、平台、基准模型。\n\n同步计价契约和 OpenAPI,补充 Token 参数别名、视频五秒向上取整及缺价回归测试。\n\n验证:go test ./...、pnpm openapi、Web 测试与构建通过。
2026-07-20 23:22:45 +08:00

108 lines
3.9 KiB
Go

package runner
import (
"testing"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
func TestFixedAmountPreservesNineDecimalPlaces(t *testing.T) {
amount, err := parseFixedAmount("0.123456789")
if err != nil {
t.Fatalf("parse fixed amount: %v", err)
}
if got, want := amount.String(), "0.123456789"; got != want {
t.Fatalf("amount.String()=%q, want %q", got, want)
}
price := mustFixedAmount(t, "0.000000001")
if got, want := price.MulInt(3).String(), "0.000000003"; got != want {
t.Fatalf("nano amount multiplication=%q, want %q", got, want)
}
}
func TestEstimatedOutputTokensUsesAliasesAndCapabilityFallback(t *testing.T) {
candidate := store.RuntimeModelCandidate{
ModelType: "text_generate",
Capabilities: map[string]any{
"text_generate": map[string]any{"max_output_tokens": 8192},
},
}
tests := []struct {
name string
body map[string]any
want int
}{
{name: "max completion has highest priority", body: map[string]any{"max_completion_tokens": 700, "max_output_tokens": 600, "max_tokens": 500}, want: 700},
{name: "max output precedes legacy max tokens", body: map[string]any{"max_output_tokens": 600, "max_tokens": 500}, want: 600},
{name: "legacy max tokens", body: map[string]any{"max_tokens": 500}, want: 500},
{name: "model capability fallback", body: map[string]any{}, want: 8192},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := estimatedOutputTokens(test.body, candidate); got != test.want {
t.Fatalf("estimatedOutputTokens()=%d, want %d", got, test.want)
}
})
}
if got := estimatedOutputTokens(nil, store.RuntimeModelCandidate{}); got != 4096 {
t.Fatalf("missing capability fallback=%d, want 4096", got)
}
}
func TestBuildEstimateResultUsesPreferredTotalAndMaximumReservation(t *testing.T) {
result, err := buildEstimateResult([]candidateEstimate{
{Items: []any{map[string]any{"amount": 1.25, "currency": "resource"}}, Amount: mustFixedAmount(t, "1.25")},
{Items: []any{map[string]any{"amount": 2.75, "currency": "resource"}}, Amount: mustFixedAmount(t, "2.75")},
}, "fingerprint")
if err != nil {
t.Fatalf("build estimate result: %v", err)
}
if result.TotalAmount != 1.25 {
t.Fatalf("preferred total=%v, want 1.25", result.TotalAmount)
}
if result.ReservationAmount != 2.75 {
t.Fatalf("reservation=%v, want 2.75", result.ReservationAmount)
}
if result.CandidateCount != 2 || result.PricingVersion != pricingVersionV2 || result.RequestFingerprint != "fingerprint" {
t.Fatalf("unexpected estimate metadata: %+v", result)
}
}
func TestPricingAvailabilityRequiresExplicitFree(t *testing.T) {
paid := resolvedPricing{Config: map[string]any{"imageBase": 1.5}, Currency: "resource"}
if _, err := paid.requiredPrice("image", "imageBase", "basePrice"); err != nil {
t.Fatalf("positive price should be available: %v", err)
}
missing := resolvedPricing{Config: map[string]any{}, Currency: "resource"}
if _, err := missing.requiredPrice("image", "imageBase", "basePrice"); !isPricingUnavailable(err) {
t.Fatalf("missing price should be unavailable, got %v", err)
}
ordinaryZero := resolvedPricing{Config: map[string]any{"imageBase": 0}, Currency: "resource"}
if _, err := ordinaryZero.requiredPrice("image", "imageBase", "basePrice"); !isPricingUnavailable(err) {
t.Fatalf("ordinary zero should be unavailable, got %v", err)
}
explicitFree := resolvedPricing{
Config: map[string]any{"imageBase": 0},
Currency: "resource",
FreeResource: map[string]bool{"image": true},
}
price, err := explicitFree.requiredPrice("image", "imageBase", "basePrice")
if err != nil || !price.IsZero() {
t.Fatalf("explicit free should resolve to zero: price=%s err=%v", price.String(), err)
}
}
func mustFixedAmount(t *testing.T, value string) fixedAmount {
t.Helper()
amount, err := parseFixedAmount(value)
if err != nil {
t.Fatalf("parse %q: %v", value, err)
}
return amount
}