baikai-0.6.0.0: src/Baikai/Options.hs
-- | The 'Options' record — the per-call knobs that vary between
-- requests on the same conversation.
--
-- 'apiKey' is 'Nothing' by default; API providers then consult the
-- host-specific env var from 'Baikai.Auth.defaultApiKeyEnvForBaseUrl'
-- after substituting their default base URL. Unknown hosts require an
-- explicit key source instead of falling back to another provider's
-- credential.
-- 'maxTokens' defaults to 'Nothing'; the handler falls back to the
-- chosen model's 'Baikai.Model.maxOutputTokens'.
-- 'topP' and 'stopSequences' are honored by the Anthropic and
-- OpenAI-compatible API providers. 'seed', 'frequencyPenalty', and
-- 'presencePenalty' are honored by the OpenAI-compatible API
-- provider. Providers with no corresponding upstream parameter
-- silently omit the field, matching the existing drop policy for
-- unsupported per-call knobs on CLI providers.
--
-- 'timeoutMs' is a wall-clock bound on the entire API streaming call
-- in the OpenAI and Claude providers: connection setup, response
-- headers, and full stream drain. On expiry the stream terminates
-- in-band with a retryable transient 'Baikai.Error.BaikaiError'.
-- @Just n@ with @n <= 0@ is refused as
-- 'Baikai.Error.InvalidRequest' before any connection is opened;
-- 'Nothing' is the only spelling of \"no bound\".
--
-- 'headers' are per-call HTTP header overrides for API providers.
-- Provider defaults are built first, then 'Baikai.Model.headers',
-- then this field; later values replace earlier ones by
-- case-insensitive header name, including auth headers for callers
-- intentionally fronting a gateway. Because that is an invitation to
-- put a credential here, the 'Show' and 'ToJSON' instances below print
-- 'Baikai.Auth.redactedMarker' in place of the value of any header
-- whose name looks credential-carrying. The field itself is untouched
-- and the header is still sent exactly as written.
--
-- @cacheRetention@, @thinking@ and @responseFormat@ are
-- provider-agnostic preferences that each provider maps onto its own
-- primitive — see 'Baikai.CacheRetention', 'Baikai.ThinkingLevel' and
-- 'Baikai.ResponseFormat' for the mappings.
--
-- 'evidence' is the per-call request for verifiable model-call
-- evidence — see 'Baikai.Evidence.EvidenceRequest'. It carries the
-- caller's run identifier and how strictly they need the evidence.
-- A call whose 'evidence' is 'Nothing', which is every call that does
-- not opt in, behaves exactly as it did before the field existed: no
-- digest is computed, no evidence is emitted, and the trace output is
-- unchanged.
module Baikai.Options
( Options,
maxTokens,
temperature,
apiKey,
timeoutMs,
headers,
metadata,
toolChoice,
cacheRetention,
thinking,
responseFormat,
evidence,
topP,
stopSequences,
seed,
frequencyPenalty,
presencePenalty,
emptyOptions,
)
where
import Baikai.Auth (ApiKeySource)
import Baikai.Auth qualified as Auth
import Baikai.CacheRetention (CacheRetention)
import Baikai.Evidence (EvidenceRequest)
import Baikai.Header (HeaderName)
import Baikai.ResponseFormat (ResponseFormat)
import Baikai.ThinkingLevel (ThinkingLevel)
import Baikai.Tool (ToolChoice)
import Data.Aeson
( ToJSON (toEncoding, toJSON),
Value,
defaultOptions,
genericToEncoding,
genericToJSON,
)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import GHC.Generics (Generic)
import Numeric.Natural (Natural)
data Options = Options
{ maxTokens :: !(Maybe Natural),
temperature :: !(Maybe Double),
apiKey :: !(Maybe ApiKeySource),
timeoutMs :: !(Maybe Int),
headers :: !(Map HeaderName Text),
metadata :: !(Map Text Value),
-- | 'Nothing' and @Just 'ToolChoiceAuto'@ are the same request: both
-- send no @tool_choice@ and let the provider apply its own default,
-- which is @auto@ at Anthropic and OpenAI. The constructor is kept
-- for a caller who wants to say "auto" explicitly.
toolChoice :: !(Maybe ToolChoice),
-- | 'Nothing' and @Just 'CacheRetentionNone'@ are the same request:
-- both send no cache-control marker. The constructor is kept for a
-- caller who wants to say "no caching" explicitly.
cacheRetention :: !(Maybe CacheRetention),
thinking :: !(Maybe ThinkingLevel),
responseFormat :: !(Maybe ResponseFormat),
evidence :: !(Maybe EvidenceRequest),
topP :: !(Maybe Double),
-- | Sequences that stop generation. Empty means "send nothing" —
-- one representation, where @Nothing@ and @Just []@ used to be two
-- indistinguishable ones.
stopSequences :: ![Text],
-- | A machine integer, like 'timeoutMs': every provider that accepts
-- a seed accepts one.
seed :: !(Maybe Int),
frequencyPenalty :: !(Maybe Double),
presencePenalty :: !(Maybe Double)
}
deriving stock (Eq, Generic)
-- | Rendered field by field rather than derived, so that the value of a
-- credential-carrying header prints as 'Auth.redactedMarker'.
--
-- The format is exactly what @deriving stock Show@ produces — the same
-- record syntax, the same field order, the same @showsPrec@ precedence
-- — because the point is to redact one value, not to invent a new
-- rendering. A test in @baikai\/test\/Main.hs@ walks the 'Generic'
-- representation and asserts that every field name appears here, so a
-- field added later cannot silently vanish from 'show'.
--
-- 'Eq' is untouched: two 'Options' whose credential headers differ are
-- still unequal.
instance Show Options where
showsPrec d o =
showParen (d >= 11) $
showString "Options {"
. field "maxTokens" (maxTokens o)
. next "temperature" (temperature o)
. next "apiKey" (apiKey o)
. next "timeoutMs" (timeoutMs o)
. next "headers" (Auth.redactHeaderValues (headers o))
. next "metadata" (metadata o)
. next "toolChoice" (toolChoice o)
. next "cacheRetention" (cacheRetention o)
. next "thinking" (thinking o)
. next "responseFormat" (responseFormat o)
. next "evidence" (evidence o)
. next "topP" (topP o)
. next "stopSequences" (stopSequences o)
. next "seed" (seed o)
. next "frequencyPenalty" (frequencyPenalty o)
. next "presencePenalty" (presencePenalty o)
. showChar '}'
where
field name v = showString name . showString " = " . showsPrec 0 v
next name v = showString ", " . field name v
-- | Encoded through the 'Generic' representation of a copy whose
-- credential headers have been replaced, so the output is byte-identical
-- to the derived instance's for every record that carries none, and
-- there is no recursion back into this instance.
instance ToJSON Options where
toJSON = genericToJSON defaultOptions . redactOptions
toEncoding = genericToEncoding defaultOptions . redactOptions
redactOptions :: Options -> Options
redactOptions o = o {headers = Auth.redactHeaderValues (headers o)}
emptyOptions :: Options
emptyOptions =
Options
{ maxTokens = Nothing,
temperature = Nothing,
apiKey = Nothing,
timeoutMs = Nothing,
headers = Map.empty,
metadata = Map.empty,
toolChoice = Nothing,
cacheRetention = Nothing,
thinking = Nothing,
responseFormat = Nothing,
evidence = Nothing,
topP = Nothing,
stopSequences = [],
seed = Nothing,
frequencyPenalty = Nothing,
presencePenalty = Nothing
}