packages feed

baikai-0.7.0.0: test/PricingPolicySpec.hs

{-# LANGUAGE OverloadedRecordDot #-}

module PricingPolicySpec (tests) where

import Baikai.CacheRetention (CacheRetention (..))
import Baikai.Cost qualified as C
import Baikai.Cost.Pricing (computeCost, computeCostAtRates, computeCostForService, computeCostWith, resolveRates)
import Baikai.Evidence qualified as Ev
import Baikai.Model qualified as M
import Baikai.Models.Generated qualified as Models
import Baikai.Usage qualified as U
import Baikai.Usage.Normalize qualified as N
import Control.Lens ((&), (.~))
import Control.Monad (forM_)
import Data.Aeson qualified as Aeson
import Data.Aeson.KeyMap qualified as KM
import Data.Set qualified as Set
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))

tests :: TestTree
tests =
  testGroup
    "Pricing policy"
    [ testCase "requested tiers never substitute for observed service" $ do
        let unknown = N.normalizeUsage N.InclusiveInput (N.ReportedUsage (Just 1000) (Just 0) (Just 0) (Just 0) Nothing)
            standard = U.observeBilling [U.BillingServiceTier "default"] unknown
            priority = U.observeBilling [U.BillingServiceTier "priority"] unknown
        (computeCostForService Nothing (Just "default") astra unknown).basis.estimateReasons @?= Set.singleton C.ServiceTierNotReported
        (computeCostForService Nothing (Just "default") astra standard).basis.estimateReasons @?= Set.empty
        (computeCostForService Nothing (Just "default") astra priority).basis.estimateReasons @?= Set.fromList [C.UnsupportedServiceTier "priority", C.ServiceTierMismatch "default" "priority"]
        assertBool "observed tier joins commitment" (Ev.usageEnvelope standard /= Ev.usageEnvelope priority),
      testCase "standard-only matches observed standard and fast remains an explicit estimate" $ do
        let u = N.normalizeUsage N.ExclusiveInput (N.ReportedUsage (Just 1000) (Just 0) (Just 0) (Just 0) Nothing)
            standard = U.observeBilling [U.BillingServiceTier "standard", U.BillingSpeed "standard"] u
            fast = U.observeBilling [U.BillingServiceTier "standard", U.BillingSpeed "fast"] u
        (computeCostForService Nothing (Just "standard_only") fable standard).basis.estimateReasons @?= Set.empty
        (computeCostForService Nothing Nothing fable fast).basis.estimateReasons @?= Set.singleton (C.UnsupportedSpeed "fast")
        (computeCostForService Nothing Nothing fable fast).usd @?= (computeCost fable u).usd,
      testCase "server-side tool products are explicitly outside token charges" $ do
        let u = U.observeBilling [U.BillingServiceTier "standard", U.BillingServerToolUse] (U.zeroUsage & #inputTokens .~ 1000)
        (computeCostForService Nothing Nothing fable u).basis.estimateReasons @?= Set.singleton C.AdditionalChargesExcluded,
      testCase "resolved rate seam prices a speed policy exactly once" $ do
        let u = U.zeroUsage & #inputTokens .~ 1000 & #outputTokens .~ 100
            doubled = M.ModelCost 20 100 2 25
            selected = computeCostAtRates doubled u
        selected.usd @?= 2 * (computeCost astra u).usd
        selected.basis.sources @?= Set.singleton C.ResolvedTokenRates,
      testCase "legacy availability JSON preserves its encoding without billing facts" $ do
        let old = Aeson.object ["missing_categories" Aeson..= ([] :: [U.UsageCategory]), "inconsistent" Aeson..= False]
        case Aeson.fromJSON old of
          Aeson.Success facts -> Aeson.toJSON (facts :: U.UsageAvailability) @?= old
          Aeson.Error err -> assertFailure err,
      testCase "context thresholds are exclusive and price the whole request" $
        forM_ [(271999, base), (272000, base), (272001, high)] $ \(n, expectedRates) -> do
          let u = U.zeroUsage & #inputTokens .~ n & #outputTokens .~ 100
          resolveRates Nothing astra u @?= Right expectedRates
          (computeCost astra u).usd @?= (fromIntegral n * expectedRates.inputCost + 100 * expectedRates.outputCost) / 1000000,
      testCase "272001 input plus 100 output costs exactly 5.44752" $
        (computeCost astra (U.zeroUsage & #inputTokens .~ 272001 & #outputTokens .~ 100)).usd @?= 544752 / 100000,
      testCase "cache reads and writes both contribute to context threshold" $ do
        forM_ [U.zeroUsage & #inputTokens .~ 272000 & #cacheReadTokens .~ 1, U.zeroUsage & #inputTokens .~ 272000 & #cacheWriteTokens .~ 1] $ \u -> resolveRates Nothing astra u @?= Right high
        resolveRates Nothing astra (U.zeroUsage & #cacheReadTokens .~ 136000 & #cacheWriteTokens .~ 136000) @?= Right base,
      testCase "Fable cache reads and shaped write duration use exact rates" $ do
        (computeCost fable (U.zeroUsage & #cacheReadTokens .~ 1000)).usd @?= 1 / 4000
        (computeCostWith (Just CacheRetentionShort) fable (U.zeroUsage & #cacheWriteTokens .~ 1000)).usd @?= 1 / 80
        (computeCostWith (Just CacheRetentionLong) fable (U.zeroUsage & #cacheWriteTokens .~ 1000)).usd @?= 1 / 50,
      testCase "reasoning is a subset of output, not an extra charge" $ do
        let u = U.zeroUsage & #outputTokens .~ 100
        computeCost astra (u & #reasoningTokens .~ Just 75) @?= computeCost astra u,
      testCase "flat policies retain base calculations" $ do
        let flat = astra & #pricingPolicy .~ Nothing
        resolveRates (Just CacheRetentionLong) flat (U.zeroUsage & #inputTokens .~ 900000) @?= Right base,
      testCase "duplicate, unordered and negative policy data are rejected" $ do
        forM_ [M.PricingPolicy [M.InputPriceTier 2 high, M.InputPriceTier 2 base] Nothing, M.PricingPolicy [M.InputPriceTier 2 high, M.InputPriceTier 1 base] Nothing, M.PricingPolicy [] (Just (-1)), M.PricingPolicy [M.InputPriceTier 1 (base & #inputCost .~ (-1))] Nothing] $ \p -> do
          assertBool "pure validation rejects" (case M.validatePricingPolicy p of Left _ -> True; _ -> False)
          case Aeson.fromJSON (Aeson.toJSON p) :: Aeson.Result M.PricingPolicy of Aeson.Error _ -> pure (); _ -> assertFailure "invalid policy decoded"
        let negative = Aeson.object ["inputTiers" Aeson..= [Aeson.object ["inputAbove" Aeson..= (-1 :: Int), "rates" Aeson..= base]]]
        case Aeson.fromJSON negative :: Aeson.Result M.PricingPolicy of Aeson.Error _ -> pure (); _ -> assertFailure "negative threshold decoded",
      testCase "old model JSON without a policy decodes and new policy round trips" $ do
        let old = case Aeson.toJSON M.emptyModel of Aeson.Object o -> Aeson.Object (KM.delete "pricingPolicy" o); v -> v
        case Aeson.fromJSON old of Aeson.Success m -> (m :: M.Model).pricingPolicy @?= Nothing; Aeson.Error err -> assertFailure err
        case Aeson.fromJSON (Aeson.toJSON astra) of Aeson.Success m -> (m :: M.Model) @?= astra; Aeson.Error err -> assertFailure err,
      testCase "estimated components retain reasons and provenance when summed" $ do
        let known = computeCost fable (U.zeroUsage & #cacheReadTokens .~ 1000)
            missing = C.estimateCost [C.CacheWriteUsageNotReported] known
            unknown = C.estimateCost [C.ServiceTierNotReported, C.CacheWriteUsageNotReported] known
            total = known <> missing <> unknown
        total.usd @?= 3 * known.usd
        total.basis.estimateReasons @?= Set.fromList [C.CacheWriteUsageNotReported, C.ServiceTierNotReported]
        total.basis.sources @?= Set.singleton C.StandardTokenRates
        mempty <> total @?= total
        total <> mempty @?= total
        (known <> missing) <> unknown @?= known <> (missing <> unknown)
        case Aeson.fromJSON (Aeson.toJSON total.basis) of Aeson.Success decoded -> decoded @?= total.basis; Aeson.Error err -> assertFailure err,
      testCase "unavailable prices carry an explicit estimate reason" $
        (computeCost M.emptyModel (U.zeroUsage & #inputTokens .~ 50)).basis.estimateReasons @?= Set.singleton C.PricingUnavailable
    ]

base :: M.ModelCost
base = M.ModelCost 10 50 1 (25 / 2)

high :: M.ModelCost
high = M.ModelCost 20 75 2 25

astra :: M.Model
astra = Models.openai_gpt_6_astra

fable :: M.Model
fable = Models.anthropic_claude_fable_5_1