Skip to content

Commit fe0164b

Browse files
karthinkkhinshankhan
authored andcommitted
gptel: Add JSON output for OpenAI, Anthropic, Gemini, Ollama
Activate support for structured outputs to the OpenAI, Anthropic, Gemini and Ollama backends. Some other backends like llama-cpp and Perplexity also work because they have OpenAI-compatible APIs. Note: There are many other providers that purport to provide OpenAI compatible APIs that do not support structured outputs. This includes Groq, Openrouter, Deepseek and others. The approach is identical for all backends: if we find a schema in `gptel--schema', we process it to a backend appropriate format and request JSON output in the payload. `gptel--schema' is intended to be let-bound around calls to `gptel--request-data'. A less stateful solution for `gptel--request-data' is planned for the future. * gptel-anthropic.el (gptel--request-data, gptel--parse-schema): The Anthropic API uses tool calls to (reliably) generate JSON output. Define an ersatz tool and check for JSON output as a special case in `gptel--handle-tool-use'. This approach has some disadvantages: we cannot use both tools and require JSON output in the same request. Them's the breaks. In all other cases, we use a backend-appropriate flag in the payload to require JSON output. * gptel-gemini.el (gptel--request-data, gptel--parse-schema): * gptel-openai.el (gptel-tools, gptel--parse-tools) gptel--request-data, gptel--parse-schema): * gptel-ollama.el (gptel--request-data): * NEWS: Mention structured output support, the previously added command to copy the Curl command for a request from the dry-run buffer and Open WebUI support.
1 parent 7cd5df2 commit fe0164b

File tree

5 files changed

+58
-1
lines changed

5 files changed

+58
-1
lines changed

NEWS

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,25 @@
77
- Add support for ~gemini-2.5-pro~, ~gemini-2.5-flash~,
88
~gemini-2.5-flash-lite-preview-06-17~.
99

10+
- Add support for Open WebUI. Open WebUI provides an
11+
OpenAI-compatible API, so the "support" is just a new section of the
12+
README with instructions.
13+
1014
** New features and UI changes
1115

16+
- Structured output support: ~gptel-request~ can now take an optional
17+
schema argument to constrain LLM output to the specified JSON
18+
schema. The JSON schema can be provided as a serialized JSON string
19+
or as an elisp object (a nested plist). This feature works with all major
20+
backends: OpenAI, Anthropic, Gemini, llama-cpp and Ollama. It is
21+
presently supported by some but not all "OpenAI-compatible API"
22+
providers. Note that this is only available via the ~gptel-request~
23+
API, and currently unsupported by ~gptel-send~.
24+
25+
- From the dry-run inspector buffer, you can now copy the Curl command
26+
for the request. Like when continuing the query, the request is
27+
constructed from the contents of the buffer, which is editable.
28+
1229
- gptel now handles Ollama models that return both reasoning content
1330
and tool calls in a single request.
1431

