packages feed

langchain-hs-0.0.1.0: src/Langchain/LLM/OpenAI.hs

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

-- This module is not tested since I don't have the OpenAI api key.

{- |
Module      : Langchain.LLM.OpenAI
Description : OpenAI integration for LangChain Haskell
Copyright   : (c) 2025 Tushar Adhatrao
License     : MIT
Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
Stability   : experimental

OpenAI implementation of LangChain's LLM interface. Not tested
-}
module Langchain.LLM.OpenAI
  ( OpenAI (..)
  -- * Data Types
  , ChatCompletionRequest (..)
  , ChatCompletionResponse (..)
  , Message (..)
  , Role (..)
  , MessageContent (..)
  , TextContent (..)
  , Tool_ (..)
  , Function_ (..)
  , ToolCall (..)
  , FunctionCall_ (..)
  , Usage (..)
  , Choice (..)
  , FinishReason (..)
  , LogProbs (..)
  , LogProbContent (..)
  , TopLogProb (..)
  , AudioConfig (..)
  , AudioResponse (..)
  , Modality (..)
  , ToolChoice (..)
  , SpecificToolChoice (..)
  , ReasoningEffort (..)
  , PredictionOutput (..)
  , PredictionContent (..)
  , ResponseFormat (..)
  , StreamOptions (..)
  , WebSearchOptions (..)
  , UserLocation (..)
  , ApproximateLocation (..)
  , CompletionTokensDetails (..)
  , PromptTokensDetails (..)
  -- * Functions
  , createChatCompletion
  , defaultChatCompletionRequest
  , defaultMessage
  ) where

import Data.Aeson
import qualified Data.List.NonEmpty as NE
import Data.Map (Map)
import Data.Maybe (listToMaybe)
import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8)
import GHC.Generics
import Langchain.Callback (Callback)
import qualified Langchain.LLM.Core as LLM
import Network.HTTP.Simple
import Network.HTTP.Types.Status (statusCode)

{- | Represents different roles in a conversation
User: Human user input
Assistant: AI-generated response
System: System-level instructions
Developer: Special role for developer messages
Tool: Tool interaction messages
Function: Function call messages
-}
data Role
  = User
  | Assistant
  | System
  | Developer
  | Tool
  | Function
  deriving (Show, Eq, Generic)

instance ToJSON Role where
  toJSON User = String "user"
  toJSON Assistant = String "assistant"
  toJSON System = String "system"
  toJSON Developer = String "developer"
  toJSON Tool = String "tool"
  toJSON Function = String "function"

instance FromJSON Role where
  parseJSON (String "user") = return User
  parseJSON (String "assistant") = return Assistant
  parseJSON (String "system") = return System
  parseJSON (String "developer") = return Developer
  parseJSON (String "tool") = return Tool
  parseJSON (String "function") = return Function
  parseJSON invalid = fail $ "Invalid role: " ++ show invalid

data TextContent = TextContent
  { text_ :: Text
  , contentType :: Text
  }
  deriving (Show, Eq, Generic)

instance ToJSON TextContent where
  toJSON TextContent {..} =
    object
      [ "text" .= text_
      , "type" .= contentType
      ]

instance FromJSON TextContent where
  parseJSON = withObject "TextContent" $ \v ->
    TextContent
      <$> v .: "text"
      <*> v .: "type"

data MessageContent
  = StringContent Text
  | ContentParts [TextContent]
  deriving (Show, Eq, Generic)

instance ToJSON MessageContent where
  toJSON (StringContent text) = String text
  toJSON (ContentParts parts) = toJSON parts

instance FromJSON MessageContent where
  parseJSON (String s) = return $ StringContent s
  parseJSON (Array arr) = ContentParts <$> parseJSON (Array arr)
  parseJSON invalid = fail $ "Invalid message content: " ++ show invalid

data Function_ = Function_
  { name :: Text
  , description :: Maybe Text
  , parameters :: Maybe Value
  , strict :: Maybe Bool
  }
  deriving (Show, Eq, Generic)

