fix(billing): 封住发布前计费竞态
ci / verify (pull_request) Failing after 8s

阻止上游提交状态不明的任务被租约接管后重复执行,并为人工复核保留可操作的结算记录。

将生产提交绑定到当前估价签名,统一复用预处理快照,并补强规则形状、定点溢出与历史规则兼容校验。

已通过 PostgreSQL 16 集成测试、Go 全量测试与静态检查、前端测试与构建、OpenAPI、依赖审计、镜像、迁移、流水线和 SemVer 门禁。
This commit is contained in:
2026-07-21 10:23:58 +08:00
parent 257ee09e58
commit 8beb8501fa
18 changed files with 762 additions and 75 deletions
@@ -2,7 +2,10 @@ package runner
import (
"context"
"errors"
"time"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
const (
@@ -19,6 +22,9 @@ func (s *Service) renewTaskExecutionLease(ctx context.Context, cancel context.Ca
return
case <-ticker.C:
if err := s.store.RenewTaskExecutionLease(ctx, taskID, executionToken, taskExecutionLeaseTTL); err != nil {
if errors.Is(err, store.ErrTaskExecutionFinished) {
return
}
s.logger.Warn("task execution lease lost", "taskID", taskID, "error_category", "task_execution_lease_lost")
cancel()
return
+5 -2
View File
@@ -38,8 +38,11 @@ func (s *Service) Estimate(ctx context.Context, kind string, model string, body
}
estimates := make([]candidateEstimate, 0, len(candidates))
for _, candidate := range candidates {
candidateBody := preprocessRequest(kind, cloneMap(body), candidate)
estimate, candidateErr := s.estimateCandidateV2(ctx, user, kind, candidateBody, candidate)
preprocessing := s.preprocessRequestWithScripts(ctx, kind, body, candidate)
if preprocessing.Err != nil {
return EstimateResult{}, parameterPreprocessClientError(preprocessing.Err)
}
estimate, candidateErr := s.estimateCandidateV2(ctx, user, kind, preprocessing.Body, candidate)
if candidateErr != nil {
return EstimateResult{}, candidateErr
}
+196 -42
View File
@@ -20,9 +20,11 @@ import (
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
@@ -129,11 +131,11 @@ func fixedAmountFromAny(value any) (fixedAmount, error) {
case json.Number:
return parseFixedAmount(typed.String())
case int:
return fixedAmount(int64(typed) * fixedScale), nil
return parseFixedAmount(strconv.FormatInt(int64(typed), 10))
case int64:
return fixedAmount(typed * fixedScale), nil
return parseFixedAmount(strconv.FormatInt(typed, 10))
case int32:
return fixedAmount(int64(typed) * fixedScale), nil
return parseFixedAmount(strconv.FormatInt(int64(typed), 10))
case float64:
if math.IsNaN(typed) || math.IsInf(typed, 0) {
return 0, fmt.Errorf("non-finite decimal")
@@ -159,26 +161,37 @@ func (a fixedAmount) String() string {
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 addFixedAmounts(left fixedAmount, right fixedAmount) (fixedAmount, error) {
value := new(big.Int).Add(big.NewInt(int64(left)), big.NewInt(int64(right)))
return fixedAmountFromBigInt(value)
}
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 multiplyFixedAmountByInt(amount fixedAmount, multiplier int) (fixedAmount, error) {
value := new(big.Int).Mul(big.NewInt(int64(amount)), big.NewInt(int64(multiplier)))
return fixedAmountFromBigInt(value)
}
func (a fixedAmount) MulRatio(numerator int, denominator int) fixedAmount {
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
return 0, fmt.Errorf("division by zero")
}
product := new(big.Int).Mul(big.NewInt(int64(a)), big.NewInt(int64(numerator)))
return fixedAmount(roundBigIntRatio(product, big.NewInt(int64(denominator))))
product := new(big.Int).Mul(big.NewInt(int64(amount)), big.NewInt(int64(numerator)))
return fixedAmountFromBigInt(roundBigIntRatio(product, big.NewInt(int64(denominator))))
}
func roundBigIntRatio(numerator *big.Int, denominator *big.Int) int64 {
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)
@@ -189,7 +202,7 @@ func roundBigIntRatio(numerator *big.Int, denominator *big.Int) int64 {
if negative {
quotient.Neg(quotient)
}
return quotient.Int64()
return quotient
}
type resolvedPricing struct {
@@ -248,6 +261,105 @@ func (pricing resolvedPricing) price(resource string, keys ...string) (fixedAmou
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
@@ -474,8 +586,14 @@ func (s *Service) billingsWithResolvedPricingV2(
items := make([]any, 0, 3)
total := fixedAmount(0)
if uncachedInputTokens > 0 || cachedInputTokens == 0 {
amount := inputPrice.MulRatio(uncachedInputTokens, 1000).Mul(discount)
total = total.Add(amount)
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(),
@@ -486,8 +604,14 @@ func (s *Service) billingsWithResolvedPricingV2(
if priceErr != nil {
return nil, 0, resolvedPricing{}, priceErr
}
amount := cachedPrice.MulRatio(cachedInputTokens, 1000).Mul(discount)
total = total.Add(amount)
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(),
@@ -498,8 +622,14 @@ func (s *Service) billingsWithResolvedPricingV2(
if priceErr != nil {
return nil, 0, resolvedPricing{}, priceErr
}
amount := outputPrice.MulRatio(outputTokens, 1000).Mul(discount)
total = total.Add(amount)
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
@@ -524,12 +654,26 @@ func (s *Service) billingsWithResolvedPricingV2(
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)
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,
@@ -555,14 +699,32 @@ func (s *Service) billingsWithResolvedPricingV2(
if err != nil {
return nil, 0, resolvedPricing{}, err
}
amount := price.MulInt(count)
amount, calculationErr := pricing.calculate(resource, price, []int{count})
if calculationErr != nil {
return nil, 0, resolvedPricing{}, calculationErr
}
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")))))
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
}
amount = amount.Mul(discount)
return []any{buildLine(resource, unit, count, amount, nil)}, amount, pricing, nil
}
@@ -580,14 +742,6 @@ func (pricing resolvedPricing) requiredTextPrice(resource string, keys ...string
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"}
+42 -1
View File
@@ -1,6 +1,8 @@
package runner
import (
"errors"
"math"
"testing"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
@@ -16,11 +18,29 @@ func TestFixedAmountPreservesNineDecimalPlaces(t *testing.T) {
}
price := mustFixedAmount(t, "0.000000001")
if got, want := price.MulInt(3).String(), "0.000000003"; got != want {
product, err := multiplyFixedAmountByInt(price, 3)
if err != nil {
t.Fatalf("multiply fixed amount: %v", err)
}
if got, want := product.String(), "0.000000003"; got != want {
t.Fatalf("nano amount multiplication=%q, want %q", got, want)
}
}
func TestFixedAmountOperationsRejectOverflow(t *testing.T) {
maximum := fixedAmount(math.MaxInt64)
if _, err := multiplyFixedAmountByInt(maximum, 2); !errors.Is(err, errFixedAmountOverflow) {
t.Fatalf("multiply overflow error=%v", err)
}
if _, err := addFixedAmounts(maximum, 1); !errors.Is(err, errFixedAmountOverflow) {
t.Fatalf("add overflow error=%v", err)
}
pricing := resolvedPricing{RuleSetID: "overflow-rule"}
if _, err := pricing.calculate("image", maximum, []int{2}); !isPricingUnavailable(err) {
t.Fatalf("pricing overflow should be unavailable: %v", err)
}
}
func TestEstimatedOutputTokensUsesAliasesAndCapabilityFallback(t *testing.T) {
candidate := store.RuntimeModelCandidate{
ModelType: "text_generate",
@@ -97,6 +117,27 @@ func TestPricingAvailabilityRequiresExplicitFree(t *testing.T) {
}
}
func TestPricingWeightsUseFixedPrecisionAndRejectInvalidValues(t *testing.T) {
pricing := resolvedPricing{Config: map[string]any{
"image": map[string]any{
"dynamicWeight": map[string]any{
"qualityFactors": map[string]any{"high": "1.123456789", "broken": 0},
},
},
}}
weight, err := pricing.weight("image", "qualityWeights", "high")
if err != nil || weight.String() != "1.123456789" {
t.Fatalf("exact weight=%s err=%v", weight.String(), err)
}
if _, err := pricing.weight("image", "qualityWeights", "broken"); !isPricingUnavailable(err) {
t.Fatalf("invalid configured weight should make pricing unavailable: %v", err)
}
defaultWeight, err := pricing.weight("image", "qualityWeights", "unconfigured")
if err != nil || defaultWeight.String() != "1.000000000" {
t.Fatalf("default weight=%s err=%v", defaultWeight.String(), err)
}
}
func mustFixedAmount(t *testing.T, value string) fixedAmount {
t.Helper()
amount, err := parseFixedAmount(value)
+4
View File
@@ -49,6 +49,10 @@ func (w *asyncTaskWorker) Work(ctx context.Context, job *river.Job[asyncTaskArgs
w.service.logger.Debug("river async task execution lease already held", "taskID", task.ID, "riverJobID", job.ID)
return nil
}
if errors.Is(runErr, store.ErrTaskExecutionManualReview) {
w.service.logger.Warn("river async task moved to manual review after ambiguous upstream submission", "taskID", task.ID, "riverJobID", job.ID)
return nil
}
var queuedErr *TaskQueuedError
if errors.As(runErr, &queuedErr) {
return river.JobSnooze(queuedErr.Delay)
+52 -8
View File
@@ -302,6 +302,7 @@ func (s *Service) executeWithToken(ctx context.Context, task store.GatewayTask,
}
}
pricingByCandidate := map[string]resolvedPricing{}
preprocessingByCandidate := map[string]parameterPreprocessResult{}
reservationBillings := []any(nil)
reservationPricingSnapshot := map[string]any(nil)
if task.RunMode == "production" {
@@ -316,9 +317,17 @@ func (s *Service) executeWithToken(ctx context.Context, task store.GatewayTask,
}
estimates := make([]candidateEstimate, 0, len(candidates))
var pricingErr error
var preprocessingErr error
var preprocessingCandidate store.RuntimeModelCandidate
for _, candidate := range candidates {
pricingBody := preprocessRequest(task.Kind, cloneMap(body), candidate)
estimate, estimateErr := s.estimateCandidateV2(ctx, user, task.Kind, pricingBody, candidate)
preprocessing := s.preprocessRequestWithScripts(ctx, task.Kind, body, candidate)
preprocessingByCandidate[pricingCandidateKey(candidate)] = preprocessing
if preprocessing.Err != nil {
preprocessingErr = parameterPreprocessClientError(preprocessing.Err)
preprocessingCandidate = candidate
break
}
estimate, estimateErr := s.estimateCandidateV2(ctx, user, task.Kind, preprocessing.Body, candidate)
if estimateErr != nil {
pricingErr = estimateErr
if billingMode == "enforce" {
@@ -329,8 +338,23 @@ func (s *Service) executeWithToken(ctx context.Context, task store.GatewayTask,
estimates = append(estimates, estimate)
pricingByCandidate[pricingCandidateKey(candidate)] = estimate.Pricing
}
if preprocessingErr != nil {
preprocessing := preprocessingByCandidate[pricingCandidateKey(preprocessingCandidate)]
s.recordFailedAttempt(ctx, failedAttemptRecord{
Task: task, Body: preprocessing.Body, Candidate: &preprocessingCandidate,
AttemptNo: task.AttemptCount + 1, Code: clients.ErrorCode(preprocessingErr), Cause: preprocessingErr,
Simulated: false, Scope: "parameter_preprocessing", Reason: "parameter_preprocessing_failed",
ExtraMetrics: []map[string]any{parameterPreprocessingMetrics(preprocessing.Log)}, Preprocessing: &preprocessing.Log,
ModelType: preprocessingCandidate.ModelType,
})
failed, finishErr := s.failTask(ctx, task.ID, task.ExecutionToken, clients.ErrorCode(preprocessingErr), preprocessingErr.Error(), false, preprocessingErr, parameterPreprocessingMetrics(preprocessing.Log))
if finishErr != nil {
return Result{}, finishErr
}
return Result{Task: failed, Output: failed.Result}, preprocessingErr
}
if billingMode == "observe" {
legacyItems, legacyAmount := s.maximumLegacyCandidateEstimate(ctx, user, task.Kind, body, candidates)
legacyItems, legacyAmount := s.maximumLegacyCandidateEstimate(ctx, user, task.Kind, body, candidates, preprocessingByCandidate)
reservationBillings = legacyItems
candidateSnapshots := make([]any, 0, len(estimates))
for _, estimate := range estimates {
@@ -396,7 +420,10 @@ func (s *Service) executeWithToken(ctx context.Context, task store.GatewayTask,
}
}()
if len(candidates) > 0 {
preprocessing := s.preprocessRequestWithScripts(ctx, task.Kind, body, candidates[0])
preprocessing, ok := preprocessingByCandidate[pricingCandidateKey(candidates[0])]
if !ok {
preprocessing = s.preprocessRequestWithScripts(ctx, task.Kind, body, candidates[0])
}
firstCandidateBody = preprocessing.Body
firstPreprocessing = preprocessing.Log
normalizedModelType = candidates[0].ModelType
@@ -472,7 +499,10 @@ candidatesLoop:
var candidateErr error
for clientAttempt := 1; clientAttempt <= clientAttempts; clientAttempt++ {
nextAttemptNo := attemptNo + 1
preprocessing := s.preprocessRequestWithScripts(ctx, task.Kind, body, candidate)
preprocessing, ok := preprocessingByCandidate[pricingCandidateKey(candidate)]
if !ok {
preprocessing = s.preprocessRequestWithScripts(ctx, task.Kind, body, candidate)
}
preprocessingLog := preprocessing.Log
lastPreprocessing = &preprocessingLog
if preprocessing.Err != nil {
@@ -768,11 +798,14 @@ func normalizedBillingEngineMode(value string) string {
}
}
func (s *Service) maximumLegacyCandidateEstimate(ctx context.Context, user *auth.User, kind string, body map[string]any, candidates []store.RuntimeModelCandidate) ([]any, fixedAmount) {
func (s *Service) maximumLegacyCandidateEstimate(ctx context.Context, user *auth.User, kind string, body map[string]any, candidates []store.RuntimeModelCandidate, preprocessingByCandidate map[string]parameterPreprocessResult) ([]any, fixedAmount) {
var maximumItems []any
maximumAmount := fixedAmount(0)
for index, candidate := range candidates {
candidateBody := preprocessRequest(kind, cloneMap(body), candidate)
if preprocessing, ok := preprocessingByCandidate[pricingCandidateKey(candidate)]; ok && preprocessing.Err == nil {
candidateBody = preprocessing.Body
}
items := s.estimatedBillings(ctx, user, kind, candidateBody, candidate)
amount := billingItemsFixedTotal(items)
if index == 0 || amount > maximumAmount {
@@ -792,7 +825,11 @@ func billingItemsFixedTotal(items []any) fixedAmount {
}
amount, err := fixedAmountFromAny(line["amount"])
if err == nil && amount > 0 {
total = total.Add(amount)
next, addErr := addFixedAmounts(total, amount)
if addErr != nil {
return maxFixedAmount
}
total = next
}
}
return total
@@ -925,6 +962,11 @@ func (s *Service) runCandidate(ctx context.Context, task store.GatewayTask, user
PreviousResponseTurns: responseExecution.PreviousTurns,
})
callFinishedAt := time.Now()
if err == nil {
if markErr := s.store.SetAttemptUpstreamSubmissionStatus(context.WithoutCancel(ctx), attemptID, "response_received"); markErr != nil {
return clients.Response{}, &upstreamSubmissionUnknownError{AttemptID: attemptID, Cause: markErr}
}
}
if response.ResponseStartedAt.IsZero() {
response.ResponseStartedAt = callStartedAt
}
@@ -939,7 +981,9 @@ func (s *Service) runCandidate(ctx context.Context, task store.GatewayTask, user
}
if err != nil {
if clients.ErrorResponseMetadata(err).StatusCode > 0 {
_ = s.store.SetAttemptUpstreamSubmissionStatus(context.WithoutCancel(ctx), attemptID, "response_received")
if markErr := s.store.SetAttemptUpstreamSubmissionStatus(context.WithoutCancel(ctx), attemptID, "response_received"); markErr != nil {
return clients.Response{}, &upstreamSubmissionUnknownError{AttemptID: attemptID, Cause: markErr}
}
}
retryable := clients.IsRetryable(err)
requestID, metrics, responseStartedAt, responseFinishedAt, responseDurationMS := failureMetrics(err, simulated)