gptel-anthropic.el

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,13 +230,29 @@ Mutate state INFO with response metadata."
230230
(gptel--model-capable-p 'cache))
231231
(nconc (aref tools-array (1- (length tools-array)))
232232
'(:cache_control (:type "ephemeral")))))))
233+
(when gptel--schema
234+
(plist-put prompts-plist :tools
235+
(vconcat
236+
(list (gptel--parse-schema backend gptel--schema))
237+
(plist-get prompts-plist :tools)))
238+
(plist-put prompts-plist :tool_choice
239+
`(:type "tool" :name ,gptel--ersatz-json-tool)))
233240
;; Merge request params with model and backend params.
234241
(gptel--merge-plists
235242
prompts-plist
236243
gptel--request-params
237244
(gptel-backend-request-params gptel-backend)
238245
(gptel--model-request-params gptel-model))))
239246

247+
(cl-defmethod gptel--parse-schema ((_backend gptel-anthropic) schema)
248+
;; Unlike the other backends, Anthropic generates JSON using a tool call. We
249+
;; write the tool here, meant to be added to :tools.
250+
(list
251+
:name "response_json"
252+
:description "Record JSON output according to user prompt"
253+
:input_schema (gptel--preprocess-schema
254+
(gptel--dispatch-schema-type schema))))
255+
240256
(cl-defmethod gptel--parse-tools ((_backend gptel-anthropic) tools)
241257
"Parse TOOLS to the Anthropic API tool definition spec.
242258

gptel-gemini.el

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ list."
145145
(when gptel-include-reasoning
146146
(setq params
147147
(plist-put params :thinkingConfig '(:includeThoughts t))))
148+
(when gptel--schema
149+
(setq params (nconc params (gptel--gemini-filter-schema
150+
(gptel--parse-schema backend gptel--schema)))))
148151
(when params
149152
(plist-put prompts-plist
150153
:generationConfig params))
@@ -155,6 +158,11 @@ list."
155158
(gptel-backend-request-params gptel-backend)
156159
(gptel--model-request-params gptel-model))))
157160

161+
(cl-defmethod gptel--parse-schema ((_backend gptel-gemini) schema)
162+
(list :responseMimeType "application/json"
163+
:responseSchema (gptel--preprocess-schema
164+
(gptel--dispatch-schema-type schema))))
165+
158166
(defun gptel--gemini-filter-schema (schema)
159167
"Destructively filter unsupported attributes from SCHEMA.
160168

gptel-ollama.el

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,10 @@ Store response metadata in state INFO."
101101
(gptel--merge-plists
102102
`(:model ,(gptel--model-name gptel-model)
103103
:messages [,@prompts]
104-
:stream ,(or gptel-stream :json-false))
104+
:stream ,(or gptel-stream :json-false)
105+
,@(and gptel--schema
106+
`(:format ,(gptel--preprocess-schema
107+
(gptel--dispatch-schema-type gptel--schema)))))
105108
gptel--request-params
106109
(gptel-backend-request-params gptel-backend)
107110
(gptel--model-request-params gptel-model)))

gptel-openai.el

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
(defvar gptel-track-media)
4141
(defvar gptel-use-tools)
4242
(defvar gptel-tools)
43+
(defvar gptel--schema)
4344
(declare-function gptel-context--collect-media "gptel-context")
4445
(declare-function gptel--base64-encode "gptel")
4546
(declare-function gptel--trim-prefixes "gptel")
@@ -58,6 +59,7 @@
5859
(declare-function gptel-context--wrap "gptel-context")
5960
(declare-function gptel--inject-prompt "gptel")
6061
(declare-function gptel--parse-tools "gptel")
62+
(declare-function gptel--parse-schema "gptel")
6163

6264
;; JSON conversion semantics used by gptel
6365
;; empty object "{}" => empty list '() == nil
@@ -307,13 +309,24 @@ Mutate state INFO with response metadata."
307309
(plist-put prompts-plist
308310
(if reasoning-model-p :max_completion_tokens :max_tokens)
309311
gptel-max-tokens))
312+
(when gptel--schema
313+
(plist-put prompts-plist
314+
:response_format (gptel--parse-schema backend gptel--schema)))
310315
;; Merge request params with model and backend params.
311316
(gptel--merge-plists
312317
prompts-plist
313318
gptel--request-params
314319
(gptel-backend-request-params gptel-backend)
315320
(gptel--model-request-params gptel-model))))
316321

322+
(cl-defmethod gptel--parse-schema ((_backend gptel-openai) schema)
323+
(list :type "json_schema"
324+
:json_schema
325+
(list :name (md5 (format "%s" (random)))
326+
:schema (gptel--preprocess-schema
327+
(gptel--dispatch-schema-type schema))
328+
:strict t)))
329+
317330
;; NOTE: No `gptel--parse-tools' method required for gptel-openai, since this is
318331
;; handled by its defgeneric implementation
319332

0 commit comments

Comments
 (0)