Using the OpenAI Large Language Model APIs in Haskell
Here we will use the library openai-hs written by Alexander Thiemann. The GitHub repository for his library is https://github.com/agrafix/openai-hs/tree/main/openai-hs.
We will start by writing client code to call the OpenAI completion API for an arbitrary input text. We will then use this completion client code for a specialized application: finding all the place names in input text and returning them as a list of strings.
In the development of practical AI systems, LLMs like those provided by OpenAI, Anthropic, and Hugging Face have emerged as pivotal tools for numerous applications including natural language processing, generation, and understanding. These models, powered by deep learning architectures, encapsulate a wealth of knowledge and computational capabilities. As a Haskell enthusiast embarking on the journey of intertwining the elegance of Haskell with the power of these modern language models, you might also want to experiment with the OpenAI Python examples that are much more complete than what we look at here.
OpenAI provides an API for developers to access models like GPT-4o. The OpenAI API is designed with simplicity and ease of use in mind, making it a common choice for developers. It provides endpoints for different types of interactions, be it text completion, translation, or semantic search among others. We will use the text completion API in this chapter. The robustness and versatility of the OpenAI API make it a valuable asset for anyone looking to integrate advanced language understanding and generation capabilities into their applications.
While we use the GPT-4o model here, you can substitute the following models:
- GPT-5 - expensive to run, for complex multi-step tasks
- GPT-5-mini - inexpensive to run, for simpler tasks (this is the default model I use)
- o1 - very expensive to run, most capable model that has massive knowledge of the real world and can solve complex multi-step reasoning problems.

