package runner import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "math" "math/big" "strconv" "strings" "github.com/easyai/easyai-ai-gateway/apps/api/internal/auth" "github.com/easyai/easyai-ai-gateway/apps/api/internal/clients" "github.com/easyai/easyai-ai-gateway/apps/api/internal/store" ) const ( pricingVersionV2 = "effective-pricing-v2" fixedScale = int64(1_000_000_000) ) var ErrPricingUnavailable = errors.New("pricing unavailable") type PricingUnavailableError struct { Reason string ResourceType string RuleSetID string } func (e *PricingUnavailableError) Error() string { message := "pricing unavailable" if e.ResourceType != "" { message += " for " + e.ResourceType } if e.Reason != "" { message += ": " + e.Reason } return message } func (e *PricingUnavailableError) Unwrap() error { return ErrPricingUnavailable } func isPricingUnavailable(err error) bool { return errors.Is(err, ErrPricingUnavailable) } func IsPricingUnavailable(err error) bool { return errors.Is(err, ErrPricingUnavailable) } func PricingUnavailableDetails(err error) map[string]any { var pricingErr *PricingUnavailableError if !errors.As(err, &pricingErr) { return nil } details := map[string]any{"reason": pricingErr.Reason} if pricingErr.ResourceType != "" { details["resourceType"] = pricingErr.ResourceType } if pricingErr.RuleSetID != "" { details["ruleSetId"] = pricingErr.RuleSetID } return details } // fixedAmount is a signed decimal with exactly nine fractional digits. // Monetary decisions use this type; float64 is only produced at JSON-compatible boundaries. type fixedAmount int64 func parseFixedAmount(value string) (fixedAmount, error) { value = strings.TrimSpace(value) if value == "" { return 0, fmt.Errorf("empty decimal") } sign := int64(1) if value[0] == '-' || value[0] == '+' { if value[0] == '-' { sign = -1 } value = value[1:] } parts := strings.Split(value, ".") if len(parts) > 2 || value == "" { return 0, fmt.Errorf("invalid decimal %q", value) } whole := parts[0] if whole == "" { whole = "0" } fraction := "" if len(parts) == 2 { fraction = parts[1] } for _, digits := range []string{whole, fraction} { for _, char := range digits { if char < '0' || char > '9' { return 0, fmt.Errorf("invalid decimal %q", value) } } } roundUp := false if len(fraction) > 9 { roundUp = fraction[9] >= '5' fraction = fraction[:9] } fraction += strings.Repeat("0", 9-len(fraction)) combined := strings.TrimLeft(whole+fraction, "0") if combined == "" { combined = "0" } number := new(big.Int) if _, ok := number.SetString(combined, 10); !ok { return 0, fmt.Errorf("invalid decimal %q", value) } if roundUp { number.Add(number, big.NewInt(1)) } if !number.IsInt64() { return 0, fmt.Errorf("decimal %q exceeds fixed amount range", value) } return fixedAmount(sign * number.Int64()), nil } func fixedAmountFromAny(value any) (fixedAmount, error) { switch typed := value.(type) { case fixedAmount: return typed, nil case string: return parseFixedAmount(typed) case json.Number: return parseFixedAmount(typed.String()) case int: return fixedAmount(int64(typed) * fixedScale), nil case int64: return fixedAmount(typed * fixedScale), nil case int32: return fixedAmount(int64(typed) * fixedScale), nil case float64: if math.IsNaN(typed) || math.IsInf(typed, 0) { return 0, fmt.Errorf("non-finite decimal") } return parseFixedAmount(strconv.FormatFloat(typed, 'f', 9, 64)) case float32: return fixedAmountFromAny(float64(typed)) default: return 0, fmt.Errorf("unsupported decimal type %T", value) } } func (a fixedAmount) String() string { value := int64(a) sign := "" if value < 0 { sign = "-" value = -value } return fmt.Sprintf("%s%d.%09d", sign, value/fixedScale, value%fixedScale) } func (a fixedAmount) Float64() float64 { return float64(a) / float64(fixedScale) } func (a fixedAmount) IsZero() bool { return a == 0 } func (a fixedAmount) Add(other fixedAmount) fixedAmount { return a + other } func (a fixedAmount) MulInt(multiplier int) fixedAmount { return fixedAmount(int64(a) * int64(multiplier)) } func (a fixedAmount) Mul(other fixedAmount) fixedAmount { product := new(big.Int).Mul(big.NewInt(int64(a)), big.NewInt(int64(other))) return fixedAmount(roundBigIntRatio(product, big.NewInt(fixedScale))) } func (a fixedAmount) MulRatio(numerator int, denominator int) fixedAmount { if denominator == 0 { return 0 } product := new(big.Int).Mul(big.NewInt(int64(a)), big.NewInt(int64(numerator))) return fixedAmount(roundBigIntRatio(product, big.NewInt(int64(denominator)))) } func roundBigIntRatio(numerator *big.Int, denominator *big.Int) int64 { negative := numerator.Sign() < 0 absolute := new(big.Int).Abs(new(big.Int).Set(numerator)) quotient, remainder := new(big.Int), new(big.Int) quotient.QuoRem(absolute, denominator, remainder) if new(big.Int).Mul(remainder, big.NewInt(2)).Cmp(denominator) >= 0 { quotient.Add(quotient, big.NewInt(1)) } if negative { quotient.Neg(quotient) } return quotient.Int64() } type resolvedPricing struct { Config map[string]any Currency string RuleSetID string RuleSetKey string Source string FreeResource map[string]bool Snapshot map[string]any } func (pricing resolvedPricing) requiredPrice(resource string, keys ...string) (fixedAmount, error) { if price, found, err := pricing.price(resource, keys...); err != nil { return 0, &PricingUnavailableError{Reason: err.Error(), ResourceType: resource, RuleSetID: pricing.RuleSetID} } else if found && price > 0 { return price, nil } else if found && price == 0 && pricing.FreeResource[resource] { return 0, nil } return 0, &PricingUnavailableError{Reason: "missing, invalid, or not explicitly free", ResourceType: resource, RuleSetID: pricing.RuleSetID} } func (pricing resolvedPricing) price(resource string, keys ...string) (fixedAmount, bool, error) { for _, key := range keys { if value, ok := pricing.Config[key]; ok { amount, err := fixedAmountFromAny(value) return amount, true, err } } resourceConfig, _ := pricing.Config[resource].(map[string]any) if len(resourceConfig) == 0 && resource == "image_edit" { resourceConfig, _ = pricing.Config["image"].(map[string]any) } for _, key := range keys { if value, ok := resourceConfig[key]; ok { amount, err := fixedAmountFromAny(value) return amount, true, err } } if formula, ok := resourceConfig["formulaConfig"].(map[string]any); ok { for _, key := range keys { if value, exists := formula[key]; exists { amount, err := fixedAmountFromAny(value) return amount, true, err } } } if value, ok := resourceConfig["basePrice"]; ok { amount, err := fixedAmountFromAny(value) return amount, true, err } if resource == "text" { return resolvedPricing{Config: pricing.Config}.price("text_total", keys...) } return 0, false, nil } type candidateEstimate struct { Items []any Amount fixedAmount Currency string Snapshot map[string]any Pricing resolvedPricing } func buildEstimateResult(estimates []candidateEstimate, fingerprint string) (EstimateResult, error) { if len(estimates) == 0 { return EstimateResult{}, &PricingUnavailableError{Reason: "no candidate has effective pricing"} } preferred := estimates[0] reservation := preferred.Amount currency := preferred.Currency if currency == "" { currency = "resource" } for _, estimate := range estimates[1:] { candidateCurrency := estimate.Currency if candidateCurrency == "" { candidateCurrency = "resource" } if candidateCurrency != currency { return EstimateResult{}, &PricingUnavailableError{Reason: "candidate currencies do not match"} } if estimate.Amount > reservation { reservation = estimate.Amount } } return EstimateResult{ Items: preferred.Items, Resolver: pricingVersionV2, TotalAmount: preferred.Amount.Float64(), ReservationAmount: reservation.Float64(), Currency: currency, CandidateCount: len(estimates), PricingVersion: pricingVersionV2, RequestFingerprint: fingerprint, }, nil } func estimatedOutputTokens(body map[string]any, candidate store.RuntimeModelCandidate) int { for _, key := range []string{"max_completion_tokens", "max_output_tokens", "max_tokens"} { if value, ok := positiveInteger(body[key]); ok { return value } } if limit, _, _, ok := candidateMaxOutputTokens(candidate, candidate.ModelType); ok { return limit } return 4096 } func pricingRequestFingerprint(kind string, model string, body map[string]any) string { payload, _ := json.Marshal(map[string]any{"kind": kind, "model": model, "request": body}) digest := sha256.Sum256(payload) return hex.EncodeToString(digest[:]) } func (s *Service) resolveEffectivePricing(ctx context.Context, candidate store.RuntimeModelCandidate) (resolvedPricing, error) { ruleSetID := firstNonEmptyString( candidate.ModelPricingRuleSetID, candidate.PlatformPricingRuleSetID, candidate.BasePricingRuleSetID, ) if ruleSetID != "" { if s.store == nil { return resolvedPricing{}, &PricingUnavailableError{Reason: "pricing store is unavailable", RuleSetID: ruleSetID} } config, err := s.store.PricingRuleSetBillingConfigV2(ctx, ruleSetID) if err != nil { return resolvedPricing{}, &PricingUnavailableError{Reason: err.Error(), RuleSetID: ruleSetID} } if len(candidate.BillingConfigOverride) > 0 { config.Config = mergeMap(config.Config, candidate.BillingConfigOverride) } return resolvedPricing{ Config: config.Config, Currency: config.Currency, RuleSetID: config.RuleSetID, RuleSetKey: config.RuleSetKey, Source: pricingRuleSource(candidate, ruleSetID), FreeResource: config.FreeResource, Snapshot: config.Snapshot, }, nil } config := candidate.BaseBillingConfig source := "base_model_config" if len(candidate.BillingConfig) > 0 { config = candidate.BillingConfig source = "platform_model_config" } if len(candidate.BillingConfigOverride) > 0 { config = mergeMap(config, candidate.BillingConfigOverride) source += "_override" } if len(config) == 0 { return resolvedPricing{}, &PricingUnavailableError{Reason: "candidate has no pricing configuration"} } freeResource := explicitFreeResources(config) snapshot := map[string]any{ "pricingVersion": pricingVersionV2, "source": source, "currency": "resource", "config": config, } return resolvedPricing{ Config: config, Currency: "resource", Source: source, FreeResource: freeResource, Snapshot: snapshot, }, nil } func pricingRuleSource(candidate store.RuntimeModelCandidate, ruleSetID string) string { switch ruleSetID { case candidate.ModelPricingRuleSetID: return "platform_model_rule_set" case candidate.PlatformPricingRuleSetID: return "platform_rule_set" default: return "base_model_rule_set" } } func explicitFreeResources(config map[string]any) map[string]bool { resources := map[string]bool{} if isFree, _ := config["isFree"].(bool); isFree { for _, resource := range []string{"text_input", "text_cached_input", "text_output", "text_total", "image", "image_edit", "video", "music", "audio"} { resources[resource] = true } } for _, resource := range []string{"text", "text_total", "image", "image_edit", "video", "music", "audio"} { resourceConfig, _ := config[resource].(map[string]any) if isFree, _ := resourceConfig["isFree"].(bool); isFree { resources[resource] = true } } return resources } func (s *Service) estimateCandidateV2(ctx context.Context, user *auth.User, kind string, body map[string]any, candidate store.RuntimeModelCandidate) (candidateEstimate, error) { usage := clients.Usage{InputTokens: estimateRequestTokens(body)} if isTextGenerationKind(kind) { usage.OutputTokens = estimatedOutputTokens(body, candidate) } usage.TotalTokens = usage.InputTokens + usage.OutputTokens response := clients.Response{Usage: usage} items, total, pricing, err := s.billingsV2(ctx, user, kind, body, candidate, response, true) if err != nil { return candidateEstimate{}, err } if isTextBillingKind(kind) { if _, err := pricing.requiredTextPrice("text_cached_input", "textCachedInputPer1k", "cachedInputTokenPrice", "cacheInputTokenPrice", "textInputCacheHitPer1k", "inputCacheHitTokenPrice"); err != nil { return candidateEstimate{}, err } } return candidateEstimate{ Items: items, Amount: total, Currency: pricing.Currency, Snapshot: pricing.Snapshot, Pricing: pricing, }, nil } func (s *Service) billingsV2( ctx context.Context, user *auth.User, kind string, body map[string]any, candidate store.RuntimeModelCandidate, response clients.Response, simulated bool, ) ([]any, fixedAmount, resolvedPricing, error) { pricing, err := s.resolveEffectivePricing(ctx, candidate) if err != nil { return nil, 0, resolvedPricing{}, err } return s.billingsWithResolvedPricingV2(ctx, user, kind, body, candidate, response, simulated, pricing) } func (s *Service) billingsWithResolvedPricingV2( ctx context.Context, user *auth.User, kind string, body map[string]any, candidate store.RuntimeModelCandidate, response clients.Response, simulated bool, pricing resolvedPricing, ) ([]any, fixedAmount, resolvedPricing, error) { discount, err := fixedAmountFromAny(effectiveDiscount(ctx, s.store, user, candidate)) if err != nil || discount <= 0 { return nil, 0, resolvedPricing{}, &PricingUnavailableError{Reason: "invalid discount factor", RuleSetID: pricing.RuleSetID} } buildLine := func(resourceType string, unit string, quantity any, amount fixedAmount, details map[string]any) map[string]any { line := billingLineWithDetails(candidate, resourceType, unit, quantity, amount.Float64(), discount.Float64(), simulated, details) line["pricingVersion"] = pricingVersionV2 line["pricingSource"] = pricing.Source if pricing.RuleSetID != "" { line["pricingRuleSetId"] = pricing.RuleSetID } line["isFree"] = amount.IsZero() return line } 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 } if inputTokens == 0 && outputTokens == 0 { inputTokens = estimateRequestTokens(body) if isTextGenerationKind(kind) { outputTokens = 1 } } if cachedInputTokens > inputTokens && inputTokens > 0 { cachedInputTokens = inputTokens } uncachedInputTokens := inputTokens - cachedInputTokens if uncachedInputTokens < 0 { uncachedInputTokens = 0 } inputPrice, err := pricing.requiredTextPrice("text_input", "textInputPer1k", "inputTokenPrice", "basePrice") if err != nil { return nil, 0, resolvedPricing{}, err } items := make([]any, 0, 3) total := fixedAmount(0) if uncachedInputTokens > 0 || cachedInputTokens == 0 { amount := inputPrice.MulRatio(uncachedInputTokens, 1000).Mul(discount) total = total.Add(amount) items = append(items, buildLine("text_input", "1k_tokens", uncachedInputTokens, amount, map[string]any{ "inputTokens": inputTokens, "uncachedInputTokens": uncachedInputTokens, "cachedInputTokens": cachedInputTokens, "pricePer1k": inputPrice.Float64(), })) } if cachedInputTokens > 0 { cachedPrice, priceErr := pricing.requiredTextPrice("text_cached_input", "textCachedInputPer1k", "cachedInputTokenPrice", "cacheInputTokenPrice", "textInputCacheHitPer1k", "inputCacheHitTokenPrice") if priceErr != nil { return nil, 0, resolvedPricing{}, priceErr } amount := cachedPrice.MulRatio(cachedInputTokens, 1000).Mul(discount) total = total.Add(amount) items = append(items, buildLine("text_cached_input", "1k_tokens", cachedInputTokens, amount, map[string]any{ "inputTokens": inputTokens, "uncachedInputTokens": uncachedInputTokens, "cachedInputTokens": cachedInputTokens, "pricePer1k": cachedPrice.Float64(), })) } if isTextGenerationKind(kind) { outputPrice, priceErr := pricing.requiredTextPrice("text_output", "textOutputPer1k", "outputTokenPrice", "basePrice") if priceErr != nil { return nil, 0, resolvedPricing{}, priceErr } amount := outputPrice.MulRatio(outputTokens, 1000).Mul(discount) total = total.Add(amount) items = append(items, buildLine("text_output", "1k_tokens", outputTokens, amount, map[string]any{"pricePer1k": outputPrice.Float64()})) } return items, total, pricing, nil } count := requestOutputCount(body) resource := "image" unit := "image" baseKey := "imageBase" if kind == "images.edits" { resource = "image_edit" baseKey = "editBase" } if kind == "videos.generations" { resource = "video" unit = "5s_video" baseKey = "videoBase" duration, durationSource := billingDurationSeconds(body, response) audioEnabled, audioSource := billingAudioEnabled(body, response) durationUnits := int(math.Max(1, math.Ceil(duration/5))) price, priceErr := pricing.requiredPrice(resource, baseKey, "basePrice") if priceErr != nil { return nil, 0, resolvedPricing{}, priceErr } amount := price.MulInt(count).MulInt(durationUnits). Mul(fixedWeight(resourceWeight(pricing.Config, resource, "resolutionWeights", firstNonEmptyString(stringFromMap(body, "resolution"), stringFromMap(body, "size"))))). Mul(fixedWeight(resourceWeight(pricing.Config, resource, "audioWeights", boolWeightKey(audioEnabled)))). Mul(fixedWeight(resourceWeight(pricing.Config, resource, "referenceVideoWeights", boolWeightKey(requestHasReferenceVideo(body))))). Mul(fixedWeight(resourceWeight(pricing.Config, resource, "voiceSpecifiedWeights", boolWeightKey(requestHasVoiceID(body, audioEnabled))))). Mul(discount) item := buildLine(resource, unit, count*durationUnits, amount, map[string]any{ "count": count, "audio": audioEnabled, "audioSource": audioSource, "durationSeconds": duration, "durationSource": durationSource, "durationUnit": "5s", "durationUnitCount": durationUnits, }) return []any{item}, amount, pricing, nil } if kind == "song.generations" || kind == "music.generations" { resource = "music" unit = "song" baseKey = "musicBase" } if kind == "speech.generations" || kind == "voice.clone" { resource = "audio" unit = "character" baseKey = "audioBase" count = len([]rune(stringFromMap(body, "text"))) if count <= 0 { count = 1 } } price, err := pricing.requiredPrice(resource, baseKey, "basePrice") if err != nil { return nil, 0, resolvedPricing{}, err } amount := price.MulInt(count) if resource == "image" || resource == "image_edit" { amount = amount. Mul(fixedWeight(resourceWeight(pricing.Config, resource, "qualityWeights", stringFromMap(body, "quality")))). Mul(fixedWeight(resourceWeight(pricing.Config, resource, "sizeWeights", stringFromMap(body, "size")))). Mul(fixedWeight(resourceWeight(pricing.Config, resource, "resolutionWeights", firstNonEmptyString(stringFromMap(body, "resolution"), stringFromMap(body, "size"))))) } amount = amount.Mul(discount) return []any{buildLine(resource, unit, count, amount, nil)}, amount, pricing, nil } func (pricing resolvedPricing) requiredTextPrice(resource string, keys ...string) (fixedAmount, error) { price, found, err := pricing.price("text", keys...) if err != nil { return 0, &PricingUnavailableError{Reason: err.Error(), ResourceType: resource, RuleSetID: pricing.RuleSetID} } if found && price > 0 { return price, nil } if found && price == 0 && (pricing.FreeResource[resource] || pricing.FreeResource["text_total"] || pricing.FreeResource["text"]) { return 0, nil } return 0, &PricingUnavailableError{Reason: "missing, invalid, or not explicitly free", ResourceType: resource, RuleSetID: pricing.RuleSetID} } func fixedWeight(value float64) fixedAmount { weight, err := fixedAmountFromAny(value) if err != nil || weight <= 0 { return fixedAmount(fixedScale) } return weight } func maximumCandidateEstimate(estimates []candidateEstimate) (candidateEstimate, error) { if len(estimates) == 0 { return candidateEstimate{}, &PricingUnavailableError{Reason: "no candidate has effective pricing"} } maximum := estimates[0] for _, estimate := range estimates[1:] { if estimate.Currency != maximum.Currency { return candidateEstimate{}, &PricingUnavailableError{Reason: "candidate currencies do not match"} } if estimate.Amount > maximum.Amount { maximum = estimate } } return maximum, nil }