Using the Google Gemini APIs

We used the OpenAI LLM APIs in the last chapter and now we provide a similar example using Google’s gemini-2.5-flash model.

Note: Added Tool Use and Search API support to the REST API code March 7, 2026.

I recommend reading Google’s online documentation for the APIs to see all the capabilities of the OpenAI APIs.

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.

In all examples you may substitute the model gemini-3.0-pro for gemini-2.5-flash.

Test Code for REST Interface and Sample Test Output

Before we look at the example code, let’s look at an example code running it and later sample output:

 1 (ns gemini-api.core-test
 2   (:require [clojure.test :refer :all]
 3             [gemini-api.core :refer :all]))
 4 
 5 (def some-text
 6   "Jupiter is the fifth planet from the Sun and the largest in the Solar System. It \
 7 is a gas giant with a mass one-thousandth that of the Sun, but two-and-a-half times \
 8 that of all the other planets in the Solar System combined. Jupiter is one of the br\
 9 ightest objects visible to the naked eye in the night sky, and has been known to anc\
10 ient civilizations since before recorded history. It is named after the Roman god Ju\
11 piter.[19] When viewed from Earth, Jupiter can be bright enough for its reflected li\
12 ght to cast visible shadows,[20] and is on average the third-brightest natural objec\
13 t in the night sky after the Moon and Venus.")
14 
15 (deftest completions-test
16   (testing "gemini completions API"
17     (let [results
18           (gemini-api.core/generate-content "He walked to the river")]
19       (println results)
20       (is (= 0 0)))))
21 
22 (deftest summarize-test
23   (testing "gemini summarize API"
24     (let [results
25           (gemini-api.core/summarize
26            some-text)]
27       (println results)
28       (is (= 0 0)))))
29 
30 (deftest question-answering-test
31   (testing "gemini question-answering API"
32     (let [results
33           (gemini-api.core/generate-content
34             ;;"If it is not used for hair, a round brush is an example of what 1. ha\
35 ir brush 2. bathroom 3. art supplies 4. shower ?"
36            "Where is the Valley of Kings?"
37             ;"Where is San Francisco?"
38            )]
39       (println results)
40       (is (= 0 0)))))
41 
42 ;; ── Google Search (grounding) ─────────────────────────────────────────────────
43 
44 (deftest search-test
45   (testing "gemini Google Search grounding API"
46     (let [[text citations]
47           (gemini-api.core/generate-with-search-and-citations
48            "Who wrote the Clojure programming language?")]
49       (println "Search result text:" text)
50       (println "Citations:")
51       (doseq [{:keys [title uri]} citations]
52         (println " -" title uri))
53       ;; text should be a non-empty string
54       (is (string? text))
55       (is (pos? (count text))))))
56 
57 ;; ── Tool / Function calling ───────────────────────────────────────────────────
58 
59 (defn- mock-get-weather
60   "Fake weather lookup  no real HTTP call; just returns a canned string."
61   [{:keys [location]}]
62   (str "It is 72°F and sunny in " location "."))
63 
64 (def ^:private weather-tool-def
65   {:name        "get_weather"
66    :description "Returns current weather conditions for a given city."
67    :parameters  {:type       "OBJECT"
68                  :properties {:location {:type        "STRING"
69                                          :description "The name of the city."}}
70                  :required   ["location"]}})
71 
72 (deftest tool-use-test
73   (testing "gemini function/tool calling API"
74     (let [result
75           (gemini-api.core/generate-with-tools
76            "What is the weather like in Paris right now?"
77            [weather-tool-def]
78            {"get_weather" mock-get-weather})]
79       (println "Tool-use result:" result)
80       ;; The model should have issued a functionCall; our dispatch fn returns
81       ;; a string describing the weather.
82       (is (map? result))
83       (is (or (contains? result :text)
84               (contains? result :function-call)))
85       (when (contains? result :function-call)
86         (is (string? (:result result)))
87         (is (re-find #"Paris" (:result result)))))))

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 \
12 next?"
13 3.  **Expand on it descriptively:** "The path was worn and led directly to the shimm\
14 ering river."
15 4.  **Imagine the scene:** "I picture a man, perhaps with a thoughtful expression, m\
16 aking his way to the water's edge."
17 5.  **Use it in a story:** "He walked to the river, a place he always found peace, h\
18 oping to clear his mind."
19 6.  **Analyze the grammar:** "It's a simple past tense sentence, indicating a comple\
20 ted action."
21 
22 Just let me know!
23 Jupiter is the fifth and largest planet in our Solar System, classified as a gas gia\
24 nt. It possesses a mass two-and-a-half times greater than all other planets combined\
25 . Extremely bright, it is visible to the naked eye and has been known since ancient \
26 times, capable of casting visible shadows. On average, it is the third-brightest nat\
27 ural object in the night sky after the Moon and Venus, and is named after the Roman \
28 god Jupiter.
29 The Valley of Kings is located in **Egypt**, specifically on the **west bank of the \
30 Nile River**, near the modern city of **Luxor**.
31 
32 This area was part of ancient Thebes, and it served as the burial place for pharaohs\
33  and powerful nobles of the New Kingdom (18th to 20th Dynasties).
34 Search result text: Rich Hickey is the creator of the Clojure programming language. \
35 He developed Clojure in the mid-2000s, releasing it publicly in October 2007. Hickey\
36  wanted a modern Lisp that was functional, compatible with the Java platform, and de\
37 signed for concurrency.
38 
39 He continues to lead the development of the language. While much of Clojure's underp\
40 innings and initial compiler were written in Java, Hickey expressed a desire to rewr\
41 ite those parts in Clojure itself.
42 Citations:
43  - clojure.org https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQ\
44 G7EkD0j4M6tohYe2mE4xsJ0UnD9mW_2k9kTfEC_KFjEjF3abclOhMEK_5lnWi9eEiYM-sLG5VLGmrz8MJr8G\
45 O78c7uuY-Y3tOYcbXERim9lE0ByuU=
46  - wikipedia.org https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZI\
47 YQGUsFsuSAgQhO0xpLuPkmnCu0uH9avs3dSlsUqtRr3rwT1IJS0j_9Zidy9xJwXxsOU4GcA9MQ7K55g8RR8H\
48 sMXx_MTykOIRitXpLw0ykzvwWWmdBUIMbR3r6y33ZsDA
49  - wikipedia.org https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZI\
50 YQHKDCXRr7I88rWWwvakVZ5cQKVXEVBaNVUL1U28qMXKJwRyB3wGCvm2t2N1a7d0oWcxXCP9ME-1x0toDWEt\
51 wjFIklLtoKWSJAmgE4wcVn1pGo7JvYdFqIp5GY-v62h92gELKQ==
52  - clojure.org https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQ\
53 Hv_3Lk5xWwXDXM1MLJiNKbW3M10klqM05QNHKmlgtqW1B88yaxN-e8W0XCQqCXp5XL3tkkyjY1PpF5GwaV1i\
54 Mkc0t01YHU2l9GZuHi4Q==
55  - nubank.com https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQH\
56 ydcJ7X1GBqp6Ix8TfWIdly167wONbnivgUq8LRk53ZtiMmX4_5RPIz3Ux1PEtfAhqePoRHPBF5SketLNaFZi\
57 HBYfIArg9CWWM2hdCVwUD28FpQE_YePas8xjZdUAw04ovQywWDvh1DyEm_bR9I4wAvxDW5K4es-iNwXbCHdg\
58 =
59  - mcqueeney.tech https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZ\
60 IYQFSqtbhsfbLZ0hKglZuYmMyHr5Me2p2G3S7c-XeRX_1V-8Iv9GtwpFI3cZa_fn8CzI8jemxFlh_nDckydR\
61 1RE17_FW-9EEMzZHkvrFp3p7RftRaYrQywASS2qV9LWwgP326Ia_4a-JXFXtkiiDdOblaP9HfgBV8IBQmnqK\
62 oEZV9pzja918uwCLLwL30PEFI6KaD_Tw898_3
63  - github.com https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQH\
64 rSB2btECWpuWNqk_iETlVTWfLqVOea9M3NocGXJhYqGSzeZwwlsWUiW4flthZXG2ZwKcziq50Gc7-WIAVkoo\
65 mzrBXMYq5gR8NavqmAwEs4iz0E5dUDI31XO3giIJ9wKTcXGuKexkl6pwock4PwTRPfTxmS-OJCnBu
66 Tool-use result: {:function-call {:name get_weather, :args {:location Paris}}, :resu\
67 lt It is 72°F and sunny in Paris.}
68 
69 Ran 5 tests containing 9 assertions.
70 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:\n\n" text)))
 72 
 73 ;;;; ── Token counting ──────────────────────────────────────────────────────────
 74 
 75 (defn count-tokens
 76   "Return the total token count for PROMPT using the Gemini countTokens API."
 77   [prompt & {:keys [model-id] :or {model-id model}}]
 78   (let [body {:contents [{:parts [{:text prompt}]}]}
 79         resp (post-json (api-url model-id "countTokens") body)]
 80     (if-let [tc (:totalTokens resp)]
 81       tc
 82       (do (log/warn "Could not retrieve token count:" resp) nil))))
 83 
 84 ;; (count-tokens "In one sentence, explain how AI works to a child.")
 85 
 86 ;;;; ── Google Search grounding ─────────────────────────────────────────────────
 87 
 88 (defn generate-with-search
 89   "Like generate-content but enables the google_search grounding tool."
 90   [prompt & {:keys [model-id] :or {model-id model}}]
 91   (let [body {:contents [{:parts [{:text prompt}]}]
 92               :tools    [{:google_search {}}]}
 93         resp (post-json (api-url model-id "generateContent") body)]
 94     (extract-text resp)))
 95 
 96 ;; (generate-with-search "What sci-fi movies are playing at Harkins 16 in Flagstaff \
 97 today?")
 98 
 99 (defn generate-with-search-and-citations
100   "Like generate-with-search but returns [text citations] where citations is a
101    seq of {:title  :uri } maps extracted from grounding metadata."
102   [prompt & {:keys [model-id] :or {model-id model}}]
103   (let [body {:contents [{:parts [{:text prompt}]}]
104               :tools    [{:google_search {}}]}
105         resp (post-json (api-url model-id "generateContent") body)]
106     (let [text      (extract-text resp)
107           candidate (first (:candidates resp))
108           chunks    (get-in candidate [:groundingMetadata :groundingChunks] [])
109           citations (keep (fn [chunk]
110                             (when-let [web (:web chunk)]
111                               {:title (:title web) :uri (:uri web)}))
112                           chunks)]
113       [text citations])))
114 
115 ;; (let [[answer sources] (generate-with-search-and-citations "Who won the Super Bow\
116 l 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 :functionDeclarati\
161 on fn-decls)}]}
162                    system (assoc :systemInstruction {:parts [{:text system}]}))
163         resp     (post-json (api-url model-id "generateContent") body)]
164     (let [candidate (first (:candidates resp))
165           parts     (get-in candidate [:content :parts] [])]
166       (if-let [fc-part (first (filter :functionCall parts))]
167         ;; Model wants to call a function
168         (let [fc     (:functionCall fc-part)
169               fname  (:name fc)
170               fargs  (:args fc)
171               f      (get dispatch-fns fname)]
172           (if f
173             {:function-call fc :result (f fargs)}
174             (do (log/warn "No dispatch function for tool:" fname)
175                 {:function-call fc :result nil})))
176         ;; Regular text response
177         {:text (get-in (first parts) [:text])}))))
178 
179 ;; ── Example: single tool call ─────────────────────────────────────────────────
180 ;;
181 ;; (defn get-weather [{:keys [location]}]
182 ;;   (str "It is 72°F and sunny in " location "."))
183 ;;
184 ;; (def weather-tool
185 ;;   {:name        "get_weather"
186 ;;    :description "Returns current weather for a city."
187 ;;    :parameters  {:type       "OBJECT"
188 ;;                  :properties {:location {:type        "STRING"
189 ;;                                          :description "City name"}}
190 ;;                  :required   ["location"]}})
191 ;;
192 ;; (generate-with-tools
193 ;;   "What is the weather like in Paris?"
194 ;;   [weather-tool]
195 ;;   {"get_weather" get-weather})
196 
197 ;;;; ── Chat (stateful, in-process) ────────────────────────────────────────────
198 
199 (defn make-chat-session
200   "Return a new chat session atom. Holds a vector of {:role  :parts []} turns."
201   []
202   (atom []))
203 
204 (defn chat-turn
205   "Send USER-MSG in the context of SESSION (an atom returned by make-chat-session).
206    Appends both the user message and the model reply to the session history.
207    Returns the model's reply text."
208   [session user-msg & {:keys [model-id] :or {model-id model}}]
209   (let [user-turn {:role "user" :parts [{:text user-msg}]}
210         history   (conj @session user-turn)
211         body      {:contents history}
212         resp      (post-json (api-url model-id "generateContent") body)
213         reply     (extract-text resp)
214         model-turn {:role "model" :parts [{:text (or reply "")}]}]
215     (swap! session conj user-turn model-turn)
216     reply))
217 
218 (defn chat-repl
219   "Simple REPL-based chat session. Type 'quit' to exit."
220   []
221   (let [session (make-chat-session)]
222     (println "Gemini Chat  type 'quit' to exit.")
223     (loop []
224       (print "You: ") (flush)
225       (let [input (read-line)]
226         (when (and input (not= (clojure.string/trim input) "quit"))
227           (let [reply (chat-turn session input)]
228             (println "Gemini:" reply))
229           (recur))))))
230 
231 ;; (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.

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 va\
 9 riable
10 
11 (defn generate-content
12   "Sends a prompt to the Gemini API using the specified model and returns
13    the text response."
14   [prompt]
15   (let [client (Client.)
16         ^GenerateContentResponse resp
17         (.generateContent (.models client)
18                           model
19                           prompt
20                           nil)]
21     (when DEBUG
22       (println (.text resp))
23       (when-let [headers
24                  (some-> resp
25                      .sdkHttpResponse (.orElse nil)
26                      .headers        (.orElse nil))]
27         (println "Response headers:" headers)))
28     (.text resp)))
29 
30 (defn summarize [text]
31   (generate-content (str "Summarize the following text:\n\n" 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.