packages feed

shikumi-0.4.0.0: test/RequestDefaultsSpec.hs

module RequestDefaultsSpec (tests) where

import Baikai (ToolChoice (..), emptyContext, emptyModel, emptyOptions, emptyResponse)
import Baikai.Evidence qualified as E
import Baikai.Speed (Speed (..))
import Baikai.ThinkingLevel (ThinkingLevel (..))
import Control.Concurrent (forkFinally, newEmptyMVar, putMVar, takeMVar)
import Control.Lens ((&), (.~), (^.))
import Data.Aeson (Value (..))
import Data.Generics.Labels ()
import Data.IORef (modifyIORef', newIORef, readIORef)
import Data.Map.Strict qualified as Map
import Effectful (liftIO, runEff)
import Effectful.Dispatch.Dynamic (interpret)
import Effectful.Error.Static (runErrorNoCallStack)
import Shikumi.Error (ShikumiError (..), isTransient)
import Shikumi.LLM (LLM (..), complete, stream)
import Shikumi.LLM.Defaults
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit

defaults :: RequestDefaults
defaults = RequestDefaults (Just ThinkingHigh) (Just SpeedFast) (Just 4096) (Just (E.evidenceRequest "default"))

tests :: TestTree
tests =
  testGroup
    "Request defaults"
    [ testCase "empty is identity and unrelated policy and metadata survive" $ do
        let opts =
              emptyOptions
                & #temperature .~ Just 0.7
                & #toolChoice .~ Just ToolChoiceAuto
                & #metadata .~ Map.fromList [("shikumi.continuation.v1", String "protected"), ("schema", Bool True)]
        applyRequestDefaults emptyRequestDefaults opts @?= opts
        let merged = applyRequestDefaults defaults opts
        merged ^. #temperature @?= opts ^. #temperature
        merged ^. #toolChoice @?= opts ^. #toolChoice
        merged ^. #metadata @?= opts ^. #metadata
        merged @?= (opts & #thinking .~ Just ThinkingHigh & #speed .~ Just SpeedFast & #maxTokens .~ Just 4096 & #evidence .~ defaultEvidence defaults),
      testCase "explicit fields and whole evidence object override every default" $ do
        let evidence = (E.evidenceRequest "explicit") {E.strictness = E.EvidenceRequired E.EvidenceFullyObserved, E.attempt = 3, E.supersedes = Just "previous"}
            opts = emptyOptions & #thinking .~ Just ThinkingLow & #speed .~ Just SpeedStandard & #maxTokens .~ Just 12 & #evidence .~ Just evidence
        applyRequestDefaults defaults opts @?= opts
        applyRequestDefaults defaults (applyRequestDefaults defaults emptyOptions) @?= applyRequestDefaults defaults emptyOptions,
      testCase "blocking and streaming use nearest scope and preserve explicit values" $ do
        ref <- newIORef []
        result <- runEff
          . runErrorNoCallStack @ShikumiError
          . interpret
            ( \_ -> \case
                Complete _ _ o -> liftIO (modifyIORef' ref (++ [o])) >> pure emptyResponse
                Stream _ _ o -> liftIO (modifyIORef' ref (++ [o])) >> pure []
            )
          . withRequestDefaults defaults
          $ do
            _ <- complete emptyModel emptyContext emptyOptions
            withRequestDefaults (emptyRequestDefaults {defaultSpeed = Just SpeedStandard}) $ do
              _ <- stream emptyModel emptyContext emptyOptions
              _ <- complete emptyModel emptyContext (emptyOptions & #speed .~ Just SpeedFast)
              pure ()
        result @?= Right ()
        captured <- readIORef ref
        map (^. #speed) captured @?= [Just SpeedFast, Just SpeedStandard, Just SpeedFast]
        map (^. #thinking) captured @?= replicate 3 (Just ThinkingHigh)
        map (^. #evidence) captured @?= replicate 3 (defaultEvidence defaults),
      testCase "zero default ceiling rejects before either dispatch or action" $ do
        ref <- newIORef (0 :: Int)
        result <- runEff
          . runErrorNoCallStack @ShikumiError
          . interpret
            ( \_ -> \case
                Complete {} -> liftIO (modifyIORef' ref (+ 1)) >> pure emptyResponse
                Stream {} -> liftIO (modifyIORef' ref (+ 1)) >> pure []
            )
          . withRequestDefaults (defaults {defaultMaxTokens = Just 0})
          $ do
            liftIO (modifyIORef' ref (+ 1))
            _ <- complete emptyModel emptyContext (emptyOptions & #maxTokens .~ Just 12)
            pure ()
        result @?= Left (ValidationFailure "request defaults: defaultMaxTokens must be positive")
        readIORef ref >>= (@?= 0)
        isTransient (ValidationFailure "invalid configuration") @?= False,
      testCase "concurrent invocations retain independent settings" $ do
        let run :: Speed -> IO [Maybe Speed]
            run speed = do
              ref <- newIORef []
              r <- runEff
                . runErrorNoCallStack @ShikumiError
                . interpret
                  ( \_ -> \case
                      Complete _ _ o -> liftIO (modifyIORef' ref (++ [o ^. #speed])) >> pure emptyResponse
                      Stream _ _ o -> liftIO (modifyIORef' ref (++ [o ^. #speed])) >> pure []
                  )
                . withRequestDefaults (emptyRequestDefaults {defaultSpeed = Just speed})
                $ do
                  _ <- complete emptyModel emptyContext emptyOptions
                  _ <- stream emptyModel emptyContext emptyOptions
                  pure ()
              r @?= Right ()
              readIORef ref
        a <- newEmptyMVar
        b <- newEmptyMVar
        _ <- forkFinally (run SpeedFast) (putMVar a)
        _ <- forkFinally (run SpeedStandard) (putMVar b)
        takeMVar a >>= either (assertFailure . show) (@?= replicate 2 (Just SpeedFast))
        takeMVar b >>= either (assertFailure . show) (@?= replicate 2 (Just SpeedStandard))
    ]