packages feed

openai-2.0.0: src/OpenAI/V1/Chat/Completions.hs

-- | @\/v1\/chat\/completions@
--
-- Streaming results are not yet supported
module OpenAI.V1.Chat.Completions
    ( -- * Main types
      CreateChatCompletion(..)
    , _CreateChatCompletion
    , ChatCompletionObject(..)
    , Choice(..)
    , Message(..)
    , messageToContent
    , Content(..)
      -- * Other types
    , InputAudio(..)
    , ImageURL(..)
    , AudioData(..)
    , Modality(..)
    , Prediction(..)
    , Voice(..)
    , AudioFormat(..)
    , AudioParameters(..)
    , ResponseFormat(..)
    , ServiceTier
    , ReasoningEffort(..)
    , SearchContextSize(..)
    , UserLocation(..)
    , WebSearchOptions(..)
    , FinishReason(..)
    , Token(..)
    , LogProbs(..)
      -- * Servant
    , API
    ) where

import OpenAI.Prelude
import OpenAI.V1.AutoOr
import OpenAI.V1.Models (Model)
import OpenAI.V1.ResponseFormat
import OpenAI.V1.Tool
import OpenAI.V1.ToolCall
import OpenAI.V1.Usage
import Prelude hiding (id)

-- | Audio content part
data InputAudio = InputAudio{ data_ :: Text, format :: AudioFormat }
    deriving stock (Generic, Show)

instance FromJSON InputAudio where
    parseJSON = genericParseJSON aesonOptions

instance ToJSON InputAudio where
    toJSON = genericToJSON aesonOptions

-- | Image content part
data ImageURL = ImageURL{ url :: Text, detail :: Maybe (AutoOr Text) }
    deriving stock (Generic, Show)
    deriving anyclass (FromJSON, ToJSON)

-- | A content part
data Content
    = Text{ text :: Text }
    | Image_URL{ image_url :: ImageURL }
    | Input_Audio{ input_audio :: InputAudio }
    deriving (Generic, Show)

instance IsString Content where
    fromString string = Text{ text = fromString string }

contentOptions :: Options
contentOptions =
    aesonOptions
        { sumEncoding =
            TaggedObject{ tagFieldName = "type", contentsFieldName = "" }

        , tagSingleConstructors = True
        }

instance FromJSON Content where
    parseJSON = genericParseJSON contentOptions

instance ToJSON Content where
    toJSON = genericToJSON contentOptions

