kioku-core-0.6.0.0: src/Kioku/AI/File.hs
-- | Versioned AI files shared by command-line and embedding hosts.
module Kioku.AI.File (loadAIRuntime, parseAIConfig) where
import Baikai.Auth (ApiKeySource (..))
import Baikai.Embedding qualified as E
import Baikai.Interactive qualified as I
import Baikai.Model qualified as M
import Baikai.Options qualified as O
import Baikai.Provider.Claude.Api qualified as Claude
import Baikai.Provider.Claude.Cli qualified as ClaudeCli
import Baikai.Provider.Claude.Interactive qualified as ClaudeInteractive
import Baikai.Provider.OpenAI.Api qualified as OpenAI
import Baikai.Provider.OpenAI.Cli qualified as Codex
import Baikai.Provider.OpenAI.Interactive qualified as CodexInteractive
import Baikai.Provider.OpenAI.Responses qualified as Responses
import Baikai.Provider.Registry (newProviderRegistryFrom)
import Control.Applicative ((<|>))
import Control.Monad (unless)
import Data.Aeson
import Data.Aeson.Key qualified as Key
import Data.Aeson.KeyMap qualified as KM
import Data.Aeson.Types (Parser, parseEither)
import Data.ByteString qualified as BS
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Kioku.AI.Config
import Kioku.AI.Runtime
import System.Environment (lookupEnv)
-- | Background workers never receive the foreground session capability.
loadAIRuntime :: Bool -> Maybe FilePath -> IO AIRuntime
loadAIRuntime foreground explicit = do
environment <- lookupEnv "KIOKU_AI_CONFIG"
case explicit <|> environment of
Nothing -> pure disabledAIRuntime
Just path -> do
bytes <- BS.readFile path
(cfg, permissions) <- either (const (ioError (userError "kioku: invalid AI configuration; check version, feature names, modes and required settings"))) pure (eitherDecodeStrict bytes >>= parseEither parseAIConfig)
api <- if API `elem` permissions then Just <$> newProviderRegistryFrom [Claude.claudeMessagesProvider, OpenAI.openaiChatProvider, Responses.openaiResponsesProvider] else pure Nothing
batch <- if Batch `elem` permissions then Just <$> newProviderRegistryFrom [ClaudeCli.claudeCliProvider ClaudeCli.defaultClaudeCliConfig, Codex.codexCliProvider Codex.defaultCodexCliConfig] else pure Nothing
let launcher feature request = case Map.findWithDefault cfg.distillationDefault feature cfg.featureOverrides of
InteractiveConfig I.InteractiveClaude _ -> either (const (Left (AIInteractiveFailed feature "Baikai refused interactive safety settings"))) Right <$> ClaudeInteractive.launchClaudeInteractive ClaudeInteractive.defaultClaudeInteractiveConfig request
InteractiveConfig I.InteractiveCodex _ -> either (const (Left (AIInteractiveFailed feature "Baikai refused interactive safety settings"))) Right <$> CodexInteractive.launchCodexInteractive CodexInteractive.defaultCodexInteractiveConfig request
_ -> pure (Left (AIExecutionRefused feature "interactive mode was not selected"))
caps =
noHostCapabilities
{ apiRegistry = api,
batchRegistry = batch,
allowInteractive = Interactive `elem` permissions,
launchInteractive = if foreground && Interactive `elem` permissions then Just launcher else Nothing,
allowEmbeddingAPI = API `elem` permissions
}
newAIRuntime caps cfg >>= either (ioError . userError . show) pure
parseAIConfig :: Value -> Parser (AIConfig, [ExecutionMode])
parseAIConfig = withObject "AI configuration" $ \o -> do
strictKeys ["version", "permissions", "distillation", "features", "embeddings"] o
version <- o .: "version"
unless (version == (1 :: Int)) (fail "unsupported AI configuration version")
permissions <- o .:? "permissions" .!= [] >>= traverse mode
defaults <- maybe (pure DistillationDisabled) distillation =<< o .:? "distillation"
overrides <- o .:? "features" .!= KM.empty >>= featureMap distillation
embeddings <- o .:? "embeddings" .!= KM.empty >>= featureMap embedding
pure (AIConfig defaults overrides embeddings, permissions)
featureMap :: (Value -> Parser a) -> Object -> Parser (Map.Map AIFeature a)
featureMap parseValue o = Map.fromList <$> traverse one (KM.toList o)
where
one (key, value) = (,) <$> either (const (fail "unknown feature")) pure (parseFeature (Key.toText key)) <*> parseValue value
mode :: Text -> Parser ExecutionMode
mode = \case
"disabled" -> pure Disabled
"api" -> pure API
"batch" -> pure Batch
"interactive" -> pure Interactive
_ -> fail "invalid execution mode"
distillation :: Value -> Parser DistillationConfig
distillation = withObject "distillation" $ \o -> do
selected <- o .: "mode" >>= mode
case selected of
Disabled -> strictKeys ["mode"] o >> pure DistillationDisabled
Interactive -> do
strictKeys ["mode", "provider", "model", "workingDir", "effort"] o
provider <-
o .: "provider" >>= \case
"claude" -> pure I.InteractiveClaude
"codex" -> pure I.InteractiveCodex
(_ :: Text) -> fail "unknown interactive provider"
model <- o .: "model"
working <- o .: "workingDir"
effort <- o .:? "effort"
let safety = case provider of
I.InteractiveClaude -> I.ClaudeAllowedTools ["Read", "Write"]
I.InteractiveCodex -> I.CodexSandbox I.CodexWorkspaceWrite I.CodexApprovalOnRequest
pure (InteractiveConfig provider ((I.interactiveLaunchRequest "") {I.modelId = Just model, I.workingDir = Just working, I.safety = safety, I.effort = effort}))
_ -> do
strictKeys ["mode", "api", "model", "provider", "baseUrl", "options"] o
api <- o .: "api"
modelId <- o .: "model"
baseUrl <- o .: "baseUrl"
provider <- o .: "provider"
options <- maybe (pure O.emptyOptions) parseOptions =<< o .:? "options"
pure (CompletionConfig selected ((M.mkModel api modelId baseUrl) {M.provider = provider}) options)
parseOptions :: Value -> Parser O.Options
parseOptions = withObject "Baikai options" $ \o -> do
strictKeys ["apiKeyEnv", "maxTokens", "temperature", "timeoutMs", "thinking"] o
key <- fmap ApiKeyEnv <$> o .:? "apiKeyEnv"
maxTokens <- o .:? "maxTokens"
temperature <- o .:? "temperature"
timeout <- o .:? "timeoutMs"
thinking <- o .:? "thinking"
pure O.emptyOptions {O.apiKey = key, O.maxTokens = maxTokens, O.temperature = temperature, O.timeoutMs = timeout, O.thinking = thinking}
embedding :: Value -> Parser E.EmbeddingModel
embedding = withObject "embedding" $ \o -> do
strictKeys ["mode", "model", "baseUrl", "dimensions", "apiKeyEnv"] o
selected <- o .: "mode" >>= mode
unless (selected == API) (fail "embeddings require independently authorized API mode")
model <- o .: "model"
baseUrl <- o .: "baseUrl"
dimensions <- o .: "dimensions"
key <- fmap ApiKeyEnv <$> o .:? "apiKeyEnv"
pure E.emptyEmbeddingModel {E.modelId = model, E.baseUrl = baseUrl, E.dimensions = Just dimensions, E.apiKey = key}
strictKeys :: [Text] -> Object -> Parser ()
strictKeys allowed o = unless (all ((`elem` allowed) . Key.toText) (KM.keys o)) (fail "unknown configuration field")