instance ToJSON Function_ where
  toJSON Function_ {..} =
    object $
      [ "name" .= name
      ]
        ++ maybe [] (\d -> ["description" .= d]) description
        ++ maybe [] (\p -> ["parameters" .= p]) parameters
        ++ maybe [] (\s -> ["strict" .= s]) strict

instance FromJSON Function_ where
  parseJSON = withObject "Function" $ \v ->
    Function_
      <$> v .: "name"
      <*> v .:? "description"
      <*> v .:? "parameters"
      <*> v .:? "strict"

data Tool_ = Tool_
  { toolType :: Text
  , function :: Function_
  }
  deriving (Show, Eq, Generic)

instance ToJSON Tool_ where
  toJSON Tool_ {..} =
    object
      [ "type" .= toolType
      , "function" .= function
      ]

instance FromJSON Tool_ where
  parseJSON = withObject "Tool" $ \v ->
    Tool_
      <$> v .: "type"
      <*> v .: "function"

data FunctionCall_ = FunctionCall_
  { name :: Text
  , arguments :: Text
  }
  deriving (Show, Eq, Generic)

instance ToJSON FunctionCall_ where
  toJSON FunctionCall_ {..} =
    object
      [ "name" .= name
      , "arguments" .= arguments
      ]

instance FromJSON FunctionCall_ where
  parseJSON = withObject "FunctionCall" $ \v ->
    FunctionCall_
      <$> v .: "name"
      <*> v .: "arguments"

data ToolCall = ToolCall
  { id_ :: Text
  , toolType :: Text
  , function :: FunctionCall_
  }
  deriving (Show, Eq, Generic)

instance ToJSON ToolCall where
  toJSON ToolCall {..} =
    object
      [ "id" .= id_
      , "type" .= toolType
      , "function" .= function
      ]

instance FromJSON ToolCall where
  parseJSON = withObject "ToolCall" $ \v ->
    ToolCall
      <$> v .: "id"
      <*> v .: "type"
      <*> v .: "function"

{- | Configuration for audio processing
Specifies format and voice preferences for text-to-speech
-}
data AudioConfig = AudioConfig
  { format :: Text
  , voice :: Text
  }
  deriving (Show, Eq, Generic)

instance ToJSON AudioConfig where
  toJSON AudioConfig {..} =
    object
      [ "format" .= format
      , "voice" .= voice
      ]

instance FromJSON AudioConfig where
  parseJSON = withObject "AudioConfig" $ \v ->
    AudioConfig
      <$> v .: "format"
      <*> v .: "voice"

data AudioResponse = AudioResponse
  { data_ :: Text
  , expiresAt :: Integer
  , id_ :: Text
  , transcript :: Text
  }
  deriving (Show, Eq, Generic)

instance ToJSON AudioResponse where
  toJSON AudioResponse {..} =
    object
      [ "data" .= data_
      , "expires_at" .= expiresAt
      , "id" .= id_
      , "transcript" .= transcript
      ]

instance FromJSON AudioResponse where
  parseJSON = withObject "AudioResponse" $ \v ->
    AudioResponse
      <$> v .: "data"
      <*> v .: "expires_at"
      <*> v .: "id"
      <*> v .: "transcript"

{- | Represents a single message in a conversation
Contains role, content, and optional metadata like function calls or audio responses.
-}
data Message = Message
  { role :: Role
  , content :: Maybe MessageContent
  , name :: Maybe Text
  , functionCall :: Maybe FunctionCall_
  , toolCalls :: Maybe [ToolCall]
  , toolCallId :: Maybe Text
  , audio :: Maybe AudioResponse
  , refusal :: Maybe Text
  }
  deriving (Show, Eq, Generic)

defaultMessage :: Message
defaultMessage =
  Message
    { role = User
    , content = Nothing
    , name = Nothing
    , functionCall = Nothing
    , toolCalls = Nothing
    , toolCallId = Nothing
    , audio = Nothing
    , refusal = Nothing
    }

