Some checks failed
ci / verify (pull_request) Failing after 8s
阻止上游提交状态不明的任务被租约接管后重复执行,并为人工复核保留可操作的结算记录。 将生产提交绑定到当前估价签名,统一复用预处理快照,并补强规则形状、定点溢出与历史规则兼容校验。 已通过 PostgreSQL 16 集成测试、Go 全量测试与静态检查、前端测试与构建、OpenAPI、依赖审计、镜像、迁移、流水线和 SemVer 门禁。
760 lines
26 KiB
Go
760 lines
26 KiB
Go
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)
|
|
maxFixedAmount = fixedAmount(math.MaxInt64)
|
|
)
|
|
|
|
var ErrPricingUnavailable = errors.New("pricing unavailable")
|
|
var errFixedAmountOverflow = errors.New("fixed amount overflow")
|
|
|
|
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 parseFixedAmount(strconv.FormatInt(int64(typed), 10))
|
|
case int64:
|
|
return parseFixedAmount(strconv.FormatInt(typed, 10))
|
|
case int32:
|
|
return parseFixedAmount(strconv.FormatInt(int64(typed), 10))
|
|
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 addFixedAmounts(left fixedAmount, right fixedAmount) (fixedAmount, error) {
|
|
value := new(big.Int).Add(big.NewInt(int64(left)), big.NewInt(int64(right)))
|
|
return fixedAmountFromBigInt(value)
|
|
}
|
|
|
|
func multiplyFixedAmountByInt(amount fixedAmount, multiplier int) (fixedAmount, error) {
|
|
value := new(big.Int).Mul(big.NewInt(int64(amount)), big.NewInt(int64(multiplier)))
|
|
return fixedAmountFromBigInt(value)
|
|
}
|
|
|
|
func multiplyFixedAmounts(left fixedAmount, right fixedAmount) (fixedAmount, error) {
|
|
product := new(big.Int).Mul(big.NewInt(int64(left)), big.NewInt(int64(right)))
|
|
return fixedAmountFromBigInt(roundBigIntRatio(product, big.NewInt(fixedScale)))
|
|
}
|
|
|
|
func multiplyFixedAmountRatio(amount fixedAmount, numerator int, denominator int) (fixedAmount, error) {
|
|
if denominator == 0 {
|
|
return 0, fmt.Errorf("division by zero")
|
|
}
|
|
product := new(big.Int).Mul(big.NewInt(int64(amount)), big.NewInt(int64(numerator)))
|
|
return fixedAmountFromBigInt(roundBigIntRatio(product, big.NewInt(int64(denominator))))
|
|
}
|
|
|
|
func fixedAmountFromBigInt(value *big.Int) (fixedAmount, error) {
|
|
if value == nil || !value.IsInt64() {
|
|
return 0, errFixedAmountOverflow
|
|
}
|
|
return fixedAmount(value.Int64()), nil
|
|
}
|
|
|
|
func roundBigIntRatio(numerator *big.Int, denominator *big.Int) *big.Int {
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (pricing resolvedPricing) weight(resource string, key string, name string) (fixedAmount, error) {
|
|
if strings.TrimSpace(name) == "" {
|
|
return fixedAmount(fixedScale), nil
|
|
}
|
|
keys := weightKeyAliases(key)
|
|
names := weightValueAliases(key, name)
|
|
value, found := pricingWeightValue(pricing.Config, resource, keys, names)
|
|
if !found {
|
|
return fixedAmount(fixedScale), nil
|
|
}
|
|
weight, err := fixedAmountFromAny(value)
|
|
if err != nil || weight <= 0 {
|
|
reason := fmt.Sprintf("invalid %s weight for %s", key, name)
|
|
if err != nil {
|
|
reason += ": " + err.Error()
|
|
}
|
|
return 0, &PricingUnavailableError{Reason: reason, ResourceType: resource, RuleSetID: pricing.RuleSetID}
|
|
}
|
|
return weight, nil
|
|
}
|
|
|
|
func (pricing resolvedPricing) calculate(resource string, base fixedAmount, integerFactors []int, fixedFactors ...fixedAmount) (fixedAmount, error) {
|
|
amount := base
|
|
var err error
|
|
for _, factor := range integerFactors {
|
|
amount, err = multiplyFixedAmountByInt(amount, factor)
|
|
if err != nil {
|
|
return 0, pricing.calculationError(resource, err)
|
|
}
|
|
}
|
|
for _, factor := range fixedFactors {
|
|
amount, err = multiplyFixedAmounts(amount, factor)
|
|
if err != nil {
|
|
return 0, pricing.calculationError(resource, err)
|
|
}
|
|
}
|
|
return amount, nil
|
|
}
|
|
|
|
func (pricing resolvedPricing) calculateRatio(resource string, base fixedAmount, numerator int, denominator int, fixedFactors ...fixedAmount) (fixedAmount, error) {
|
|
amount, err := multiplyFixedAmountRatio(base, numerator, denominator)
|
|
if err != nil {
|
|
return 0, pricing.calculationError(resource, err)
|
|
}
|
|
return pricing.calculate(resource, amount, nil, fixedFactors...)
|
|
}
|
|
|
|
func (pricing resolvedPricing) add(resource string, left fixedAmount, right fixedAmount) (fixedAmount, error) {
|
|
amount, err := addFixedAmounts(left, right)
|
|
if err != nil {
|
|
return 0, pricing.calculationError(resource, err)
|
|
}
|
|
return amount, nil
|
|
}
|
|
|
|
func (pricing resolvedPricing) calculationError(resource string, err error) error {
|
|
return &PricingUnavailableError{Reason: "pricing calculation failed: " + err.Error(), ResourceType: resource, RuleSetID: pricing.RuleSetID}
|
|
}
|
|
|
|
func pricingWeightValue(config map[string]any, resource string, keys []string, names []string) (any, bool) {
|
|
if value, ok := pricingWeightValueFromConfig(config, keys, names); ok {
|
|
return value, true
|
|
}
|
|
resourceConfig, _ := config[resource].(map[string]any)
|
|
if len(resourceConfig) == 0 && resource == "image_edit" {
|
|
resourceConfig, _ = config["image"].(map[string]any)
|
|
}
|
|
return pricingWeightValueFromConfig(resourceConfig, keys, names)
|
|
}
|
|
|
|
func pricingWeightValueFromConfig(config map[string]any, keys []string, names []string) (any, bool) {
|
|
if len(config) == 0 {
|
|
return nil, false
|
|
}
|
|
for _, key := range keys {
|
|
weights, _ := config[key].(map[string]any)
|
|
for _, name := range names {
|
|
if value, ok := weights[name]; ok {
|
|
return value, true
|
|
}
|
|
}
|
|
}
|
|
dynamic, _ := config["dynamicWeight"].(map[string]any)
|
|
for _, name := range names {
|
|
if value, ok := dynamic[name]; ok {
|
|
return value, true
|
|
}
|
|
}
|
|
for _, key := range keys {
|
|
weights, _ := dynamic[key].(map[string]any)
|
|
for _, name := range names {
|
|
if value, ok := weights[name]; ok {
|
|
return value, true
|
|
}
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
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, calculationErr := pricing.calculateRatio("text_input", inputPrice, uncachedInputTokens, 1000, discount)
|
|
if calculationErr != nil {
|
|
return nil, 0, resolvedPricing{}, calculationErr
|
|
}
|
|
total, calculationErr = pricing.add("text", total, amount)
|
|
if calculationErr != nil {
|
|
return nil, 0, resolvedPricing{}, calculationErr
|
|
}
|
|
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, calculationErr := pricing.calculateRatio("text_cached_input", cachedPrice, cachedInputTokens, 1000, discount)
|
|
if calculationErr != nil {
|
|
return nil, 0, resolvedPricing{}, calculationErr
|
|
}
|
|
total, calculationErr = pricing.add("text", total, amount)
|
|
if calculationErr != nil {
|
|
return nil, 0, resolvedPricing{}, calculationErr
|
|
}
|
|
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, calculationErr := pricing.calculateRatio("text_output", outputPrice, outputTokens, 1000, discount)
|
|
if calculationErr != nil {
|
|
return nil, 0, resolvedPricing{}, calculationErr
|
|
}
|
|
total, calculationErr = pricing.add("text", total, amount)
|
|
if calculationErr != nil {
|
|
return nil, 0, resolvedPricing{}, calculationErr
|
|
}
|
|
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
|
|
}
|
|
resolutionWeight, weightErr := pricing.weight(resource, "resolutionWeights", firstNonEmptyString(stringFromMap(body, "resolution"), stringFromMap(body, "size")))
|
|
if weightErr != nil {
|
|
return nil, 0, resolvedPricing{}, weightErr
|
|
}
|
|
audioWeight, weightErr := pricing.weight(resource, "audioWeights", boolWeightKey(audioEnabled))
|
|
if weightErr != nil {
|
|
return nil, 0, resolvedPricing{}, weightErr
|
|
}
|
|
referenceVideoWeight, weightErr := pricing.weight(resource, "referenceVideoWeights", boolWeightKey(requestHasReferenceVideo(body)))
|
|
if weightErr != nil {
|
|
return nil, 0, resolvedPricing{}, weightErr
|
|
}
|
|
voiceWeight, weightErr := pricing.weight(resource, "voiceSpecifiedWeights", boolWeightKey(requestHasVoiceID(body, audioEnabled)))
|
|
if weightErr != nil {
|
|
return nil, 0, resolvedPricing{}, weightErr
|
|
}
|
|
amount, calculationErr := pricing.calculate(resource, price, []int{count, durationUnits}, resolutionWeight, audioWeight, referenceVideoWeight, voiceWeight, discount)
|
|
if calculationErr != nil {
|
|
return nil, 0, resolvedPricing{}, calculationErr
|
|
}
|
|
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, calculationErr := pricing.calculate(resource, price, []int{count})
|
|
if calculationErr != nil {
|
|
return nil, 0, resolvedPricing{}, calculationErr
|
|
}
|
|
if resource == "image" || resource == "image_edit" {
|
|
qualityWeight, weightErr := pricing.weight(resource, "qualityWeights", stringFromMap(body, "quality"))
|
|
if weightErr != nil {
|
|
return nil, 0, resolvedPricing{}, weightErr
|
|
}
|
|
sizeWeight, weightErr := pricing.weight(resource, "sizeWeights", stringFromMap(body, "size"))
|
|
if weightErr != nil {
|
|
return nil, 0, resolvedPricing{}, weightErr
|
|
}
|
|
resolutionWeight, weightErr := pricing.weight(resource, "resolutionWeights", firstNonEmptyString(stringFromMap(body, "resolution"), stringFromMap(body, "size")))
|
|
if weightErr != nil {
|
|
return nil, 0, resolvedPricing{}, weightErr
|
|
}
|
|
amount, calculationErr = pricing.calculate(resource, amount, nil, qualityWeight, sizeWeight, resolutionWeight)
|
|
if calculationErr != nil {
|
|
return nil, 0, resolvedPricing{}, calculationErr
|
|
}
|
|
}
|
|
amount, calculationErr = pricing.calculate(resource, amount, nil, discount)
|
|
if calculationErr != nil {
|
|
return nil, 0, resolvedPricing{}, calculationErr
|
|
}
|
|
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 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
|
|
}
|