Client Library for the Google Gemini LLM APIs
While the Google Gemini APIs offer a compelling suite of advantages for developers seeking to integrate cutting-edge, multimodal AI capabilities into their applications. A primary benefit is the large one million token context size and very fast inference speeds. Gemini is very cost effective for natural language processing tasks such as text summarization, question answering, code generation, creative content creation, and conversational AI.
The source code for this Gemini library is in my GitHub repository https://github.com/mark-watson/gemini. As usual you want to git clone this repository in your local directory ~/quicklisp/local-projects/ so Quicklisp can find this library with (ql:quickload :gemini). We will list the code below and then look at example use.
package.lisp
We need the function post in the external library dexador:
1 ;;;; package.lisp
2
3 (defpackage #:gemini
4 (:use #:cl)
5
6 (:export #:generate #:count-tokens #:send-chat-message
7 #:generate-streaming #:generate-with-search
8 #:generate-with-search-and-citations))
gemini.asd
1 ;;;; gemini.asd
2
3 (asdf:defsystem #:gemini
4 :description "Library for using the perplexity search+LLM APIs"
5 :author "Mark Watson"
6 :license "Apache 2"
7 :depends-on (#:uiop #:cl-json #:alexandria)
8 :components ((:file "package")
9 (:file "gemini")))
gemini.lisp
This code defines a function that sends a user-provided text prompt to an external API for generative language processing. It first retrieves a Google API key from the environment and sets the API endpoint URL, then constructs a nested JSON payload embedding the prompt within a specific structure. Using a POST request with appropriate headers - including the API key - the function submits the payload to the API. It then decodes the JSON response, traverses the nested data to extract the generated text, and finally returns plain text as the result.
Note: later we will look at the last part of the file gemini.lisp for code to use Google’s “grounding search”.
1 (in-package #:gemini)
2
3 (defvar *google-api-key* (uiop:getenv "GOOGLE_API_KEY"))
4 (defvar
5 *gemini-api-base-url*
6 "https://generativelanguage.googleapis.com/v1beta/models/")
7
8 (defvar *model* "gemini-3-flash-preview") ;; model used for all use cases in this file.
9
10 (defun escape-json (json-string)
11 (with-output-to-string (s)
12 (loop for char across json-string
13 do (case char
14 (#\" (write-string "\\\"" s))
15 (#\\ (write-string "\\\\" s))
16 (t (write-char char s))))))
17
18 (defun run-curl-command (curl-command)
19 (multiple-value-bind (output error-output exit-code)
20 (uiop:run-program curl-command
21 :output :string
22 :error-output :string
23 :ignore-error-status t)
24 (if (zerop exit-code)
25 output
26 (error "Curl command failed: ~A~%Error: ~A" curl-command error-output))))
27
28 (defun generate (prompt &optional (model-id *model*))
29 "Generates text from a given prompt using the specified model.
30 Uses *model* defined at the top of this file as default.
31 PROMPT: The text prompt to generate content from.
32 MODEL-ID: Optional. The ID of the model to use.
33 Returns the generated text as a string."
34 (let* ((payload (make-hash-table :test 'equal)))
35 (setf (gethash "contents" payload)
36 (list (let ((contents (make-hash-table :test 'equal)))
37 (setf (gethash "parts" contents)
38 (list (let ((part (make-hash-table :test 'equal)))
39 (setf (gethash "text" part) prompt)
40 part)))
41 contents)))
42 (let* ((api-url (concatenate 'string *gemini-api-base-url* model-id ":generateContent"))
43 (data (cl-json:encode-json-to-string payload))
44 (escaped-json (escape-json data))
45 (curl-cmd (format nil "curl -s -X POST ~A -H \"Content-Type: application/json\" -H \"x-goog-api-key: ~A\" -d \"~A\""
46 api-url *google-api-key* escaped-json))
47 (response-string (run-curl-command curl-cmd))
48 (decoded-response (cl-json:decode-json-from-string response-string))
49 (candidates-pair (assoc :CANDIDATES decoded-response))
50 (candidates (cdr candidates-pair))
51 (candidate (first candidates))
52 (content-pair (assoc :CONTENT candidate))
53 (content (cdr content-pair))
54 (parts-pair (assoc :PARTS content))
55 (parts (cdr parts-pair))
56 (first-part (first parts))
57 (text-pair (assoc :TEXT first-part))
58 (text (cdr text-pair)))
59 text)))
60
61 ;; (gemini:generate "In one sentence, explain how AI works to a child.")
62 ;; (gemini:generate "Write a short, four-line poem about coding in Python.")
63
64 (defun count-tokens (prompt &optional (model-id *model*))
65 "Counts the number of tokens for a given prompt and model.
66 Uses *model* defined at top of this file as default.
67 PROMPT: The text prompt to count tokens for.
68 MODEL-ID: Optional. The ID of the model to use.
69 Returns the total token count as an integer."
70 (let* ((api-url (concatenate 'string *gemini-api-base-url* model-id ":countTokens"))
71 (payload (make-hash-table :test 'equal)))
72 ;; Construct payload similar to generate function
73 (setf (gethash "contents" payload)
74 (list (let ((contents (make-hash-table :test 'equal)))
75 (setf (gethash "parts" contents)
76 (list (let ((part (make-hash-table :test 'equal)))
77 (setf (gethash "text" part) prompt)
78 part)))
79 contents)))
80 (let* ((data (cl-json:encode-json-to-string payload))
81 (escaped-json (escape-json data))
82 (curl-cmd (format nil "curl -s -X POST ~A -H \"Content-Type: application/json\" -H \"x-goog-api-key: ~A\" -d \"~A\""
83 api-url *google-api-key* escaped-json))
84 (response-string (run-curl-command curl-cmd))
85 (decoded-response (cl-json:decode-json-from-string response-string))
86 ;; cl-json by default uses :UPCASE for keys, so :TOTAL-TOKENS should be correct
87 (total-tokens-pair (assoc :TOTAL-TOKENS decoded-response)))
88 (if total-tokens-pair
89 (cdr total-tokens-pair)
90 (error "Could not retrieve token count from API response: ~S" decoded-response)))))
91
92 ;; (gemini:count-tokens "In one sentence, explain how AI works to a child.")
93
94 (defun run-tests ()
95 "Runs tests for generate and count-tokens functions."
96 (let* ((prompt "In one sentence, explain how AI works to a child.")
97 (generated-text (generate prompt))
98 (token-count (count-tokens prompt)))
99 (format t "Generated Text: ~A~%Token Count: ~A~%" generated-text token-count)))
100
101 ;; Running the test
102 ;; (gemini::run-tests)
103
104 (defparameter *chat-history* '())
105
106 (defun chat ()
107 (let ((*chat-history* ""))
108 (loop
109 (princ "Enter a prompt: ")
110 (finish-output)
111 (let ((user-prompt (read-line)))
112 (princ user-prompt)
113 (finish-output)
114 (let ((gemini-response (gemini:generate
115 (concatenate 'string *chat-history* "\nUser: " user-prompt))))
116 (princ gemini-response)
117 (finish-output)
118 (setf *chat-history*
119 (concatenate 'string "User: " user-prompt "\n" "Gemini: " gemini-response
120 "\n" *chat-history* "\n\n")))))))
121
122 ;; (gemini::chat)
Example Use
1 CL-USER 4 > (gemini:generate "In one sentence, explain how AI works to a child.")
2 "AI is like teaching a computer with lots and lots of examples, so it can learn to figure things out and act smart all by itself."
3
4 CL-USER 5 > (gemini:count-tokens "How many tokens is this sentence?")
5 7
Using Google’s “Grounding Search”
Google’s “Grounding with Google Search” is a powerful feature that connects the Gemini model to real-time web information, allowing it to answer queries about current events and reducing the likelihood of “hallucinations” by anchoring responses in verifiable external data. The following Common Lisp program utilizes this feature by defining a function, generate-with-search, which builds a JSON payload that specifically includes a tools configuration. By inserting an empty Google Search object into this configuration, the code explicitly instructs the API to perform a web search—such as looking up the winner of a recent tournament like Euro 2024—and synthesize those findings into the final response, which is then parsed and returned as a string. The following code snippet is near the bottom of the file gemini.lisp:
1 (defun generate-with-search (prompt &optional (model-id *model*))
2 (let* ((payload (make-hash-table :test 'equal)))
3 (setf (gethash "contents" payload)
4 (list (let ((contents (make-hash-table :test 'equal)))
5 (setf (gethash "parts" contents)
6 (list (let ((part (make-hash-table :test 'equal)))
7 (setf (gethash "text" part) prompt)
8 part)))
9 contents)))
10 (setf (gethash "tools" payload)
11 (list (let ((tool (make-hash-table :test 'equal)))
12 (setf (gethash "google_search" tool)
13 (make-hash-table :test 'equal))
14 tool)))
15 (let* ((api-url (concatenate 'string *gemini-api-base-url* model-id ":generateContent"))
16 (data (cl-json:encode-json-to-string payload))
17 (escaped-json (escape-json data))
18 (curl-cmd (format nil "curl -s -X POST ~A -H \"Content-Type: application/json\" -H \"x-goog-api-key: ~A\" -d \"~A\""
19 api-url *google-api-key* escaped-json))
20 (response-string (run-curl-command curl-cmd))
21 (decoded-response (cl-json:decode-json-from-string response-string))
22 (candidates-pair (assoc :CANDIDATES decoded-response))
23 (candidates (cdr candidates-pair))
24 (candidate (first candidates))
25 (content-pair (assoc :CONTENT candidate))
26 (content (cdr content-pair))
27 (parts-pair (assoc :PARTS content))
28 (parts (cdr parts-pair))
29 (first-part (first parts))
30 (text-pair (assoc :TEXT first-part))
31 (text (cdr text-pair)))
32 text)))
33
34 ;; (gemini:generate-with-search "Consultant Mark Watson has written Common Lisp, semantic web, Clojure, Java, and AI books. What musical instruments does he play?")
The core mechanism of this implementation relies on the payload hash table construction, specifically where the tools key is populated. Unlike a standard generation request, this payload includes a list containing a Google Search object; the presence of this specific key acts as a switch, granting the model permission to query Google’s search index before formulating its answer. This is particularly critical for the example query regarding “Euro 2024,” as the model’s static training data would likely cut off before the event occurred, making the search tool indispensable for factual accuracy.
Once the API processes the request, the function handles the JSON response by traversing the nested structure of candidates and content parts. While the API response for a grounded query actually includes rich metadata—such as groundingMetadata with search queries, source titles, and URLs—this specific function is designed to filter that out, drilling down solely to extract the synthesized text string from the first part of the first candidate. This provides a clean, human-readable answer while abstracting away the complexity of the underlying search-and-retrieve operations that generated it.
Here is example output:
1 $ sbcl
2 This is SBCL 2.5.10, an implementation of ANSI Common Lisp.
3 * (ql :gemini)
4 To load "gemini":
5 Load 1 ASDF system:
6 gemini
7 ; Loading "gemini"
8 nil
9 * (gemini:generate-with-search "What movies are playing in Flagstaff Arizona today at the Harkens Theater?")
10 "Today, **Thursday, December 18, 2025**, the following movies are playing at the **Harkins Flagstaff 16** theater.
11
12 Please note that many major releases, such as *Avatar: Fire and Ash*, are currently running early \"preview\" screenings ahead of their official opening tomorrow.
13
14 ### **Movies & Showtimes**
15
16 * **Avatar: Fire and Ash** (PG-13)
17 * **Ciné XL:** 2:00 PM, 6:15 PM, 10:30 PM
18 * **3D HFR:** 2:30 PM, 5:00 PM, 6:45 PM, 9:15 PM
19 * **Digital:** 3:00 PM, 4:00 PM, 7:15 PM, 8:15 PM, 9:30 PM
20 * **Five Nights at Freddy's 2** (PG-13)
21 * 11:30 AM, 12:30 PM, 2:05 PM, 4:50 PM, 7:35 PM, 10:20 PM
22 * **Zootopia 2** (PG)
23 * 1:45 PM, 5:10 PM, 7:25 PM, 8:10 PM
24 * **David** (PG)
25 * 12:45 PM, 3:30 PM, 6:15 PM, 9:00 PM
26 * **Ella McCay** (PG-13)
27 * 10:20 AM, 1:05 PM, 3:50 PM, 6:35 PM
28 * **Eternity** (PG-13)
29 * 10:45 AM, 1:35 PM, 4:20 PM, 7:05 PM
30 * **Hamnet** (PG-13)
31 * 12:20 PM, 3:25 PM, 6:30 PM
32 * **The Housemaid** (R)
33 * 2:00 PM, 5:00 PM, 8:00 PM, 10:15 PM
34 * **Wicked: For Good**
35 * *Check theater for exact late-afternoon and evening showtimes.*
36
37 ### **Special Holiday Events**
38 * **How the Grinch Stole Christmas (25th Anniversary):** Screening as part of the Harkins Holiday Series.
39 * **Harkins Holiday Series:** Various seasonal classics are playing through December 22.
40
41 ---
42 **Theater Information:**
43 * **Address:** 4751 East Marketplace Dr, Flagstaff, AZ 86004
44 * **Phone:** (928) 233-3005
45
46 *Showtimes are subject to change. It is recommended to verify specific times on the official Harkins website or app before heading to the theater.*"
47 *
Using Google’s “Grounding Search” With Citations
This example is also near the bottom of the file gemini.lisp and adds the display of citations that consist of web sites searched. You can use the code in the web spidering chapter to fetch the text contents of these reference search results.
Here is the added code:
1 (defun generate-with-search-and-citations (prompt &optional (model-id *model*))
2 (let* ((payload (make-hash-table :test 'equal)))
3 ;; Payload construction same as previous example):
4 (setf (gethash "contents" payload)
5 (list (let ((contents (make-hash-table :test 'equal)))
6 (setf (gethash "parts" contents)
7 (list (let ((part (make-hash-table :test 'equal)))
8 (setf (gethash "text" part) prompt)
9 part)))
10 contents)))
11 (setf (gethash "tools" payload)
12 (list (let ((tool (make-hash-table :test 'equal)))
13 (setf (gethash "google_search" tool)
14 (make-hash-table :test 'equal))
15 tool)))
16
17 (let* ((api-url (concatenate 'string *gemini-api-base-url* model-id ":generateContent"))
18 (data (cl-json:encode-json-to-string payload))
19 (escaped-json (escape-json data))
20 (curl-cmd (format nil "curl -s -X POST ~A -H \"Content-Type: application/json\" -H \"x-goog-api-key: ~A\" -d \"~A\""
21 api-url *google-api-key* escaped-json))
22 (response-string (run-curl-command curl-cmd))
23 (decoded-response (cl-json:decode-json-from-string response-string))
24 (candidates-pair (assoc :CANDIDATES decoded-response))
25 (candidates (cdr candidates-pair))
26 (candidate (first candidates))
27 ;; 1. Extract Content Text
28 (content-pair (assoc :CONTENT candidate))
29 (content (cdr content-pair))
30 (parts-pair (assoc :PARTS content))
31 (parts (cdr parts-pair))
32 (first-part (first parts))
33 (text-pair (assoc :TEXT first-part))
34 (text (cdr text-pair))
35 ;; 2. Extract Grounding Metadata
36 (metadata-pair (assoc :GROUNDING-METADATA candidate))
37 (metadata (cdr metadata-pair))
38 (chunks-pair (assoc :GROUNDING-CHUNKS metadata))
39 (chunks (cdr chunks-pair))
40 ;; 3. Loop through chunks to find Web sources
41 (citations (loop for chunk in chunks
42 for web-data-pair = (assoc :WEB chunk)
43 for web-data = (cdr web-data-pair)
44 when web-data
45 collect (cons (cdr (assoc :TITLE web-data))
46 (cdr (assoc :URI web-data))))))
47 ;; Return both text and citations
48 (values text citations))))
Here is the sample output (output shortened: redirect URIs shortened for brevity):
1 * (multiple-value-bind (response sources)
2 (gemini::generate-with-search-and-citations "gemini-2.0-flash" "Who won the Super Bowl in 2024?")
3 (format t "Answer: ~a~%~%Sources:~%" response)
4 (loop for (title . url) in sources
5 do (format t "- ~a: ~a~%" title url)))
6
7 Answer: The Kansas City Chiefs won Super Bowl LVIII in 2024, defeating the San Francisco 49ers 25-22 in overtime. The game was held in Las Vegas on February 11, 2024. This victory marked the Chiefs' third Super Bowl title in five years and made them the first back-to-back NFL champions in almost 20 years. Patrick Mahomes, the Chiefs' quarterback, was named Super Bowl MVP for the third time.
8
9 Sources:
10 - olympics.com: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUjJ4MF1eTcc...
11 - kcur.org: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUOKyR9rZ...
12 - theguardian.com: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZItCsAVG...
13 - foxsports.com: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYrbCcX...
14 nil
15 *