instance ToJSON Message where
  toJSON Message {..} =
    object $
      ["role" .= role]
        ++ maybe [] (\c -> ["content" .= c]) content
        ++ maybe [] (\n -> ["name" .= n]) name
        ++ maybe [] (\fc -> ["function_call" .= fc]) functionCall
        ++ maybe [] (\tc -> ["tool_calls" .= tc]) toolCalls
        ++ maybe [] (\tcid -> ["tool_call_id" .= tcid]) toolCallId
        ++ maybe [] (\a -> ["audio" .= a]) audio
        ++ maybe [] (\r -> ["refusal" .= r]) refusal

instance FromJSON Message where
  parseJSON = withObject "Message" $ \v ->
    Message
      <$> v .: "role"
      <*> v .:? "content"
      <*> v .:? "name"
      <*> v .:? "function_call"
      <*> v .:? "tool_calls"
      <*> v .:? "tool_call_id"
      <*> v .:? "audio"
      <*> v .:? "refusal"

data Modality = TextModality | AudioModality
  deriving (Show, Eq, Generic)

instance ToJSON Modality where
  toJSON TextModality = String "text"
  toJSON AudioModality = String "audio"

instance FromJSON Modality where
  parseJSON (String "text") = return TextModality
  parseJSON (String "audio") = return AudioModality
  parseJSON invalid = fail $ "Invalid modality: " ++ show invalid

data ToolChoice = None | Auto | Required | SpecificTool SpecificToolChoice
  deriving (Show, Eq, Generic)

instance ToJSON ToolChoice where
  toJSON None = String "none"
  toJSON Auto = String "auto"
  toJSON Required = String "required"
  toJSON (SpecificTool choice) = toJSON choice

instance FromJSON ToolChoice where
  parseJSON (String "none") = return None
  parseJSON (String "auto") = return Auto
  parseJSON (String "required") = return Required
  parseJSON o@(Object _) = SpecificTool <$> parseJSON o
  parseJSON invalid = fail $ "Invalid tool choice: " ++ show invalid

data SpecificToolChoice = SpecificToolChoice
  { toolType :: Text
  , function :: Value
  }
  deriving (Show, Eq, Generic)

instance ToJSON SpecificToolChoice where
  toJSON SpecificToolChoice {..} =
    object
      [ "type" .= toolType
      , "function" .= function
      ]

instance FromJSON SpecificToolChoice where
  parseJSON = withObject "SpecificToolChoice" $ \v ->
    SpecificToolChoice
      <$> v .: "type"
      <*> v .: "function"

data ReasoningEffort = Low | Medium | High
  deriving (Show, Eq, Generic)

instance ToJSON ReasoningEffort where
  toJSON Low = String "low"
  toJSON Medium = String "medium"
  toJSON High = String "high"

instance FromJSON ReasoningEffort where
  parseJSON (String "low") = return Low
  parseJSON (String "medium") = return Medium
  parseJSON (String "high") = return High
  parseJSON invalid = fail $ "Invalid reasoning effort: " ++ show invalid

data PredictionContent = PredictionContent
  { content :: MessageContent
  , contentType :: Text
  }
  deriving (Show, Eq, Generic)

instance ToJSON PredictionContent where
  toJSON PredictionContent {..} =
    object
      [ "content" .= content
      , "type" .= contentType
      ]

instance FromJSON PredictionContent where
  parseJSON = withObject "PredictionContent" $ \v ->
    PredictionContent
      <$> v .: "content"
      <*> v .: "type"

data PredictionOutput = PredictionOutput
  { predictionType :: Text
  , content :: MessageContent
  }
  deriving (Show, Eq, Generic)

instance ToJSON PredictionOutput where
  toJSON PredictionOutput {..} =
    object
      [ "type" .= predictionType
      , "content" .= content
      ]

