packages feed

shikumi-tools-0.4.0.0: test/ResponsesSchemaSpec.hs

module ResponsesSchemaSpec (tests) where

import Baikai qualified as B
import Control.Lens ((&), (.~), (^.))
import Data.Aeson (Value (..), object, (.=))
import Data.Aeson.Key (Key)
import Data.Aeson.KeyMap qualified as KM
import Data.Generics.Labels ()
import Data.Vector qualified as V
import Effectful (runEff)
import Effectful.Error.Static (runErrorNoCallStack)
import Shikumi.Adapter (attachSchema)
import Shikumi.Error (ShikumiError)
import Shikumi.LLM qualified as L
import Shikumi.LLM.Defaults
import Shikumi.Program (runProgram)
import Shikumi.Routing (routeLLM, runRouting)
import Shikumi.Schema (deriveSchema)
import Shikumi.Testing.Fixtures
import Shikumi.Testing.Responses
import Test.Tasty
import Test.Tasty.HUnit

field :: [Key] -> Value -> Maybe Value
field [] v = Just v
field (k : ks) (Object o) = KM.lookup k o >>= field ks
field _ _ = Nothing

tests :: TestTree
tests =
  testGroup
    "Responses real adapter"
    [ testCase "typed schema, defaults and streamed terminal cross actual HTTP" $
        withResponsesFixture (replicate 2 (sseReply [completed [messageItem "{\"answer\":\"Forty-two\",\"confidence\":0.9}"]])) $ \f -> do
          let defaults = emptyRequestDefaults {defaultThinking = Just B.ThinkingHigh, defaultMaxTokens = Just 128}
              schema = object ["type" .= String "object", "properties" .= object ["answer" .= object ["type" .= String "string"]]]
          result <-
            runEff
              . runErrorNoCallStack @ShikumiError
              . runRouting (model f)
              . L.runLLMWith (registry f)
              . withTransportOptions fixtureOptions
              . withRequestDefaults defaults
              . routeLLM
              $ runProgram instructedProg (Question "What is the answer?")
          result @?= Right (Answer "Forty-two" 0.9)
          streamed <-
            runEff
              . runErrorNoCallStack @ShikumiError
              . runRouting (model f)
              . L.runLLMWith (registry f)
              . withTransportOptions fixtureOptions
              . withRequestDefaults defaults
              . routeLLM
              $ L.stream (model f) B.emptyContext (attachSchema schema B.emptyOptions)
          case streamed of
            Right events -> case [p | B.EventDone tp <- events, B.AssistantMessage p <- [tp ^. #message]] of
              [p] -> do
                p ^. #usage . #inputTokens @?= 20
                p ^. #usage . #outputTokens @?= 5
              _ -> assertFailure "missing terminal payload"
            Left e -> assertFailure (show e)
          bodies <- requests f
          length bodies @?= 2
          map (field ["text", "format", "schema"]) bodies @?= [Just (deriveSchema @Answer), Just schema]
          mapM_
            ( \body -> do
                field ["model"] body @?= Just (String "fixture-reasoner")
                field ["stream"] body @?= Just (Bool True)
                field ["store"] body @?= Just (Bool False)
                field ["max_output_tokens"] body @?= Just (Number 128)
                field ["reasoning", "effort"] body @?= Just (String "high")
                field ["text", "format", "type"] body @?= Just (String "json_schema")
                field ["text", "format", "strict"] body @?= Just (Bool True)
                field ["include"] body @?= Just (Array (V.singleton (String "reasoning.encrypted_content")))
            )
            bodies,
      testCase "unsupported stop and image tool output fail before HTTP" $
        withResponsesFixture [] $ \f -> do
          let badImage = B.ToolResultMessage (B.ToolResultPayload "call" "lookup" (V.singleton (B.ToolResultImage (B.ImageContent "bytes" "image/png"))) False Nothing)
          -- Both failures are exercised through the released mapper.
          a <-
            runEff . runErrorNoCallStack @ShikumiError . L.runLLMWith (registry f) $
              L.complete (model f) B.emptyContext (fixtureOptions & #stopSequences .~ ["stop"])
          b <-
            runEff . runErrorNoCallStack @ShikumiError . L.runLLMWith (registry f) $
              L.complete (model f) (B.emptyContext & #messages .~ V.singleton badImage) fixtureOptions
          assertBool "stop rejected" (either (const True) (const False) a)
          assertBool "image result rejected" (either (const True) (const False) b)
          requests f >>= (@?= [])
    ]