feat: support cached input token billing
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/clients"
|
||||
@@ -29,6 +30,44 @@ func TestTokenUsageAmountsFallsBackToInputOutputTotal(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBillingsSplitsCachedInputTokens(t *testing.T) {
|
||||
service := &Service{}
|
||||
candidate := store.RuntimeModelCandidate{
|
||||
ModelName: "test-model",
|
||||
BillingConfig: map[string]any{
|
||||
"textInputPer1k": 1.0,
|
||||
"textCachedInputPer1k": 0.25,
|
||||
"textOutputPer1k": 2.0,
|
||||
},
|
||||
}
|
||||
response := clients.Response{
|
||||
Usage: clients.Usage{
|
||||
InputTokens: 1000,
|
||||
CachedInputTokens: 400,
|
||||
OutputTokens: 500,
|
||||
TotalTokens: 1500,
|
||||
},
|
||||
}
|
||||
|
||||
lines := service.billings(context.Background(), nil, "chat.completions", nil, candidate, response, false)
|
||||
|
||||
if len(lines) != 3 {
|
||||
t.Fatalf("expected uncached input, cached input, and output lines, got %+v", lines)
|
||||
}
|
||||
uncached, _ := lines[0].(map[string]any)
|
||||
cached, _ := lines[1].(map[string]any)
|
||||
output, _ := lines[2].(map[string]any)
|
||||
if uncached["resourceType"] != "text_input" || uncached["quantity"] != 600 || uncached["amount"] != 0.6 {
|
||||
t.Fatalf("unexpected uncached input billing line: %+v", uncached)
|
||||
}
|
||||
if cached["resourceType"] != "text_cached_input" || cached["quantity"] != 400 || cached["amount"] != 0.1 {
|
||||
t.Fatalf("unexpected cached input billing line: %+v", cached)
|
||||
}
|
||||
if output["resourceType"] != "text_output" || output["quantity"] != 500 || output["amount"] != 1.0 {
|
||||
t.Fatalf("unexpected output billing line: %+v", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveRateLimitPolicyTreatsEmptyRuntimePolicyAsUnlimited(t *testing.T) {
|
||||
policy := effectiveRateLimitPolicy(store.RuntimeModelCandidate{
|
||||
PlatformRateLimitPolicy: map[string]any{"rules": []any{
|
||||
|
||||
@@ -62,6 +62,7 @@ func (s *Service) billings(ctx context.Context, user *auth.User, kind string, bo
|
||||
if isTextBillingKind(kind) {
|
||||
inputTokens := response.Usage.InputTokens
|
||||
outputTokens := response.Usage.OutputTokens
|
||||
cachedInputTokens := response.Usage.CachedInputTokens
|
||||
if isTextInputOnlyKind(kind) && inputTokens == 0 && response.Usage.TotalTokens > 0 {
|
||||
inputTokens = response.Usage.TotalTokens
|
||||
}
|
||||
@@ -71,8 +72,37 @@ func (s *Service) billings(ctx context.Context, user *auth.User, kind string, bo
|
||||
outputTokens = 1
|
||||
}
|
||||
}
|
||||
inputAmount := roundPrice(float64(inputTokens) / 1000 * resourcePrice(config, "text", "textInputPer1k", "inputTokenPrice", "basePrice") * discount)
|
||||
lines := []any{billingLine(candidate, "text_input", "1k_tokens", inputTokens, inputAmount, discount, simulated)}
|
||||
if cachedInputTokens > inputTokens && inputTokens > 0 {
|
||||
cachedInputTokens = inputTokens
|
||||
}
|
||||
uncachedInputTokens := inputTokens - cachedInputTokens
|
||||
if uncachedInputTokens < 0 {
|
||||
uncachedInputTokens = 0
|
||||
}
|
||||
inputPrice := resourcePrice(config, "text", "textInputPer1k", "inputTokenPrice", "basePrice")
|
||||
cachedInputPrice := resourcePrice(config, "text", "textCachedInputPer1k", "cachedInputTokenPrice", "cacheInputTokenPrice", "textInputCacheHitPer1k", "inputCacheHitTokenPrice")
|
||||
if cachedInputPrice <= 0 {
|
||||
cachedInputPrice = inputPrice
|
||||
}
|
||||
inputAmount := roundPrice(float64(uncachedInputTokens) / 1000 * inputPrice * discount)
|
||||
lines := []any{}
|
||||
if uncachedInputTokens > 0 || cachedInputTokens == 0 {
|
||||
lines = append(lines, billingLineWithDetails(candidate, "text_input", "1k_tokens", uncachedInputTokens, inputAmount, discount, simulated, map[string]any{
|
||||
"inputTokens": inputTokens,
|
||||
"uncachedInputTokens": uncachedInputTokens,
|
||||
"cachedInputTokens": cachedInputTokens,
|
||||
"pricePer1k": inputPrice,
|
||||
}))
|
||||
}
|
||||
if cachedInputTokens > 0 {
|
||||
cachedInputAmount := roundPrice(float64(cachedInputTokens) / 1000 * cachedInputPrice * discount)
|
||||
lines = append(lines, billingLineWithDetails(candidate, "text_cached_input", "1k_tokens", cachedInputTokens, cachedInputAmount, discount, simulated, map[string]any{
|
||||
"inputTokens": inputTokens,
|
||||
"uncachedInputTokens": uncachedInputTokens,
|
||||
"cachedInputTokens": cachedInputTokens,
|
||||
"pricePer1k": cachedInputPrice,
|
||||
}))
|
||||
}
|
||||
if isTextGenerationKind(kind) {
|
||||
outputAmount := roundPrice(float64(outputTokens) / 1000 * resourcePrice(config, "text", "textOutputPer1k", "outputTokenPrice", "basePrice") * discount)
|
||||
lines = append(lines, billingLine(candidate, "text_output", "1k_tokens", outputTokens, outputAmount, discount, simulated))
|
||||
@@ -234,10 +264,22 @@ func resourcePrice(config map[string]any, resource string, keys ...string) float
|
||||
return value
|
||||
}
|
||||
}
|
||||
if formulaConfig, ok := resourceConfig["formulaConfig"].(map[string]any); ok {
|
||||
for _, key := range keys {
|
||||
if value := floatFromAny(formulaConfig[key]); value > 0 {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
if value := floatFromAny(resourceConfig["basePrice"]); value > 0 {
|
||||
return value
|
||||
}
|
||||
}
|
||||
if resource == "text" {
|
||||
if value := resourcePrice(config, "text_total", keys...); value > 0 {
|
||||
return value
|
||||
}
|
||||
}
|
||||
if resource == "image_edit" {
|
||||
return resourcePrice(config, "image", keys...)
|
||||
}
|
||||
|
||||
@@ -151,6 +151,10 @@ func usageToMap(usage clients.Usage) map[string]any {
|
||||
out["inputTokens"] = usage.InputTokens
|
||||
out["promptTokens"] = usage.InputTokens
|
||||
}
|
||||
if usage.CachedInputTokens > 0 {
|
||||
out["cachedInputTokens"] = usage.CachedInputTokens
|
||||
out["cachedPromptTokens"] = usage.CachedInputTokens
|
||||
}
|
||||
if usage.OutputTokens > 0 {
|
||||
out["outputTokens"] = usage.OutputTokens
|
||||
out["completionTokens"] = usage.OutputTokens
|
||||
|
||||
Reference in New Issue
Block a user