instance FromJSON PredictionOutput where
  parseJSON = withObject "PredictionOutput" $ \v ->
    PredictionOutput
      <$> v .: "type"
      <*> v .: "content"

data ResponseFormat = JsonObjectFormat | JsonSchemaFormat Value
  deriving (Show, Eq, Generic)

instance ToJSON ResponseFormat where
  toJSON JsonObjectFormat = object ["type" .= ("json_object" :: Text)]
  toJSON (JsonSchemaFormat schema) =
    object
      [ "type" .= ("json_schema" :: Text)
      , "json_schema" .= schema
      ]

instance FromJSON ResponseFormat where
  parseJSON = withObject "ResponseFormat" $ \v -> do
    formatType <- v .: "type"
    case formatType of
      String "json_object" -> return JsonObjectFormat
      String "json_schema" -> JsonSchemaFormat <$> v .: "json_schema"
      _ -> fail $ "Invalid response format type: " ++ show formatType

data StreamOptions = StreamOptions
  { includeUsage :: Bool
  }
  deriving (Show, Eq, Generic)

instance ToJSON StreamOptions where
  toJSON StreamOptions {..} =
    object
      [ "include_usage" .= includeUsage
      ]

instance FromJSON StreamOptions where
  parseJSON = withObject "StreamOptions" $ \v ->
    StreamOptions <$> v .: "include_usage"

data ApproximateLocation = ApproximateLocation
  { locationType :: Text
  }
  deriving (Show, Eq, Generic)

instance ToJSON ApproximateLocation where
  toJSON ApproximateLocation {..} =
    object
      [ "type" .= locationType
      ]

instance FromJSON ApproximateLocation where
  parseJSON = withObject "ApproximateLocation" $ \v ->
    ApproximateLocation <$> v .: "type"

data UserLocation = UserLocation
  { approximate :: ApproximateLocation
  }
  deriving (Show, Eq, Generic)

instance ToJSON UserLocation where
  toJSON UserLocation {..} =
    object
      [ "approximate" .= approximate
      ]

instance FromJSON UserLocation where
  parseJSON = withObject "UserLocation" $ \v ->
    UserLocation <$> v .: "approximate"

data WebSearchOptions = WebSearchOptions
  { searchContextSize :: Maybe Text
  , userLocation :: Maybe UserLocation
  }
  deriving (Show, Eq, Generic)

instance ToJSON WebSearchOptions where
  toJSON WebSearchOptions {..} =
    object $
      maybe [] (\s -> ["search_context_size" .= s]) searchContextSize
        ++ maybe [] (\l -> ["user_location" .= l]) userLocation

instance FromJSON WebSearchOptions where
  parseJSON = withObject "WebSearchOptions" $ \v ->
    WebSearchOptions
      <$> v .:? "search_context_size"
      <*> v .:? "user_location"

{- | Main request type for chat completions
Contains all parameters for configuring the OpenAI chat completion API call.
-}
data ChatCompletionRequest = ChatCompletionRequest
  { messages :: [Message]
  , model :: Text
  , frequencyPenalty :: Maybe Double
  , logitBias :: Maybe (Map Text Double)
  , logprobs :: Maybe Bool
  , maxCompletionTokens :: Maybe Int
  , maxTokens :: Maybe Int
  , metadata :: Maybe (Map Text Text)
  , modalities :: Maybe [Modality]
  , n :: Maybe Int
  , parallelToolCalls :: Maybe Bool
  , prediction :: Maybe PredictionOutput
  , presencePenalty :: Maybe Double
  , reasoningEffort :: Maybe ReasoningEffort
  , responseFormat :: Maybe ResponseFormat
  , seed :: Maybe Int
  , serviceTier :: Maybe Text
  , stop :: Maybe (Either Text [Text])
  , store :: Maybe Bool
  , stream :: Maybe Bool
  , streamOptions :: Maybe StreamOptions
  , temperature :: Maybe Double
  , toolChoice :: Maybe ToolChoice
  , tools :: Maybe [Tool_]
  , topLogprobs :: Maybe Int
  , topP :: Maybe Double
  , user :: Maybe Text
  , webSearchOptions :: Maybe WebSearchOptions
  , audio :: Maybe AudioConfig
  }
  deriving (Show, Eq, Generic)

