packages feed

openai 1.2.0 → 2.0.0

raw patch · 32 files changed

+516/−204 lines, 32 filesdep +unordered-containersdep ~basenew-component:exe:responses-tool-call-examplenew-uploader

Dependencies added: unordered-containers

Dependency ranges changed: base

Files

CHANGELOG.md view
@@ -1,3 +1,28 @@+2.0.0:++- **BREAKING CHANGE**: Renamed `Item_InputMessage` to `Item_Input_Message` in `OpenAI.V1.Responses` for consistency with new constructors++  The `InputItem` data type in the Responses API has been updated to use a consistent naming scheme. If you are using the Responses API, you will need to update your code:++  ```haskell+  -- Before (v1.2.0):+  Item_InputMessage { role = ..., content = ..., status = ... }++  -- After (v2.0.0):+  Item_Input_Message { role = ..., content = ..., status = ... }+  ```++- Add support for function tool calls in Responses API+  - New `InputItem` constructors: `Item_Input_Function_Call`, `Item_Input_Function_Call_Output`, `Item_Input_Item_Reference`+  - Add flattened tool JSON representation for Responses API compatibility+  - Export status constants: `statusIncomplete`, `statusCompleted`+  - Export tool choice constants: `toolChoiceNoneText`, `toolChoiceAutoText`, `toolChoiceRequiredText`++- Code quality improvements:+  - Optimize `isFunctionField` using `HashSet` for O(1) lookups+  - Simplify `unflattenChoice` with guards+  - Extract magic strings as named constants+ 1.2.0:  - [`/v1/responses`: Add support for Responses API](https://platform.openai.com/docs/api-reference/responses)
README.md view
@@ -61,7 +61,7 @@     let req = Responses._CreateResponse             { Responses.model = "gpt-5"             , Responses.input = Just (Responses.Input-                [ Responses.Item_InputMessage+                [ Responses.Item_Input_Message                     { Responses.role = Responses.User                     , Responses.content = [ Responses.Input_Text{ Responses.text = "Say hello in one sentence." } ]                     , Responses.status = Nothing
examples/openai-example/Main.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE NamedFieldPuns        #-}-{-# LANGUAGE OverloadedStrings     #-} {-# LANGUAGE OverloadedLists       #-}+{-# LANGUAGE OverloadedStrings     #-}  module Main where 
examples/responses-example/Main.hs view
@@ -1,16 +1,17 @@-{-# LANGUAGE OverloadedLists #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE NamedFieldPuns        #-}+{-# LANGUAGE OverloadedLists       #-}+{-# LANGUAGE OverloadedStrings     #-}  module Main where  import Data.Foldable (toList)+import System.Environment (getEnv)+ import qualified Data.Text as Text import qualified Data.Text.IO as TextIO import qualified OpenAI.V1 as V1 import qualified OpenAI.V1.Responses as Responses-import System.Environment (getEnv)  main :: IO () main = do@@ -22,7 +23,7 @@     let req = Responses._CreateResponse             { Responses.model = "gpt-5"             , Responses.input = Just (Responses.Input-                [ Responses.Item_InputMessage+                [ Responses.Item_Input_Message                     { Responses.role = Responses.User                     , Responses.content = [ Responses.Input_Text{ Responses.text = "Tell me a three sentence bedtime story about a unicorn." } ]                     , Responses.status = Nothing
examples/responses-stream-example/Main.hs view
@@ -1,17 +1,18 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE NamedFieldPuns        #-}+{-# LANGUAGE OverloadedLists       #-}+{-# LANGUAGE OverloadedStrings     #-}  module Main where +import System.Environment (getEnv)+import System.IO (hFlush, hPutStrLn, stderr, stdout)+ import qualified Data.Text as T import qualified Data.Text.IO as TIO import qualified OpenAI.V1 as V1 import qualified OpenAI.V1.Responses as Responses import qualified OpenAI.V1.Tool as Tool-import System.Environment (getEnv)-import System.IO (hFlush, hPutStrLn, stderr, stdout)  main :: IO () main = do@@ -33,7 +34,7 @@     let reqHaiku = Responses._CreateResponse             { Responses.model = "gpt-5-mini"             , Responses.input = Just (Responses.Input-                [ Responses.Item_InputMessage+                [ Responses.Item_Input_Message                     { Responses.role = Responses.User                     , Responses.content = [ Responses.Input_Text{ Responses.text = "Write a short haiku about the sea." } ]                     , Responses.status = Nothing@@ -42,14 +43,14 @@             }      createResponseStreamTyped reqHaiku onEvent-    +     putStrLn "--------------------------------"      -- 2) Web search example     let reqSearch = Responses._CreateResponse             { Responses.model = "gpt-5-mini"             , Responses.input = Just (Responses.Input-                [ Responses.Item_InputMessage+                [ Responses.Item_Input_Message                     { Responses.role = Responses.User                     , Responses.content = [ Responses.Input_Text{ Responses.text = "Use web_search to find current news about France and display a concise summary. Do not include citations, references, or URLs in the output; provide only the summary text." } ]                     , Responses.status = Nothing@@ -67,7 +68,7 @@             { Responses.model = "gpt-5-mini"             , Responses.instructions = Just "You are a personal math tutor. When asked a math question, write and run code using the python tool to answer the question."             , Responses.input = Just (Responses.Input-                [ Responses.Item_InputMessage+                [ Responses.Item_Input_Message                     { Responses.role = Responses.User                     , Responses.content = [ Responses.Input_Text{ Responses.text = "I need to solve the equation 3x + 11 = 14. Can you help me?" } ]                     , Responses.status = Nothing
+ examples/responses-tool-call-example/Main.hs view
@@ -0,0 +1,168 @@+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedLists #-}+{-# LANGUAGE OverloadedStrings #-}++module Main where++import Data.Aeson (FromJSON (..), (.:), (.=), withObject)+import qualified Data.Aeson as Aeson+import qualified Data.ByteString.Lazy as LBS+import Data.Foldable (toList)+import Data.Text (Text)+import qualified Data.Text as Text+import qualified Data.Text.Encoding as Text.Encoding+import qualified Data.Text.IO as TextIO+import qualified Data.Vector as Vector+import OpenAI.V1 (Methods (..))+import qualified OpenAI.V1 as V1+import qualified OpenAI.V1.Responses as Responses+import OpenAI.V1.Tool (Function (..), Tool (..))+import System.Environment (getEnv)++-- | Simple JSON payload for parsing function arguments+newtype HoroscopeArgs = HoroscopeArgs { sign :: Text }++instance FromJSON HoroscopeArgs where+    parseJSON = withObject "HoroscopeArgs" $ \obj -> do+        sign <- obj .: "sign"+        pure HoroscopeArgs{ sign }++getHoroscope :: Text -> Text+getHoroscope sign = sign <> ": Next Tuesday you will befriend a baby otter."++horoscopeTool :: Tool+horoscopeTool =+    Tool_Function+        Function+            { description = Just "Get today's horoscope for an astrological sign."+            , name = "get_horoscope"+            , parameters =+                Just . Aeson.object $+                    [ "type" .= ("object" :: Text)+                    , "properties"+                        .= Aeson.object+                            [ "sign"+                                .= Aeson.object+                                    [ "type" .= ("string" :: Text)+                                    , "description" .= ("An astrological sign like Taurus or Aquarius" :: Text)+                                    ]+                            ]+                    , "required" .= (["sign"] :: [Text])+                    , "additionalProperties" .= False+                    ]+            , strict = Just True+            }++main :: IO ()+main = do+    key <- Text.pack <$> getEnv "OPENAI_KEY"+    env <- V1.getClientEnv "https://api.openai.com"++    let Methods{ createResponse } = V1.makeMethods env key Nothing Nothing+        userMessage =+            Responses.Item_Input_Message+                { Responses.role = Responses.User+                , Responses.content = Vector.singleton Responses.Input_Text{ Responses.text = "What is my horoscope? I am an Aquarius." }+                , Responses.status = Nothing+                }+        initialItems = [userMessage]+        initialInput = Responses.Input (Vector.fromList initialItems)+        firstRequest =+            Responses._CreateResponse+                { Responses.model = "gpt-5"+                , Responses.input = Just initialInput+                , Responses.tools = Just [horoscopeTool]+                }++    firstResponse <- createResponse firstRequest++    let Responses.ResponseObject{ Responses.output = outputItems } = firstResponse+        outputList = toList outputItems+        hasFunctionCall = Prelude.any isFunctionCall outputList++    if not hasFunctionCall+        then mapM_ TextIO.putStrLn (collectText firstResponse)+        else do+            additionalInputs <- gatherInputs outputList++            let finalItems = initialItems <> additionalInputs+                finalInput = Responses.Input (Vector.fromList finalItems)+                secondRequest =+                    Responses._CreateResponse+                        { Responses.model = "gpt-5"+                        , Responses.instructions = Just "Respond only with a horoscope generated by a tool."+                        , Responses.input = Just finalInput+                        , Responses.tools = Just [horoscopeTool]+                        }++            finalResponse <- createResponse secondRequest++            mapM_ TextIO.putStrLn (collectText finalResponse)++isFunctionCall :: Responses.OutputItem -> Bool+isFunctionCall Responses.Item_FunctionToolCall{} = True+isFunctionCall _ = False++gatherInputs :: [Responses.OutputItem] -> IO [Responses.InputItem]+gatherInputs [] = pure []+gatherInputs (item : rest) =+    case item of+        Responses.Item_Reasoning{ Responses.reasoning_id } -> do+            restInputs <- gatherInputs rest+            let reference = Responses.Item_Input_Item_Reference{ Responses.id = Just reasoning_id }+            pure (reference : restInputs)+        call@Responses.Item_FunctionToolCall{} -> do+            (callInput, callOutput) <- processFunctionCall call+            restInputs <- gatherInputs rest+            pure (callInput : callOutput : restInputs)+        _ -> gatherInputs rest++processFunctionCall+    :: Responses.OutputItem+    -> IO (Responses.InputItem, Responses.InputItem)+processFunctionCall+    Responses.Item_FunctionToolCall+        { Responses.function_id = functionId+        , Responses.function_call_id = callId+        , Responses.function_name = functionName+        , Responses.function_arguments = argumentsText+        , Responses.function_status = statusText+        } = do+    let callInput =+            Responses.Item_Input_Function_Call+                { Responses.id = functionId+                , Responses.call_id = callId+                , Responses.name = functionName+                , Responses.arguments = argumentsText+                , Responses.status = statusText+                }+        argumentsBytes = Text.Encoding.encodeUtf8 argumentsText++        makeOutput status payload =+            Responses.Item_Input_Function_Call_Output+                { Responses.id = Nothing+                , Responses.call_id = callId+                , Responses.output = encodePayload payload+                , Responses.status = Just status+                }++        encodePayload = Text.Encoding.decodeUtf8 . LBS.toStrict . Aeson.encode++    result <- case Aeson.eitherDecodeStrict' argumentsBytes of+        Left err ->+            pure $ makeOutput Responses.statusIncomplete (Aeson.object ["error" .= Text.pack err])+        Right HoroscopeArgs{ sign } -> do+            let horoscope = getHoroscope sign+            pure $ makeOutput Responses.statusCompleted (Aeson.object ["horoscope" .= horoscope])++    pure (callInput, result)+processFunctionCall other =+    error $ "Unexpected output item: " <> show other++collectText :: Responses.ResponseObject -> [Text]+collectText Responses.ResponseObject{ Responses.output } =+    [ text+    | Responses.Item_OutputMessage{ Responses.message_content } <- toList output+    , Responses.Output_Text{ Responses.text } <- toList message_content+    ]
examples/weather-chatbot-example/Main.hs view
@@ -1,12 +1,12 @@ {-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE OverloadedLists #-}-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE NamedFieldPuns        #-}+{-# LANGUAGE OverloadedLists       #-}+{-# LANGUAGE OverloadedStrings     #-}  module Main where  import Control.Monad (foldM, when)-import Data.Aeson (Value (..), (.=))+import Data.Aeson (Value(..), (.=)) import Data.Text (Text) import Data.Vector (Vector) import GHC.Exts (toList)
openai.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.4 name:               openai-version:            1.2.0+version:            2.0.0 synopsis:           Servant bindings to OpenAI description:        This package provides comprehensive and type-safe bindings                     to OpenAI, providing both a Servant interface and@@ -42,6 +42,7 @@                       , servant-multipart-client                       , text                       , time+                      , unordered-containers                       , vector     exposed-modules:    OpenAI.V1                         OpenAI.V1.Assistants@@ -145,6 +146,18 @@     hs-source-dirs:   examples/responses-example     main-is:          Main.hs     build-depends:    base+                    , openai+                    , text+                    , vector+    ghc-options:      -Wall++executable responses-tool-call-example+    default-language: Haskell2010+    hs-source-dirs:   examples/responses-tool-call-example+    main-is:          Main.hs+    build-depends:    base+                    , aeson+                    , bytestring                     , openai                     , text                     , vector
src/OpenAI/Prelude.hs view
@@ -3,13 +3,11 @@       aesonOptions     , stripPrefix     , labelModifier-       -- * Multipart Form Data     , input     , renderIntegral     , renderRealFloat     , getExtension-       -- * Re-exports     , module Data.Aeson     , module Data.ByteString.Lazy@@ -43,9 +41,9 @@  import Data.Aeson     ( FromJSON(..)-    , ToJSON(..)     , Options(..)     , SumEncoding(..)+    , ToJSON(..)     , Value(..)     , genericParseJSON     , genericToJSON@@ -73,8 +71,8 @@     , Input(..)     , MultipartData(..)     , MultipartForm-    , ToMultipart(..)     , Tmp+    , ToMultipart(..)     )  import qualified Data.Aeson as Aeson@@ -83,8 +81,8 @@ import qualified Data.Text as Text import qualified Data.Text.Lazy as Text.Lazy import qualified Data.Text.Lazy.Builder as Builder-import qualified Data.Text.Lazy.Builder.RealFloat as RealFloat import qualified Data.Text.Lazy.Builder.Int as Int+import qualified Data.Text.Lazy.Builder.RealFloat as RealFloat import qualified System.FilePath as FilePath  dropTrailingUnderscore :: String -> String
src/OpenAI/V1.hs view
@@ -40,50 +40,43 @@  module OpenAI.V1     ( -- * Methods-      getClientEnv+      Methods(..)+    , getClientEnv     , makeMethods-    , Methods(..)-       -- * Servant     , API     ) where +import Control.Monad (foldM) import Data.ByteString.Char8 () import Data.Proxy (Proxy(..)) import OpenAI.Prelude import OpenAI.V1.Audio.Speech (CreateSpeech)-import OpenAI.V1.Embeddings (CreateEmbeddings, EmbeddingObject)+import OpenAI.V1.Audio.Transcriptions (CreateTranscription, TranscriptionObject)+import OpenAI.V1.Audio.Translations (CreateTranslation, TranslationObject) import OpenAI.V1.Batches (BatchID, BatchObject, CreateBatch)+import OpenAI.V1.Chat.Completions (ChatCompletionObject, CreateChatCompletion) import OpenAI.V1.DeletionStatus (DeletionStatus)+import OpenAI.V1.Embeddings (CreateEmbeddings, EmbeddingObject) import OpenAI.V1.Files (FileID, FileObject, UploadFile)-import OpenAI.V1.Images.Image (ImageObject)-import OpenAI.V1.Images.Generations (CreateImage) import OpenAI.V1.Images.Edits (CreateImageEdit)+import OpenAI.V1.Images.Generations (CreateImage)+import OpenAI.V1.Images.Image (ImageObject) import OpenAI.V1.Images.Variations (CreateImageVariation) import OpenAI.V1.ListOf (ListOf(..)) import OpenAI.V1.Message (Message) import OpenAI.V1.Models (Model, ModelObject) import OpenAI.V1.Moderations (CreateModeration, Moderation) import OpenAI.V1.Order (Order)-import OpenAI.V1.Threads (Thread, ThreadID, ModifyThread, ThreadObject)+import OpenAI.V1.Responses (CreateResponse, InputItem, ResponseObject)+import OpenAI.V1.Threads (ModifyThread, Thread, ThreadID, ThreadObject)+import OpenAI.V1.Threads.Messages (MessageID, MessageObject, ModifyMessage) import OpenAI.V1.Threads.Runs.Steps (RunStepObject(..), StepID) import Servant.Client (ClientEnv) import Servant.Multipart.Client ()  import OpenAI.V1.Assistants     (AssistantID, AssistantObject, CreateAssistant, ModifyAssistant)-import OpenAI.V1.Audio.Transcriptions-    (CreateTranscription, TranscriptionObject)-import OpenAI.V1.Audio.Translations-    (CreateTranslation, TranslationObject)-import OpenAI.V1.Chat.Completions-    (ChatCompletionObject, CreateChatCompletion)-import OpenAI.V1.Responses-    ( CreateResponse-    , ResponseObject-    , InputItem-    )-import qualified OpenAI.V1.Responses as Responses import OpenAI.V1.FineTuning.Jobs     ( CheckpointObject     , CreateFineTuningJob@@ -91,8 +84,6 @@     , FineTuningJobID     , JobObject     )-import OpenAI.V1.Threads.Messages-    (MessageID, MessageObject, ModifyMessage) import OpenAI.V1.Threads.Runs     ( CreateRun     , CreateThreadAndRun@@ -115,45 +106,42 @@     , VectorStoreID     , VectorStoreObject(..)     )-import OpenAI.V1.VectorStores.Files-    ( CreateVectorStoreFile(..)-    , VectorStoreFileID-    , VectorStoreFileObject(..)-    ) import OpenAI.V1.VectorStores.FileBatches     ( CreateVectorStoreFileBatch(..)-    , VectorStoreFilesBatchObject(..)     , VectorStoreFileBatchID+    , VectorStoreFilesBatchObject(..)     )+import OpenAI.V1.VectorStores.Files+    (CreateVectorStoreFile(..), VectorStoreFileID, VectorStoreFileObject(..))  import qualified Control.Exception as Exception-import qualified Data.Text as Text-import qualified Network.HTTP.Client as HTTP.Client-import qualified Network.HTTP.Client.TLS as TLS-import qualified Network.HTTP.Types.Status as Status import qualified Data.Aeson as Aeson import qualified Data.ByteString as SBS import qualified Data.ByteString.Char8 as S8-import Control.Monad (foldM) import qualified Data.IORef as IORef+import qualified Data.Text as Text+import qualified Network.HTTP.Client as HTTP.Client+import qualified Network.HTTP.Client.TLS as TLS+import qualified Network.HTTP.Types.Status as Status import qualified OpenAI.V1.Assistants as Assistants import qualified OpenAI.V1.Audio as Audio import qualified OpenAI.V1.Batches as Batches import qualified OpenAI.V1.Chat.Completions as Chat.Completions import qualified OpenAI.V1.Embeddings as Embeddings-import qualified OpenAI.V1.FineTuning.Jobs as FineTuning.Jobs import qualified OpenAI.V1.Files as Files+import qualified OpenAI.V1.FineTuning.Jobs as FineTuning.Jobs import qualified OpenAI.V1.Images as Images import qualified OpenAI.V1.Models as Models import qualified OpenAI.V1.Moderations as Moderations-import qualified OpenAI.V1.Threads.Runs as Threads.Runs-import qualified OpenAI.V1.Threads.Runs.Steps as Threads.Runs.Steps+import qualified OpenAI.V1.Responses as Responses import qualified OpenAI.V1.Threads as Threads import qualified OpenAI.V1.Threads.Messages as Messages+import qualified OpenAI.V1.Threads.Runs as Threads.Runs+import qualified OpenAI.V1.Threads.Runs.Steps as Threads.Runs.Steps import qualified OpenAI.V1.Uploads as Uploads import qualified OpenAI.V1.VectorStores as VectorStores-import qualified OpenAI.V1.VectorStores.Files as VectorStores.Files import qualified OpenAI.V1.VectorStores.FileBatches as VectorStores.FileBatches+import qualified OpenAI.V1.VectorStores.Files as VectorStores.Files import qualified OpenAI.V1.VectorStores.Status as VectorStores.Status import qualified Servant.Client as Client @@ -271,7 +259,7 @@                 :<|>  retrieveRunStep                 )             )-      +       :<|>  (   (\x -> x "assistants=v2")             ->  (     createVectorStore                 :<|>  listVectorStores_
src/OpenAI/V1/Assistants.hs view
@@ -7,7 +7,6 @@     , ModifyAssistant(..)     , _ModifyAssistant     , AssistantObject(..)-       -- * Other types     , RankingOptions     , FileSearch(..)@@ -16,7 +15,6 @@     , CodeInterpreterResources(..)     , FileSearchResources(..)     , ToolResources(..)-       -- * Servant     , API     ) where
src/OpenAI/V1/Audio.hs view
@@ -5,6 +5,7 @@     ) where  import OpenAI.Prelude+ import qualified OpenAI.V1.Audio.Speech as Speech import qualified OpenAI.V1.Audio.Transcriptions as Transcriptions import qualified OpenAI.V1.Audio.Translations as Translations
src/OpenAI/V1/Chat/Completions.hs view
@@ -28,7 +28,7 @@     , FinishReason(..)     , Token(..)     , LogProbs(..)-    -- * Servant+      -- * Servant     , API     ) where 
src/OpenAI/V1/ChunkingStrategy.hs view
@@ -2,7 +2,6 @@ module OpenAI.V1.ChunkingStrategy     ( -- * Main types       ChunkingStrategy(..)-       -- * Other types     , Static(..)     ) where
src/OpenAI/V1/Files.hs view
@@ -5,11 +5,11 @@     , UploadFile(..)     , _UploadFile     , FileObject(..)-    -- * Other types+      -- * Other types     , Order(..)     , Purpose(..)     , DeletionStatus(..)-    -- * Servant+      -- * Servant     , API     ) where 
src/OpenAI/V1/FineTuning/Jobs.hs view
@@ -7,7 +7,6 @@     , JobObject(..)     , EventObject(..)     , CheckpointObject(..)-       -- * Other types     , AutoOr(..)     , Hyperparameters(..)@@ -16,7 +15,6 @@     , Status(..)     , Level(..)     , Metrics(..)-       -- * Servant     , API     ) where@@ -25,8 +23,8 @@ import OpenAI.V1.AutoOr import OpenAI.V1.Error import OpenAI.V1.Files (FileID)-import OpenAI.V1.Models (Model) import OpenAI.V1.ListOf+import OpenAI.V1.Models (Model)  -- | Fine tuning job ID newtype FineTuningJobID = FineTuningJobID{ text :: Text }
src/OpenAI/V1/Images.hs view
@@ -6,8 +6,8 @@  import OpenAI.Prelude -import qualified OpenAI.V1.Images.Generations as Generations import qualified OpenAI.V1.Images.Edits as Edits+import qualified OpenAI.V1.Images.Generations as Generations import qualified OpenAI.V1.Images.Variations as Variations  -- | Servant API
src/OpenAI/V1/Message.hs view
@@ -2,7 +2,6 @@ module OpenAI.V1.Message     ( -- * Main types       Message(..)-       -- * Other types     , ImageFile(..)     , ImageURL(..)
src/OpenAI/V1/Models.hs view
@@ -3,7 +3,6 @@     ( -- * Main types       Model(..)     , ModelObject(..)-       -- * Servant     , API     ) where
src/OpenAI/V1/Moderations.hs view
@@ -4,11 +4,9 @@       CreateModeration(..)     , _CreateModeration     , Moderation(..)-       -- * Other types     , InputType(..)     , Result(..)-       -- * Servant     , API     ) where@@ -44,9 +42,10 @@     { flagged :: Bool     , categories :: Map Text Bool     , category_scores :: Map Text Double-    , category_applied_input_types :: Maybe (Map Text InputType)+    , category_applied_input_types :: Maybe (Map Text [InputType])     -- According to the OpenAPI spec the `category_applied_input_types`     -- field is required but their actual implementation omits this field.+    -- Note: Each category maps to a list of input types (e.g., ["text"], ["image"])     } deriving stock (Generic, Show)       deriving anyclass (FromJSON, ToJSON) 
src/OpenAI/V1/Responses.hs view
@@ -30,18 +30,38 @@     , ResponseUsage(..)     , InputTokensDetails(..)     , OutputTokensDetails(..)-+      -- * Constants+    , statusIncomplete+    , statusCompleted       -- * Servant     , API     ) where -import OpenAI.Prelude hiding (Input(..)) import Data.Aeson (Object)+import qualified Data.Aeson as Aeson+import OpenAI.Prelude hiding (Input(..)) -- no TH; inline JSON instances for payloads import OpenAI.V1.ListOf (ListOf) import OpenAI.V1.Models (Model)-import OpenAI.V1.Tool (Tool, ToolChoice)+import qualified Data.Aeson.Key as Key+import qualified Data.Aeson.KeyMap as KeyMap+import qualified Data.Vector as Vector+import OpenAI.V1.Tool+    ( Tool+    , ToolChoice+    , toolChoiceAutoText+    , toolChoiceNoneText+    , toolChoiceRequiredText+    , toolChoiceToResponsesValue+    , toolToResponsesValue+    , unflattenToolValue+    ) +-- | Status constants for function call outputs+statusIncomplete, statusCompleted :: Text+statusIncomplete = "incomplete"+statusCompleted = "completed"+ -- | Input for the Responses API: a list of input items newtype Input = Input (Vector InputItem)     deriving stock (Generic, Show)@@ -87,11 +107,27 @@  -- | An input item data InputItem-    = Item_InputMessage+    = Item_Input_Message         { role :: InputRole         , content :: Vector InputContent         , status :: Maybe Text         }+    | Item_Input_Function_Call+        { id :: Maybe Text+        , call_id :: Text+        , name :: Text+        , arguments :: Text+        , status :: Maybe Text+        }+    | Item_Input_Function_Call_Output+        { id :: Maybe Text+        , call_id :: Text+        , output :: Text+        , status :: Maybe Text+        }+    | Item_Input_Item_Reference+        { id :: Maybe Text+        }     deriving stock (Generic, Show)  inputItemOptions :: Options@@ -99,7 +135,7 @@     aesonOptions         { sumEncoding = TaggedObject{ tagFieldName = "type", contentsFieldName = "" }         , tagSingleConstructors = True-        , constructorTagModifier = stripPrefix "Item_Input"+        , constructorTagModifier = stripPrefix "Item_Input_"         }  instance FromJSON InputItem where@@ -108,6 +144,50 @@ instance ToJSON InputItem where     toJSON = genericToJSON inputItemOptions +keyTools :: Key.Key+keyTools = Key.fromText "tools"++keyToolChoice :: Key.Key+keyToolChoice = Key.fromText "tool_choice"++flattenResponseToolFields+    :: Maybe (Vector Tool)+    -> Maybe ToolChoice+    -> KeyMap.KeyMap Value+    -> KeyMap.KeyMap Value+flattenResponseToolFields mTools mChoice o =+    let oWithTools = case mTools of+            Nothing -> o+            Just ts -> KeyMap.insert keyTools (Aeson.Array (Vector.map toolToResponsesValue ts)) o+        oWithChoice = case mChoice of+            Nothing -> oWithTools+            Just choice -> KeyMap.insert keyToolChoice (toolChoiceToResponsesValue choice) oWithTools+    in oWithChoice++unflattenResponseToolFields :: KeyMap.KeyMap Value -> KeyMap.KeyMap Value+unflattenResponseToolFields = adjustChoice . adjustTools+  where+    adjustTools = adjustKey keyTools (mapArrayValues unflattenToolValue)+    adjustChoice = adjustKey keyToolChoice unflattenChoice++    unflattenChoice (String s)+        | s `elem` ([toolChoiceNoneText, toolChoiceAutoText, toolChoiceRequiredText] :: [Text]) = String s+    unflattenChoice other = unflattenToolValue other++mapArrayValues :: (Value -> Value) -> Value -> Value+mapArrayValues f (Aeson.Array arr) = Aeson.Array (Vector.map f arr)+mapArrayValues _ other = other++adjustKey+    :: Key.Key+    -> (Value -> Value)+    -> KeyMap.KeyMap Value+    -> KeyMap.KeyMap Value+adjustKey key f obj =+    case KeyMap.lookup key obj of+        Nothing -> obj+        Just v -> KeyMap.insert key (f v) obj+ -- | Output content from the model data OutputContent     = Output_Text@@ -674,8 +754,20 @@     , user :: Maybe Text     , metadata :: Maybe (Map Text Text)     } deriving stock (Generic, Show)-      deriving anyclass (FromJSON, ToJSON) +instance FromJSON ResponseObject where+    parseJSON (Aeson.Object o) =+        genericParseJSON aesonOptions (Aeson.Object (unflattenResponseToolFields o))+    parseJSON other = genericParseJSON aesonOptions other++instance ToJSON ResponseObject where+    toJSON response@ResponseObject{ tools = mTools, tool_choice = mChoice } =+        case genericToJSON aesonOptions response of+            Aeson.Object o ->+                Aeson.Object+                    (flattenResponseToolFields mTools mChoice o)+            other -> other+ -- | Request body for /v1/responses data CreateResponse = CreateResponse     { model :: Model@@ -694,10 +786,17 @@     } deriving stock (Generic, Show)  instance FromJSON CreateResponse where-    parseJSON = genericParseJSON aesonOptions+    parseJSON (Aeson.Object o) =+        genericParseJSON aesonOptions (Aeson.Object (unflattenResponseToolFields o))+    parseJSON other = genericParseJSON aesonOptions other  instance ToJSON CreateResponse where-    toJSON = genericToJSON aesonOptions+    toJSON request@CreateResponse{ tools = mTools, tool_choice = mChoice } =+        case genericToJSON aesonOptions request of+            Aeson.Object o ->+                Aeson.Object+                    (flattenResponseToolFields mTools mChoice o)+            other -> other  -- | Default CreateResponse _CreateResponse :: CreateResponse
src/OpenAI/V1/Threads.hs view
@@ -9,12 +9,10 @@     , Message(..)     , Content(..)     , ThreadObject(..)-       -- * Other types     , ImageURL(..)     , ImageFile(..)     , Attachment(..)-       -- * Servant     , API     ) where
src/OpenAI/V1/Threads/Messages.hs view
@@ -1,21 +1,19 @@ -- | @\/v1\/threads/:thread_id/messages@ module OpenAI.V1.Threads.Messages     ( -- * Main types-        MessageID(..)-      , Message(..)-      , ModifyMessage(..)-      , _ModifyMessage-      , MessageObject(..)-+      MessageID(..)+    , Message(..)+    , ModifyMessage(..)+    , _ModifyMessage+    , MessageObject(..)       -- * Other types-      , Status(..)-      , IncompleteDetails(..)-      , File(..)-      , Annotation(..)-      , TextObject(..)-+    , Status(..)+    , IncompleteDetails(..)+    , File(..)+    , Annotation(..)+    , TextObject(..)       -- * Servant-      , API+    , API     ) where  import OpenAI.Prelude
src/OpenAI/V1/Threads/Runs.hs view
@@ -11,7 +11,6 @@     , SubmitToolOutputsToRun(..)     , _SubmitToolOutputsToRun     , RunObject(..)-       -- * Other types     , TruncationStrategy(..)     , SubmitToolOutputs(..)@@ -19,7 +18,6 @@     , IncompleteDetails(..)     , ToolOutput(..)     , Status(..)-       -- * Servant     , API     ) where@@ -33,11 +31,10 @@ import OpenAI.V1.Models (Model) import OpenAI.V1.Order import OpenAI.V1.ResponseFormat-import OpenAI.V1.Threads (Thread)+import OpenAI.V1.Threads (Thread, ThreadID) import OpenAI.V1.Tool import OpenAI.V1.ToolCall import OpenAI.V1.ToolResources-import OpenAI.V1.Threads (ThreadID) import OpenAI.V1.Usage  -- | Run ID
src/OpenAI/V1/Threads/Runs/Steps.hs view
@@ -3,7 +3,6 @@     ( -- * Main types       StepID(..)     , RunStepObject(..)-       -- * Other types     , Status(..)     , Image(..)@@ -16,7 +15,6 @@     , Function(..)     , ToolCall(..)     , StepDetails(..)-       -- * Servant     , API     ) where@@ -26,9 +24,9 @@ import OpenAI.V1.Error import OpenAI.V1.Files (FileID) import OpenAI.V1.ListOf-import OpenAI.V1.Threads.Messages (MessageID) import OpenAI.V1.Order import OpenAI.V1.Threads (ThreadID)+import OpenAI.V1.Threads.Messages (MessageID) import OpenAI.V1.Threads.Runs (RunID) import OpenAI.V1.Usage 
src/OpenAI/V1/Tool.hs view
@@ -7,17 +7,39 @@     , Function(..)     , ToolChoice(..)     , CodeInterpreterContainer(..)-    -- * Helpers+      -- * Constants+    , toolChoiceNoneText+    , toolChoiceAutoText+    , toolChoiceRequiredText+      -- * Helpers     , codeInterpreter     , codeInterpreterAuto     , codeInterpreterWithFiles+    , toolToResponsesValue+    , parseResponsesToolValue+    , toolChoiceToResponsesValue+    , parseResponsesToolChoiceValue+    , flattenToolValue+    , unflattenToolValue     ) where -import OpenAI.Prelude import Data.Aeson ((.:), (.:?), (.=))+import Data.Aeson.Types (Parser)+import OpenAI.Prelude import qualified Data.Aeson as Aeson+import qualified Data.Aeson.Key as Key+import qualified Data.Aeson.KeyMap as KeyMap+import qualified Data.HashSet as HashSet+import Data.HashSet (HashSet)+import Data.List (partition) import qualified Data.Vector as V +-- | Tool choice string constants+toolChoiceNoneText, toolChoiceAutoText, toolChoiceRequiredText :: Text+toolChoiceNoneText = "none"+toolChoiceAutoText = "auto"+toolChoiceRequiredText = "required"+ -- | The ranking options for the file search data RankingOptions = RankingOptions     { ranker :: Maybe Text@@ -65,6 +87,60 @@ instance ToJSON Tool where     toJSON = genericToJSON toolOptions +toolToResponsesValue :: Tool -> Value+toolToResponsesValue = flattenToolValue . toJSON++parseResponsesToolValue :: Value -> Parser Tool+parseResponsesToolValue = parseJSON . unflattenToolValue++toolChoiceToResponsesValue :: ToolChoice -> Value+toolChoiceToResponsesValue ToolChoiceNone = String toolChoiceNoneText+toolChoiceToResponsesValue ToolChoiceAuto = String toolChoiceAutoText+toolChoiceToResponsesValue ToolChoiceRequired = String toolChoiceRequiredText+toolChoiceToResponsesValue (ToolChoiceTool tool) = toolToResponsesValue tool++parseResponsesToolChoiceValue :: Value -> Parser ToolChoice+parseResponsesToolChoiceValue (String s)+    | s == toolChoiceNoneText = pure ToolChoiceNone+    | s == toolChoiceAutoText = pure ToolChoiceAuto+    | s == toolChoiceRequiredText = pure ToolChoiceRequired+parseResponsesToolChoiceValue other = ToolChoiceTool <$> parseResponsesToolValue other++keyFunction, keyType :: Key.Key+keyFunction = Key.fromText "function"+keyType = Key.fromText "type"++functionFieldKeys :: HashSet Key.Key+functionFieldKeys = HashSet.fromList $ Key.fromText <$> ["description", "name", "parameters", "strict"]++isFunctionField :: Key.Key -> Bool+isFunctionField = (`HashSet.member` functionFieldKeys)++partitionFunctionFields+    :: KeyMap.KeyMap Value+    -> (KeyMap.KeyMap Value, KeyMap.KeyMap Value)+partitionFunctionFields obj =+    let (fnPairs, restPairs) = partition (isFunctionField . fst) (KeyMap.toList obj)+    in (KeyMap.fromList fnPairs, KeyMap.fromList restPairs)++flattenToolValue :: Value -> Value+flattenToolValue value@(Aeson.Object o) =+    maybe value flattenFunction (KeyMap.lookup keyFunction o)+  where+    flattenFunction (Aeson.Object fnFields) =+        Aeson.Object (KeyMap.delete keyFunction o <> fnFields)+    flattenFunction _ = value+flattenToolValue value = value++unflattenToolValue :: Value -> Value+unflattenToolValue value@(Aeson.Object o)+    | KeyMap.lookup keyType o == Just (String "function")+    , not (KeyMap.member keyFunction o) =+        let (fnFields, rest) = partitionFunctionFields o+        in Aeson.Object (KeyMap.insert keyFunction (Aeson.Object fnFields) rest)+    | otherwise = value+unflattenToolValue value = value+ -- | Controls which (if any) tool is called by the model data ToolChoice     = ToolChoiceNone@@ -94,7 +170,7 @@ instance ToJSON CodeInterpreterContainer where     toJSON (CodeInterpreterContainer_ID container_id) = toJSON container_id     toJSON (CodeInterpreterContainer_Auto file_ids) =-        Aeson.object $ "type" .= String "auto" : +        Aeson.object $ "type" .= String "auto" :                       maybe [] (\ids -> ["file_ids" .= ids]) file_ids  instance FromJSON CodeInterpreterContainer where
src/OpenAI/V1/ToolResources.hs view
@@ -2,7 +2,6 @@ module OpenAI.V1.ToolResources     ( -- * Main types       ToolResources(..)-       -- * Other types     , CodeInterpreterResources(..)     , Static(..)
src/OpenAI/V1/Usage.hs view
@@ -2,7 +2,6 @@ module OpenAI.V1.Usage     ( -- * Main types       Usage(..)-       -- * Other types     , CompletionTokensDetails(..)     , PromptTokensDetails(..)
src/OpenAI/V1/VectorStores.hs view
@@ -7,11 +7,9 @@     , ModifyVectorStore(..)     , _ModifyVectorStore     , VectorStoreObject(..)-       -- * Other types     , ExpiresAfter(..)     , Status(..)-       -- * Servant     , API     ) where@@ -21,8 +19,8 @@ import OpenAI.V1.ChunkingStrategy import OpenAI.V1.DeletionStatus import OpenAI.V1.Files (FileID)-import OpenAI.V1.Order import OpenAI.V1.ListOf+import OpenAI.V1.Order import OpenAI.V1.VectorStores.FileCounts  -- | Vector store ID
src/OpenAI/V1/VectorStores/FileBatches.hs view
@@ -5,7 +5,6 @@     , CreateVectorStoreFileBatch(..)     , _CreateVectorStoreFileBatch     , VectorStoreFilesBatchObject(..)-       -- * Servant     , API     ) where
src/OpenAI/V1/VectorStores/Files.hs view
@@ -5,7 +5,6 @@     , CreateVectorStoreFile(..)     , _CreateVectorStoreFile     , VectorStoreFileObject(..)-       -- * Servant     , API     ) where@@ -13,8 +12,8 @@ import OpenAI.Prelude import OpenAI.V1.ChunkingStrategy import OpenAI.V1.DeletionStatus-import OpenAI.V1.Files (FileID) import OpenAI.V1.Error+import OpenAI.V1.Files (FileID) import OpenAI.V1.ListOf import OpenAI.V1.Order import OpenAI.V1.VectorStores (VectorStoreID)
tasty/Main.hs view
@@ -1,104 +1,69 @@-{-# LANGUAGE BlockArguments #-}-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE OverloadedLists #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE BlockArguments, DuplicateRecordFields, NamedFieldPuns #-}+{-# LANGUAGE OverloadedLists, OverloadedStrings, RecordWildCards   #-}+{-# LANGUAGE ScopedTypeVariables                                   #-} {-# OPTIONS_GHC -Wno-orphans #-}  module Main where -import qualified Control.Concurrent as Concurrent import Control.Exception (SomeException, catch)+import OpenAI.V1 (Methods(..))+import OpenAI.V1.Audio.Speech (CreateSpeech(..), Voice(..), _CreateSpeech)+import OpenAI.V1.Audio.Transcriptions (CreateTranscription(..))+import OpenAI.V1.Audio.Translations (CreateTranslation(..))+import OpenAI.V1.AutoOr (AutoOr(..))+import OpenAI.V1.Batches (BatchObject(..), CreateBatch(..))+import OpenAI.V1.Chat.Completions (CreateChatCompletion(..), Modality(..))+import OpenAI.V1.Embeddings (CreateEmbeddings(..), EncodingFormat(..))+import OpenAI.V1.Files (FileObject(..), Order(..), UploadFile(..))+import OpenAI.V1.Images.Edits (CreateImageEdit(..))+import OpenAI.V1.Images.Generations (CreateImage(..), Quality(..), Style(..))+import OpenAI.V1.Images.Variations (CreateImageVariation(..))+import OpenAI.V1.Message (Message(..))+import OpenAI.V1.Moderations (CreateModeration(..))+import OpenAI.V1.Threads.Messages (MessageObject(..), ModifyMessage(..))+import OpenAI.V1.Tool (Tool(..), ToolChoice(..))+import OpenAI.V1.ToolCall (ToolCall(..))+import Prelude hiding (id)++import OpenAI.V1.Assistants+    (AssistantObject(..), CreateAssistant(..), ModifyAssistant(..))+import OpenAI.V1.FineTuning.Jobs+    (CreateFineTuningJob(..), Hyperparameters(..), JobObject(..))+import OpenAI.V1.Threads+    (ModifyThread(..), Thread(..), ThreadID(..), ThreadObject(..))+import OpenAI.V1.Threads.Runs+    (CreateRun(..), ModifyRun(..), RunID(..), RunObject(..), Status(..))+import OpenAI.V1.Uploads+    ( AddUploadPart(..)+    , CompleteUpload(..)+    , CreateUpload(..)+    , PartObject(..)+    , UploadObject(..)+    )+import OpenAI.V1.VectorStores+    (CreateVectorStore(..), ModifyVectorStore(..), VectorStoreObject(..))+import OpenAI.V1.VectorStores.FileBatches+    (CreateVectorStoreFileBatch(..), VectorStoreFilesBatchObject(..))+import OpenAI.V1.VectorStores.Files+    (CreateVectorStoreFile(..), VectorStoreFileObject(..))++import qualified Control.Concurrent as Concurrent+import qualified Data.IORef as IORef import qualified Data.Text as Text import qualified Network.HTTP.Client as HTTP.Client import qualified Network.HTTP.Client.TLS as TLS-import OpenAI.V1 (Methods (..)) import qualified OpenAI.V1 as V1-import qualified OpenAI.V1.Responses as Responses-import OpenAI.V1.Assistants-  ( AssistantObject (..),-    CreateAssistant (..),-    ModifyAssistant (..),-  )-import OpenAI.V1.Audio.Speech-  ( CreateSpeech (..),-    Voice (..),-    _CreateSpeech,-  )-import OpenAI.V1.Audio.Transcriptions (CreateTranscription (..))-import OpenAI.V1.Audio.Translations (CreateTranslation (..))-import OpenAI.V1.AutoOr (AutoOr (..))-import OpenAI.V1.Batches (BatchObject (..), CreateBatch (..))-import OpenAI.V1.Chat.Completions-  ( CreateChatCompletion (..),-    Modality (..),-  ) import qualified OpenAI.V1.Chat.Completions as Completions-import OpenAI.V1.Embeddings (CreateEmbeddings (..), EncodingFormat (..))-import OpenAI.V1.Files (FileObject (..), Order (..), UploadFile (..)) import qualified OpenAI.V1.Files as Files-import OpenAI.V1.FineTuning.Jobs-  ( CreateFineTuningJob (..),-    Hyperparameters (..),-    JobObject (..),-  ) import qualified OpenAI.V1.FineTuning.Jobs as Jobs-import OpenAI.V1.Images.Edits (CreateImageEdit (..))-import OpenAI.V1.Images.Generations-  ( CreateImage (..),-    Quality (..),-    Style (..),-  ) import qualified OpenAI.V1.Images.ResponseFormat as ResponseFormat-import OpenAI.V1.Images.Variations (CreateImageVariation (..))-import OpenAI.V1.Message (Message (..))-import OpenAI.V1.Moderations (CreateModeration (..))-import OpenAI.V1.Threads-  ( ModifyThread (..),-    Thread (..),-    ThreadID (..),-    ThreadObject (..),-  )-import OpenAI.V1.Threads.Messages (MessageObject (..), ModifyMessage (..))-import OpenAI.V1.Threads.Runs-  ( CreateRun (..),-    ModifyRun (..),-    RunID (..),-    RunObject (..),-    Status (..),-  )-import OpenAI.V1.Tool (Tool (..), ToolChoice (..))+import qualified OpenAI.V1.Responses as Responses import qualified OpenAI.V1.Tool as Tool-import OpenAI.V1.ToolCall (ToolCall (..)) import qualified OpenAI.V1.ToolCall as ToolCall-import OpenAI.V1.Uploads-  ( AddUploadPart (..),-    CompleteUpload (..),-    CreateUpload (..),-    PartObject (..),-    UploadObject (..),-  )-import OpenAI.V1.VectorStores-  ( CreateVectorStore (..),-    ModifyVectorStore (..),-    VectorStoreObject (..),-  )-import OpenAI.V1.VectorStores.FileBatches-  ( CreateVectorStoreFileBatch (..),-    VectorStoreFilesBatchObject (..),-  )-import OpenAI.V1.VectorStores.Files-  ( CreateVectorStoreFile (..),-    VectorStoreFileObject (..),-  ) import qualified Servant.Client as Client import qualified System.Environment as Environment import qualified Test.Tasty as Tasty import qualified Test.Tasty.HUnit as HUnit-import Prelude hiding (id)-import qualified Data.IORef as IORef  main :: IO () main = do@@ -892,7 +857,7 @@               Responses._CreateResponse                 { Responses.model = chatModel,                   Responses.input = Just (Responses.Input-                    [ Responses.Item_InputMessage+                    [ Responses.Item_Input_Message                         { Responses.role = Responses.User                         , Responses.content = [ Responses.Input_Text{ Responses.text = "Say hello in one sentence." } ]                         , Responses.status = Nothing@@ -919,7 +884,7 @@                 Responses._CreateResponse                   { Responses.model = chatModel,                     Responses.input = Just (Responses.Input-                      [ Responses.Item_InputMessage+                      [ Responses.Item_Input_Message                           { Responses.role = Responses.User                           , Responses.content = [ Responses.Input_Text{ Responses.text = "Stream a short haiku about the sea." } ]                           , Responses.status = Nothing@@ -963,7 +928,7 @@                 Responses._CreateResponse                   { Responses.model = chatModel,                     Responses.input = Just (Responses.Input-                      [ Responses.Item_InputMessage+                      [ Responses.Item_Input_Message                           { Responses.role = Responses.User                           , Responses.content = [ Responses.Input_Text{ Responses.text = "Solve 3x + 11 = 14 and provide x as a number. Use the code interpreter." } ]                           , Responses.status = Nothing