textmachine/backend/internal/ledger/pricing_test.go

72 lines
3.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package ledger
import (
"math"
"testing"
"textmachine/backend/internal/llm"
)
func approx(t *testing.T, got, want float64, msg string) {
t.Helper()
if math.Abs(got-want) > 1e-12 {
t.Fatalf("%s: got %v, want %v", msg, got, want)
}
}
// Формула денег — ядро юнит-экономики; проверяем каждое слагаемое, включая
// новое (cache write), и деградацию к формуле vojo без кэш-полей.
func TestCostUSD(t *testing.T) {
price := ModelPrice{InputPerM: 2.0, CachedPerM: 0.2, CacheWritePerM: 2.5, OutputPerM: 10.0}
// Anthropic-подобный вызов: 10k всего входа = 6k некэш + 3k cache-read +
// 1k cache-write; 2k выхода.
u := llm.Usage{PromptTokens: 10_000, CachedTokens: 3_000, CacheCreationTokens: 1_000, CompletionTokens: 2_000}
want := 6_000*2.0/1e6 + 3_000*0.2/1e6 + 1_000*2.5/1e6 + 2_000*10.0/1e6
approx(t, CostUSD(price, u), want, "anthropic-style usage")
// OpenAI-совместимый вызов без кэш-полей — формула vojo.
u2 := llm.Usage{PromptTokens: 10_000, CompletionTokens: 2_000}
approx(t, CostUSD(price, u2), 10_000*2.0/1e6+2_000*10.0/1e6, "plain usage")
// Reasoning-токены биллятся по output-ставке ПОВЕРХ completion (xAI).
u3 := llm.Usage{PromptTokens: 1_000, CompletionTokens: 500, ReasoningTokens: 1_500}
approx(t, CostUSD(price, u3), 1_000*2.0/1e6+2_000*10.0/1e6, "additive reasoning")
// Защита от провайдера, отрапортовавшего cached > prompt: некэш-слагаемое
// зажимается в 0, а не уходит в минус.
u4 := llm.Usage{PromptTokens: 100, CachedTokens: 500, CompletionTokens: 10}
want4 := 0*2.0/1e6 + 500*0.2/1e6 + 10*10.0/1e6
approx(t, CostUSD(price, u4), want4, "cached > prompt guard")
}
func TestPricerNeverZero(t *testing.T) {
def := ModelPrice{InputPerM: 1, OutputPerM: 2}
p, err := NewPricer(map[string]ModelPrice{"known": {InputPerM: 5, CachedPerM: 1, OutputPerM: 10}}, def)
if err != nil {
t.Fatal(err)
}
if got := p.PriceFor("known").InputPerM; got != 5 {
t.Fatalf("known model price: got %v", got)
}
// Неизвестная модель (фактическая после фейловера) — дефолтный якорь, не $0.
if got := p.PriceFor("mystery-model").InputPerM; got != 1 {
t.Fatalf("unknown model must fall back to default price, got %v", got)
}
// $0-дефолт ослепил бы потолки — конструктор обязан отказать.
if _, err := NewPricer(nil, ModelPrice{}); err == nil {
t.Fatal("zero default price must be rejected")
}
}
func TestEstimateUSDIsPessimistic(t *testing.T) {
price := ModelPrice{InputPerM: 2.0, CachedPerM: 0.2, OutputPerM: 10.0}
est := EstimateUSD(price, 1_000, 2_000)
approx(t, est, 1_000*2.0/1e6+2_000*10.0/1e6, "estimate")
// Реальный вызов с кэш-хитом обязан стоить не больше оценки — иначе
// settle раздует committed выше потолка, пропущенного на допуске.
real := CostUSD(price, llm.Usage{PromptTokens: 1_000, CachedTokens: 900, CompletionTokens: 2_000})
if real > est {
t.Fatalf("real cost %v exceeds reservation estimate %v", real, est)
}
}