packages feed

llm-simple-0.4.2.0: src/LLM/Load/ModelCatalog.hs

module LLM.Load.ModelCatalog
  ( ModelCatalogItem (..),
    ModelCatalogMap,
    loadModelCatalog,
  )
where

import Control.Monad.Except (ExceptT, liftEither)
import Data.Aeson (FromJSON, ToJSON)
import Data.Map (Map)
import Data.Map qualified as Map
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Generics (Generic)
import LLM.Core.Usage (PricingInfo (..))
import LLM.Generate.ModelConfig (ModelCapabilities)
import LLM.Load.Types (LoadConfigError (..))
import LLM.Load.Utils (decodeJsonFile)

data ModelCatalogItem = ModelCatalogItem
  { modelConfigName :: Text,
    providerName :: Text, -- key from providers.json (built-in or custom)
    modelName :: Text,
    pricing :: PricingInfo,
    maxTokens :: Int,
    temperature :: Maybe Double,
    requestTimeout :: Maybe Int,
    thinking :: Maybe Text,
    -- | Optional capability flags; missing object or keys default to false.
    capabilities :: Maybe ModelCapabilities,
    throttleDelay :: Maybe Int,
    retryCount :: Int,
    jitterBackoff :: Int
  }
  deriving (Show, Eq, Ord, Generic, FromJSON, ToJSON)

type ModelCatalogMap = Map Text ModelCatalogItem

-- | Read a model catalog file. Fails with 'LoadModelCatalogError' if the
-- file cannot be read or parsed, or if an entry is invalid: a
-- @modelConfigName@ used twice, @maxTokens <= 0@, or a negative
-- @requestTimeout@, @throttleDelay@, @retryCount@, @jitterBackoff@ or price.
-- The message names the entry.
loadModelCatalog :: FilePath -> ExceptT LoadConfigError IO ModelCatalogMap
loadModelCatalog filePath = do
  items <- decodeJsonFile filePath (LoadModelCatalogError . T.pack)
  liftEither (checkItems items)
  pure (Map.fromList [(item.modelConfigName, item) | item <- items])
  where
    checkItems :: [ModelCatalogItem] -> Either LoadConfigError ()
    checkItems items = case duplicates (map (.modelConfigName) items) of
      name : _ -> Left (LoadModelCatalogError ("duplicate modelConfigName: " <> name))
      [] -> mapM_ checkItem items
    duplicates names = [n | (i, n) <- zip [0 :: Int ..] names, n `elem` take i names]
    checkItem item = case problems item of
      [] -> Right ()
      problem : _ -> Left (LoadModelCatalogError ("model " <> item.modelConfigName <> ": " <> problem))
    problems item =
      [ "maxTokens must be positive, got " <> showT item.maxTokens
      | item.maxTokens <= 0
      ]
        ++ negativeInt "requestTimeout" item.requestTimeout
        ++ negativeInt "throttleDelay" item.throttleDelay
        ++ negativeInt "retryCount" (Just item.retryCount)
        ++ negativeInt "jitterBackoff" (Just item.jitterBackoff)
        ++ negativePrice "pricePerMillionInput" (Just item.pricing.pricePerMillionInput)
        ++ negativePrice "pricePerMillionOutput" (Just item.pricing.pricePerMillionOutput)
        ++ negativePrice "pricePerMillionCacheRead" item.pricing.pricePerMillionCacheRead
        ++ negativePrice "pricePerMillionCacheWrite" item.pricing.pricePerMillionCacheWrite
    negativeInt field v = [field <> " must not be negative, got " <> showT n | Just n <- [v], n < 0]
    negativePrice field v = [field <> " must not be negative, got " <> showT p | Just p <- [v], p < 0]
    showT :: (Show a) => a -> Text
    showT = T.pack . show