-- | Data about a previous audio response from the model.
-- [Learn more](https://platform.openai.com/docs/guides/audio)
data AudioData = AudioData{ id :: Text }
    deriving stock (Generic, Show)
    deriving anyclass (FromJSON, ToJSON)

-- | A message from the conversation so far
data Message content
    = System
        { content :: content
        , name :: Maybe Text
        }
    | User
        { content :: content
        , name :: Maybe Text
        }
    | Assistant
        { assistant_content :: Maybe content
        , refusal :: Maybe Text
        , name :: Maybe Text
        , assistant_audio :: Maybe AudioData
        , tool_calls :: Maybe (Vector ToolCall)
        }
    | Tool
        { content :: content
        , tool_call_id :: Text
        }
    deriving stock (Generic, Show)

messageOptions :: Options
messageOptions = aesonOptions
    { sumEncoding =
        TaggedObject{ tagFieldName = "role", contentsFieldName = "" }

    , tagSingleConstructors = True

    , fieldLabelModifier = stripPrefix "assistant_"
    }

instance FromJSON content => FromJSON (Message content) where
    parseJSON = genericParseJSON messageOptions

instance ToJSON content => ToJSON (Message content) where
    toJSON = genericToJSON messageOptions

-- | Extract the message body from a `Message`
--
-- Normally this would just be the @content@ field selector, but the problem
-- is that the content field for the `Assistant` constructor is not required
-- to be present, so we provide a utility function to default to extract the
-- @content@ field for all constructors, defaulting to `mempty` for the special
-- case where the `Message` is an `Assistant` constructor with a missing
-- @content@ field
messageToContent :: Monoid content => Message content -> content
messageToContent System{ content } = content
messageToContent User{ content } = content
messageToContent Assistant{ assistant_content = Just content } = content
messageToContent Assistant{ assistant_content = Nothing } = mempty
messageToContent Tool{ content } = content

-- | Output types that you would like the model to generate for this request
data Modality = Modality_Text | Modality_Audio
    deriving stock (Generic, Show)

modalityOptions :: Options
modalityOptions =
    aesonOptions
        { constructorTagModifier = stripPrefix "Modality_" }

instance FromJSON Modality where
    parseJSON = genericParseJSON modalityOptions

instance ToJSON Modality where
    toJSON = genericToJSON modalityOptions

-- | Configuration for a
-- [Predicted Output](https://platform.openai.com/docs/guides/predicted-outputs),
-- which can greatly improve response times when large parts of the model
-- response are known ahead of time. This is most common when you are
-- regenerating a file with only minor changes to most of the content
data Prediction = Content{ content :: Text }
    deriving stock (Generic, Show)

predictionOptions :: Options
predictionOptions =
    aesonOptions
        { sumEncoding =
            TaggedObject{ tagFieldName = "type", contentsFieldName = "" }

        , tagSingleConstructors = True
        }

instance FromJSON Prediction where
    parseJSON = genericParseJSON predictionOptions

instance ToJSON Prediction where
    toJSON = genericToJSON predictionOptions

-- | The voice the model uses to respond
data Voice = Ash | Ballad | Coral | Sage | Verse
    deriving stock (Generic, Show)

instance FromJSON Voice where
    parseJSON = genericParseJSON aesonOptions

instance ToJSON Voice where
    toJSON = genericToJSON aesonOptions

-- | Specifies the output audio format
data AudioFormat = WAV | MP3 | FLAC | Opus | PCM16
    deriving stock (Generic, Show)

instance FromJSON AudioFormat where
    parseJSON = genericParseJSON aesonOptions

instance ToJSON AudioFormat where
    toJSON = genericToJSON aesonOptions

-- | Parameters for audio output
data AudioParameters = AudioParameters
    { voice :: Voice
    , format :: AudioFormat
    } deriving stock (Generic, Show)
      deriving anyclass (FromJSON, ToJSON)

-- | Specifies the latency tier to use for processing the request
type ServiceTier = Text

-- | Constrains effort on reasoning for reasoning models
data ReasoningEffort
    = ReasoningEffort_Minimal
    | ReasoningEffort_Low
    | ReasoningEffort_Medium
    | ReasoningEffort_High
    deriving stock (Generic, Show)

reasoningEffortOptions :: Options
reasoningEffortOptions =
    aesonOptions
        { constructorTagModifier = stripPrefix "ReasoningEffort_" }

instance FromJSON ReasoningEffort where
    parseJSON = genericParseJSON reasoningEffortOptions

instance ToJSON ReasoningEffort where
    toJSON = genericToJSON reasoningEffortOptions

-- | High level guidance for the amount of context window space to use for the
-- search
data SearchContextSize
    = SearchContextSize_Low
    | SearchContextSize_Medium
    | SearchContextSize_High
    deriving stock (Generic, Show)

instance FromJSON SearchContextSize where
    parseJSON = genericParseJSON searchContextSizeOptions

instance ToJSON SearchContextSize where
    toJSON = genericToJSON searchContextSizeOptions

-- | Approximate location parameters for the search
data UserLocation = Approximate
    { city :: Maybe Text
    , country :: Maybe Text
    , region :: Maybe Text
    , timezone :: Maybe Text
    } deriving stock (Generic, Show)

instance FromJSON UserLocation where
    parseJSON = genericParseJSON userLocationOptions

instance ToJSON UserLocation where
    toJSON = genericToJSON userLocationOptions

userLocationOptions :: Options
userLocationOptions =
    aesonOptions
        { sumEncoding =
            TaggedObject{ tagFieldName = "type", contentsFieldName = "" }

        , tagSingleConstructors = True
        }

searchContextSizeOptions :: Options
searchContextSizeOptions =
    aesonOptions
        { constructorTagModifier = stripPrefix "SearchContextSize_" }

-- | Search the web for relevant results to use in a response
data WebSearchOptions = WebSearchOptions
    { search_context_size :: Maybe SearchContextSize
    , user_location :: Maybe UserLocation
    } deriving stock (Generic, Show)
      deriving anyclass (FromJSON, ToJSON)

-- | Request body for @\/v1\/chat\/completions@
data CreateChatCompletion = CreateChatCompletion
    { messages :: Vector (Message (Vector Content))
    , model :: Model
    , store :: Maybe Bool
    , metadata :: Maybe (Map Text Text)
    , frequency_penalty :: Maybe Double
    , logit_bias :: Maybe (Map Word Int)
    , logprobs :: Maybe Bool
    , top_logprobs :: Maybe Word
    , max_completion_tokens :: Maybe Natural
    , n :: Maybe Natural
    , modalities :: Maybe (Vector Modality)
    , prediction :: Maybe Prediction
    , audio :: Maybe AudioParameters
    , presence_penalty :: Maybe Double
    , reasoning_effort :: Maybe ReasoningEffort
    , response_format :: Maybe ResponseFormat
    , seed :: Maybe Integer
    , service_tier :: Maybe (AutoOr ServiceTier)
    , stop :: Maybe (Vector Text)
    , temperature :: Maybe Double
    , top_p :: Maybe Double
    , tools :: Maybe (Vector Tool)
    , tool_choice :: Maybe ToolChoice
    , parallel_tool_calls :: Maybe Bool
    , user :: Maybe Text
    , web_search_options :: Maybe WebSearchOptions
    } deriving stock (Generic, Show)

instance FromJSON CreateChatCompletion where
    parseJSON = genericParseJSON aesonOptions

instance ToJSON CreateChatCompletion where
    toJSON = genericToJSON aesonOptions

-- | Default `CreateChatCompletion`
_CreateChatCompletion :: CreateChatCompletion
_CreateChatCompletion = CreateChatCompletion
    { store = Nothing
    , metadata = Nothing
    , frequency_penalty = Nothing
    , logit_bias = Nothing
    , logprobs = Nothing
    , top_logprobs = Nothing
    , max_completion_tokens = Nothing
    , n = Nothing
    , modalities = Nothing
    , prediction = Nothing
    , audio = Nothing
    , presence_penalty = Nothing
    , reasoning_effort = Nothing
    , response_format = Nothing
    , seed = Nothing
    , service_tier = Nothing
    , stop = Nothing
    , temperature = Nothing
    , top_p = Nothing
    , tools = Nothing
    , tool_choice = Nothing
    , parallel_tool_calls = Nothing
    , user = Nothing
    , web_search_options = Nothing
    }

-- | The reason the model stopped generating tokens
data FinishReason
    = Stop
    | Length
    | Content_Filter
    | Tool_Calls
    deriving stock (Generic, Show)

instance FromJSON FinishReason where
    parseJSON = genericParseJSON aesonOptions

instance ToJSON FinishReason where
    toJSON = genericToJSON aesonOptions

-- | Message tokens with log probability information
data Token = Token
    { token :: Text
    , logprob :: Double
    , bytes :: Maybe (Vector Word8)
    , top_logprobs :: Maybe (Vector Token)
    } deriving stock (Generic, Show)
      deriving anyclass (FromJSON, ToJSON)

-- | Log probability information for the choice
data LogProbs = LogProbs
    { content :: Maybe (Vector Token)
    , refusal :: Maybe (Vector Token)
    } deriving stock (Generic, Show)
      deriving anyclass (FromJSON, ToJSON)

-- | A chat completion choice
data Choice = Choice
    { finish_reason :: Text
    , index :: Natural
    , message :: Message Text
    , logprobs :: Maybe LogProbs
    } deriving stock (Generic, Show)
      deriving anyclass (FromJSON, ToJSON)

-- | ChatCompletion body
data ChatCompletionObject = ChatCompletionObject
    { id :: Text
    , choices :: Vector Choice
    , created :: POSIXTime
    , model :: Model
    , reasoning_effort :: Maybe ReasoningEffort
    , service_tier :: Maybe ServiceTier
    , system_fingerprint :: Maybe Text
    , object :: Text
    , usage :: Usage CompletionTokensDetails PromptTokensDetails
    } deriving stock (Generic, Show)
      deriving anyclass (FromJSON, ToJSON)

-- | Servant API
type API =
        "chat"
    :>  "completions"
    :>  ReqBody '[JSON] CreateChatCompletion
    :>  Post '[JSON] ChatCompletionObject