- o1-mini - slightly less expensive to run than o1-preview, less real world knowledge and simpler reasoning capabilities.
Example Client Code
This Haskell program demonstrates how to interact with the OpenAI ChatCompletion API using the Openai-hs library. The code sends a prompt to the OpenAI API and prints the assistant’s response to the console. It’s a practical example of how to set up an OpenAI client, create a request, handle the response, and manage potential errors in a Haskell application.
Firstly, the code imports necessary modules and libraries. It imports OpenAI.Client for interacting with the OpenAI API and Network.HTTP.Client along with Network.HTTP.Client.TLS for handling HTTP requests over TLS. The System.Environment module is used to access environment variables via lookupEnv, specifically to retrieve the OpenAI API key. The System.Exit module provides exitFailure for graceful error handling when the key is missing. Additionally, Data.Text is imported for efficient text manipulation, and Data.Maybe is used for handling optional values.
The core of the program is the completionRequestToString function. This function takes a shared HTTP Manager, an API key as T.Text, and a String argument prompt, and returns an IO String, representing the assistant’s response. By accepting the manager and API key as parameters (rather than creating them internally), the function avoids opening a new TLS connection on every call.
What is an IO String? In Haskell, IO String represents an action that, when executed, produces a String, whereas String is simply a value.
- IO String: A computation in the IO monad that will produce a String when executed.
- String: A pure value, just a sequence of characters.
You can’t directly extract a String from IO String; you need to perform the IO action (e.g., using main or inside the do notation) to get the result.
Inside function completionRequestToString, the shared HTTP manager and API key are received as parameters rather than being created anew on each call.
An OpenAI client is instantiated using makeOpenAIClient, passing the API key, the HTTP manager, and an integer 4, which represents a maximum number of retries. The code then constructs a ChatCompletionRequest, specifying the model to use (in this case, ModelId “gpt-5-mini”) and the messages to send. The messages consist of a single ChatMessage with the user’s prompt, setting chmContent to Just (T.pack prompt) and chmRole to “user”. All other optional parameters in the request are left as Nothing, implying default values will be used.
The function then sends the chat completion request using completeChat client request and pattern matches on the result to handle both success and failure cases. If the request fails (Left failure), it returns a string representation of the failure. On success (Right success), it extracts the assistant’s reply from the chrChoices field. It unpacks the content from Text to String, handling the case where content might be absent by providing a default message “No content”.
Finally, the function main serves as the entry point of the program. It first checks for the OPENAI_API_KEY environment variable using lookupEnv, exiting with a friendly error message if it is not set. It then creates a single HTTPS manager shared across all requests and calls completionRequestToString with the prompt “Write a hello world program in Haskell”, printing the assistant’s response using putStrLn. This demonstrates how to use the function in a real-world scenario, providing a complete example of sending a prompt to the OpenAI API and displaying the result.
1 {-# LANGUAGE OverloadedStrings #-}
2 import OpenAI.Client
3
4 import Network.HTTP.Client
5 import Network.HTTP.Client.TLS
6 import System.Environment (lookupEnv)
7 import System.Exit (exitFailure)
8 import qualified Data.Text as T
9 import Data.Maybe (fromMaybe)
10 import Data.Text (splitOn)
11
12 -- | This module uses the @openai-hs@ library (and its dependency @openai-servant@)
13 -- to call the OpenAI chat-completion API. You must set the @OPENAI_API_KEY@
14 -- environment variable to a valid API key before running, e.g.:
15 --
16 -- > export OPENAI_API_KEY=sk-...
17
18 -- | Sends a chat prompt and returns the assistant's text as String.
19 -- Requires a shared 'Manager' (created once in 'main') to avoid
20 -- opening a new TLS connection on every call.
21 completionRequestToString :: Manager -> T.Text -> String -> IO String
22 completionRequestToString manager apiKey prompt = do
23 -- Build a client; the last argument (4) retries on transient network errors
24 let client = makeOpenAIClient apiKey manager 4
25 -- Describe the chat request to send
26 let request = ChatCompletionRequest
27 { chcrModel = ModelId "gpt-5-mini" -- model to use
28 , chcrMessages =
29 [ ChatMessage
30 { chmContent = Just (T.pack prompt) -- user prompt
31 , chmRole = "user"
32 , chmFunctionCall = Nothing
33 , chmName = Nothing
34 }
35 ]
36 , chcrFunctions = Nothing
37 , chcrTemperature = Nothing
38 , chcrTopP = Nothing
39 , chcrN = Nothing
40 , chcrStream = Nothing
41 , chcrStop = Nothing
42 , chcrMaxTokens = Nothing
43 , chcrPresencePenalty = Nothing
44 , chcrFrequencyPenalty = Nothing
45 , chcrLogitBias = Nothing
46 , chcrUser = Nothing
47 }
48 -- Perform the API call
49 result <- completeChat client request
50 -- Unpack the result and extract the text content from the first choice
51 case result of
52 Left failure -> return (show failure)
53 Right success ->
54 case chrChoices success of
55 (ChatChoice {chchMessage = ChatMessage {chmContent = content}} : _) ->
56 return $ fromMaybe "No content" $ T.unpack <$> content
57 _ -> return "No choices returned"
58
59 -- | Extracts place names from @text@ (comma-separated) using the chat model.
60 findPlaces :: Manager -> T.Text -> String -> IO [String]
61 findPlaces manager apiKey text = do
62 let prompt = "Extract only the place names separated by commas from the following text:\n\n" ++ text
63 response <- completionRequestToString manager apiKey prompt
64 let places = filter (not . null) $ map T.unpack $ splitOn "," (T.pack response)
65 return $ map (T.unpack . T.strip . T.pack) places
66
67 -- | Extracts person names from @text@ (comma-separated) using the chat model.
68 findPeople :: Manager -> T.Text -> String -> IO [String]
69 findPeople manager apiKey text = do
70 let prompt = "Extract only the person names separated by commas from the following text:\n\n" ++ text
71 response <- completionRequestToString manager apiKey prompt
72 let people = filter (not . null) $ map T.unpack $ splitOn "," (T.pack response)
73 return $ map (T.unpack . T.strip . T.pack) people
74
75 -- Demo: generate text, then extract places and people
76 main :: IO ()
77 main = do
78 -- Look up the API key; exit with a friendly message if missing
79 maybeKey <- lookupEnv "OPENAI_API_KEY"
80 apiKey <- case maybeKey of
81 Nothing -> do
82 putStrLn "Error: OPENAI_API_KEY environment variable not set."
83 putStrLn "Please set it with: export OPENAI_API_KEY=sk-..."
84 exitFailure
85 Just k -> return (T.pack k)
86
87 -- Create a single HTTPS manager shared across all requests
88 manager <- newManager tlsManagerSettings
89
90 -- Generic text generation
91 response <- completionRequestToString manager apiKey "Write a hello world program in Haskell"
92 putStrLn response
93
94 -- Extract place names
95 places <- findPlaces manager apiKey "I visited London, Paris, and New York last year."
96 print places
97
98 -- Extract person names
99 people <- findPeople manager apiKey "John Smith met with Sarah Johnson and Michael Brown at the conference."
100 print people
Here is sample output generated by the gpt-5-mini OpenAI model:
1 $ cabal build
2 Build profile: -w ghc-9.8.1 -O1
3 $ cabal run
4 Sure! Here is a simple "Hello, World!" program in Haskell:
5
6 main :: IO ()
7 main = putStrLn "Hello, World!"
8
9 Explanation:
10 - `main :: IO ()` declares that `main` is an I/O action that returns no meaningful value (indicated by `()`).
11 - `putStrLn "Hello, World!"` is the I/O action that outputs the string "Hello, World!" followed by a newline.
12
13 To run this program:
14 1. Save the code in a file, for example, `hello.hs`.
15 2. Open a terminal.
16 3. Navigate to the directory containing `hello.hs`.
17 4. Run the program using the Haskell compiler/interpreter (GHC). You can do this by running:
18
19 runhaskell hello.hs
20
21 or by compiling it and then running the executable:
22
23 ghc -o hello hello.hs
24 ./hello
For completeness, here is a partial listing of the OpenAiApiClient.cabal file:
1 name: OpenAiApiClient
2 version: 0.1.0.0
3 author: Mark watson and the author of OpenAI Client library Alexander Thiemann
4 build-type: Simple
5 cabal-version: >=1.10
6
7 executable GenText
8 hs-source-dirs: .
9 main-is: GenText.hs
10 default-language: Haskell2010
11 build-depends: base >= 4.7 && < 5, mtl >= 2.2.2, text, http-client >= 0.7.13.1, openai-hs, http-client-tls
Adding a Simple Application: Find Place Names in Input Text
The example file GenText.hs contains a small application example that uses the function completionRequestToString prompt that we defined in the last section.
Here we define a new function:
1 findPlaces :: Manager -> T.Text -> String -> IO [String]
2 findPlaces manager apiKey text = do
3 let prompt = "Extract only the place names separated by commas from the following text:\n\n" ++ text
4 response <- completionRequestToString manager apiKey prompt
5 let places = filter (not . null) $ map T.unpack $ splitOn "," (T.pack response)
6 return $ map (T.unpack . T.strip . T.pack) places
The function findPlaces takes a shared HTTP Manager, an API key, and a text string, and extracts a list of place names from the text using an LLM (Large Language Model).
- It constructs a prompt instructing the LLM to extract only comma-separated place names.
- It sends this prompt to the LLM using the completionRequestToString function.
- It processes the LLM’s response, splitting it into a list of potential place names, filtering out empty entries, and stripping leading/trailing whitespace.
- It returns the final list of extracted place names.
You should use the function findPlaces as a template for prompting the OpenAI completion models like GPT-4o to perform specific tasks.
Given the example code:
1 -- Extract place names
2 places <- findPlaces manager apiKey "I visited London, Paris, and New York last year."
3 print places
The output would look like:
1 ["London","Paris","New York"]