Using the OpenAI APIs

I have been working as an artificial intelligence practitioner since 1982 and most of my early use of LLMs was using ChatGPT and the OpenAI APIs. I now in 2026 mostly use the Google Gemini APIs and local models, but I still also frequently use the small, fast, and inexpensive OpenAI gpt-5-nano model, which we will also use here.

Let’s start by jumping into the example code.

The library that I wrote for this chapter supports four functions: completing text, summarizing text, answering general questions, and calculating embeddings. The single OpenAI model that the OpenAI APIs use is fairly general purpose and can perform tasks like:

  • 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.

Given the examples from https://platform.openai.com (will require you to login) and the Clojure examples here, you should be able to modify my example code to use any of the functionality that OpenAI documents.

We will look closely at the function completions and then just look at the small differences to the other example functions. The definitions for all four exported functions are kept in the file src/openai_api/core.clj*. 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_API_KEY=sa-hdffds7&dhdhsdgffd

to your .profile or other shell resource file. Here the API token “sa-hdffds7&dhdhsdgffd” is made up - that is not my API token.

Architecture for OpenAI API example

When experimenting with OpenAI APIs it is often start by using the curl utility. An example curl command line call to the beta OpenAI APIs is (note: this CURL example uses an earlier API):

 1 curl https://api.openai.com/v1/chat/completions -H "Content-Type: application/json" -H "Authorization: Bearer $OPENAI_API_KEY"   -d '{
 2     "model": "gpt-5-nano",
 3     "messages": [
 4       {
 5         "role": "system",
 6         "content": "You are an assistant, skilled in explaining complex programming and other technical problems."
 7       },
 8       {
 9         "role": "user",
10         "content": "Write a Python function foo to add two argument"
11       }
12     ]
13   }'

