diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Author name here (c) 2020
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Author name here nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,53 @@
+# openai-hs
+
+Unofficial OpenAI SDK/client for Haskell. It's generated via `servant-client` from `openai-servant` with a small amount of hand-written code. Contributions are welcome!
+
+## Install
+
+``` sh
+# stack
+stack install openai-hs
+
+# cabal
+cabal install openai-hs
+```
+
+## Example
+
+``` haskell
+{-# LANGUAGE OverloadedStrings #-}
+import OpenAI.Client
+
+import Network.HTTP.Client
+import Network.HTTP.Client.TLS
+import System.Environment (getEnv)
+import qualified Data.Vector as V
+
+main :: IO ()
+main =
+  do manager <- newManager tlsManagerSettings
+     apiKey <- T.pack <$> getEnv "OPENAI_KEY"
+     -- create a openai client that automatically retries up to 4 times on network
+     -- errors
+     let client = makeOpenAIClient apiKey manager 4
+     result <-
+         searchDocuments cli (eId firstEngine) $
+         SearchResultCreate
+         { sccrDocuments = V.fromList ["pool", "gym", "night club"]
+         , sccrQuery = "swimmer"
+         }
+     print result
+```
+
+## Features
+
+Supported actions:
+
+* List engines
+* Retrieve engine
+* Create text completion
+* Run semantic search
+
+## Running the tests
+
+You can run all tests with `stack test`. You'll need an OpenAI API Key assigned to the `OPENAI_KEY` environment variable.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/openai-hs.cabal b/openai-hs.cabal
new file mode 100644
--- /dev/null
+++ b/openai-hs.cabal
@@ -0,0 +1,81 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.33.0.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: bc145a17f925189f4d8d879579d0e37dc7d796a46ca6bb0303538c1f3f2c1f16
+
+name:           openai-hs
+version:        0.1.1.0
+synopsis:       Unofficial OpenAI client
+description:    Unofficial OpenAI client
+category:       Web
+homepage:       https://github.com/agrafix/openai-hs#readme
+bug-reports:    https://github.com/agrafix/openai-hs/issues
+author:         Alexander Thiemann <mail@thiemann.at>
+maintainer:     Alexander Thiemann <mail@thiemann.at>
+copyright:      2021 Alexander Thiemann <mail@thiemann.at>
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.md
+
+source-repository head
+  type: git
+  location: https://github.com/agrafix/openai-hs
+
+library
+  exposed-modules:
+      OpenAI.Client
+      OpenAI.Client.Internal.Helpers
+  other-modules:
+      Paths_openai_hs
+  hs-source-dirs:
+      src
+  default-extensions: OverloadedStrings DataKinds TypeOperators TypeFamilies GADTs FlexibleInstances FlexibleContexts MultiParamTypeClasses StrictData ScopedTypeVariables DeriveGeneric DeriveFunctor
+  ghc-options: -Wall -fwarn-tabs -fwarn-incomplete-uni-patterns -fwarn-incomplete-record-updates
+  build-depends:
+      aeson
+    , base >=4.7 && <5
+    , casing
+    , cpphs
+    , http-client
+    , http-types
+    , openai-servant
+    , servant
+    , servant-client
+    , text
+  default-language: Haskell2010
+
+test-suite openai-hs-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      ApiSpec
+      HelperSpec
+      Paths_openai_hs
+  hs-source-dirs:
+      test
+  default-extensions: OverloadedStrings DataKinds TypeOperators TypeFamilies GADTs FlexibleInstances FlexibleContexts MultiParamTypeClasses StrictData ScopedTypeVariables DeriveGeneric DeriveFunctor
+  ghc-options: -Wall -fwarn-tabs -fwarn-incomplete-uni-patterns -fwarn-incomplete-record-updates -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      aeson
+    , base >=4.7 && <5
+    , bytestring
+    , casing
+    , containers
+    , cpphs
+    , hspec
+    , http-client
+    , http-client-tls
+    , http-types
+    , openai-hs
+    , openai-servant
+    , servant
+    , servant-client
+    , servant-client-core
+    , text
+    , vector
+  default-language: Haskell2010
diff --git a/src/OpenAI/Client.hs b/src/OpenAI/Client.hs
new file mode 100644
--- /dev/null
+++ b/src/OpenAI/Client.hs
@@ -0,0 +1,84 @@
+{-# OPTIONS_GHC -cpp -pgmPcpphs -optP--cpp #-}
+{-# LANGUAGE CPP #-}
+module OpenAI.Client
+  ( -- * Basics
+    ApiKey, OpenAIClient, makeOpenAIClient, ClientError(..)
+    -- * Helper types
+  , TimeStamp(..), OpenAIList(..)
+    -- * Engine
+  , EngineId(..), Engine(..)
+  , listEngines
+  , getEngine
+    -- * Text completion
+  , TextCompletionId(..), TextCompletionChoice(..), TextCompletion(..), TextCompletionCreate(..)
+  , defaultTextCompletionCreate
+  , completeText
+    -- * Searching
+  , SearchResult(..), SearchResultCreate(..)
+  , searchDocuments
+  )
+where
+
+import OpenAI.Api
+import OpenAI.Client.Internal.Helpers
+import OpenAI.Resources
+
+import Data.Proxy
+import Network.HTTP.Client (Manager)
+import Servant.API
+import Servant.Client
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as T
+
+-- | Your OpenAI API key. Can be obtained from the OpenAI dashboard. Format: @sk-<redacted>@
+type ApiKey = T.Text
+
+-- | Holds a 'Manager' and your API key.
+data OpenAIClient
+  = OpenAIClient
+  { scBasicAuthData :: BasicAuthData
+  , scManager :: Manager
+  , scMaxRetries :: Int
+  }
+
+-- | Construct a 'OpenAIClient'. Note that the passed 'Manager' must support https (e.g. via @http-client-tls@)
+makeOpenAIClient ::
+  ApiKey
+  -> Manager
+  -> Int
+  -- ^ Number of automatic retries the library should attempt.
+  -> OpenAIClient
+makeOpenAIClient k = OpenAIClient (BasicAuthData "" (T.encodeUtf8 k))
+
+api :: Proxy OpenAIApi
+api = Proxy
+
+openaiBaseUrl :: BaseUrl
+openaiBaseUrl = BaseUrl Https "api.openai.com" 443 ""
+
+#define EP0(N, R) \
+    N##' :: BasicAuthData -> ClientM R;\
+    N :: OpenAIClient -> IO (Either ClientError R);\
+    N sc = runRequest (scMaxRetries sc) 0 $ runClientM (N##' (scBasicAuthData sc)) (mkClientEnv (scManager sc) openaiBaseUrl)
+
+#define EP(N, ARG, R) \
+    N##' :: BasicAuthData -> ARG -> ClientM R;\
+    N :: OpenAIClient -> ARG -> IO (Either ClientError R);\
+    N sc a = runRequest (scMaxRetries sc) 0 $ runClientM (N##' (scBasicAuthData sc) a) (mkClientEnv (scManager sc) openaiBaseUrl)
+
+#define EP2(N, ARG, ARG2, R) \
+    N##' :: BasicAuthData -> ARG -> ARG2 -> ClientM R;\
+    N :: OpenAIClient -> ARG -> ARG2 -> IO (Either ClientError R);\
+    N sc a b = runRequest (scMaxRetries sc) 0 $ runClientM (N##' (scBasicAuthData sc) a b) (mkClientEnv (scManager sc) openaiBaseUrl)
+
+EP2(completeText, EngineId, TextCompletionCreate, TextCompletion)
+EP2(searchDocuments, EngineId, SearchResultCreate, (OpenAIList SearchResult))
+
+EP0(listEngines, (OpenAIList Engine))
+EP(getEngine, EngineId, Engine)
+
+listEngines'
+  :<|> getEngine'
+  :<|> completeText'
+  :<|> searchDocuments'
+  = client api
diff --git a/src/OpenAI/Client/Internal/Helpers.hs b/src/OpenAI/Client/Internal/Helpers.hs
new file mode 100644
--- /dev/null
+++ b/src/OpenAI/Client/Internal/Helpers.hs
@@ -0,0 +1,23 @@
+-- | Private helper functions. Note that all contents of this module are excluded from the versioning scheme.
+{-# LANGUAGE BangPatterns #-}
+module OpenAI.Client.Internal.Helpers where
+
+import Network.HTTP.Types.Status
+import Servant.Client
+
+runRequest :: Int -> Int -> IO (Either ClientError a) -> IO (Either ClientError a)
+runRequest maxRetries !retryCount makeRequest =
+  do res <- makeRequest
+     case res of
+       Right ok -> pure (Right ok)
+       Left err@(ConnectionError _) -> maybeRetry err
+       Left err@(FailureResponse _ resp)
+         | responseStatusCode resp == conflict409 -> maybeRetry err
+         | statusCode (responseStatusCode resp) >= 500 -> maybeRetry err
+         | otherwise -> pure (Left err)
+       Left err -> pure (Left err)
+  where
+    maybeRetry err =
+      if retryCount + 1 >= maxRetries
+      then pure (Left err)
+      else runRequest maxRetries (retryCount + 1) makeRequest
diff --git a/test/ApiSpec.hs b/test/ApiSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/ApiSpec.hs
@@ -0,0 +1,63 @@
+module ApiSpec (apiSpec) where
+
+import Network.HTTP.Client
+import Network.HTTP.Client.TLS
+import System.Environment (getEnv)
+import Test.Hspec
+import qualified Data.Text as T
+import qualified Data.Vector as V
+
+import OpenAI.Client
+
+makeClient :: IO OpenAIClient
+makeClient =
+  do manager <- newManager tlsManagerSettings
+     apiKey <- T.pack <$> getEnv "OPENAI_KEY"
+     pure (makeOpenAIClient apiKey manager 2)
+
+forceSuccess :: (MonadFail m, Show a) => m (Either a b) -> m b
+forceSuccess req =
+  req >>= \res ->
+  case res of
+    Left err -> fail (show err)
+    Right ok -> pure ok
+
+apiSpec :: Spec
+apiSpec =
+  describe "core api" apiTests
+
+apiTests :: SpecWith ()
+apiTests =
+  beforeAll makeClient $
+  do describe "engines" $
+       do it "lists engines" $ \cli ->
+            do res <- forceSuccess $ listEngines cli
+               V.null (olData res) `shouldBe` False
+          -- TODO: This doesn't work for some reason, even the cURL example from the docs fail.
+          --it "retrieve engine" $ \cli ->
+          --  do engineList <- forceSuccess $ listEngines cli
+          --     print engineList
+          --     let firstEngine = V.head (olData engineList)
+          --     engine <- forceSuccess $ getEngine cli (eId firstEngine)
+          --     engine `shouldBe` firstEngine
+     describe "text completion" $
+       do it "works (smoke test)" $ \cli ->
+            do firstEngine <- V.head . olData <$> forceSuccess (listEngines cli)
+               completionResults <-
+                 forceSuccess $
+                 completeText cli (eId firstEngine) $
+                 (defaultTextCompletionCreate "Why is the house ")
+                 { tccrMaxTokens = Just 2 }
+               V.length (tcChoices completionResults) `shouldBe` 1
+               T.length (tccText (V.head (tcChoices completionResults))) `shouldNotBe` 0
+     describe "document search" $
+       do it "works (smoke test)" $ \cli ->
+            do firstEngine <- V.head . olData <$> forceSuccess (listEngines cli)
+               searchResults <-
+                 forceSuccess $
+                 searchDocuments cli (eId firstEngine) $
+                 SearchResultCreate
+                 { sccrDocuments = V.fromList ["pool", "gym", "night club"]
+                 , sccrQuery = "swimmer"
+                 }
+               V.length (olData searchResults) `shouldBe` 3
diff --git a/test/HelperSpec.hs b/test/HelperSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/HelperSpec.hs
@@ -0,0 +1,88 @@
+module HelperSpec (helperSpec) where
+
+import Control.Exception.Base
+import Data.IORef
+import Data.Maybe
+import Network.HTTP.Types.Header
+import Network.HTTP.Types.Status
+import Network.HTTP.Types.Version
+import Servant.Client
+import Servant.Client.Core.Request
+import System.Exit
+import Test.Hspec
+import qualified Data.ByteString as BS
+import qualified Data.Sequence as Seq
+
+import OpenAI.Client.Internal.Helpers
+
+helperSpec :: Spec
+helperSpec =
+  do describe "retries" retryTests
+
+makeFakeAction ::
+  (Int -> Either ClientError a)
+  -> IO (IO Int, IO (Either ClientError a))
+makeFakeAction makeResult =
+  do calls <- newIORef 0
+     let action =
+           do call <- atomicModifyIORef calls $ \i -> (i + 1, i)
+              pure (makeResult call)
+     pure (readIORef calls, action)
+
+dummyReq :: RequestF () (BaseUrl, BS.ByteString)
+dummyReq =
+  defaultRequest
+  { requestBody = Nothing
+  , requestPath = (fromJust $ parseBaseUrl "api.example.com", "")
+  }
+
+retryAction ::
+  Int
+  -> Status
+  -> Seq.Seq Header
+  -> IO (ClientError, IO Int, IO (Either ClientError Bool))
+retryAction n status headers =
+  do let errResp =
+           Response
+           { responseStatusCode = status
+           , responseHeaders = headers
+           , responseHttpVersion = http11
+           , responseBody = mempty
+           }
+         err = FailureResponse dummyReq errResp
+     (getCalls, action) <-
+       makeFakeAction $ \call ->
+       if call < n
+       then Left $ err
+       else Right True
+     pure (err, getCalls, action)
+
+retryTests :: SpecWith ()
+retryTests =
+  do it "does not retry on success" $
+       do (getCalls, action) <- makeFakeAction (const $ Right True)
+          runRequest 10 0 action `shouldReturn` Right True
+          getCalls `shouldReturn` 1
+     it "retries on connection errors" $
+       do (getCalls, action) <-
+            makeFakeAction $ \call ->
+            if call == 0
+            then Left (ConnectionError $ toException ExitSuccess)
+            else Right True
+          runRequest 10 0 action `shouldReturn` Right True
+          getCalls `shouldReturn` 2
+     it "retries on 409 status code" $
+       do (_, getCalls, action) <-
+            retryAction 1 status409 mempty
+          runRequest 10 0 action `shouldReturn` Right True
+          getCalls `shouldReturn` 2
+     it "retries on 500 status code" $
+       do (_, getCalls, action) <-
+            retryAction 1 status500 mempty
+          runRequest 10 0 action `shouldReturn` Right True
+          getCalls `shouldReturn` 2
+     it "does not retry on status 500 if limit exceeded" $
+       do (err, getCalls, action) <-
+            retryAction 11 status500 mempty
+          runRequest 10 0 action `shouldReturn` Left err
+          getCalls `shouldReturn` 10
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,10 @@
+import Test.Hspec
+
+import ApiSpec
+import HelperSpec
+
+main :: IO ()
+main =
+  hspec $
+  do apiSpec
+     helperSpec
