packages feed

shikumi-0.4.0.0: src/Shikumi/LLM/Defaults.hs

-- | Invocation-scoped, fill-only defaults for every LLM operation.
-- Compose the base interpreter with cache/trace, then 'withRequestDefaults',
-- then routing. The rightmost wrapper sees the originating call first.
module Shikumi.LLM.Defaults
  ( RequestDefaults (..),
    emptyRequestDefaults,
    applyRequestDefaults,
    withRequestDefaults,
  )
where

import Baikai (Options)
import Baikai.Evidence (EvidenceRequest)
import Baikai.Speed (Speed)
import Baikai.ThinkingLevel (ThinkingLevel)
import Control.Applicative ((<|>))
import Control.Lens ((&), (.~), (^.))
import Control.Monad (when)
import Data.Generics.Labels ()
import Effectful (Eff, (:>))
import Effectful.Dispatch.Dynamic (interpose)
import Effectful.Error.Static (Error, throwError)
import Numeric.Natural (Natural)
import Shikumi.Error (ShikumiError (ValidationFailure))
import Shikumi.LLM (LLM (..), complete, stream)

-- | Only request preferences belong here. Models, schemas, tools, credentials,
-- temperatures and private continuation metadata remain owned by their callers.
-- Evidence is replaced as a whole; requested settings are not provider evidence.
data RequestDefaults = RequestDefaults
  { defaultThinking :: !(Maybe ThinkingLevel),
    defaultSpeed :: !(Maybe Speed),
    defaultMaxTokens :: !(Maybe Natural),
    defaultEvidence :: !(Maybe EvidenceRequest)
  }
  deriving stock (Eq, Show)

-- | No implicit reasoning, fast mode, token ceiling or evidence collection.
emptyRequestDefaults :: RequestDefaults
emptyRequestDefaults = RequestDefaults Nothing Nothing Nothing Nothing

-- | Fill absent options only. This pure merge does not validate token ceilings;
-- use 'withRequestDefaults' to reject a zero configured default before execution.
applyRequestDefaults :: RequestDefaults -> Options -> Options
applyRequestDefaults defaults opts =
  opts
    & #thinking .~ (opts ^. #thinking <|> defaultThinking defaults)
    & #speed .~ (opts ^. #speed <|> defaultSpeed defaults)
    & #maxTokens .~ (opts ^. #maxTokens <|> defaultMaxTokens defaults)
    & #evidence .~ (opts ^. #evidence <|> defaultEvidence defaults)

-- | Apply the same merge to blocking and streaming calls. Per-call values win;
-- the innermost default scope fills first and therefore wins over outer scopes.
-- A zero default ceiling is invalid even when a call supplies its own ceiling.
-- No mutable state is shared between invocations.
withRequestDefaults :: (LLM :> es, Error ShikumiError :> es) => RequestDefaults -> Eff es a -> Eff es a
withRequestDefaults defaults action = do
  when (defaultMaxTokens defaults == Just 0) $
    throwError (ValidationFailure "request defaults: defaultMaxTokens must be positive")
  interpose
    ( \_ -> \case
        Complete model ctx opts -> complete model ctx (applyRequestDefaults defaults opts)
        Stream model ctx opts -> stream model ctx (applyRequestDefaults defaults opts)
    )
    action