Skip to content

Commit d89a25c

Browse files
authored
Merge pull request #304 from DinoChiesa/goog-model-discovery
feat: discover Gemini models dynamically via API
2 parents 5f5fca3 + fce67c3 commit d89a25c

File tree

1 file changed

+137
-2
lines changed

1 file changed

+137
-2
lines changed

chatgpt-shell-google.el

Lines changed: 137 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
;;; chatgpt-shell-google.el --- Google-specific logic -*- lexical-binding: t -*-
22

3-
;; Copyright (C) 2023 Alvaro Ramirez
3+
;; Copyright (C) 2023-2025 Alvaro Ramirez
44

55
;; Author: Alvaro Ramirez https://xenodium.com
66
;; URL: https://github.com/xenodium/chatgpt-shell
@@ -27,8 +27,11 @@
2727

2828
(eval-when-compile
2929
(require 'cl-lib))
30+
(require 'let-alist)
3031
(require 'shell-maker)
3132
(require 'map)
33+
(require 'rx)
34+
(require 'json)
3235

3336
(defvar chatgpt-shell-proxy)
3437

@@ -82,6 +85,136 @@ VALIDATE-COMMAND, and GROUNDING-SEARCH handler."
8285
(:key . chatgpt-shell-google-key)
8386
(:validate-command . chatgpt-shell-google--validate-command)))
8487

88+
(defun chatgpt-shell-google--current-generative-model-p (api-response)
89+
"This is a predicate that looks at a model description within
90+
API-RESPONSE.
91+
92+
It returns non-nil if the model described in API-RESPONSE is current and
93+
supports \"generateContent\".
94+
95+
This is used to filter the list of models returned from
96+
https://generativelanguage.googleapis.com"
97+
(let-alist api-response
98+
(if (not (string-match-p (rx (or "discontinued" "deprecated")) .description))
99+
(seq-contains-p .supportedGenerationMethods "generateContent"))))
100+
101+
(defun chatgpt-shell-google--fetch-model-versions ()
102+
"Retrieves the list of generative models from the Google API."
103+
(if-let* ((api-key (chatgpt-shell-google-key)))
104+
(let ((url (concat chatgpt-shell-google-api-url-base "/v1beta/models?key=" api-key)))
105+
(with-current-buffer (url-retrieve-synchronously url)
106+
(goto-char (if (boundp 'url-http-end-of-headers)
107+
url-http-end-of-headers
108+
(error "`url-http-end-of-headers' marker is not defined")))
109+
(if-let* ((parsed-response
110+
(shell-maker--json-parse-string
111+
(buffer-substring-no-properties (point) (point-max)))))
112+
(let-alist parsed-response
113+
(seq-filter #'chatgpt-shell-google--current-generative-model-p .models)))))
114+
(error "Set your Google API Key.")))
115+
116+
(defun chatgpt-shell-google--parse-model (api-response)
117+
"Convert the API-RESPONSE returned by Gemini into a
118+
the model description needed by `chatgpt-shell'."
119+
(let-alist api-response
120+
(let* ((model-version (string-remove-prefix "models/" .name))
121+
(model-shortversion (string-remove-prefix "gemini-" model-version))
122+
(model-urlpath (concat "/v1beta/" .name))
123+
;; The api-response descriptor does not stipulate whether grounding is supported.
124+
;; This logic applies a heuristic based on the model name (aka version).
125+
(model-supports-grounding
126+
(if (string-match-p (rx bol (or "gemini-1.5" "gemini-2.0")) model-version) t nil)))
127+
(chatgpt-shell-google-make-model :version model-version
128+
:short-version model-shortversion
129+
:grounding-search model-supports-grounding
130+
:path model-urlpath
131+
:token-width 4
132+
:context-window .inputTokenLimit))))
133+
134+
(cl-defun chatgpt-shell-google-load-models (&key override)
135+
"Query Google for the list of Gemini LLM models available.
136+
137+
By default, this package uses a static list of models as returned from
138+
`chatgpt-shell-google-models'. But some users may want to choose from
139+
a fresher set of available models.
140+
141+
This function retrieves data from
142+
https://ai.google.dev/gemini-api/docs/models/gemini. This fn then
143+
appends the models retrieved to the `chatgpt-shell-models' list, unless
144+
a model with the same name is already present.
145+
146+
By default, replace the existing Google models in `chatgpt-shell-models'
147+
with the newly retrieved models. When OVERRIDE is non-nil, which
148+
happens when the function is invoked interactively with a prefix
149+
argument, replace all the Google models with those retrieved."
150+
151+
(interactive (list :override current-prefix-arg))
152+
(let* ((goog-predicate (lambda (model)
153+
(string= (map-elt model :provider) "Google")))
154+
(goog-index (or (cl-position-if goog-predicate chatgpt-shell-models)
155+
(length chatgpt-shell-models))))
156+
(setq chatgpt-shell-models (and (not override)
157+
(cl-remove-if goog-predicate chatgpt-shell-models)))
158+
(let* ((existing-gemini-models
159+
(mapcar (lambda (model) (map-elt model :version))
160+
(cl-remove-if-not goog-predicate chatgpt-shell-models)))
161+
(new-gemini-models
162+
(mapcar #'chatgpt-shell-google--parse-model (chatgpt-shell-google--fetch-model-versions))))
163+
(setq chatgpt-shell-models
164+
(append (seq-take chatgpt-shell-models goog-index)
165+
new-gemini-models
166+
(seq-drop chatgpt-shell-models goog-index)))
167+
(message "Added %d Gemini model(s); kept %d existing Gemini model(s)"
168+
(length new-gemini-models)
169+
(length existing-gemini-models)))))
170+
171+
(defun chatgpt-shell-google-toggle-grounding-with-google-search ()
172+
"Toggle the `:grounding-search' boolean for the currently-selected model.
173+
174+
Google's documentation states that All Gemini 1.5 and 2.0 models support
175+
grounding with Google search, and `:grounding-search' will be `t' for
176+
those models. For models that support grounding, this package will
177+
include a
178+
179+
(tools .((google_search . ())))
180+
181+
in the request payload for 2.0+ models, or
182+
183+
(tools .((google_search_retrieval . ())))
184+
185+
for 1.5-era models.
186+
187+
But some of the experimental models of those versions may not support
188+
grounding. If `chatgpt-shell' tries to send a tools parameter as above
189+
to a model that does not support grounding, the API returns an error.
190+
191+
And in some cases users may wish to not _use_ grounding in Search, even
192+
though it is available.
193+
194+
In either case, the user can invoke this function to toggle
195+
grounding-in-google-search on the model. This package will send the
196+
tools parameter in subsequent outbound requests to that model, when
197+
grounding is enabled.
198+
199+
Returns the new boolean value of `:grounding-search'."
200+
(interactive)
201+
(when-let* ((current-model (chatgpt-shell--resolved-model))
202+
(is-google (string= (map-elt current-model :provider) "Google"))
203+
(current-grounding-cons (assq :grounding-search current-model)))
204+
(let ((toggled (not (cdr current-grounding-cons))))
205+
(setf (cdr current-grounding-cons) toggled)
206+
(message "Grounding in Google search: %s" (if toggled "ON" "OFF"))
207+
toggled)))
208+
209+
(defun chatgpt-shell-google--get-grounding-in-search-tool-keyword (model)
210+
"Retrieves the keyword for the grounding tool.
211+
212+
This gets set once for each model, based on a heuristic."
213+
(when-let* ((current-model model)
214+
(is-google (string= (map-elt current-model :provider) "Google"))
215+
(version (map-elt current-model :version)))
216+
(if (string-match "1\\.5" version) "google_search_retrieval" "google_search")))
217+
85218
(defun chatgpt-shell-google-models ()
86219
"Build a list of Google LLM models available."
87220
;; Context windows have been verified as of 11/26/2024. See
@@ -188,7 +321,9 @@ or
188321
(when prompt
189322
(list (cons prompt nil))))))))
190323
(when (map-elt model :grounding-search)
191-
'((tools . ((google_search . ())))))
324+
;; Grounding in Google Search is supported for both Gemini 1.5 and 2.0 models.
325+
;; But the API is slightly different between them. This uses the correct tool name.
326+
`((tools . ((,(intern (chatgpt-shell-google--get-grounding-in-search-tool-keyword model)) . ())))))
192327
`((generation_config . ((temperature . ,(or (map-elt settings :temperature) 1))
193328
;; 1 is most diverse output.
194329
(topP . 1))))))

0 commit comments

Comments
 (0)