instance ToJSON ChatCompletionRequest where
  toJSON ChatCompletionRequest {..} =
    object $
      [ "messages" .= messages
      , "model" .= model
      ]
        ++ maybe [] (\fp -> ["frequency_penalty" .= fp]) frequencyPenalty
        ++ maybe [] (\lb -> ["logit_bias" .= lb]) logitBias
        ++ maybe [] (\lp -> ["logprobs" .= lp]) logprobs
        ++ maybe [] (\mt -> ["max_completion_tokens" .= mt]) maxCompletionTokens
        ++ maybe [] (\mt -> ["max_tokens" .= mt]) maxTokens
        ++ maybe [] (\md -> ["metadata" .= md]) metadata
        ++ maybe [] (\m -> ["modalities" .= m]) modalities
        ++ maybe [] (\n' -> ["n" .= n']) n
        ++ maybe [] (\ptc -> ["parallel_tool_calls" .= ptc]) parallelToolCalls
        ++ maybe [] (\p -> ["prediction" .= p]) prediction
        ++ maybe [] (\pp -> ["presence_penalty" .= pp]) presencePenalty
        ++ maybe [] (\re -> ["reasoning_effort" .= re]) reasoningEffort
        ++ maybe [] (\rf -> ["response_format" .= rf]) responseFormat
        ++ maybe [] (\s -> ["seed" .= s]) seed
        ++ maybe [] (\st -> ["service_tier" .= st]) serviceTier
        ++ maybe [] (\s -> ["stop" .= s]) stop
        ++ maybe [] (\s -> ["store" .= s]) store
        ++ maybe [] (\s -> ["stream" .= s]) stream
        ++ maybe [] (\so -> ["stream_options" .= so]) streamOptions
        ++ maybe [] (\t -> ["temperature" .= t]) temperature
        ++ maybe [] (\tc -> ["tool_choice" .= tc]) toolChoice
        ++ maybe [] (\t -> ["tools" .= t]) tools
        ++ maybe [] (\tlp -> ["top_logprobs" .= tlp]) topLogprobs
        ++ maybe [] (\tp -> ["top_p" .= tp]) topP
        ++ maybe [] (\u -> ["user" .= u]) user
        ++ maybe [] (\wso -> ["web_search_options" .= wso]) webSearchOptions
        ++ maybe [] (\a -> ["audio" .= a]) audio

-- Response Types
data FinishReason = Stop | Length | ContentFilter | ToolCalls | FunctionCall
  deriving (Show, Eq, Generic)

instance FromJSON FinishReason where
  parseJSON (String "stop") = return Stop
  parseJSON (String "length") = return Length
  parseJSON (String "content_filter") = return ContentFilter
  parseJSON (String "tool_calls") = return ToolCalls
  parseJSON (String "function_call") = return FunctionCall
  parseJSON invalid = fail $ "Invalid finish reason: " ++ show invalid

data TopLogProb = TopLogProb
  { bytes :: Maybe [Int]
  , logprob :: Double
  , token :: Text
  }
  deriving (Show, Eq, Generic)

instance FromJSON TopLogProb where
  parseJSON = withObject "TopLogProb" $ \v ->
    TopLogProb
      <$> v .:? "bytes"
      <*> v .: "logprob"
      <*> v .: "token"

data LogProbContent = LogProbContent
  { bytes :: Maybe [Int]
  , logprob :: Double
  , token :: Text
  , topLogprobs :: [TopLogProb]
  }
  deriving (Show, Eq, Generic)

instance FromJSON LogProbContent where
  parseJSON = withObject "LogProbContent" $ \v ->
    LogProbContent
      <$> v .:? "bytes"
      <*> v .: "logprob"
      <*> v .: "token"
      <*> v .: "top_logprobs"

