packages feed

llm-simple-0.4.0.0: src/LLM/Load/LoadModels.hs

module LLM.Load.LoadModels
  ( ModelConfigMap,
    loadModelConfigMap,
    loadModels,
    loadModel,
    loadModelsOrThrow,
    loadModelOrThrow,
    loadModelsOrThrow_,
    loadModelOrThrow_,
  )
where

import Control.Exception (throwIO)
import Control.Lens (Each (each), mapMOf)
import Control.Monad.Except (ExceptT, liftEither, runExceptT)
import Control.Monad.IO.Class (MonadIO (liftIO))
import Data.Map (Map)
import Data.Map qualified as Map
import Data.Text (Text)
import Data.Maybe (fromMaybe)
import LLM.Core.Types (LLMGateway, ThinkingMode (..))
import LLM.Generate.ModelConfig
  ( ModelConfig (..),
    defaultModelCapabilities,
  )
import LLM.Load.LoadGateways (GatewayMap, ProviderUnavailable, describeProviderUnavailable, resolveGateways)
import LLM.Load.ModelCatalog (ModelCatalogItem (..), ModelCatalogMap, loadModelCatalog)
import LLM.Load.ProviderCatalog (loadProviderCatalogForModelCatalog)
import LLM.Load.Types (LoadConfigError (LoadModelConfigError))

-- | Model configs by catalog name.
type ModelConfigMap = Map Text ModelConfig

-- | Providers by name, each either its gateway or why it is unavailable.
type ResolvedProviders = Map Text (Either ProviderUnavailable LLMGateway)

-- | Parse the model and provider catalogs and resolve every provider. Fails
-- only if a catalog cannot be read or parsed; unavailable providers are kept
-- with the reason.
loadCatalogs :: FilePath -> ExceptT LoadConfigError IO (ModelCatalogMap, ResolvedProviders)
loadCatalogs filePath = do
  providerCatalog <- loadProviderCatalogForModelCatalog filePath
  modelCatalog <- loadModelCatalog filePath
  providers <- liftIO $ resolveGateways providerCatalog
  pure (modelCatalog, providers)

-- | Load the catalogs next to @filePath@ and return all models and gateways
-- that are currently available. Models whose provider is unavailable (API key
-- env var not set, invalid base URL, unknown provider) are left out; this
-- never fails because of them.
loadModelConfigMap :: FilePath -> ExceptT LoadConfigError IO (ModelConfigMap, GatewayMap)
loadModelConfigMap filePath = available <$> loadCatalogs filePath

available :: (ModelCatalogMap, ResolvedProviders) -> (ModelConfigMap, GatewayMap)
available (models, providers) =
  ( Map.mapMaybe (either (const Nothing) Just . resolveModel providers) models,
    Map.mapMaybe (either (const Nothing) Just) providers
  )

-- | Look up a requested model. Fails with the reason when the model is not in
-- the catalog or its provider is unavailable.
lookupModel :: ModelCatalogMap -> ResolvedProviders -> Text -> Either LoadConfigError ModelConfig
lookupModel models providers name =
  case Map.lookup name models of
    Nothing -> Left $ LoadModelConfigError $ "Model not found: " <> name
    Just item -> resolveModel providers item

resolveModel :: ResolvedProviders -> ModelCatalogItem -> Either LoadConfigError ModelConfig
resolveModel providers item =
  case Map.lookup item.providerName providers of
    Nothing -> unavailable "is not in the provider catalog"
    Just (Left reason) -> unavailable (describeProviderUnavailable reason)
    Just (Right gateway) ->
      Right
        ModelConfig
          { mcGateway = gateway,
            mcModel = item.modelName,
            mcPricing = item.pricing,
            mcMaxTokens = item.maxTokens,
            mcTemperature = item.temperature,
            mcThinking = fmap (\x -> ThinkingMode {tmEnabled = True, tmEffort = Just x}) item.thinking,
            mcCapabilities = fromMaybe defaultModelCapabilities item.capabilities,
            mcRequestTimeout = item.requestTimeout,
            mcThrottleDelay = item.throttleDelay,
            mcRetryCount = item.retryCount,
            mcJitterBackoff = item.jitterBackoff
          }
  where
    unavailable reason =
      Left $
        LoadModelConfigError $
          "Model " <> item.modelConfigName <> " needs provider " <> item.providerName <> ", but " <> reason

-- | Load the requested models. Also returns all available models and
-- gateways (see 'loadModelConfigMap').
loadModels :: (Each s t Text ModelConfig) => FilePath -> s -> ExceptT LoadConfigError IO (t, ModelConfigMap, GatewayMap)
loadModels filePath s = do
  catalogs@(models, providers) <- loadCatalogs filePath
  r <- liftEither $ mapMOf each (lookupModel models providers) s
  let (modelConfigMap, gatewayMap) = available catalogs
  pure (r, modelConfigMap, gatewayMap)

-- | Load one model. Also returns all available models and gateways (see
-- 'loadModelConfigMap').
loadModel :: FilePath -> Text -> ExceptT LoadConfigError IO (ModelConfig, ModelConfigMap, GatewayMap)
loadModel filePath modelConfigName = do
  catalogs@(models, providers) <- loadCatalogs filePath
  modelConfig <- liftEither $ lookupModel models providers modelConfigName
  let (modelConfigMap, gatewayMap) = available catalogs
  pure (modelConfig, modelConfigMap, gatewayMap)

-- | Load models from a json file and throw an error if any model is not found
--
-- > (gpt, llama, haiku) <- loadModelsOrThrow "./model-catalog.json" ("gpt_4_1", "llama_3_2", "haiku_4_5")
loadModelsOrThrow :: (Each s t Text ModelConfig) => FilePath -> s -> IO t
loadModelsOrThrow filepath s =
  (\(x, _, _) -> x) <$> loadModelsOrThrow_ filepath s

loadModelsOrThrow_ :: (Each s t Text ModelConfig) => FilePath -> s -> IO (t, ModelConfigMap, GatewayMap)
loadModelsOrThrow_ filePath s = do
  r <- runExceptT $ loadModels filePath s
  case r of
    Left err -> throwIO err
    Right result -> pure result

-- | Load a single model from a json file and throw an error if the model is not found
loadModelOrThrow :: FilePath -> Text -> IO ModelConfig
loadModelOrThrow filePath modelConfigName = do
  (\(x, _, _) -> x) <$> loadModelOrThrow_ filePath modelConfigName

loadModelOrThrow_ :: FilePath -> Text -> IO (ModelConfig, ModelConfigMap, GatewayMap)
loadModelOrThrow_ filePath modelConfigName =
  runExceptT (loadModel filePath modelConfigName) >>= either throwIO pure