package runner import ( "context" "math" "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" ) type EstimateResult struct { Items []any `json:"items"` Resolver string `json:"resolver"` } func (s *Service) Estimate(ctx context.Context, kind string, model string, body map[string]any, user *auth.User) (EstimateResult, error) { candidates, err := s.store.ListModelCandidates(ctx, model, modelTypeFromKind(kind), user) if err != nil { return EstimateResult{}, err } candidate := candidates[0] usage := clients.Usage{InputTokens: estimateRequestTokens(body), OutputTokens: int(floatFromAny(body["max_tokens"]))} if usage.OutputTokens == 0 { usage.OutputTokens = 64 } usage.TotalTokens = usage.InputTokens + usage.OutputTokens response := clients.Response{Usage: usage, Result: map[string]any{"usage": map[string]any{ "prompt_tokens": usage.InputTokens, "completion_tokens": usage.OutputTokens, "total_tokens": usage.TotalTokens, }}} return EstimateResult{ Items: s.billings(ctx, user, kind, body, candidate, response, true), Resolver: "effective-pricing-v1", }, nil } func (s *Service) billings(ctx context.Context, user *auth.User, kind string, body map[string]any, candidate store.RuntimeModelCandidate, response clients.Response, simulated bool) []any { config := effectiveBillingConfig(candidate) discount := effectiveDiscount(ctx, s.store, user, candidate) if isTextGenerationKind(kind) { inputTokens := response.Usage.InputTokens outputTokens := response.Usage.OutputTokens if inputTokens == 0 && outputTokens == 0 { inputTokens = estimateRequestTokens(body) outputTokens = 1 } inputAmount := roundPrice(float64(inputTokens) / 1000 * resourcePrice(config, "text", "textInputPer1k", "inputTokenPrice", "basePrice") * discount) outputAmount := roundPrice(float64(outputTokens) / 1000 * resourcePrice(config, "text", "textOutputPer1k", "outputTokenPrice", "basePrice") * discount) return []any{ billingLine(candidate, "text_input", "1k_tokens", inputTokens, inputAmount, discount, simulated), billingLine(candidate, "text_output", "1k_tokens", outputTokens, outputAmount, discount, simulated), } } count := int(floatFromAny(body["n"])) if count <= 0 { count = 1 } resource := "image" unit := "image" baseKey := "imageBase" if kind == "images.edits" { resource = "image_edit" baseKey = "editBase" } if kind == "videos.generations" { resource = "video" unit = "video" baseKey = "videoBase" } amount := float64(count) * resourcePrice(config, resource, baseKey, "basePrice") * resourceWeight(config, resource, "qualityWeights", stringFromMap(body, "quality")) * resourceWeight(config, resource, "sizeWeights", stringFromMap(body, "size")) * resourceWeight(config, resource, "resolutionWeights", firstNonEmptyString(stringFromMap(body, "resolution"), stringFromMap(body, "size"))) * discount return []any{billingLine(candidate, resource, unit, count, roundPrice(amount), discount, simulated)} } func effectiveBillingConfig(candidate store.RuntimeModelCandidate) map[string]any { base := candidate.BaseBillingConfig if len(candidate.BillingConfig) > 0 { base = candidate.BillingConfig } if len(candidate.BillingConfigOverride) > 0 { base = mergeMap(base, candidate.BillingConfigOverride) } return base } func effectiveDiscount(ctx context.Context, db *store.Store, user *auth.User, candidate store.RuntimeModelCandidate) float64 { discount := candidate.DefaultDiscountFactor if candidate.DiscountFactor > 0 { discount = candidate.DiscountFactor } if discount <= 0 { discount = 1 } if group, err := db.ResolveUserGroupPolicy(ctx, user); err == nil { groupDiscount := floatFromAny(group.BillingDiscountPolicy["discountFactor"]) if groupDiscount > 0 { discount *= groupDiscount } } return discount } func billingLine(candidate store.RuntimeModelCandidate, resourceType string, unit string, quantity any, amount float64, discount float64, simulated bool) map[string]any { return map[string]any{ "model": candidate.ModelName, "modelAlias": candidate.ModelAlias, "provider": candidate.Provider, "platformId": candidate.PlatformID, "platformModelId": candidate.PlatformModelID, "resourceType": resourceType, "unit": unit, "quantity": quantity, "amount": amount, "currency": "resource", "discountFactor": discount, "simulated": simulated, } } func price(config map[string]any, key string) float64 { value := floatFromAny(config[key]) if value > 0 { return value } return 0 } func resourcePrice(config map[string]any, resource string, keys ...string) float64 { for _, key := range keys { if value := price(config, key); value > 0 { return value } } if resourceConfig, ok := config[resource].(map[string]any); ok { for _, key := range keys { if value := floatFromAny(resourceConfig[key]); value > 0 { return value } } if value := floatFromAny(resourceConfig["basePrice"]); value > 0 { return value } } if resource == "image_edit" { return resourcePrice(config, "image", keys...) } return 0 } func weighted(config map[string]any, key string, name string) float64 { if strings.TrimSpace(name) == "" { return 1 } weights, _ := config[key].(map[string]any) if value := floatFromAny(weights[name]); value > 0 { return value } return 1 } func resourceWeight(config map[string]any, resource string, key string, name string) float64 { if value := weighted(config, key, name); value != 1 { return value } if strings.TrimSpace(name) == "" { return 1 } resourceConfig, _ := config[resource].(map[string]any) if len(resourceConfig) == 0 && resource == "image_edit" { resourceConfig, _ = config["image"].(map[string]any) } if weights, ok := resourceConfig["dynamicWeight"].(map[string]any); ok { if value := floatFromAny(weights[name]); value > 0 { return value } } if weights, ok := resourceConfig[key].(map[string]any); ok { if value := floatFromAny(weights[name]); value > 0 { return value } } return 1 } func firstNonEmptyString(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return strings.TrimSpace(value) } } return "" } func roundPrice(value float64) float64 { return math.Round(value*1000000) / 1000000 } func mergeMap(base map[string]any, override map[string]any) map[string]any { out := map[string]any{} for key, value := range base { out[key] = value } for key, value := range override { out[key] = value } return out }