data LogProbs = LogProbs
  { content :: Maybe [LogProbContent]
  , refusal :: Maybe [LogProbContent]
  }
  deriving (Show, Eq, Generic)

instance FromJSON LogProbs where
  parseJSON = withObject "LogProbs" $ \v ->
    LogProbs
      <$> v .:? "content"
      <*> v .:? "refusal"

data CompletionTokensDetails = CompletionTokensDetails
  { acceptedPredictionTokens :: Int
  , audioTokens :: Int
  , reasoningTokens :: Int
  , rejectedPredictionTokens :: Int
  }
  deriving (Show, Eq, Generic)

instance FromJSON CompletionTokensDetails where
  parseJSON = withObject "CompletionTokensDetails" $ \v ->
    CompletionTokensDetails
      <$> v .: "accepted_prediction_tokens"
      <*> v .: "audio_tokens"
      <*> v .: "reasoning_tokens"
      <*> v .: "rejected_prediction_tokens"

data PromptTokensDetails = PromptTokensDetails
  { audioTokens :: Int
  , cachedTokens :: Int
  }
  deriving (Show, Eq, Generic)

instance FromJSON PromptTokensDetails where
  parseJSON = withObject "PromptTokensDetails" $ \v ->
    PromptTokensDetails
      <$> v .: "audio_tokens"
      <*> v .: "cached_tokens"

data Usage = Usage
  { completionTokens :: Int
  , promptTokens :: Int
  , totalTokens :: Int
  , completionTokensDetails :: Maybe CompletionTokensDetails
  , promptTokensDetails :: Maybe PromptTokensDetails
  }
  deriving (Show, Eq, Generic)

instance FromJSON Usage where
  parseJSON = withObject "Usage" $ \v ->
    Usage
      <$> v .: "completion_tokens"
      <*> v .: "prompt_tokens"
      <*> v .: "total_tokens"
      <*> v .:? "completion_tokens_details"
      <*> v .:? "prompt_tokens_details"

data Choice = Choice
  { finishReason :: FinishReason
  , index :: Int
  , logprobs :: Maybe LogProbs
  , message :: Message
  }
  deriving (Show, Eq, Generic)

instance FromJSON Choice where
  parseJSON = withObject "Choice" $ \v ->
    Choice
      <$> v .: "finish_reason"
      <*> v .: "index"
      <*> v .:? "logprobs"
      <*> v .: "message"

data ChatCompletionResponse = ChatCompletionResponse
  { choices :: [Choice]
  , created :: Integer
  , id_ :: Text
  , responseModel :: Text
  , object_ :: Text
  , serviceTier :: Maybe Text
  , systemFingerprint :: Text
  , usage :: Usage
  }
  deriving (Show, Eq, Generic)

instance FromJSON ChatCompletionResponse where
  parseJSON = withObject "ChatCompletionResponse" $ \v ->
    ChatCompletionResponse
      <$> v .: "choices"
      <*> v .: "created"
      <*> v .: "id"
      <*> v .: "model"
      <*> v .: "object"
      <*> v .:? "service_tier"
      <*> v .: "system_fingerprint"
      <*> v .: "usage"

{- | Default chat completion request
Uses "gpt-4o-mini-2024-07-18" as the default model. All other parameters are set to Nothing.
-}
defaultChatCompletionRequest :: ChatCompletionRequest
defaultChatCompletionRequest =
  ChatCompletionRequest
    { messages = []
    , model = "gpt-4o-mini-2024-07-18"
    , frequencyPenalty = Nothing
    , logitBias = Nothing
    , logprobs = Nothing
    , maxCompletionTokens = Nothing
    , maxTokens = Nothing
    , metadata = Nothing
    , modalities = Nothing
    , n = Nothing
    , parallelToolCalls = Nothing
    , prediction = Nothing
    , presencePenalty = Nothing
    , reasoningEffort = Nothing
    , responseFormat = Nothing
    , seed = Nothing
    , serviceTier = Nothing
    , stop = Nothing
    , store = Nothing
    , stream = Nothing
    , streamOptions = Nothing
    , temperature = Nothing
    , toolChoice = Nothing
    , tools = Nothing
    , topLogprobs = Nothing
    , topP = Nothing
    , user = Nothing
    , webSearchOptions = Nothing
    , audio = Nothing
    }

