baikai-0.7.0.0: src/Baikai/Cost/Pricing.hs
-- | Cost computation from a 'Baikai.Model.Model' and a 'Usage'.
--
-- Base prices and optional context/duration policies live on the model.
-- Arithmetic stays exact, and unavailable pricing carries an estimate reason.
module Baikai.Cost.Pricing
( computeCost,
computeCostAtSpeed,
attachCost,
resolveRates,
computeCostWith,
computeCostForService,
computeCostAtRates,
)
where
import Baikai.CacheRetention (CacheRetention (..))
import Baikai.Cost (Cost (..), CostBreakdown (..), CostEstimateReason (..), CostSource (..), estimateCost, standardCostBasis)
import Baikai.Message (AssistantPayload (..))
import Baikai.Model (InputPriceTier (..), Model, ModelCost (..), PricingPolicy (..), validatePricingPolicy, zeroModelCost)
import Baikai.Prelude
import Baikai.Response (Response (..))
import Baikai.Speed (Speed (..))
import Baikai.Usage (BillingFact (..), Usage (..), UsageAvailability (..), UsageCategory (..))
import Data.Set qualified as Set
-- | Compute a 'Cost' from a model's per-million-token rates and a
-- 'Usage'. Zero rates retain the old numeric total and now mark pricing
-- unavailable. This entry point assumes the standard cache duration.
-- Use 'computeCostAtSpeed' to select premium speed rates explicitly.
computeCost :: Model -> Usage -> Cost
computeCost = computeCostWith Nothing
-- | Duration must be the value selected by request shaping, not merely
-- requested by the caller. Service-tier and usage availability are supplied
-- by adapters as estimation reasons on the resulting cost.
computeCostWith :: Maybe CacheRetention -> Model -> Usage -> Cost
computeCostWith duration m u = priceUsage (resolveRates duration m u) u
-- | Shared terminal pricing entry point. Observed tiers and speed come from
-- Usage availability, never from the caller's preference. Uncurated products
-- retain a standard-rate estimate with a specific reason.
computeCostForService :: Maybe CacheRetention -> Maybe Text -> Model -> Usage -> Cost
computeCostForService duration requested m u =
let facts = maybe [] (Set.toList . billingFacts) (u ^. #availability)
tiers = [t | BillingServiceTier t <- facts]
speeds = [s | BillingSpeed s <- facts]
reasons =
[ServiceTierNotReported | null tiers]
<> [AdditionalChargesExcluded | BillingServerToolUse `elem` facts]
<> [UnsupportedServiceTier t | t <- tiers, t `notElem` ["default", "standard"]]
<> [UnsupportedSpeed s | s <- speeds, s /= "standard", s /= "fast" || m ^. #fastModeCost == Nothing]
<> [InconsistentUsage | length speeds > 1]
<> [ServiceTierMismatch wanted actual | Just wanted <- [requested], wanted /= "auto", actual <- tiers, not (matches wanted actual)]
in estimateCost reasons (if speeds == ["fast"] then priceAtSpeed duration m SpeedFast u else computeCostWith duration m u)
where
matches wanted actual = wanted == actual || (wanted == "standard_only" && actual == "standard") || (wanted == "fast" && actual == "priority")
-- | Price an explicitly selected speed with the standard cache duration.
-- Standard agrees exactly with 'computeCost'. Missing fast rates retain a
-- standard-rate estimate with 'UnsupportedSpeed', never a fabricated zero.
-- This helper does not claim the provider observed the selected speed.
computeCostAtSpeed :: Model -> Speed -> Usage -> Cost
computeCostAtSpeed = priceAtSpeed Nothing
priceAtSpeed :: Maybe CacheRetention -> Model -> Speed -> Usage -> Cost
priceAtSpeed duration m SpeedStandard u = computeCostWith duration m u
priceAtSpeed duration m SpeedFast u = case m ^. #fastModeCost of
Nothing -> estimateCost [UnsupportedSpeed "fast"] (computeCostWith duration m u)
Just fast ->
let resolved = do
validatePricingPolicy (PricingPolicy [InputPriceTier 0 fast] Nothing)
standard <- resolveRates duration m u
let base = m ^. #cost
-- Apply each premium rate's ratio to the resolved policy once.
-- A zero base cannot define a ratio for a nonzero policy rate.
scale b f r
| b > 0 = Right (r * f / b)
| r == 0 = Right f
| otherwise = Left "Cannot apply fast rates to a zero-base pricing policy"
ModelCost
<$> scale (inputCost base) (inputCost fast) (inputCost standard)
<*> scale (outputCost base) (outputCost fast) (outputCost standard)
<*> scale (cacheReadCost base) (cacheReadCost fast) (cacheReadCost standard)
<*> scale (cacheWriteCost base) (cacheWriteCost fast) (cacheWriteCost standard)
computed = priceUsage resolved u
in computed & #basis . #sources .~ Set.singleton ResolvedTokenRates
-- | Price one resolved rate record exactly once.
computeCostAtRates :: ModelCost -> Usage -> Cost
computeCostAtRates rates u =
let resolved = validatePricingPolicy (PricingPolicy [InputPriceTier 0 rates] Nothing) >> pure rates
computed = priceUsage resolved u
in computed & #basis . #sources .~ Set.singleton ResolvedTokenRates
priceUsage :: Either Text ModelCost -> Usage -> Cost
priceUsage resolved u =
let selected = either (const zeroModelCost) id resolved
problems = [InvalidPricingPolicy | Left _ <- [resolved]] <> [PricingUnavailable | selected == ModelCost 0 0 0 0]
rates = selected
inRate = inputCost rates
outRate = outputCost rates
crRate = cacheReadCost rates
cwRate = cacheWriteCost rates
inUsd = toRational (u ^. #inputTokens) * inRate / 1_000_000
outUsd = toRational (u ^. #outputTokens) * outRate / 1_000_000
cachedUsd = toRational (u ^. #cacheReadTokens) * crRate / 1_000_000
cacheWriteUsd = toRational (u ^. #cacheWriteTokens) * cwRate / 1_000_000
total = inUsd + outUsd + cachedUsd + cacheWriteUsd
in estimateCost
(problems <> usageProblems u)
Cost
{ usd = total,
basis = standardCostBasis,
breakdown =
CostBreakdown
{ inputUsd = inUsd,
outputUsd = outUsd,
cachedInputUsd = cachedUsd,
cachedWriteUsd = cacheWriteUsd
}
}
-- | Legacy, manually constructed usages have no availability annotation.
-- Normalized provider usages always carry one, even for an entirely absent body.
usageProblems :: Usage -> [CostEstimateReason]
usageProblems u = case u ^. #availability of
Nothing -> []
Just facts ->
[InconsistentUsage | inconsistent facts]
<> if Set.size (missingCategories facts) == 4
then [UsageNotReported]
else map reason (Set.toList (missingCategories facts))
where
reason InputUsage = InputUsageNotReported
reason OutputUsage = OutputUsageNotReported
reason CacheReadUsage = CacheReadUsageNotReported
reason CacheWriteUsage = CacheWriteUsageNotReported
-- | Choose one complete rate record. Thresholds are exclusive and use
-- disjoint normalized input categories, including both cache counters.
resolveRates :: Maybe CacheRetention -> Model -> Usage -> Either Text ModelCost
resolveRates duration m u = do
validatePricingPolicy (PricingPolicy [InputPriceTier 0 (m ^. #cost)] Nothing)
case m ^. #pricingPolicy of
Nothing -> pure (m ^. #cost)
Just policy -> do
validatePricingPolicy policy
let totalInput = (u ^. #inputTokens) + (u ^. #cacheReadTokens) + (u ^. #cacheWriteTokens)
selected = foldl' (\current tier -> if totalInput > inputAbove tier then rates tier else current) (m ^. #cost) (inputTiers policy)
pure $ case (duration, longCacheWriteCost policy) of
(Just CacheRetentionLong, Just price) -> selected {cacheWriteCost = price}
_ -> selected
-- | Replace the assistant response payload's embedded 'Cost' with one
-- computed from the supplied model.
attachCost :: Model -> Response -> Response
attachCost m r =
let AssistantPayload
{ usage = u,
content = c,
stopReason = sr,
errorMessage = em,
timestamp = ts
} = r ^. #message
computed = computeCost m u
u' = u & #cost .~ computed
msg' =
AssistantPayload
{ content = c,
usage = u',
stopReason = sr,
errorMessage = em,
timestamp = ts
}
in r & #message .~ msg'