Skip to content

Insert newlines after streaming tool results #770

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a3493ad
Insert newlines after streaming tool results
jdormit Apr 7, 2025
0ad659c
gptel-rewrite: Clear and restore overlay face around ediff
karthink Jun 25, 2025
e075972
openai-extras: Refine reasoning block detection for Deepseek
karthink Jun 25, 2025
2238f45
gptel-transient: Add log level and inspect log options
karthink Jun 25, 2025
3665dae
gptel: Ensure zero-arg tools have valid parameters struct
tazjin Jun 12, 2025
13b954b
gptel: Add AI/ML API Integration (#930)
D1m7asis Jun 25, 2025
3e57fe2
gptel-rewrite: Use plain window setup in ediff
karthink Jun 26, 2025
78d56e6
gptel-gemini: Construct request data without backquotes
karthink Jun 30, 2025
7b94cd5
gptel-openai: Appease byte-compiler (#945)
karthink Jun 30, 2025
e0f6398
Use git to produce better merge conflicts for rewrite
meain Jun 26, 2025
ba2ab53
rewrite: Refactor merge conflict handling to not use strings
karthink Jun 29, 2025
178d1bf
README: Add gptel-commit to related projects (#944)
lakkiy Jun 30, 2025
dc126ff
gptel-org: Update docstring for clarity (#936)
rgkirch Jun 30, 2025
4cfce86
gptel-gemini: Add gemini-2.5 pro, flash and flash-lite (#926)
stribb Jun 30, 2025
ae720af
README: Mention more packages using gptel
karthink Jun 30, 2025
aad5ced
gptel-org: Set org-complex-heading-regexp in prompt buffer
karthink Jul 1, 2025
d72871c
gptel-curl: Fix text handling around end of reasoning block
karthink Jul 3, 2025
703428e
gptel: Fix warnings for :pre and :post in a preset (#952)
kmontag Jul 4, 2025
c20ca5c
gptel: Add copy Curl command feature to dry-run
karthink Jul 6, 2025
bdf19df
README: Add setup instructions for Open WebUI (#954)
algal Jul 9, 2025
9a4927e
gptel: Add JSON response schema parsing and preprocessing
karthink Jul 7, 2025
f0205da
gptel: Add JSON output for OpenAI, Anthropic, Gemini, Ollama
karthink Jul 7, 2025
afceb1b
gptel: Update comments, indentation and test submodule
karthink Jul 9, 2025
be391ab
gptel-transient: Fix log-level selection menu (#960)
nano-o Jul 9, 2025
cac22aa
gptel-openai: Don't parse tools named "null" (#935) (#951)
timfel Jul 9, 2025
26e3dac
gptel: Include full file path in context (#863)
karthink Jul 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,33 @@
- Add support for ~gemini-2.5-pro~, ~gemini-2.5-flash~,
~gemini-2.5-flash-lite-preview-06-17~.

- Add support for Open WebUI. Open WebUI provides an
OpenAI-compatible API, so the "support" is just a new section of the
README with instructions.

** New features and UI changes

- When including a file in the context, the abbreviated full path of
the file is included is now included instead of the basename.
Specifically, =/home/user/path/to/file= is included as
=~/path/to/file=. This is to provide additional context for LLM
actions, including tool-use in subsequent conversation turns. This
applies to context included via ~gptel-add~ or as a link in a
buffer.

- Structured output support: ~gptel-request~ can now take an optional
schema argument to constrain LLM output to the specified JSON
schema. The JSON schema can be provided as a serialized JSON string
or as an elisp object (a nested plist). This feature works with all major
backends: OpenAI, Anthropic, Gemini, llama-cpp and Ollama. It is
presently supported by some but not all "OpenAI-compatible API"
providers. Note that this is only available via the ~gptel-request~
API, and currently unsupported by ~gptel-send~.

- From the dry-run inspector buffer, you can now copy the Curl command
for the request. Like when continuing the query, the request is
constructed from the contents of the buffer, which is editable.

- gptel now handles Ollama models that return both reasoning content
and tool calls in a single request.

Expand Down
120 changes: 112 additions & 8 deletions README.org
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ gptel is a simple Large Language Model chat client for Emacs, with support for m
| Anthropic (Claude) | ✓ | [[https://www.anthropic.com/api][API key]] |
| Gemini | ✓ | [[https://makersuite.google.com/app/apikey][API key]] |
| Ollama | ✓ | [[https://ollama.ai/][Ollama running locally]] |
| Open WebUI | ✓ | [[https://openwebui.com/][Open WebUI running locally]] |
| Llama.cpp | ✓ | [[https://github.com/ggml-org/llama.cpp/tree/master/tools/server#quick-start][Llama.cpp running locally]] |
| Llamafile | ✓ | [[https://github.com/Mozilla-Ocho/llamafile#quickstart][Local Llamafile server]] |
| GPT4All | ✓ | [[https://gpt4all.io/index.html][GPT4All running locally]] |
Expand All @@ -24,6 +25,7 @@ gptel is a simple Large Language Model chat client for Emacs, with support for m
| Mistral Le Chat | ✓ | [[https://console.mistral.ai/api-keys][API key]] |
| Perplexity | ✓ | [[https://docs.perplexity.ai/docs/getting-started][API key]] |
| OpenRouter | ✓ | [[https://openrouter.ai/keys][API key]] |
| AI/ML API | ✓ | [[https://aimlapi.com/app/?utm_source=gptel&utm_medium=github&utm_campaign=integration][API key]] |
| together.ai | ✓ | [[https://api.together.xyz/settings/api-keys][API key]] |
| Anyscale | ✓ | [[https://docs.endpoints.anyscale.com/][API key]] |
| PrivateGPT | ✓ | [[https://github.com/zylon-ai/private-gpt#-documentation][PrivateGPT running locally]] |
Expand Down Expand Up @@ -101,6 +103,7 @@ gptel uses Curl if available, but falls back to the built-in url-retrieve to wor
- [[#azure][Azure]]
- [[#gpt4all][GPT4All]]
- [[#ollama][Ollama]]
- [[#open-webui][Open WebUI]]
- [[#gemini][Gemini]]
- [[#llamacpp-or-llamafile][Llama.cpp or Llamafile]]
- [[#kagi-fastgpt--summarizer][Kagi (FastGPT & Summarizer)]]
Expand All @@ -118,6 +121,7 @@ gptel uses Curl if available, but falls back to the built-in url-retrieve to wor
- [[#github-models][Github Models]]
- [[#novita-ai][Novita AI]]
- [[#xai][xAI]]
- [[#aiml-api][AI/ML API]]
- [[#github-copilotchat][Github CopilotChat]]
- [[#aws-bedrock][AWS Bedrock]]
- [[#usage][Usage]]
Expand Down Expand Up @@ -333,6 +337,62 @@ The above code makes the backend available to select. If you want it to be the

#+html: </details>

#+html: <details><summary>
**** Open WebUI
#+html: </summary>

[[https://openwebui.com/][Open WebUI]] is an open source, self-hosted system which provides a multi-user web chat interface and an API endpoint for accessing LLMs, especially LLMs running locally on inference servers like Ollama.

Because it presents an OpenAI-compatible endpoint, you use ~gptel-make-openai~ to register it as a backend.

For instance, you can use this form to register a backend for a local instance of Open Web UI served via http on port 3000:

#+begin_src emacs-lisp
(gptel-make-openai "OpenWebUI"
:host "localhost:3000"
:protocol "http"
:key "KEY_FOR_ACCESSING_OPENWEBUI"
:endpoint "/api/chat/completions"
:stream t
:models '("gemma3n:latest"))
#+end_src

Or if you are running Open Web UI on another host on your local network (~box.local~), serving via https with self-signed certificates, this will work:

#+begin_src emacs-lisp
(gptel-make-openai "OpenWebUI"
:host "box.local"
:curl-args '("--insecure") ; needed for self-signed certs
:key "KEY_FOR_ACCESSING_OPENWEBUI"
:endpoint "/api/chat/completions"
:stream t
:models '("gemma3n:latest"))
#+end_src

To find your API key in Open WebUI, click the user name in the bottom left, Settings, Account, and then Show by API Keys section.

Refer to the documentation of =gptel-make-openai= for more configuration options.

You can pick this backend from the menu when using gptel (see [[#usage][Usage]])

***** (Optional) Set as the default gptel backend

The above code makes the backend available to select. If you want it to be the default backend for gptel, you can set this as the value of =gptel-backend=. Use this instead of the above.
#+begin_src emacs-lisp
;; OPTIONAL configuration
(setq
gptel-model "gemma3n:latest"
gptel-backend (gptel-make-openai "OpenWebUI"
:host "localhost:3000"
:protocol "http"
:key "KEY_FOR_ACCESSING_OPENWEBUI"
:endpoint "/api/chat/completions"
:stream t
:models '("gemma3n:latest")))
#+end_src

#+html: </details>

#+html: <details><summary>
**** Gemini
#+html: </summary>
Expand Down Expand Up @@ -924,6 +984,41 @@ The above code makes the backend available to select. If you want it to be the

#+html: </details>

#+html: <details><summary>
**** AI/ML API
#+html: </summary>

AI/ML API provides 300+ AI models including Deepseek, Gemini, ChatGPT. The models run at enterprise-grade rate limits and uptimes.

Register a backend with
#+begin_src emacs-lisp
;; AI/ML API offers an OpenAI compatible API
(gptel-make-openai "AI/ML API" ;Any name you want
:host "api.aimlapi.com"
:endpoint "/v1/chat/completions"
:stream t
:key "your-api-key" ;can be a function that returns the key
:models '(deepseek-chat gemini-pro gpt-4o))
#+end_src

You can pick this backend from the menu when using gptel (see [[#usage][Usage]]).

***** (Optional) Set as the default gptel backend

The above code makes the backend available to select. If you want it to be the default backend for gptel, you can set this as the value of =gptel-backend=. Use this instead of the above.
#+begin_src emacs-lisp
;; OPTIONAL configuration
(setq gptel-model 'gpt-4o
gptel-backend
(gptel-make-openai "AI/ML API"
:host "api.aimlapi.com"
:endpoint "/v1/chat/completions"
:stream t
:key "your-api-key"
:models '(deepseek-chat gemini-pro gpt-4o)))
#+end_src

#+html: </details>
#+html: <details><summary>
**** Github CopilotChat
#+html: </summary>
Expand Down Expand Up @@ -1526,13 +1621,15 @@ Other Emacs clients for LLMs prescribe the format of the interaction (a comint s
all)
#+end_src

|----------------------+--------------------------------------------------------------------|
| *Connection options* | |
|----------------------+--------------------------------------------------------------------|
| =gptel-use-curl= | Use Curl (default), fallback to Emacs' built-in =url=. |
| =gptel-proxy= | Proxy server for requests, passed to curl via =--proxy=. |
| =gptel-api-key= | Variable/function that returns the API key for the active backend. |
|----------------------+--------------------------------------------------------------------|
|-------------------------+--------------------------------------------------------------------|
| *Connection options* | |
|-------------------------+--------------------------------------------------------------------|
| =gptel-use-curl= | Use Curl? (default), fallback to Emacs' built-in =url=. |
| | You can also specify the Curl path here. |
| =gptel-proxy= | Proxy server for requests, passed to curl via =--proxy=. |
| =gptel-curl-extra-args= | Extra arguments passed to Curl. |
| =gptel-api-key= | Variable/function that returns the API key for the active backend. |
|-------------------------+--------------------------------------------------------------------|

|-----------------------+---------------------------------------------------------|
| *LLM request options* | /(Note: not supported uniformly across LLMs)/ |
Expand Down Expand Up @@ -1677,10 +1774,17 @@ There are several more: [[https://github.com/iwahbe/chat.el][chat.el]], [[https:
gptel is a general-purpose package for chat and ad-hoc LLM interaction. The following packages use gptel to provide additional or specialized functionality:

- [[https://github.com/karthink/gptel-quick][gptel-quick]]: Quickly look up the region or text at point.
- [[https://github.com/jwiegley/gptel-prompts][gptel-prompts]]: System prompt manager for gptel.
- [[https://github.com/dolmens/gptel-aibo/][gptel-aibo]]: A writing assistant system built on top of gptel.
- [[https://github.com/kmontag/macher][Macher]]: Project-aware multi-file LLM editing for Emacs, based on gptel.
- [[https://github.com/daedsidog/evedel][Evedel]]: Instructed LLM Programmer/Assistant.
- [[https://github.com/lanceberge/elysium][Elysium]]: Automatically apply AI-generated changes as you code.
- [[https://github.com/jwiegley/ob-gptel][ob-gptel]]: Org-babel backend for running gptel queries.
- [[https://github.com/JDNdeveloper/gptel-autocomplete][gptel-autocomplete]]: Inline completions using gptel.
- [[https://github.com/kamushadenes/ai-blog.el][ai-blog.el]]: Streamline generation of blog posts in Hugo.
- [[https://github.com/douo/magit-gptcommit][magit-gptcommit]]: Generate Commit Messages within magit-status Buffer using gptel.
- [[https://github.com/lakkiy/gptel-commit][gptel-commit]]: Generate commit messages using gptel.
- [[https://github.com/douo/magit-gptcommit][magit-gptcommit]]: Generate commit messages within magit-status Buffer using gptel.
- [[https://github.com/ragnard/gptel-magit/][gptel-magit]]: Generate commit messages for magit using gptel.
- [[https://github.com/armindarvish/consult-omni][consult-omni]]: Versatile multi-source search package. It includes gptel as one of its many sources.
- [[https://github.com/ultronozm/ai-org-chat.el][ai-org-chat]]: Provides branching conversations in Org buffers using gptel. (Note that gptel includes this feature as well (see =gptel-org-branching-context=), but requires a recent version of Org mode 9.7 or later to be installed.)
- [[https://github.com/rob137/Corsair][Corsair]]: Helps gather text to populate LLM prompts for gptel.
Expand Down
16 changes: 16 additions & 0 deletions gptel-anthropic.el
Original file line number Diff line number Diff line change
Expand Up @@ -230,13 +230,29 @@ Mutate state INFO with response metadata."
(gptel--model-capable-p 'cache))
(nconc (aref tools-array (1- (length tools-array)))
'(:cache_control (:type "ephemeral")))))))
(when gptel--schema
(plist-put prompts-plist :tools
(vconcat
(list (gptel--parse-schema backend gptel--schema))
(plist-get prompts-plist :tools)))
(plist-put prompts-plist :tool_choice
`(:type "tool" :name ,gptel--ersatz-json-tool)))
;; Merge request params with model and backend params.
(gptel--merge-plists
prompts-plist
gptel--request-params
(gptel-backend-request-params gptel-backend)
(gptel--model-request-params gptel-model))))

(cl-defmethod gptel--parse-schema ((_backend gptel-anthropic) schema)
;; Unlike the other backends, Anthropic generates JSON using a tool call. We
;; write the tool here, meant to be added to :tools.
(list
:name "response_json"
:description "Record JSON output according to user prompt"
:input_schema (gptel--preprocess-schema
(gptel--dispatch-schema-type schema))))

(cl-defmethod gptel--parse-tools ((_backend gptel-anthropic) tools)
"Parse TOOLS to the Anthropic API tool definition spec.

Expand Down
17 changes: 9 additions & 8 deletions gptel-curl.el
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,6 @@ REQUEST-DATA is the data to send, TOKEN is a unique identifier."
collect (format "-H%s: %s" key val))
(list url))))

;;TODO: The :transformer argument here is an alternate implementation of
;;`gptel-response-filter-functions'. The two need to be unified.
;;;###autoload
(defun gptel-curl-get-response (fsm)
"Fetch response to prompt in state FSM from the LLM using Curl.
Expand Down Expand Up @@ -283,6 +281,9 @@ Optional RAW disables text properties and transformation."
(set-marker-insertion-type tracking-marker t)
(plist-put info :tracking-marker tracking-marker))
(goto-char tracking-marker)
(when (plist-get info :last-was-tool-result)
(insert gptel-response-separator)
Copy link
Owner

@karthink karthink May 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's another edge case.

When you test Claude it usually responds with text ("I'll run this tool for you"), then a tool call, then more text ("The results of the tool are X"). This fix works fine for those cases.

Suppose the LLM responds instead with a tool call followed by a response, as Gemini and OpenAI models do. (No preamble before the tool call.)

Then you get a response that looks like this (with response-prefix Response: )

User: What is the time in Greece?

Response:

The time in Greece is ...

instead of

User: What is the time in Greece?

Response: The time in Greece is...

The extra gptel-response-separator between Response: and the result is from your :last-was-tool-call tracking.

(plist-put info :last-was-tool-result nil))
(unless raw
(when transformer
(setq response (funcall transformer response)))
Expand All @@ -297,7 +298,8 @@ Optional RAW disables text properties and transformation."
(`(tool-call . ,tool-calls)
(gptel--display-tool-calls tool-calls info))
(`(tool-result . ,tool-results)
(gptel--display-tool-results tool-results info))))
(gptel--display-tool-results tool-results info)
(plist-put info :last-was-tool-result t))))

(defun gptel-curl--stream-filter (process output)
(let* ((fsm (car (alist-get process gptel--request-alist)))
Expand Down Expand Up @@ -366,18 +368,17 @@ Optional RAW disables text properties and transformation."
(progn (setq response (cons 'reasoning response))
(plist-put proc-info :reasoning-block 'in))
(plist-put proc-info :reasoning-block 'done)))
((length> response 0)
((and (not (eq reasoning-block t)) (length> response 0))
(if-let* ((idx (string-match-p "</think>" response)))
(progn
(funcall callback
(cons 'reasoning ;last reasoning chunk
(string-trim-left
(substring response nil (+ idx 8))))
proc-info)
;; Signal end of reasoning stream
(funcall callback '(reasoning . t) proc-info)
(setq response (substring response (+ idx 8)))
(plist-put proc-info :reasoning-block 'done))
(setq reasoning-block t) ;Signal end of reasoning stream
(plist-put proc-info :reasoning-block t)
(setq response (substring response (+ idx 8))))
(setq response (cons 'reasoning response)))))
(when (eq reasoning-block t) ;End of reasoning block
(funcall callback '(reasoning . t) proc-info)
Expand Down
Loading