{- | Creates a chat completion request
Sends the request to OpenAI API and returns the parsed response.

Example usage:
@
response <- createChatCompletion "your-api-key" request
case response of
  Right res -> print (choices res)
  Left err -> putStrLn err
@
-}
createChatCompletion :: Text -> ChatCompletionRequest -> IO (Either String ChatCompletionResponse)
createChatCompletion apiKey r = do
  request_ <- parseRequest "https://api.openai.com/v1/chat/completions"
  let req =
        setRequestMethod "POST" $
          setRequestSecure True $
            setRequestHost "api.openai.com" $
              setRequestPath "/v1/chat/completions" $
                setRequestHeader "Content-Type" ["application/json"] $
                  setRequestHeader "Authorization" ["Bearer " <> encodeUtf8 apiKey] $
                    setRequestBodyJSON r $
                      request_

  response <- httpLBS req
  let status = statusCode $ getResponseStatus response
  if status >= 200 && status < 300
    then case eitherDecode (getResponseBody response) of
      Left err -> return $ Left $ "JSON parse error: " <> err
      Right completionResponse -> return $ Right completionResponse
    else return $ Left $ "API error: " <> show status <> " " <> show (getResponseBody response)

data OpenAI = OpenAI
  { apiKey :: Text
  , openAIModelName :: Text
  , callbacks :: [Callback]
  }

instance Show OpenAI where
  show OpenAI {..} = "OpenAI " ++ show openAIModelName

instance LLM.LLM OpenAI where
  generate OpenAI {..} prompt _ = do
    eRes <-
      createChatCompletion
        apiKey
        ( defaultChatCompletionRequest
            { model = openAIModelName
            , messages =
                [ Message
                    { role = User
                    , content = Just (StringContent prompt)
                    , name = Nothing
                    , functionCall = Nothing
                    , toolCalls = Nothing
                    , toolCallId = Nothing
                    , audio = Nothing
                    , refusal = Nothing
                    }
                ]
            }
        )
    case eRes of
      Left err -> return $ Left err
      Right r -> do
        case listToMaybe (choices r) of
          Nothing -> return $ Left "Did not received any response"
          Just resp ->
            let Message {..} = message resp
             in pure $
                  Right $
                    maybe
                      ""
                      ( \c -> case c of
                          StringContent t -> t
                          ContentParts _ -> ""
                      )
                      content
  chat OpenAI {..} msgs _ = do
    eRes <-
      createChatCompletion
        apiKey
        ( defaultChatCompletionRequest
            { model = openAIModelName
            , messages = toOpenAIMessages msgs
            }
        )
    case eRes of
      Left err -> return $ Left err
      Right r -> do
        case listToMaybe (choices r) of
          Nothing -> return $ Left "Did not received any response"
          Just resp ->
            let Message {..} = message resp
             in pure $
                  Right $
                    maybe
                      ""
                      ( \c -> case c of
                          StringContent t -> t
                          ContentParts _ -> ""
                      )
                      content

  stream _ _ _ _ = return $ Left "stream functionality for OpenAI is not supported yet"

toOpenAIMessages :: LLM.ChatMessage -> [Message]
toOpenAIMessages msgs = map go (NE.toList msgs)
  where
    toRole r = case r of
      LLM.System -> System
      LLM.User -> User
      LLM.Assistant -> Assistant
      LLM.Tool -> Tool

    go :: LLM.Message -> Message
    go msg =
      defaultMessage
        { role = toRole $ LLM.role msg
        , content = Just $ StringContent (LLM.content msg)
        }