packages feed

baikai-openai-0.7.3.0: src/Baikai/Provider/OpenAI/Internal/Cache.hs

{-# LANGUAGE OverloadedRecordDot #-}

-- | Internal native cache planning and terminal pricing. One prepared policy
-- supplies the wire controls, prefix placement and effective billing duration.
module Baikai.Provider.OpenAI.Internal.Cache
  ( CachePlan (..),
    responsesCachePlan,
    chatCachePlan,
    cacheCost,
  )
where

import Baikai.CacheRetention (CacheRetention (..))
import Baikai.Compat
import Baikai.Cost qualified as C
import Baikai.Cost.Pricing qualified as Pricing
import Baikai.Model qualified as M
import Baikai.Options (Options (..))
import Baikai.Usage qualified as U
import Data.Aeson (Value (..), object, (.=))
import Data.Aeson.Key (Key)
import Data.Text (Text)

data CachePlan = CachePlan
  { cacheFields :: ![(Key, Value)],
    markPrefix :: !Bool,
    effectiveRetention :: !(Maybe CacheRetention)
  }
  deriving stock (Eq, Show)

responsesCachePlan :: M.Model -> Options -> Either Text CachePlan
responsesCachePlan m opts = do
  let c = M.openaiResponsesCompatFor m
      -- Absent metadata preserves the prior Responses mappings, but never
      -- invents explicit breakpoint support.
      policy = case c.nativeCachePolicy of
        Just p -> p
        Nothing
          | c.supportsPromptCacheOptions -> NativeCacheModern False False
          | otherwise -> NativeCacheLegacy True c.supportsLongCacheRetention
  plan m Nothing (Just policy) opts

chatCachePlan :: M.Model -> Options -> Either Text CachePlan
chatCachePlan m opts =
  let c = M.openaiCompletionsCompatFor m
   in do
        native <- plan m c.cacheControlFormat c.nativeCachePolicy opts
        -- The existing compatible-host downgrade is an independent wire contract.
        pure $ case (c.cacheControlFormat, opts.cacheRetention) of
          (Just CacheControlFormatAnthropic, Just CacheRetentionShort) -> native {effectiveRetention = Just CacheRetentionShort}
          (Just CacheControlFormatAnthropic, Just CacheRetentionLong) -> native {effectiveRetention = Just (if c.supportsLongCacheRetention then CacheRetentionLong else CacheRetentionShort)}
          _ -> native

plan :: M.Model -> Maybe CacheControlFormat -> Maybe NativeCachePolicy -> Options -> Either Text CachePlan
plan m format policy opts = named $ do
  validateNativeCachePolicy format policy
  case M.pricingPolicy m of
    Just p -> M.validatePricingPolicy p
    Nothing -> Right ()
  case policy of
    Just (NativeCacheModern True _) -> requireLongRates
    _ -> Right ()
  case opts.cacheRetention of
    Nothing -> pure absent
    Just CacheRetentionNone -> pure absent
    Just requested -> case policy of
      Nothing -> pure absent -- native Chat is opt-in through endpoint facts
      Just NativeCacheUnsupported -> refuse
      Just (NativeCacheLegacy short long) -> case requested of
        CacheRetentionShort | short -> legacy "in_memory"
        CacheRetentionLong | long -> legacy "24h"
        _ -> refuse
      Just (NativeCacheModern long prefix) -> case requested of
        CacheRetentionShort -> modern "30m" prefix CacheRetentionShort
        CacheRetentionLong | long -> modern "12h" prefix CacheRetentionLong
        _ -> refuse
  where
    named = either (Left . ((M.modelId m <> ": ") <>)) Right
    absent = CachePlan [] False $ case policy of
      Just (NativeCacheModern _ _) -> Just CacheRetentionShort
      _ -> Nothing
    refuse = Left "native cache policy cannot honor requested retention"
    -- Legacy 24h has no duration premium. Always resolve base write prices.
    legacy ttl = pure (CachePlan [("prompt_cache_retention", String ttl)] False (Just CacheRetentionShort))
    modern ttl prefix duration = pure (CachePlan [("prompt_cache_options", object ["ttl" .= (ttl :: Text)])] prefix (Just duration))
    requireLongRates = case M.pricingPolicy m of
      Just p
        | Just _ <- p.longCacheWriteCost,
          all (\t -> t.longCacheWriteCost /= Nothing) p.inputTiers ->
            Right ()
      _ -> Left "modern Long requires base and every tier long cache-write rates"

-- | Unknown duration remains an explicit estimate only when it matters to
-- reported writes. Do not infer duration from a read counter.
cacheCost :: Maybe CacheRetention -> M.Model -> U.Usage -> C.Cost
cacheCost duration m usage = C.estimateCost reasons (Pricing.computeCostForService duration Nothing m usage)
  where
    reasons = [C.CacheDurationNotReported | duration == Nothing, usage.cacheWriteTokens > 0, durationSensitive]
    durationSensitive = case m.pricingPolicy of
      Nothing -> False
      Just p -> p.longCacheWriteCost /= Nothing || any (\t -> t.longCacheWriteCost /= Nothing) p.inputTiers