Output might look like this:

 1 {
 2   "id": "chatcmpl-8nqUrlNsCPQgUkSIjW7ytvN5GlH3C",
 3   "object": "chat.completion",
 4   "created": 1706890561,
 5   "model": "gpt-5-nano",
 6   "choices": [
 7     {
 8       "index": 0,
 9       "message": {
10         "role": "assistant",
11         "content": "Certainly! Here is a Python function named `foo` that takes two arguments `a` and `b` and returns their sum:
12 
13 ```python
14 def foo(a, b):
15     return a + b

To use this function, simply call it and pass in two arguments:

1 result = foo(3, 5)
2 print(result)  # Output: 8

In this example, result will store the sum of 3 and 5, which is 8. You can change the arguments a and b to any other numbers to get different results.“ }, “logprobs”: null, “finish_reason”: “stop” } ], “usage”: { “prompt_tokens”: 35, “completion_tokens”: 127, “total_tokens”: 162 }, “system_fingerprint”: null }

  1  All of the OpenAI APIs expect JSON data with query parameters. To use the completion API, we set values for **prompt**. We will look at several examples later.
  2 
  3 The file **src/openai_api/core.clj** contains the implementation of our wrapper library:
  4 
  5 {lang="clojure",linenos=on}
  6 ~~~~~~~~
  7 (ns openai-api.core
  8   (:require [clj-http.client :as client])
  9   (:require [clojure.data.json :as json]))
 10 
 11 (def model2 "gpt-5-nano")
 12 
 13 (def api-key (System/getenv "OPENAI_API_KEY"))
 14 
 15 (defn completions [prompt]
 16   (let [url "https://api.openai.com/v1/chat/completions"
 17         headers {"Authorization" (str "Bearer " api-key)
 18                  "Content-Type" "application/json"}
 19         body {:model model2
 20               :messages [{:role "user" :content prompt}]}
 21         response (client/post url {:headers headers
 22                                    :body (json/write-str body)})]
 23     ;;(println (:body response))
 24     (get
 25      (get
 26       (first
 27        (get
 28         (json/read-str (:body response)  :key-fn keyword)
 29         :choices))
 30       :message)
 31      :content)))
 32 
 33 (defn summarize [text]
 34   (completions (str "Summarize the following text:
 35 
 36 " text)))
 37 
 38 (defn answer-question
 39   "Use the OpenAI API for question answering"
 40   [text]
 41   (completions (str "Answer the following question:
 42 
 43 " text)))
 44 
 45 (defn embeddings [text]
 46   (try
 47     (let* [body
 48            (str
 49             "{\"input\": \""
 50             (clojure.string/replace
 51              (clojure.string/replace text #"[\" 
 52  :]" " ")
 53              #"\s+" " ")
 54             "\", \"model\": \"text-embedding-ada-002\"}")
 55            json-results
 56            (client/post
 57             "https://api.openai.com/v1/embeddings"
 58             {:accept :json
 59              :headers
 60              {"Content-Type"  "application/json"
 61               "Authorization" (str "Bearer " api-key)}
 62              :body   body})]
 63           ((first ((json/read-str (json-results :body)) "data")) "embedding"))
 64     (catch Exception e
 65       (println "Error:" (.getMessage e))
 66       "")))
 67 
 68 (defn dot-product [a b]
 69   (reduce + (map * a b)))
 70 ~~~~~~~~
 71 
 72 Note that the OpenAI 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:
 73 
 74 - the 0.9
 75 - that 0.1
 76 - a 0.1
 77 
 78 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 using the same prompt text. Notice the stochastic nature of the returned results:
 79 
 80 {lang="clojure",linenos=on}
 81 ~~~~~~~~
 82 $ lein repl
 83 openai-api.core=> (openai-api.core/completions "He walked to the river")
 84 " and breathed in the new day, looking out to the lake where the Mire was displacing the Wold by its"
 85 openai-api.core=> (openai-api.core/completions "He walked to the river")
 86 ". He waded in, not caring about his expensive suit pants. He was going to do this right, even if"
 87 openai-api.core=> (openai-api.core/completions "He walked to the river")
 88 " every day. The salty air puffed through their pores. He had enjoyed her company. Maybe he did need a companion"
 89 ~~~~~~~~
 90 
 91 The function **summarize** is very similar to the function **completions** except I changed the system prompt string. The function **answer-question** is similarly structured, using a prompt that asks the model to answer a question. Here is some example output:
 92 
 93 {lang="clojure",linenos=on}
 94 ~~~~~~~~
 95 openai-api.core=> (def some-text
 96              #_=>   "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.")
 97 #'openai-api.core/some-text
 98 
 99 openai-api.core=> (openai-api.core/summarize some-text openai-api.core=> (openai-api.core/summarize some-text)
100 "Jupiter is classified as a gas giant along with Saturn, Uranus, and Neptune. Jupiter is composed primarily of gaseous and liquid matter.[21] It is the largest of the four giant planets in the Solar System and hence its largest planet. It has a diameter of 142,984 km at its equator, which is 0.11 times the diameter of Earth. Jupiter is a gas giant because the mass of the planet"
101 ~~~~~~~~
102 
103 
104 In addition to reading the OpenAI API documentation you might want to read general material on the use of OpenAI's GPT-5 models.
105 
106 ## Optional Practice Problems
107 
108 1. **Chat History Management**: Implement a chat history manager in `source-code/openai_api` that appends user and assistant messages to maintain conversational context.
109 2. **System Persona Customization**: Modify the system prompt configuration to customize the model's persona (e.g., acting as a strict code reviewer).
110 3. **Parameter Overrides**: Expose parameters like `temperature`, `top_p`, and `max_tokens` as optional arguments in the API call interface.
111 
112 # Using the Google Gemini APIs
113 
114 We used the OpenAI LLM APIs in the last chapter and now we provide a similar example using Google's **gemini-3-flash-preview** model.
115 
116 I recommend reading Google's [online documentation for the APIs](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference) to see all the capabilities of the OpenAI APIs.
117 
118 We first use a REST interface and write a Gemini access library from scratch using only low-level Clojure libraries. Later we use the Google Java Gemini SDK.
119 
120 In all examples you may substitute the model **gemini-3.0-pro** for **gemini-2.5-flash**.
121 
122 
123 ## Test Code for REST Interface and Sample Test Output
124 
125 Before we look at the example code, let's look at an example code running it and later sample output:
126 
127 
128 ```clojure
129 (ns gemini-api.core-test
130   (:require [clojure.test :refer :all]
131             [gemini-api.core :refer :all]))
132 
133 (def some-text
134   "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.")
135 
136 (deftest completions-test
137   (testing "gemini completions API"
138     (let [results
139           (gemini-api.core/generate-content "He walked to the river")]
140       (println results)
141       (is (= 0 0)))))
142 
143 (deftest summarize-test
144   (testing "gemini summarize API"
145     (let [results
146           (gemini-api.core/summarize
147            some-text)]
148       (println results)
149       (is (= 0 0)))))
150 
151 (deftest question-answering-test
152   (testing "gemini question-answering API"
153     (let [results
154           (gemini-api.core/generate-content
155             ;;"If it is not used for hair, a round brush is an example of what 1. hair brush 2. bathroom 3. art supplies 4. shower ?"
156            "Where is the Valley of Kings?"
157             ;"Where is San Francisco?"
158            )]
159       (println results)
160       (is (= 0 0)))))
161 
162 ;; ── Google Search (grounding) ─────────────────────────────────────────────────
163 
164 (deftest search-test
165   (testing "gemini Google Search grounding API"
166     (let [[text citations]
167           (gemini-api.core/generate-with-search-and-citations
168            "Who wrote the Clojure programming language?")]
169       (println "Search result text:" text)
170       (println "Citations:")
171       (doseq [{:keys [title uri]} citations]
172         (println " -" title uri))
173       ;; text should be a non-empty string
174       (is (string? text))
175       (is (pos? (count text))))))
176 
177 ;; ── Tool / Function calling ───────────────────────────────────────────────────
178 
179 (defn- mock-get-weather
180   "Fake weather lookup – no real HTTP call; just returns a canned string."
181   [{:keys [location]}]
182   (str "It is 72°F and sunny in " location "."))
183 
184 (def ^:private weather-tool-def
185   {:name        "get_weather"
186    :description "Returns current weather conditions for a given city."
187    :parameters  {:type       "OBJECT"
188                  :properties {:location {:type        "STRING"
189                                          :description "The name of the city."}}
190                  :required   ["location"]}})
191 
192 (deftest tool-use-test
193   (testing "gemini function/tool calling API"
194     (let [result
195           (gemini-api.core/generate-with-tools
196            "What is the weather like in Paris right now?"
197            [weather-tool-def]
198            {"get_weather" mock-get-weather})]
199       (println "Tool-use result:" result)
200       ;; The model should have issued a functionCall; our dispatch fn returns
201       ;; a string describing the weather.
202       (is (map? result))
203       (is (or (contains? result :text)
204               (contains? result :function-call)))
205       (when (contains? result :function-call)
206         (is (string? (:result result)))
207         (is (re-find #"Paris" (:result result)))))))
Architecture for Gemini REST API example

The output (edited for brevity) looks like this:

 1  $ lein test
 2 
 3 lein test gemini-api.core-test
 4 Okay, "He walked to the river."
 5 
 6 That's a simple and clear sentence! What would you like to do with it?
 7 
 8 For example, I can:
 9 
10 1.  **Acknowledge it:** "Got it." or "I understand."
11 2.  **Ask for more information:** "Why did he walk to the river?" or "What happened next?"
12 3.  **Expand on it descriptively:** "The path was worn and led directly to the shimmering river."
13 4.  **Imagine the scene:** "I picture a man, perhaps with a thoughtful expression, making his way to the water's edge."
14 5.  **Use it in a story:** "He walked to the river, a place he always found peace, hoping to clear his mind."
15 6.  **Analyze the grammar:** "It's a simple past tense sentence, indicating a completed action."
16 
17 Just let me know!
18 Jupiter is the fifth and largest planet in our Solar System, classified as a gas giant. It possesses a mass two-and-a-half times greater than all other planets combined. Extremely bright, it is visible to the naked eye and has been known since ancient times, capable of casting visible shadows. On average, it is the third-brightest natural object in the night sky after the Moon and Venus, and is named after the Roman god Jupiter.
19 The Valley of Kings is located in **Egypt**, specifically on the **west bank of the Nile River**, near the modern city of **Luxor**.
20 
21 This area was part of ancient Thebes, and it served as the burial place for pharaohs and powerful nobles of the New Kingdom (18th to 20th Dynasties).
22 Search result text: Rich Hickey is the creator of the Clojure programming language. He developed Clojure in the mid-2000s, releasing it publicly in October 2007. Hickey wanted a modern Lisp that was functional, compatible with the Java platform, and designed for concurrency.
23 
24 He continues to lead the development of the language. While much of Clojure's underpinnings and initial compiler were written in Java, Hickey expressed a desire to rewrite those parts in Clojure itself.
25 Citations:
26  - clojure.org https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQG7EkD0j4M6tohYe2mE4xsJ0UnD9mW_2k9kTfEC_KFjEjF3abclOhMEK_5lnWi9eEiYM-sLG5VLGmrz8MJr8GO78c7uuY-Y3tOYcbXERim9lE0ByuU=
27  - wikipedia.org https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGUsFsuSAgQhO0xpLuPkmnCu0uH9avs3dSlsUqtRr3rwT1IJS0j_9Zidy9xJwXxsOU4GcA9MQ7K55g8RR8HsMXx_MTykOIRitXpLw0ykzvwWWmdBUIMbR3r6y33ZsDA
28  - wikipedia.org https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHKDCXRr7I88rWWwvakVZ5cQKVXEVBaNVUL1U28qMXKJwRyB3wGCvm2t2N1a7d0oWcxXCP9ME-1x0toDWEtwjFIklLtoKWSJAmgE4wcVn1pGo7JvYdFqIp5GY-v62h92gELKQ==
29  - clojure.org https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHv_3Lk5xWwXDXM1MLJiNKbW3M10klqM05QNHKmlgtqW1B88yaxN-e8W0XCQqCXp5XL3tkkyjY1PpF5GwaV1iMkc0t01YHU2l9GZuHi4Q==
30  - nubank.com https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHydcJ7X1GBqp6Ix8TfWIdly167wONbnivgUq8LRk53ZtiMmX4_5RPIz3Ux1PEtfAhqePoRHPBF5SketLNaFZiHBYfIArg9CWWM2hdCVwUD28FpQE_YePas8xjZdUAw04ovQywWDvh1DyEm_bR9I4wAvxDW5K4es-iNwXbCHdg=
31  - mcqueeney.tech https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFSqtbhsfbLZ0hKglZuYmMyHr5Me2p2G3S7c-XeRX_1V-8Iv9GtwpFI3cZa_fn8CzI8jemxFlh_nDckydR1RE17_FW-9EEMzZHkvrFp3p7RftRaYrQywASS2qV9LWwgP326Ia_4a-JXFXtkiiDdOblaP9HfgBV8IBQmnqKoEZV9pzja918uwCLLwL30PEFI6KaD_Tw898_3
32  - github.com https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHrSB2btECWpuWNqk_iETlVTWfLqVOea9M3NocGXJhYqGSzeZwwlsWUiW4flthZXG2ZwKcziq50Gc7-WIAVkoomzrBXMYq5gR8NavqmAwEs4iz0E5dUDI31XO3giIJ9wKTcXGuKexkl6pwock4PwTRPfTxmS-OJCnBu
33 Tool-use result: {:function-call {:name get_weather, :args {:location Paris}}, :result It is 72°F and sunny in Paris.}
34 
35 Ran 5 tests containing 9 assertions.
36 0 failures, 0 errors.

Gemini API Library Implementation for REST Interface

Here is the library implementation, we will discuss the code after the listing:

  1 (ns gemini-api.core
  2   (:require [clj-http.client :as client]
  3             [clojure.data.json :as json]
  4             [clojure.tools.logging :as log]))
  5 
  6 ;;;; ── Configuration ──────────────────────────────────────────────────────────
  7 
  8 (def model "gemini-2.5-flash")   ; default model
  9 
 10 (def google-api-key (System/getenv "GOOGLE_API_KEY"))
 11 (when (nil? google-api-key)
 12   (log/error "GOOGLE_API_KEY environment variable not set!"))
 13 
 14 (def base-url "https://generativelanguage.googleapis.com/v1beta/models")
 15 
 16 ;;;; ── Helpers ─────────────────────────────────────────────────────────────────
 17 
 18 (defn- api-url
 19   "Build a full API endpoint URL."
 20   ([endpoint] (api-url model endpoint))
 21   ([model-id endpoint]
 22    (str base-url "/" model-id ":" endpoint "?key=" google-api-key)))
 23 
 24 (defn- post-json
 25   "POST body (a Clojure map) to url; return parsed JSON response as a map."
 26   [url body]
 27   (let [opts {:body            (json/write-str body)
 28               :content-type    :json
 29               :accept          :json
 30               :socket-timeout     30000
 31               :connection-timeout 10000}]
 32     (try
 33       (let [resp (client/post url opts)]
 34         (json/read-str (:body resp) :key-fn keyword))
 35       (catch Exception e
 36         (log/error "Request error:" (.getMessage e))
 37         (when-let [rb (-> e ex-data :body)]
 38           (log/error "Response body:" rb))
 39         nil))))
 40 
 41 (defn- extract-text
 42   "Pull the main generated text out of a parsed API response."
 43   [parsed-response]
 44   (let [candidates (:candidates parsed-response)]
 45     (if (seq candidates)
 46       (let [text (get-in (first candidates) [:content :parts 0 :text])]
 47         (if text
 48           text
 49           (do (log/warn "No text in response:" parsed-response) nil)))
 50       (do (log/warn "No candidates in response:" parsed-response) nil))))
 51 
 52 ;;;; ── Basic generation ────────────────────────────────────────────────────────
 53 
 54 (defn generate-content
 55   "Generate text from PROMPT.
 56    Optional keyword args:
 57      :model-id  – model to use (default: model)
 58      :system    – system instruction string"
 59   [prompt & {:keys [model-id system]
 60              :or   {model-id model}}]
 61   (let [body (cond-> {:contents [{:parts [{:text prompt}]}]}
 62                system (assoc :systemInstruction
 63                              {:parts [{:text system}]}))
 64         resp (post-json (api-url model-id "generateContent") body)]
 65     (extract-text resp)))
 66 
 67 ;; (generate-content "In one sentence, explain how AI works to a child.")
 68 ;; (generate-content "What is 2+2?" :system "You are a concise math tutor.")
 69 
 70 (defn summarize [text]
 71   (generate-content (str "Summarize the following text:
 72 
 73 " text)))
 74 
 75 ;;;; ── Token counting ──────────────────────────────────────────────────────────
 76 
 77 (defn count-tokens
 78   "Return the total token count for PROMPT using the Gemini countTokens API."
 79   [prompt & {:keys [model-id] :or {model-id model}}]
 80   (let [body {:contents [{:parts [{:text prompt}]}]}
 81         resp (post-json (api-url model-id "countTokens") body)]
 82     (if-let [tc (:totalTokens resp)]
 83       tc
 84       (do (log/warn "Could not retrieve token count:" resp) nil))))
 85 
 86 ;; (count-tokens "In one sentence, explain how AI works to a child.")
 87 
 88 ;;;; ── Google Search grounding ─────────────────────────────────────────────────
 89 
 90 (defn generate-with-search
 91   "Like generate-content but enables the google_search grounding tool."
 92   [prompt & {:keys [model-id] :or {model-id model}}]
 93   (let [body {:contents [{:parts [{:text prompt}]}]
 94               :tools    [{:google_search {}}]}
 95         resp (post-json (api-url model-id "generateContent") body)]
 96     (extract-text resp)))
 97 
 98 ;; (generate-with-search "What sci-fi movies are playing at Harkins 16 in Flagstaff today?")
 99 
100 (defn generate-with-search-and-citations
101   "Like generate-with-search but returns [text citations] where citations is a
102    seq of {:title … :uri …} maps extracted from grounding metadata."
103   [prompt & {:keys [model-id] :or {model-id model}}]
104   (let [body {:contents [{:parts [{:text prompt}]}]
105               :tools    [{:google_search {}}]}
106         resp (post-json (api-url model-id "generateContent") body)]
107     (let [text      (extract-text resp)
108           candidate (first (:candidates resp))
109           chunks    (get-in candidate [:groundingMetadata :groundingChunks] [])
110           citations (keep (fn [chunk]
111                             (when-let [web (:web chunk)]
112                               {:title (:title web) :uri (:uri web)}))
113                           chunks)]
114       [text citations])))
115 
116 ;; (let [[answer sources] (generate-with-search-and-citations "Who won the Super Bowl in 2024?")]
117 ;;   (println "Answer:" answer)
118 ;;   (doseq [{:keys [title uri]} sources]
119 ;;     (println " -" title uri)))
120 
121 ;;;; ── Function / Tool calling ─────────────────────────────────────────────────
122 ;;
123 ;; Tool definitions follow the Gemini function-declaration schema:
124 ;;
125 ;;   {:name        "get_weather"
126 ;;    :description "Returns current weather for a location."
127 ;;    :parameters  {:type       "OBJECT"
128 ;;                  :properties {:location {:type        "STRING"
129 ;;                                          :description "City name"}}
130 ;;                  :required   ["location"]}}
131 ;;
132 ;; The dispatch-fn is a Clojure function (map of name→fn) that the caller
133 ;; supplies to handle function-call requests from the model.
134 ;;
135 ;; generate-with-tools implements a single round-trip:
136 ;;   1. Send the prompt + tool declarations.
137 ;;   2. If the model returns a functionCall part, invoke the matching dispatch-fn
138 ;;      and return {:function-call {:name … :args …} :result <return-value>}.
139 ;;   3. Otherwise return the plain text.
140 ;;
141 ;; For a multi-turn agentic loop, wrap generate-with-tools yourself and keep
142 ;; accumulating the conversation history (see the docstring example).
143 
144 (defn generate-with-tools
145   "Call the Gemini API with PROMPT and a seq of TOOL-DEFS.
146    DISPATCH-FNS is a map of tool-name (string) → (fn [args-map] …).
147 
148    Returns a map:
149      {:text \"…\"}               – if the model responded with text
150      {:function-call {:name … :args …}
151       :result        <dispatch-fn return value>}  – if a tool was called
152 
153    Optional kwargs: :model-id :system"
154   [prompt tool-defs dispatch-fns
155    & {:keys [model-id system]
156       :or   {model-id model}}]
157   (let [fn-decls (mapv (fn [td] {:functionDeclaration td}) tool-defs)
158         body     (cond-> {:contents [{:role  "user"
159                                       :parts [{:text prompt}]}]
160                           :tools    [{:functionDeclarations (mapv :functionDeclaration fn-decls)}]}
161                    system (assoc :systemInstruction {:parts [{:text system}]}))
162         resp     (post-json (api-url model-id "generateContent") body)]
163     (let [candidate (first (:candidates resp))
164           parts     (get-in candidate [:content :parts] [])]
165       (if-let [fc-part (first (filter :functionCall parts))]
166         ;; Model wants to call a function
167         (let [fc     (:functionCall fc-part)
168               fname  (:name fc)
169               fargs  (:args fc)
170               f      (get dispatch-fns fname)]
171           (if f
172             {:function-call fc :result (f fargs)}
173             (do (log/warn "No dispatch function for tool:" fname)
174                 {:function-call fc :result nil})))
175         ;; Regular text response
176         {:text (get-in (first parts) [:text])}))))
177 
178 ;; ── Example: single tool call ─────────────────────────────────────────────────
179 ;;
180 ;; (defn get-weather [{:keys [location]}]
181 ;;   (str "It is 72°F and sunny in " location "."))
182 ;;
183 ;; (def weather-tool
184 ;;   {:name        "get_weather"
185 ;;    :description "Returns current weather for a city."
186 ;;    :parameters  {:type       "OBJECT"
187 ;;                  :properties {:location {:type        "STRING"
188 ;;                                          :description "City name"}}
189 ;;                  :required   ["location"]}})
190 ;;
191 ;; (generate-with-tools
192 ;;   "What is the weather like in Paris?"
193 ;;   [weather-tool]
194 ;;   {"get_weather" get-weather})
195 
196 ;;;; ── Chat (stateful, in-process) ────────────────────────────────────────────
197 
198 (defn make-chat-session
199   "Return a new chat session atom. Holds a vector of {:role … :parts […]} turns."
200   []
201   (atom []))
202 
203 (defn chat-turn
204   "Send USER-MSG in the context of SESSION (an atom returned by make-chat-session).
205    Appends both the user message and the model reply to the session history.
206    Returns the model's reply text."
207   [session user-msg & {:keys [model-id] :or {model-id model}}]
208   (let [user-turn {:role "user" :parts [{:text user-msg}]}
209         history   (conj @session user-turn)
210         body      {:contents history}
211         resp      (post-json (api-url model-id "generateContent") body)
212         reply     (extract-text resp)
213         model-turn {:role "model" :parts [{:text (or reply "")}]}]
214     (swap! session conj user-turn model-turn)
215     reply))
216 
217 (defn chat-repl
218   "Simple REPL-based chat session. Type 'quit' to exit."
219   []
220   (let [session (make-chat-session)]
221     (println "Gemini Chat – type 'quit' to exit.")
222     (loop []
223       (print "You: ") (flush)
224       (let [input (read-line)]
225         (when (and input (not= (clojure.string/trim input) "quit"))
226           (let [reply (chat-turn session input)]
227             (println "Gemini:" reply))
228           (recur))))))
229 
230 ;; (chat-repl)

This Clojure code is designed to interact with Google’s Gemini API to generate text content and specifically to summarize text. It sets up the necessary components to communicate with the API, including importing libraries for making HTTP requests and handling JSON data. Crucially, it retrieves your Google API key from an environment variable, ensuring secure access. The code also defines configuration like the Gemini model to use and the base API endpoint URL. It’s structured within a Clojure namespace for organization and includes basic error handling and debug printing to aid in development and troubleshooting.

The core of the functionality lies in the generate-content function. This function takes a text prompt as input, constructs the API request URL with the chosen model and your API key, and then sends this request to Google’s servers. It handles the API response, parsing the JSON result to extract the generated text content. The code also checks for potential errors, both in the API request itself and in the structure of the response, providing informative error messages if something goes wrong. Building on this, the function summarize offers a higher-level interface, taking text as input and using generate-content to send a “summarize” prompt to the API, effectively providing a convenient way to get text summaries using the Gemini models.

(New) Gemini Client Library Using Google’s Java SDK for Gemini

The code for this section can be found in the directory ** Clojure-AI-Book-Code/gemini_java_api**.

Here we test code that is almost identical to that used earlier for the REST interface library so we don’t list the test code here.

Architecture for Gemini Java SDK example

Here is the library implementation:

 1 (ns gemini-java-api.core
 2   (:import (com.google.genai Client)
 3            (com.google.genai.types GenerateContentResponse)))
 4 
 5 (def DEBUG false)
 6 
 7 (def model "gemini-2.5-flash") ; or gemini-2.5-pro, etc.
 8 (def google-api-key (System/getenv "GOOGLE_API_KEY")) ; Make sure to set this env variable
 9 
10 (defn generate-content
11   "Sends a prompt to the Gemini API using the specified model and returns
12    the text response."
13   [prompt]
14   (let [client (Client.)
15         ^GenerateContentResponse resp
16         (.generateContent (.models client)
17                           model
18                           prompt
19                           nil)]
20     (when DEBUG
21       (println (.text resp))
22       (when-let [headers
23                  (some-> resp
24                      .sdkHttpResponse (.orElse nil)
25                      .headers        (.orElse nil))]
26         (println "Response headers:" headers)))
27     (.text resp)))
28 
29 (defn summarize [text]
30   (generate-content (str "Summarize the following text:
31 
32 " text)))

I used the previous REST interface library implementation for over one year but now I have switched to using this shorter implementation that uses interop with the Java Gemini SDK.

Gemini APIs Wrap Up

The Gemini APIs also support a message-based API for optionally adding extra context data, configuration data, and AI safety settings. The example code using the REST interface provides a simple completion style of interacting with the Gemini models.

If you use my Java SDK example library you can clone it in your own projects and optionally use those features of the Java SDK that you might find useful. Reference: https://github.com/googleapis/java-genai.

Optional Practice Problems

  1. Structured JSON Output: Utilize the structured output configuration options of Gemini in source-code/gemini_api to ensure that model responses strictly match a predefined schema.
  2. Java SDK Wrapper: Call a Gemini model using the Java SDK wrapper (source-code/gemini_java_api) and parse the resulting response into a Clojure map structure.
  3. Function Calling: Implement tool/function calling where the Gemini model decides when to execute a custom Clojure math function.