Using the OpenAI and Mistral APIs
I have been working as an artificial intelligence practitioner since 1982 and the capability of the beta OpenAI APIs is the most impressive thing that I have seen in my career so far. These APIs use the GPT-5 models (a October 2025 update).
Note: in January 2024 I expanded this chapter to also include the hosted Mistral LLM APIs. I recently added material for locally hosted open weight LLM models like mistral-7b and lama2 to my Racket Scheme AI (link to read online) book. I plan on adding that material to this book soon.
History of OpenAI
OpenAI has been working on large language models (LLMs) since they were founded in December 2015. LLMs are artificial neural networks that can contain up to a trillion weights, and are trained using self-supervised learning and semi-supervised learning. OpenAI’s LLMs can respond to written prompts with various types of content. The release of ChatGPT in November 2022 mainstreamed the idea that generative artificial intelligence could be used by companies and consumers to automate tasks, help with creative ideas, and write software in many different programming languages.
Common Lisp Library for Using OpenAI APIs
I recommend reading the online documentation for the online documentation for the APIs to see all the capabilities of the beta OpenAI APIs. Let’s start by jumping into the example code. As seen in the package.lisp file we use the UIOP and cl-json libraries and we export three top level functions:
1 ;;;; package.lisp
2
3 (defpackage #:openai
4 (:use #:cl #:uiop #:cl-json)
5 (:export #:completions #:summarize #:answer-question))
The library that I wrote for this chapter supports three functions that are exported from the package openai: for completing text, summarizing text, and answering general questions. The single OpenAI model that the beta OpenAI APIs use is fairly general purpose and can generate cooking directions when given an ingredient list, grammar correction, write an advertisement from a product description, generate spreadsheet data from data descriptions in English text, etc.
Given the examples from https://beta.openai.com and the Common Lisp examples here, you should be able to modify my example code to use any of the functionality that OpenAI documents.
1 ;;;; openai.asd
2
3 (asdf:defsystem #:openai
4 :description "Library for using the beta OpenAI APIs"
5 :author "Mark Watson"
6 :license "Apache 2"
7 :depends-on (#:uiop #:cl-json)
8 :components ((:file "package")
9 (:file "openai")))
We will look closely at the function completions and then just look at the small differences to the other two example functions. The definitions for all three exported functions are kept in the file openai.lisp. You need to request an API key (I had to wait a few weeks to receive my key) and set the value of the environment variable OPENAI_KEY to your key. You can add a statement like:
1 export OPENAI_KEY=sa-hdffds7&dhdhsdgffd
to your .profile or other shell resource file.
While I sometimes use pure Common Lisp libraries to make HTTP requests, I prefer running the curl utility as a separate process for these reasons:
- No problems with system specific dependencies.
- Use the standard library UIOP to run a shell command and capture the output as a string.
- I use curl from the command line when experimenting with web services. After I get working curl options, it is very easy to translate this into Common Lisp code.
An example curl command line call to the beta OpenAI APIs is:
1 curl \
2 https://api.openai.com/v1/engines/davinci/completions \
3 -H "Content-Type: application/json"
4 -H "Authorization: Bearer sa-hdffds7&dhdhsdgffd" \
5 -d '{"prompt": "The President went to Congress", \
6 "max_tokens": 22}'
Here the API token “sa-hdffds7&dhdhsdgffd” on line 4 is made up - that is not my API token. All of the OpenAI APIs expect JSON data with query parameters. To use the completion API, we set values for prompt and max_tokens. The value of max_tokens is the requested number of returns words or tokens. We will look at several examples later.
Function call support was added to this library in April 2025. The following functions handle registering and using functions:
1 ;; Hash table to store available functions for tool calling
2 (defvar *available-functions* (make-hash-table :test 'equal))
3
4 (defstruct openai-function
5 name
6 description
7 parameters
8 func)
9
10 (defun register-function (name description parameters fn)
11 (format t "Registering ~A ~A~%" name fn)
12 (setf (gethash name *available-functions*)
13 (make-openai-function
14 :name name
15 :description description
16 :parameters parameters
17 :func fn)))
18
19 (defun lisp-to-json-string (data)
20 (with-output-to-string (s)
21 (json:encode-json data s)))
22
23 (defun substitute-subseq (string old new &key (test #'eql))
24 (let ((pos (search old string :test test)))
25 (if pos
26 (concatenate 'string
27 (subseq string 0 pos)
28 new
29 (subseq string (+ pos (length old))))
30 string)))
31
32 (defun escape-json (str)
33 (with-output-to-string (out)
34 (loop for ch across str do
35 (if (char= ch #\")
36 (write-string "\\\"" out)
37 (write-char ch out)))))
38
39
40 (defun handle-function-call (function-call)
41 ;; function-call looks like: ((:name . "get_weather") (:arguments . "{\"location\":\"New York\"}"))
42 (format t "~% ** handle-function-call (DUMMY) fucntion-call: ~A~%" function-call)
43 (let* ((name (cdr (assoc :name function-call)))
44 (args-string (cdr (assoc :arguments function-call)))
45 (args (and args-string (cl-json:decode-json-from-string args-string)))
46 (func (openai-function-func (gethash name *available-functions*))))
47 (format t "~% handle-function-call name: ~A" name)
48 (format t "~% handle-function-call args-string: ~A" args-string)
49 (format t "~% handle-function-call args: ~A" args)
50 (format t "~% handle-function-call func: ~A" func)
51 (if (not (null func))
52 (let ()
53 (format t "~%Calling function ~a called with args: ~a~%" name args)
54 (let ((f-val (apply func (mapcar #'cdr args))))
55 (format t "~%Return value from func ~A is ~A~%" name f-val)
56 f-val))
57 (error "Unknown function: ~a" name))))
In the file openai.lisp we define a helper function openai-helper that takes a string with the OpenAI API call arguments encoded as a curl command, calls the service, and then extracts the results from the returned JSON data:
1 (defun openai-helper (curl-command)
2 ;;(terpri)
3 ;;(princ curl-command)
4 ;;(terpri)
5 (let ((response (uiop:run-program curl-command
6 :output :string
7 :error-output :string)))
8 (terpri)
9 ;;(princ response)
10 (terpri)
11 (with-input-from-string (s response)
12 (let* ((json-as-list (json:decode-json s))
13 (choices (cdr (assoc :choices json-as-list)))
14 (first-choice (car choices))
15 (message (cdr (assoc :message first-choice)))
16 (function-call (cdr (assoc :function--call message)))
17 (content (cdr (assoc :content message))))
18 ;;(format t "~% json-as-list: ~A~%" json-as-list)
19 ;;(format t "~% choices: ~A~%" choices)
20 ;;(format t "~% first-choice: ~A~%" first-choice)
21 ;;(format t "~% message: ~A~%" message)
22 ;;(format t "~% function-call: ~A~%" function-call)
23 ;;(format t "~% content: ~A~%" content)
24 (if function-call
25 (handle-function-call function-call)
26 (or content "No response content"))))))
I convert JSON data to a Lisp list in line 12 and in line 14 I reach into the nested results list for the generated text string. You might want to add a debug printout statement to see the value of json-as-list.
The three example functions all use this openai-helper function. The first example function completions sets the parameters to complete a text fragment. You have probably seen examples of the OpenAI GPT models writing stories, given a starting sentence. We are using the functionality here:
1 (defun completions (starter-text &optional functions)
2 (let* ((function-defs (when functions
3 (mapcar (lambda (f)
4 (let ((func (gethash f *available-functions*)))
5 (list (cons :name (openai-function-name func))
6 (cons :description (openai-function-description func))
7 (cons :parameters (openai-function-parameters func)))))
8 functions)))
9 (message (list (cons :role "user")
10 (cons :content starter-text)))
11 (base-data `((model . ,*model*)
12 (messages . ,(list message))))
13 (data (if function-defs
14 (append base-data (list (cons :functions function-defs)))
15 base-data))
16 (request-body (cl-json:encode-json-to-string data))
17 (fixed-json-data (substitute-subseq request-body ":null" ":false" :test #'string=))
18 (escaped-json (escape-json fixed-json-data))
19 (curl-command
20 (format nil "curl ~A -H \"Content-Type: application/json\" -H \"Authorization: Bearer ~A\" -d \"~A\""
21 *model-host*
22 (uiop:getenv "OPENAI_KEY")
23 escaped-json)))
24 (openai-helper curl-command))
Note that the OpenAI API models are stochastic. When generating output words (or tokens), the model assigns probabilities to possible words to generate and samples a word using these probabilities. As a simple example, suppose given prompt text “it fell and”, then the model could only generate three words, with probabilities for each word based on this prompt text:
- the 0.9
- that 0.1
- a 0.1
The model would emit the word the 90% of the time, the word that 10% of the time, or the word a 10% of the time. As a result, the model can generate different completion text for the same text prompt. Let’s look at some examples. We request 22 output tokens (words or punctuation) in the first two examples and 100 tokens in the third example:
1 cl-user> (openai:completions "The President went to Congress")
2 " yesterday and proposed a single tax rate for all corporate taxpayers, which he envisions will be lower than what our"
3
4 cl-user> (openai:completions "The President went to Congress")
5 " last month, asking for authorization of a program, which had previously been approved by the Foreign Intelligence Surveillance court as"
6
7 cl-user> (openai:completions "The President went to Congress")
8 " worried about what the massive unpopular bill would do to his low approvals. Democrats lost almost every situation to discuss any legislation about this controversial subject. Even more so, President Obama failed and had to watch himself be attacked by his own party for not leading.
9
10 There were also two celebrated (in DC) pieces of student loan legislation, which aimed to make college cheaper. Harkin teamed up with Congressman Roddenbery on one, Student Loan Affordability Act, and Senator Jack Reed (D"
11 cl-user>
The function summarize is very similar to the function completions except the JSON data passed to the API has a few additional parameters that let the API know that we want a text summary:
- presence_penalty - penalize words found in the original text (we set this to zero)
- temperature - higher values the randomness used to select output tokens. If you set this to zero, then the same prompt text will always yield the same results (I never use a zero value).
- top_p - also affects randomness. All examples I have seen use a value of 1.
- frequency_penalty - penalize using the same words repeatedly (I usually set this to zero, but you should experiment with different values)
An example:
1 (defvar s "Jupiter is the fifth planet from the Sun and the largest in the Solar System. It is a gas giant with a mass one-thousandth that of the Sun, but two-and-a-half times that of all the other planets in the Solar System combined. Jupiter is one of the brightest objects visible to the naked eye in the night sky, and has been known to ancient civilizations since before recorded history. It is named after the Roman god Jupiter.[19] When viewed from Earth, Jupiter can be bright enough for its reflected light to cast visible shadows,[20] and is on average the third-brightest natural object in the night sky after the Moon and Venus.")
2
3 cl-user> (openai:summarize s)
4 "Jupiter is a gas giant because it is predominantly composed of hydrogen and helium; it has a solid core that is composed of heavier elements. It is the largest of the four giant planets in the Solar System and the largest in the Solar System"
Let’s look at a few question answering examples and we will discuss possible problems and workarounds:
1 cl-user> (openai:answer-question "Where is the Valley of Kings?")
2 "It's in Egypt."
Let’s explore some issues with the question answering model. In the last example there is one good answer and the model works well. The next example “What rivers are in Arizona?” shows some problems because there are many rivers in Arizona. Sometimes the model misses a few rivers and often river names are repeated in the output. You also don’t necessarily get the same answer for the same input arguments. Here is an example:
1 cl-user> (openai:answer-question "What rivers are in Arizona?")
2 "The Colorado River, the Gila River, the Little Colorado River, the Salt River, the Verde River, the San Pedro River, the Santa Cruz River, the San Juan River, the Agua Fria River, the Hassayampa River, the Bill Williams River, the Little Colorado River, the San Francisco River, the San Pedro River, the Santa Cruz River, the San Juan River, the Agua Fria River, the Hassayampa River, the Bill Williams River, the Little Colorado River, the San Francisco River, the San Pedro River, the Santa Cruz River, the San Juan River, the Agua Fria River, the Hassayampa River, the Bill Williams River, the Little Colorado River, the San Francisco River, the San Pedro River, the Santa Cruz"
My library does not handle embedded single quote characters in questions so the question “Who is Bill Clinton’s wife?” will throw an error. Leaving out the single quote character works fine:
1 cl-user> (openai:answer-question "Who is Bill Clintons wife?")
2 "Hillary Clinton."
3 cl-user>
The function embeddings (defined in utils.lisp) is used to convert a chunk of text to an embedding. What are embeddings? Embeddings take complex content like natural language words and sentences or software code and converts text into a special sequence of numbers. This process lets machines model the underlying concepts and relationships within the content, just like understanding the main ideas in a book even if you don’t know every word. Embeddings are often used in RAG (Retrieval Augmented Generation) applications to work around the problem of the limits of the amount of context text a LLM can process. For example, if an LLM can only accept 8192 tokens a RAG application might “chunk” input text into 2K segments. Using embeddings with a vector store database, we could find the 3 chunks of original text that most closely match a query. The text for these three matched chunks could be supplied to a LLM as context text for answering a question of query.
1 (defun embeddings (text)
2 "Get embeddings using text-embedding-3-small model (1536 dimensions)"
3 (let* ((curl-command
4 (concatenate 'string
5 "curl https://api.openai.com/v1/embeddings "
6 " -H \"Content-Type: application/json\""
7 " -H \"Authorization: Bearer " (uiop:getenv "OPENAI_KEY") "\" "
8 " -d '{\"input\": \"" text
9 "\", \"model\": \"text-embedding-3-small\"}'"))
10 (response (uiop:run-program curl-command :output :string)))
11 (with-input-from-string (s response)
12 (let ((json-as-list (json:decode-json s)))
13 (cdr (nth 2 (cadr (cadr json-as-list))))))))
The following output is edited for brevity:
1 CL-USER 5 > (openai::embeddings "John bought a new car")
2 (0.0038357755 0.007082982 -7.8207086E-4 -0.003108127 -0.038506914 ...
3 CL-USER 6 > (length (openai::embeddings "John bought a new car"))
4 1536
In addition to reading the beta OpenAI API documentation you might want to read general material on the use of OpenAI’s GPT-5 models. Since the APIs we are using are beta they may change. I will update this chapter and the source code on GitHub if the APIs change.
History of Mistral AI
Mistral AI is a French company that is a leader in the development and utilization of Large Language Models (LLMs). The company was founded in 2018 by a team of AI experts who previously worked at Google and Meta to harness the power of language models for various applications.
I am a fan of Mistral because they supply both hosted APIs for their models as well as publicly releasing the weights for their smaller models like mistral-7b and mixtral-8-7b with commercial-friendly licensing. I run both of these models locally for much of my personal research on my Mac mini with 32G of memory. They have a larger model mistral-medium that is only available through their hosted API.
Client Library for Mistral APIs
Mistral designed their API schemas to be similar to those of OPenAI so the client code for Mistral is very similar to what we saw in the previous sections.
The following code is found in the GitHub repository https://github.com/mark-watson/mistral:
1 (in-package #:mistral)
2
3 ;; define the environment variable "MISTRAL_API_KEY" with the value of your mistral API key
4
5
6 (defvar model-host "https://api.mistral.ai/v1/chat/completions")
7
8 ;; Available models:
9 ;;
10 ;; "mistral-tiny" powered by Mistral-7B-v0.2
11 ;; "mistral-small" powered Mixtral-8X7B-v0.1, a sparse mixture of experts model with 12B active parameters
12 ;; "mistral-medium" powered by a larger internal prototype model
13 ;;
14
15 (defun mistral-helper (curl-command)
16 (let ((response
17 (uiop:run-program
18 curl-command
19 :output :string)))
20 (with-input-from-string
21 (s response)
22 (let* ((json-as-list (json:decode-json s)))
23 ;; extract text (this might change if mistral changes JSON return format):
24 (cdr (assoc :content (cdr (assoc :message (cadr (assoc :choices json-as-list))))))))))
25
26 (defun completions (starter-text max-tokens)
27 (let* ((d
28 (cl-json:encode-json-to-string
29 `((:messages . (((:role . "user") (:content . ,starter-text))))
30 (:model . "mistral-small")
31 (:max_tokens . ,max-tokens))))
32 (curl-command
33 (concatenate
34 'string
35 "curl " model-host
36 " -H \"Content-Type: application/json\""
37 " -H \"Authorization: Bearer " (uiop:getenv "MISTRAL_API_KEY") "\" "
38 " -d '" d "'")))
39 (mistral-helper curl-command)))
40
41 (defun summarize (some-text max-tokens)
42 (let* ((curl-command
43 (concatenate
44 'string
45 "curl " model-host
46 " -H \"Content-Type: application/json\""
47 " -H \"Authorization: Bearer " (uiop:getenv "MISTRAL_API_KEY") "\" "
48 " -d '{\"messages\": [{\"role\": \"user\", \"content\": \"Sumarize: " some-text
49 "\"}], \"model\": \"mistral-small\", \"max_tokens\": " (write-to-string max-tokens) "}'")))
50 (mistral-helper curl-command)))
51
52 (defun answer-question (question-text max-tokens)
53 (let ((q-text
54 (concatenate
55 'string
56 "\nQ: " question-text "\nA:")))
57 (completions question-text max-tokens)))
58
59 (defun embeddings (text)
60 (let* ((curl-command
61 (concatenate
62 'string
63 "curl https://api.mistral.ai/v1/embeddings "
64 " -H \"Content-Type: application/json\""
65 " -H \"Authorization: Bearer " (uiop:getenv "MISTRAL_API_KEY") "\" "
66 " -d '{\"input\": [\"" text "\"], \"model\": \"mistral-embed\"}'")))
67 (let ((response
68 (uiop:run-program
69 curl-command
70 :output :string)))
71 ;;(princ curl-command)
72 ;;(pprint response)
73 (with-input-from-string
74 (s response)
75 (let ((json-as-list (json:decode-json s)))
76 ;; return a list of 1024 floats:
77 (cdr (assoc :embedding (cadr (assoc :data json-as-list)))))))))
78
79 (defun dot-product-recursive (a b) ;; generated by Bing+ChatGPT Search
80 (print "dot-product")
81 (if (or (null a) (null b))
82 0
83 (+ (* (first a) (first b))
84 (dot-product (rest a) (rest b)))))
85
86 (defun dot-product (list1 list2)
87 (let ((sum 0))
88 (loop for x in list1
89 for y in list2
90 do
91 (setf sum (+ sum (* x y))))
92 sum))
Here is a simple example using the Mistral code completion API:
1 cl-user> (ql:quickload :mistral)
2 cl-user> (mistral:completions "The President went to Congress" 200)
3 "When the President of a country goes to Congress, it typically means that they are making a formal address to a joint session of the legislative body. This is often done to present the State of the Union address, which outlines the administration's goals and priorities for the upcoming year. The President may also go to Congress to propose new legislation, rally support for existing bills, or address important national issues.
4
5 During the address, members of Congress from both parties are usually present in the chamber, and they may respond with applause, standing ovations, or other forms of expression. The President's speech is typically broadcast live on television and radio, and it is covered extensively by the news media.
6
7 The practice of the President going to Congress to deliver a State of the Union address dates back to the early years of the United States, when President George Washington gave the first such address in 1790. Since then, it has become a regular tradition for"