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 +25/−0
- README.md +1/−1
- examples/openai-example/Main.hs +1/−1
- examples/responses-example/Main.hs +6/−5
- examples/responses-stream-example/Main.hs +10/−9
- examples/responses-tool-call-example/Main.hs +168/−0
- examples/weather-chatbot-example/Main.hs +4/−4
- openai.cabal +14/−1
- src/OpenAI/Prelude.hs +3/−5
- src/OpenAI/V1.hs +25/−37
- src/OpenAI/V1/Assistants.hs +0/−2
- src/OpenAI/V1/Audio.hs +1/−0
- src/OpenAI/V1/Chat/Completions.hs +1/−1
- src/OpenAI/V1/ChunkingStrategy.hs +0/−1
- src/OpenAI/V1/Files.hs +2/−2
- src/OpenAI/V1/FineTuning/Jobs.hs +1/−3
- src/OpenAI/V1/Images.hs +1/−1
- src/OpenAI/V1/Message.hs +0/−1
- src/OpenAI/V1/Models.hs +0/−1
- src/OpenAI/V1/Moderations.hs +2/−3
- src/OpenAI/V1/Responses.hs +107/−8
- src/OpenAI/V1/Threads.hs +0/−2
- src/OpenAI/V1/Threads/Messages.hs +11/−13
- src/OpenAI/V1/Threads/Runs.hs +1/−4
- src/OpenAI/V1/Threads/Runs/Steps.hs +1/−3
- src/OpenAI/V1/Tool.hs +79/−3
- src/OpenAI/V1/ToolResources.hs +0/−1
- src/OpenAI/V1/Usage.hs +0/−1
- src/OpenAI/V1/VectorStores.hs +1/−3
- src/OpenAI/V1/VectorStores/FileBatches.hs +0/−1
- src/OpenAI/V1/VectorStores/Files.hs +1/−2
- tasty/Main.hs +50/−85
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