gptel: Add multi-llm support
README.org: Update README with new information and a multi-llm demo. gptel.el (gptel-host, gptel--known-backends, gptel--api-key, gptel--create-prompt, gptel--request-data, gptel--parse-buffer, gptel-request, gptel--parse-response, gptel--openai, gptel--debug, gptel--restore-state, gptel, gptel-backend): Integrate multiple LLMs through the introcution of gptel-backends. Each backend is composed of two pieces: 1. An instance of a cl-struct, containing connection, authentication and model information. See the cl-struct `gptel-backend` for details. A separate cl-struct type is defined for each supported backend (OpenAI, Azure, GPT4All and Ollama) that inherits from the generic gptel-backend type. 2. cl-generic implementations of specific tasks, like gathering up and formatting context (previous user queries and LLM responses), parsing responses or responses streams etc. The four tasks currently specialized this way are carried out by `gptel--parse-buffer` and `gptel--request-data` (for constructing the query) and `gptel--parse-response` and `gptel-curl--parse-stream` (for parsing the response). See their implementations for details. Some effort has been made to limit the number of times dispatching is done when reading streaming responses. When a backend is created, it is registered in the collection `gptel--known-backends` and can be accessed by name later, such as from the transient menu. Only one of these backends is active at any time in a buffer, stored in the buffer-local variable `gptel-backend`. Most messaging, authentication etc accounts for the active backend, although there might be some leftovers. When using `gptel-request` or `gptel-send`, the active backend can be changed or let-bound. - Obsolete `gptel-host` - Fix the rear-sticky property when restoring sessions from files. - Document some variables (not user options), like `gptel--debug` gptel-openai.el (gptel-backend, gptel-make-openai, gptel-make-azure, gptel-make-gpt4all): This file (currently always loaded) sets up the generic backend struct and includes constructors for creating OpenAI, GPT4All and Azure backends. They all use the same API so a single set of defgeneric implemenations suffices for all of them. gptel-ollama.el (gptel-make-ollama): This file includes the cl-struct, constructor and requisite defgeneric implementations for Ollama support. gptel-transient.el (gptel-menu, gptel-provider-variable, gptel--infix-provider, gptel-suffix-send): - Provide access to all available LLM backends and models from `gptel-menu`. - Adjust keybindings in gptel-menu: setting the model and query parameters is now bound to two char keybinds, while redirecting input and output is bound to single keys.
This commit is contained in:
parent
61c0df5e19
commit
6419e8f021
6 changed files with 717 additions and 165 deletions
137
README.org
137
README.org
|
@ -1,20 +1,34 @@
|
||||||
#+title: GPTel: A simple ChatGPT client for Emacs
|
#+title: GPTel: A simple LLM client for Emacs
|
||||||
|
|
||||||
[[https://melpa.org/#/gptel][file:https://melpa.org/packages/gptel-badge.svg]]
|
[[https://melpa.org/#/gptel][file:https://melpa.org/packages/gptel-badge.svg]]
|
||||||
|
|
||||||
GPTel is a simple, no-frills ChatGPT client for Emacs.
|
GPTel is a simple Large Language Model chat client for Emacs, with support for multiple models/backends.
|
||||||
|
|
||||||
|
| LLM Backend | Supports | Requires |
|
||||||
|
|-------------+----------+------------------------|
|
||||||
|
| ChatGPT | ✓ | [[https://platform.openai.com/account/api-keys][API key]] |
|
||||||
|
| Azure | ✓ | Deployment and API key |
|
||||||
|
| Ollama | ✓ | An LLM running locally |
|
||||||
|
| GPT4All | ✓ | An LLM running locally |
|
||||||
|
| PrivateGPT | Planned | - |
|
||||||
|
| Llama.cpp | Planned | - |
|
||||||
|
|
||||||
|
*General usage*:
|
||||||
|
|
||||||
https://user-images.githubusercontent.com/8607532/230516812-86510a09-a2fb-4cbd-b53f-cc2522d05a13.mp4
|
https://user-images.githubusercontent.com/8607532/230516812-86510a09-a2fb-4cbd-b53f-cc2522d05a13.mp4
|
||||||
|
|
||||||
https://user-images.githubusercontent.com/8607532/230516816-ae4a613a-4d01-4073-ad3f-b66fa73c6e45.mp4
|
https://user-images.githubusercontent.com/8607532/230516816-ae4a613a-4d01-4073-ad3f-b66fa73c6e45.mp4
|
||||||
|
|
||||||
- Requires an [[https://platform.openai.com/account/api-keys][OpenAI API key]].
|
*Multi-LLM support demo*:
|
||||||
|
|
||||||
|
https://github-production-user-asset-6210df.s3.amazonaws.com/8607532/278854024-ae1336c4-5b87-41f2-83e9-e415349d6a43.mp4
|
||||||
|
|
||||||
- It's async and fast, streams responses.
|
- It's async and fast, streams responses.
|
||||||
- Interact with ChatGPT from anywhere in Emacs (any buffer, shell, minibuffer, wherever)
|
- Interact with LLMs from anywhere in Emacs (any buffer, shell, minibuffer, wherever)
|
||||||
- ChatGPT's responses are in Markdown or Org markup.
|
- LLM responses are in Markdown or Org markup.
|
||||||
- Supports conversations and multiple independent sessions.
|
- Supports conversations and multiple independent sessions.
|
||||||
- Save chats as regular Markdown/Org/Text files and resume them later.
|
- Save chats as regular Markdown/Org/Text files and resume them later.
|
||||||
- You can go back and edit your previous prompts, or even ChatGPT's previous responses when continuing a conversation. These will be fed back to ChatGPT.
|
- You can go back and edit your previous prompts or LLM responses when continuing a conversation. These will be fed back to the model.
|
||||||
|
|
||||||
GPTel uses Curl if available, but falls back to url-retrieve to work without external dependencies.
|
GPTel uses Curl if available, but falls back to url-retrieve to work without external dependencies.
|
||||||
|
|
||||||
|
@ -25,13 +39,20 @@ GPTel uses Curl if available, but falls back to url-retrieve to work without ext
|
||||||
- [[#manual][Manual]]
|
- [[#manual][Manual]]
|
||||||
- [[#doom-emacs][Doom Emacs]]
|
- [[#doom-emacs][Doom Emacs]]
|
||||||
- [[#spacemacs][Spacemacs]]
|
- [[#spacemacs][Spacemacs]]
|
||||||
|
- [[#setup][Setup]]
|
||||||
|
- [[#chatgpt][ChatGPT]]
|
||||||
|
- [[#other-llm-backends][Other LLM backends]]
|
||||||
|
- [[#azure][Azure]]
|
||||||
|
- [[#gpt4all][GPT4All]]
|
||||||
|
- [[#ollama][Ollama]]
|
||||||
- [[#usage][Usage]]
|
- [[#usage][Usage]]
|
||||||
- [[#in-any-buffer][In any buffer:]]
|
- [[#in-any-buffer][In any buffer:]]
|
||||||
- [[#in-a-dedicated-chat-buffer][In a dedicated chat buffer:]]
|
- [[#in-a-dedicated-chat-buffer][In a dedicated chat buffer:]]
|
||||||
|
- [[#save-and-restore-your-chat-sessions][Save and restore your chat sessions]]
|
||||||
- [[#using-it-your-way][Using it your way]]
|
- [[#using-it-your-way][Using it your way]]
|
||||||
- [[#extensions-using-gptel][Extensions using GPTel]]
|
- [[#extensions-using-gptel][Extensions using GPTel]]
|
||||||
- [[#additional-configuration][Additional Configuration]]
|
- [[#additional-configuration][Additional Configuration]]
|
||||||
- [[#why-another-chatgpt-client][Why another ChatGPT client?]]
|
- [[#why-another-llm-client][Why another LLM client?]]
|
||||||
- [[#will-you-add-feature-x][Will you add feature X?]]
|
- [[#will-you-add-feature-x][Will you add feature X?]]
|
||||||
- [[#alternatives][Alternatives]]
|
- [[#alternatives][Alternatives]]
|
||||||
- [[#acknowledgments][Acknowledgments]]
|
- [[#acknowledgments][Acknowledgments]]
|
||||||
|
@ -41,7 +62,7 @@ GPTel uses Curl if available, but falls back to url-retrieve to work without ext
|
||||||
|
|
||||||
** Installation
|
** Installation
|
||||||
|
|
||||||
GPTel is on MELPA. Install it with =M-x package-install⏎= =gptel=.
|
GPTel is on MELPA. Ensure that MELPA is in your list of sources, then install gptel with =M-x package-install⏎= =gptel=.
|
||||||
|
|
||||||
(Optional: Install =markdown-mode=.)
|
(Optional: Install =markdown-mode=.)
|
||||||
|
|
||||||
|
@ -84,9 +105,8 @@ After installation with =M-x package-install⏎= =gptel=
|
||||||
- Add =gptel= to =dotspacemacs-additional-packages=
|
- Add =gptel= to =dotspacemacs-additional-packages=
|
||||||
- Add =(require 'gptel)= to =dotspacemacs/user-config=
|
- Add =(require 'gptel)= to =dotspacemacs/user-config=
|
||||||
#+html: </details>
|
#+html: </details>
|
||||||
|
** Setup
|
||||||
** Usage
|
*** ChatGPT
|
||||||
|
|
||||||
Procure an [[https://platform.openai.com/account/api-keys][OpenAI API key]].
|
Procure an [[https://platform.openai.com/account/api-keys][OpenAI API key]].
|
||||||
|
|
||||||
Optional: Set =gptel-api-key= to the key. Alternatively, you may choose a more secure method such as:
|
Optional: Set =gptel-api-key= to the key. Alternatively, you may choose a more secure method such as:
|
||||||
|
@ -97,6 +117,72 @@ machine api.openai.com login apikey password TOKEN
|
||||||
#+end_src
|
#+end_src
|
||||||
- Setting it to a function that returns the key.
|
- Setting it to a function that returns the key.
|
||||||
|
|
||||||
|
*** Other LLM backends
|
||||||
|
#+html: <details><summary>
|
||||||
|
**** Azure
|
||||||
|
#+html: </summary>
|
||||||
|
|
||||||
|
Register a backend with
|
||||||
|
#+begin_src emacs-lisp
|
||||||
|
(gptel-make-azure
|
||||||
|
"Azure-1" ;Name, whatever you'd like
|
||||||
|
:protocol "https" ;optional -- https is the default
|
||||||
|
:host "YOUR_RESOURCE_NAME.openai.azure.com"
|
||||||
|
:endpoint "/openai/deployments/YOUR_DEPLOYMENT_NAME/completions?api-version=2023-05-15" ;or equivalent
|
||||||
|
:stream t ;Enable streaming responses
|
||||||
|
:models '("gpt-3.5-turbo" "gpt-4"))
|
||||||
|
#+end_src
|
||||||
|
Refer to the documentation of =gptel-make-azure= to set more parameters.
|
||||||
|
|
||||||
|
You can pick this backend from the transient menu when using gptel. (See usage)
|
||||||
|
|
||||||
|
If you want it to be the default, set it as the default value of =gptel-backend=:
|
||||||
|
#+begin_src emacs-lisp
|
||||||
|
(setq-default gptel-backend
|
||||||
|
(gptel-make-azure
|
||||||
|
"Azure-1"
|
||||||
|
...))
|
||||||
|
#+end_src
|
||||||
|
#+html: </details>
|
||||||
|
|
||||||
|
#+html: <details><summary>
|
||||||
|
**** GPT4All
|
||||||
|
#+html: </summary>
|
||||||
|
|
||||||
|
Register a backend with
|
||||||
|
#+begin_src emacs-lisp
|
||||||
|
(gptel-make-gpt4all
|
||||||
|
"GPT4All" ;Name of your choosing
|
||||||
|
:protocol "http"
|
||||||
|
:host "localhost:4891" ;Where it's running
|
||||||
|
:models '("mistral-7b-openorca.Q4_0.gguf")) ;Available models
|
||||||
|
#+end_src
|
||||||
|
These are the required parameters, refer to the documentation of =gptel-make-gpt4all= for more.
|
||||||
|
|
||||||
|
You can pick this backend from the transient menu when using gptel (see usage), or set this as the default value of =gptel-backend=.
|
||||||
|
|
||||||
|
#+html: </details>
|
||||||
|
|
||||||
|
#+html: <details><summary>
|
||||||
|
**** Ollama
|
||||||
|
#+html: </summary>
|
||||||
|
|
||||||
|
Register a backend with
|
||||||
|
#+begin_src emacs-lisp
|
||||||
|
(defvar gptel--ollama
|
||||||
|
(gptel-make-ollama
|
||||||
|
"Ollama" ;Any name of your choosing
|
||||||
|
:host "localhost:11434" ;Where it's running
|
||||||
|
:models '("mistral:latest") ;Installed models
|
||||||
|
:stream t)) ;Stream responses
|
||||||
|
#+end_src
|
||||||
|
These are the required parameters, refer to the documentation of =gptel-make-gpt4all= for more.
|
||||||
|
|
||||||
|
You can pick this backend from the transient menu when using gptel (see usage), or set this as the default value of =gptel-backend=.
|
||||||
|
|
||||||
|
#+html: </details>
|
||||||
|
|
||||||
|
** Usage
|
||||||
*** In any buffer:
|
*** In any buffer:
|
||||||
|
|
||||||
1. Select a region of text and call =M-x gptel-send=. The response will be inserted below your region.
|
1. Select a region of text and call =M-x gptel-send=. The response will be inserted below your region.
|
||||||
|
@ -122,11 +208,11 @@ With a region selected, you can also rewrite prose or refactor code from here:
|
||||||
|
|
||||||
*** In a dedicated chat buffer:
|
*** In a dedicated chat buffer:
|
||||||
|
|
||||||
1. Run =M-x gptel= to start or switch to the ChatGPT buffer. It will ask you for the key if you skipped the previous step. Run it with a prefix-arg (=C-u M-x gptel=) to start a new session.
|
1. Run =M-x gptel= to start or switch to the chat buffer. It will ask you for the key if you skipped the previous step. Run it with a prefix-arg (=C-u M-x gptel=) to start a new session.
|
||||||
|
|
||||||
2. In the gptel buffer, send your prompt with =M-x gptel-send=, bound to =C-c RET=.
|
2. In the gptel buffer, send your prompt with =M-x gptel-send=, bound to =C-c RET=.
|
||||||
|
|
||||||
3. Set chat parameters (GPT model, directives etc) for the session by calling =gptel-send= with a prefix argument (=C-u C-c RET=):
|
3. Set chat parameters (LLM provider, model, directives etc) for the session by calling =gptel-send= with a prefix argument (=C-u C-c RET=):
|
||||||
|
|
||||||
[[https://user-images.githubusercontent.com/8607532/224946059-9b918810-ab8b-46a6-b917-549d50c908f2.png]]
|
[[https://user-images.githubusercontent.com/8607532/224946059-9b918810-ab8b-46a6-b917-549d50c908f2.png]]
|
||||||
|
|
||||||
|
@ -157,17 +243,29 @@ These are packages that depend on GPTel to provide additional functionality
|
||||||
- [[https://github.com/kamushadenes/ai-blog.el][ai-blog.el]]: Streamline generation of blog posts in Hugo.
|
- [[https://github.com/kamushadenes/ai-blog.el][ai-blog.el]]: Streamline generation of blog posts in Hugo.
|
||||||
|
|
||||||
** Additional Configuration
|
** Additional Configuration
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: f885adac-58a3-4eba-a6b7-91e9e7a17829
|
||||||
|
:END:
|
||||||
|
|
||||||
- =gptel-host=: Overrides the OpenAI API host. This is useful for those who transform Azure API into OpenAI API format, utilize reverse proxy, or employ third-party proxy services for the OpenAI API.
|
#+begin_src emacs-lisp :exports none
|
||||||
|
(let ((all))
|
||||||
|
(mapatoms (lambda (sym)
|
||||||
|
(when (and (string-match-p "^gptel-[^-]" (symbol-name sym))
|
||||||
|
(get sym 'variable-documentation))
|
||||||
|
(push sym all))))
|
||||||
|
all)
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
- =gptel-stream=: Stream responses (if the model supports streaming). Defaults to true.
|
||||||
|
|
||||||
- =gptel-proxy=: Path to a proxy to use for GPTel interactions. This is passed to Curl via the =--proxy= argument.
|
- =gptel-proxy=: Path to a proxy to use for GPTel interactions. This is passed to Curl via the =--proxy= argument.
|
||||||
|
|
||||||
** Why another ChatGPT client?
|
** Why another LLM client?
|
||||||
|
|
||||||
Other Emacs clients for ChatGPT prescribe the format of the interaction (a comint shell, org-babel blocks, etc). I wanted:
|
Other Emacs clients for LLMs prescribe the format of the interaction (a comint shell, org-babel blocks, etc). I wanted:
|
||||||
|
|
||||||
1. Something that is as free-form as possible: query ChatGPT using any text in any buffer, and redirect the response as required. Using a dedicated =gptel= buffer just adds some visual flair to the interaction.
|
1. Something that is as free-form as possible: query the model using any text in any buffer, and redirect the response as required. Using a dedicated =gptel= buffer just adds some visual flair to the interaction.
|
||||||
2. Integration with org-mode, not using a walled-off org-babel block, but as regular text. This way ChatGPT can generate code blocks that I can run.
|
2. Integration with org-mode, not using a walled-off org-babel block, but as regular text. This way the model can generate code blocks that I can run.
|
||||||
|
|
||||||
** Will you add feature X?
|
** Will you add feature X?
|
||||||
|
|
||||||
|
@ -183,13 +281,14 @@ Maybe, I'd like to experiment a bit more first. Features added since the incept
|
||||||
- A built-in refactor/rewrite prompt
|
- A built-in refactor/rewrite prompt
|
||||||
- Limiting conversation context to Org headings using properties (#58)
|
- Limiting conversation context to Org headings using properties (#58)
|
||||||
- Saving and restoring chats (#17)
|
- Saving and restoring chats (#17)
|
||||||
|
- Support for local LLMs.
|
||||||
|
|
||||||
Features being considered or in the pipeline:
|
Features being considered or in the pipeline:
|
||||||
- Fully stateless design (#17)
|
- Fully stateless design (#17)
|
||||||
|
|
||||||
** Alternatives
|
** Alternatives
|
||||||
|
|
||||||
Other Emacs clients for ChatGPT include
|
Other Emacs clients for LLMs include
|
||||||
|
|
||||||
- [[https://github.com/xenodium/chatgpt-shell][chatgpt-shell]]: comint-shell based interaction with ChatGPT. Also supports DALL-E, executable code blocks in the responses, and more.
|
- [[https://github.com/xenodium/chatgpt-shell][chatgpt-shell]]: comint-shell based interaction with ChatGPT. Also supports DALL-E, executable code blocks in the responses, and more.
|
||||||
- [[https://github.com/rksm/org-ai][org-ai]]: Interaction through special =#+begin_ai ... #+end_ai= Org-mode blocks. Also supports DALL-E, querying ChatGPT with the contents of project files, and more.
|
- [[https://github.com/rksm/org-ai][org-ai]]: Interaction through special =#+begin_ai ... #+end_ai= Org-mode blocks. Also supports DALL-E, querying ChatGPT with the contents of project files, and more.
|
||||||
|
|
143
gptel-curl.el
143
gptel-curl.el
|
@ -41,14 +41,16 @@
|
||||||
"Produce list of arguments for calling Curl.
|
"Produce list of arguments for calling Curl.
|
||||||
|
|
||||||
PROMPTS is the data to send, TOKEN is a unique identifier."
|
PROMPTS is the data to send, TOKEN is a unique identifier."
|
||||||
(let* ((url (format "%s://%s/v1/chat/completions"
|
(let* ((url (gptel-backend-url gptel-backend))
|
||||||
gptel-protocol gptel-host))
|
|
||||||
(data (encode-coding-string
|
(data (encode-coding-string
|
||||||
(json-encode (gptel--request-data prompts))
|
(json-encode (gptel--request-data gptel-backend prompts))
|
||||||
'utf-8))
|
'utf-8))
|
||||||
(headers
|
(headers
|
||||||
`(("Content-Type" . "application/json")
|
(append '(("Content-Type" . "application/json"))
|
||||||
("Authorization" . ,(concat "Bearer " (gptel--api-key))))))
|
(when-let ((backend-header (gptel-backend-header gptel-backend)))
|
||||||
|
(if (functionp backend-header)
|
||||||
|
(funcall backend-header)
|
||||||
|
backend-header)))))
|
||||||
(append
|
(append
|
||||||
(list "--location" "--silent" "--compressed" "--disable"
|
(list "--location" "--silent" "--compressed" "--disable"
|
||||||
(format "-X%s" "POST")
|
(format "-X%s" "POST")
|
||||||
|
@ -81,14 +83,33 @@ the response is inserted into the current buffer after point."
|
||||||
(random) (emacs-pid) (user-full-name)
|
(random) (emacs-pid) (user-full-name)
|
||||||
(recent-keys))))
|
(recent-keys))))
|
||||||
(args (gptel-curl--get-args (plist-get info :prompt) token))
|
(args (gptel-curl--get-args (plist-get info :prompt) token))
|
||||||
|
(stream (and gptel-stream (gptel-backend-stream gptel-backend)))
|
||||||
(process (apply #'start-process "gptel-curl"
|
(process (apply #'start-process "gptel-curl"
|
||||||
(generate-new-buffer "*gptel-curl*") "curl" args)))
|
(generate-new-buffer "*gptel-curl*") "curl" args)))
|
||||||
|
(when gptel--debug
|
||||||
|
(message "%S" args))
|
||||||
(with-current-buffer (process-buffer process)
|
(with-current-buffer (process-buffer process)
|
||||||
(set-process-query-on-exit-flag process nil)
|
(set-process-query-on-exit-flag process nil)
|
||||||
(setf (alist-get process gptel-curl--process-alist)
|
(setf (alist-get process gptel-curl--process-alist)
|
||||||
(nconc (list :token token
|
(nconc (list :token token
|
||||||
|
;; FIXME `aref' breaks `cl-struct' abstraction boundary
|
||||||
|
;; FIXME `cl--generic-method' is an internal `cl-struct'
|
||||||
|
:parser (cl--generic-method-function
|
||||||
|
(if stream
|
||||||
|
(cl-find-method
|
||||||
|
'gptel-curl--parse-stream nil
|
||||||
|
(list
|
||||||
|
(aref (buffer-local-value
|
||||||
|
'gptel-backend (plist-get info :buffer))
|
||||||
|
0) t))
|
||||||
|
(cl-find-method
|
||||||
|
'gptel--parse-response nil
|
||||||
|
(list
|
||||||
|
(aref (buffer-local-value
|
||||||
|
'gptel-backend (plist-get info :buffer))
|
||||||
|
0) t t))))
|
||||||
:callback (or callback
|
:callback (or callback
|
||||||
(if gptel-stream
|
(if stream
|
||||||
#'gptel-curl--stream-insert-response
|
#'gptel-curl--stream-insert-response
|
||||||
#'gptel--insert-response))
|
#'gptel--insert-response))
|
||||||
:transformer (when (eq (buffer-local-value
|
:transformer (when (eq (buffer-local-value
|
||||||
|
@ -97,7 +118,7 @@ the response is inserted into the current buffer after point."
|
||||||
'org-mode)
|
'org-mode)
|
||||||
(gptel--stream-convert-markdown->org)))
|
(gptel--stream-convert-markdown->org)))
|
||||||
info))
|
info))
|
||||||
(if gptel-stream
|
(if stream
|
||||||
(progn (set-process-sentinel process #'gptel-curl--stream-cleanup)
|
(progn (set-process-sentinel process #'gptel-curl--stream-cleanup)
|
||||||
(set-process-filter process #'gptel-curl--stream-filter))
|
(set-process-filter process #'gptel-curl--stream-filter))
|
||||||
(set-process-sentinel process #'gptel-curl--sentinel)))))
|
(set-process-sentinel process #'gptel-curl--sentinel)))))
|
||||||
|
@ -252,22 +273,21 @@ See `gptel--url-get-response' for details."
|
||||||
(when (equal http-status "200")
|
(when (equal http-status "200")
|
||||||
(funcall (or (plist-get proc-info :callback)
|
(funcall (or (plist-get proc-info :callback)
|
||||||
#'gptel-curl--stream-insert-response)
|
#'gptel-curl--stream-insert-response)
|
||||||
(let* ((json-object-type 'plist)
|
(funcall (plist-get proc-info :parser) nil proc-info)
|
||||||
(content-strs))
|
|
||||||
(condition-case nil
|
|
||||||
(while (re-search-forward "^data:" nil t)
|
|
||||||
(save-match-data
|
|
||||||
(unless (looking-at " *\\[DONE\\]")
|
|
||||||
(when-let* ((response (json-read))
|
|
||||||
(delta (map-nested-elt
|
|
||||||
response '(:choices 0 :delta)))
|
|
||||||
(content (plist-get delta :content)))
|
|
||||||
(push content content-strs)))))
|
|
||||||
(error
|
|
||||||
(goto-char (match-beginning 0))))
|
|
||||||
(apply #'concat (nreverse content-strs)))
|
|
||||||
proc-info))))))
|
proc-info))))))
|
||||||
|
|
||||||
|
(cl-defgeneric gptel-curl--parse-stream (backend proc-info)
|
||||||
|
"Stream parser for gptel-curl.
|
||||||
|
|
||||||
|
Implementations of this function run as part of the process
|
||||||
|
filter for the active query, and return partial responses from
|
||||||
|
the LLM.
|
||||||
|
|
||||||
|
BACKEND is the LLM backend in use.
|
||||||
|
|
||||||
|
PROC-INFO is a plist with process information and other context.
|
||||||
|
See `gptel-curl--get-response' for its contents.")
|
||||||
|
|
||||||
(defun gptel-curl--sentinel (process _status)
|
(defun gptel-curl--sentinel (process _status)
|
||||||
"Process sentinel for GPTel curl requests.
|
"Process sentinel for GPTel curl requests.
|
||||||
|
|
||||||
|
@ -278,61 +298,58 @@ PROCESS and _STATUS are process parameters."
|
||||||
(clone-buffer "*gptel-error*" 'show)))
|
(clone-buffer "*gptel-error*" 'show)))
|
||||||
(when-let* (((eq (process-status process) 'exit))
|
(when-let* (((eq (process-status process) 'exit))
|
||||||
(proc-info (alist-get process gptel-curl--process-alist))
|
(proc-info (alist-get process gptel-curl--process-alist))
|
||||||
(proc-token (plist-get proc-info :token))
|
|
||||||
(proc-callback (plist-get proc-info :callback)))
|
(proc-callback (plist-get proc-info :callback)))
|
||||||
(pcase-let ((`(,response ,http-msg ,error)
|
(pcase-let ((`(,response ,http-msg ,error)
|
||||||
(gptel-curl--parse-response proc-buf proc-token)))
|
(with-current-buffer proc-buf
|
||||||
|
(gptel-curl--parse-response proc-info))))
|
||||||
(plist-put proc-info :status http-msg)
|
(plist-put proc-info :status http-msg)
|
||||||
(when error (plist-put proc-info :error error))
|
(when error (plist-put proc-info :error error))
|
||||||
(funcall proc-callback response proc-info)))
|
(funcall proc-callback response proc-info)))
|
||||||
(setf (alist-get process gptel-curl--process-alist nil 'remove) nil)
|
(setf (alist-get process gptel-curl--process-alist nil 'remove) nil)
|
||||||
(kill-buffer proc-buf)))
|
(kill-buffer proc-buf)))
|
||||||
|
|
||||||
(defun gptel-curl--parse-response (buf token)
|
(defun gptel-curl--parse-response (proc-info)
|
||||||
"Parse the buffer BUF with curl's response.
|
"Parse the buffer BUF with curl's response.
|
||||||
|
|
||||||
TOKEN is used to disambiguate multiple requests in a single
|
TOKEN is used to disambiguate multiple requests in a single
|
||||||
buffer."
|
buffer."
|
||||||
(with-current-buffer buf
|
(let ((token (plist-get proc-info :token))
|
||||||
(progn
|
(parser (plist-get proc-info :parser)))
|
||||||
(goto-char (point-max))
|
(goto-char (point-max))
|
||||||
(search-backward token)
|
(search-backward token)
|
||||||
(backward-char)
|
(backward-char)
|
||||||
(pcase-let* ((`(,_ . ,header-size) (read (current-buffer))))
|
(pcase-let* ((`(,_ . ,header-size) (read (current-buffer))))
|
||||||
;; (if (search-backward token nil t)
|
(goto-char (point-min))
|
||||||
;; (search-forward ")" nil t)
|
|
||||||
;; (goto-char (point-min)))
|
|
||||||
(goto-char (point-min))
|
|
||||||
|
|
||||||
(if-let* ((http-msg (string-trim
|
(if-let* ((http-msg (string-trim
|
||||||
(buffer-substring (line-beginning-position)
|
(buffer-substring (line-beginning-position)
|
||||||
(line-end-position))))
|
(line-end-position))))
|
||||||
(http-status
|
(http-status
|
||||||
(save-match-data
|
(save-match-data
|
||||||
(and (string-match "HTTP/[.0-9]+ +\\([0-9]+\\)" http-msg)
|
(and (string-match "HTTP/[.0-9]+ +\\([0-9]+\\)" http-msg)
|
||||||
(match-string 1 http-msg))))
|
(match-string 1 http-msg))))
|
||||||
(json-object-type 'plist)
|
(json-object-type 'plist)
|
||||||
(response (progn (goto-char header-size)
|
(response (progn (goto-char header-size)
|
||||||
(condition-case nil
|
(condition-case nil
|
||||||
(json-read)
|
(json-read)
|
||||||
(json-readtable-error 'json-read-error)))))
|
(json-readtable-error 'json-read-error)))))
|
||||||
(cond
|
(cond
|
||||||
((equal http-status "200")
|
((equal http-status "200")
|
||||||
(list (string-trim
|
(list (string-trim
|
||||||
(map-nested-elt response '(:choices 0 :message :content)))
|
(funcall parser nil response proc-info))
|
||||||
http-msg))
|
http-msg))
|
||||||
((plist-get response :error)
|
((plist-get response :error)
|
||||||
(let* ((error-plist (plist-get response :error))
|
(let* ((error-plist (plist-get response :error))
|
||||||
(error-msg (plist-get error-plist :message))
|
(error-msg (plist-get error-plist :message))
|
||||||
(error-type (plist-get error-plist :type)))
|
(error-type (plist-get error-plist :type)))
|
||||||
(list nil (concat "(" http-msg ") " (string-trim error-type)) error-msg)))
|
(list nil (concat "(" http-msg ") " (string-trim error-type)) error-msg)))
|
||||||
((eq response 'json-read-error)
|
((eq response 'json-read-error)
|
||||||
(list nil (concat "(" http-msg ") Malformed JSON in response.")
|
(list nil (concat "(" http-msg ") Malformed JSON in response.")
|
||||||
"Malformed JSON in response"))
|
"Malformed JSON in response"))
|
||||||
(t (list nil (concat "(" http-msg ") Could not parse HTTP response.")
|
(t (list nil (concat "(" http-msg ") Could not parse HTTP response.")
|
||||||
"Could not parse HTTP response.")))
|
"Could not parse HTTP response.")))
|
||||||
(list nil (concat "(" http-msg ") Could not parse HTTP response.")
|
(list nil (concat "(" http-msg ") Could not parse HTTP response.")
|
||||||
"Could not parse HTTP response."))))))
|
"Could not parse HTTP response.")))))
|
||||||
|
|
||||||
(provide 'gptel-curl)
|
(provide 'gptel-curl)
|
||||||
;;; gptel-curl.el ends here
|
;;; gptel-curl.el ends here
|
||||||
|
|
143
gptel-ollama.el
Normal file
143
gptel-ollama.el
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
;;; gptel-ollama.el --- Ollama support for gptel -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;; Copyright (C) 2023 Karthik Chikmagalur
|
||||||
|
|
||||||
|
;; Author: Karthik Chikmagalur <karthikchikmagalur@gmail.com>
|
||||||
|
;; Keywords: hypermedia
|
||||||
|
|
||||||
|
;; This program is free software; you can redistribute it and/or modify
|
||||||
|
;; it under the terms of the GNU General Public License as published by
|
||||||
|
;; the Free Software Foundation, either version 3 of the License, or
|
||||||
|
;; (at your option) any later version.
|
||||||
|
|
||||||
|
;; This program is distributed in the hope that it will be useful,
|
||||||
|
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
;; GNU General Public License for more details.
|
||||||
|
|
||||||
|
;; You should have received a copy of the GNU General Public License
|
||||||
|
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
|
||||||
|
;; This file adds support for the Ollama LLM API to gptel
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
(require 'gptel)
|
||||||
|
(require 'cl-generic)
|
||||||
|
|
||||||
|
;;; Ollama
|
||||||
|
(cl-defstruct (gptel-ollama (:constructor gptel--make-ollama)
|
||||||
|
(:copier nil)
|
||||||
|
(:include gptel-backend)))
|
||||||
|
|
||||||
|
(cl-defmethod gptel-curl--parse-stream ((_backend gptel-ollama) info)
|
||||||
|
";TODO: "
|
||||||
|
(when (bobp)
|
||||||
|
(re-search-forward "^{")
|
||||||
|
(forward-line 0))
|
||||||
|
(let* ((json-object-type 'plist)
|
||||||
|
(content-strs)
|
||||||
|
(content))
|
||||||
|
(condition-case nil
|
||||||
|
(while (setq content (json-read))
|
||||||
|
(let ((done (map-elt content :done))
|
||||||
|
(response (map-elt content :response)))
|
||||||
|
(push response content-strs)
|
||||||
|
(unless (eq done json-false)
|
||||||
|
(with-current-buffer (plist-get info :buffer)
|
||||||
|
(setq gptel--ollama-context (map-elt content :context)))
|
||||||
|
(end-of-buffer))))
|
||||||
|
(error (forward-line 0)))
|
||||||
|
(apply #'concat (nreverse content-strs))))
|
||||||
|
|
||||||
|
(cl-defmethod gptel--parse-response ((_backend gptel-ollama) response info)
|
||||||
|
(when-let ((context (map-elt response :context)))
|
||||||
|
(with-current-buffer (plist-get info :buffer)
|
||||||
|
(setq gptel--ollama-context context)))
|
||||||
|
(map-elt response :response))
|
||||||
|
|
||||||
|
(cl-defmethod gptel--request-data ((_backend gptel-ollama) prompts)
|
||||||
|
"JSON encode PROMPTS for sending to ChatGPT."
|
||||||
|
(let ((prompts-plist
|
||||||
|
`(:model ,gptel-model
|
||||||
|
,@prompts
|
||||||
|
:stream ,(or (and gptel-stream gptel-use-curl
|
||||||
|
(gptel-backend-stream gptel-backend))
|
||||||
|
:json-false))))
|
||||||
|
(when gptel--ollama-context
|
||||||
|
(plist-put prompts-plist :context gptel--ollama-context))
|
||||||
|
prompts-plist))
|
||||||
|
|
||||||
|
(cl-defmethod gptel--parse-buffer ((_backend gptel-ollama) &optional _max-entries)
|
||||||
|
(let ((prompts) (prop))
|
||||||
|
(setq prop (text-property-search-backward
|
||||||
|
'gptel 'response
|
||||||
|
(when (get-char-property (max (point-min) (1- (point)))
|
||||||
|
'gptel)
|
||||||
|
t)))
|
||||||
|
(if (prop-match-value prop)
|
||||||
|
(user-error "No user prompt found!")
|
||||||
|
(setq prompts (list
|
||||||
|
:system gptel--system-message
|
||||||
|
:prompt
|
||||||
|
(string-trim (buffer-substring-no-properties (prop-match-beginning prop)
|
||||||
|
(prop-match-end prop))
|
||||||
|
"[*# \t\n\r]+"))))))
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(cl-defun gptel-make-ollama
|
||||||
|
(name &key host header key models stream
|
||||||
|
(protocol "http")
|
||||||
|
(endpoint "/api/generate"))
|
||||||
|
"Register an Ollama backend for gptel with NAME.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
|
||||||
|
HOST is where Ollama runs (with port), typically localhost:11434
|
||||||
|
|
||||||
|
MODELS is a list of available model names.
|
||||||
|
|
||||||
|
STREAM is a boolean to toggle streaming responses, defaults to
|
||||||
|
false.
|
||||||
|
|
||||||
|
PROTOCOL (optional) specifies the protocol, http by default.
|
||||||
|
|
||||||
|
ENDPOINT (optional) is the API endpoint for completions, defaults to
|
||||||
|
\"/api/generate\".
|
||||||
|
|
||||||
|
HEADER (optional) is for additional headers to send with each
|
||||||
|
request. It should be an alist or a function that retuns an
|
||||||
|
alist, like:
|
||||||
|
((\"Content-Type\" . \"application/json\"))
|
||||||
|
|
||||||
|
KEY (optional) is a variable whose value is the API key, or
|
||||||
|
function that returns the key. This is typically not required for
|
||||||
|
local models like Ollama."
|
||||||
|
(let ((backend (gptel--make-ollama
|
||||||
|
:name name
|
||||||
|
:host host
|
||||||
|
:header header
|
||||||
|
:key key
|
||||||
|
:models models
|
||||||
|
:protocol protocol
|
||||||
|
:endpoint endpoint
|
||||||
|
:stream stream
|
||||||
|
:url (if protocol
|
||||||
|
(concat protocol "://" host endpoint)
|
||||||
|
(concat host endpoint)))))
|
||||||
|
(prog1 backend
|
||||||
|
(setf (alist-get name gptel--known-backends
|
||||||
|
nil nil #'equal)
|
||||||
|
backend))))
|
||||||
|
|
||||||
|
(defvar-local gptel--ollama-context nil
|
||||||
|
"Context for ollama conversations.
|
||||||
|
|
||||||
|
This variable holds the context array for conversations with
|
||||||
|
Ollama models.")
|
||||||
|
|
||||||
|
(provide 'gptel-ollama)
|
||||||
|
;;; gptel-ollama.el ends here
|
||||||
|
|
||||||
|
|
216
gptel-openai.el
Normal file
216
gptel-openai.el
Normal file
|
@ -0,0 +1,216 @@
|
||||||
|
;;; gptel-openai.el --- ChatGPT suppport for gptel -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;; Copyright (C) 2023 Karthik Chikmagalur
|
||||||
|
|
||||||
|
;; Author: Karthik Chikmagalur <karthikchikmagalur@gmail.com>
|
||||||
|
;; Keywords:
|
||||||
|
|
||||||
|
;; This program is free software; you can redistribute it and/or modify
|
||||||
|
;; it under the terms of the GNU General Public License as published by
|
||||||
|
;; the Free Software Foundation, either version 3 of the License, or
|
||||||
|
;; (at your option) any later version.
|
||||||
|
|
||||||
|
;; This program is distributed in the hope that it will be useful,
|
||||||
|
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
;; GNU General Public License for more details.
|
||||||
|
|
||||||
|
;; You should have received a copy of the GNU General Public License
|
||||||
|
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
|
||||||
|
;; This file adds support for the ChatGPT API to gptel
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
(require 'cl-generic)
|
||||||
|
|
||||||
|
;;; Common backend struct for LLM support
|
||||||
|
(cl-defstruct
|
||||||
|
(gptel-backend (:constructor gptel--make-backend)
|
||||||
|
(:copier gptel--copy-backend))
|
||||||
|
name host header protocol stream
|
||||||
|
endpoint key models url)
|
||||||
|
|
||||||
|
;;; OpenAI (ChatGPT)
|
||||||
|
(cl-defstruct (gptel-openai (:constructor gptel--make-openai)
|
||||||
|
(:copier nil)
|
||||||
|
(:include gptel-backend)))
|
||||||
|
|
||||||
|
(cl-defmethod gptel-curl--parse-stream ((_backend gptel-openai) _info)
|
||||||
|
(let* ((json-object-type 'plist)
|
||||||
|
(content-strs))
|
||||||
|
(condition-case nil
|
||||||
|
(while (re-search-forward "^data:" nil t)
|
||||||
|
(save-match-data
|
||||||
|
(unless (looking-at " *\\[DONE\\]")
|
||||||
|
(when-let* ((response (json-read))
|
||||||
|
(delta (map-nested-elt
|
||||||
|
response '(:choices 0 :delta)))
|
||||||
|
(content (plist-get delta :content)))
|
||||||
|
(push content content-strs)))))
|
||||||
|
(error
|
||||||
|
(goto-char (match-beginning 0))))
|
||||||
|
(apply #'concat (nreverse content-strs))))
|
||||||
|
|
||||||
|
(cl-defmethod gptel--parse-response ((_backend gptel-openai) response _info)
|
||||||
|
(map-nested-elt response '(:choices 0 :message :content)))
|
||||||
|
|
||||||
|
(cl-defmethod gptel--request-data ((_backend gptel-openai) prompts)
|
||||||
|
"JSON encode PROMPTS for sending to ChatGPT."
|
||||||
|
(let ((prompts-plist
|
||||||
|
`(:model ,gptel-model
|
||||||
|
:messages [,@prompts]
|
||||||
|
:stream ,(or (and gptel-stream gptel-use-curl
|
||||||
|
(gptel-backend-stream gptel-backend))
|
||||||
|
:json-false))))
|
||||||
|
(when gptel-temperature
|
||||||
|
(plist-put prompts-plist :temperature gptel-temperature))
|
||||||
|
(when gptel-max-tokens
|
||||||
|
(plist-put prompts-plist :max_tokens gptel-max-tokens))
|
||||||
|
prompts-plist))
|
||||||
|
|
||||||
|
(cl-defmethod gptel--parse-buffer ((_backend gptel-openai) &optional max-entries)
|
||||||
|
(let ((prompts) (prop))
|
||||||
|
(while (and
|
||||||
|
(or (not max-entries) (>= max-entries 0))
|
||||||
|
(setq prop (text-property-search-backward
|
||||||
|
'gptel 'response
|
||||||
|
(when (get-char-property (max (point-min) (1- (point)))
|
||||||
|
'gptel)
|
||||||
|
t))))
|
||||||
|
(push (list :role (if (prop-match-value prop) "assistant" "user")
|
||||||
|
:content
|
||||||
|
(string-trim
|
||||||
|
(buffer-substring-no-properties (prop-match-beginning prop)
|
||||||
|
(prop-match-end prop))
|
||||||
|
"[*# \t\n\r]+"))
|
||||||
|
prompts)
|
||||||
|
(and max-entries (cl-decf max-entries)))
|
||||||
|
(cons (list :role "system"
|
||||||
|
:content gptel--system-message)
|
||||||
|
prompts)))
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(cl-defun gptel-make-openai
|
||||||
|
(name &key header key models stream
|
||||||
|
(host "api.openai.com")
|
||||||
|
(protocol "https")
|
||||||
|
(endpoint "/v1/chat/completions"))
|
||||||
|
"Register a ChatGPT backend for gptel with NAME.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
|
||||||
|
HOST (optional) is the API host, typically \"api.openai.com\".
|
||||||
|
|
||||||
|
MODELS is a list of available model names.
|
||||||
|
|
||||||
|
STREAM is a boolean to toggle streaming responses, defaults to
|
||||||
|
false.
|
||||||
|
|
||||||
|
PROTOCOL (optional) specifies the protocol, https by default.
|
||||||
|
|
||||||
|
ENDPOINT (optional) is the API endpoint for completions, defaults to
|
||||||
|
\"/v1/chat/completions\".
|
||||||
|
|
||||||
|
HEADER (optional) is for additional headers to send with each
|
||||||
|
request. It should be an alist or a function that retuns an
|
||||||
|
alist, like:
|
||||||
|
((\"Content-Type\" . \"application/json\"))
|
||||||
|
|
||||||
|
KEY (optional) is a variable whose value is the API key, or
|
||||||
|
function that returns the key."
|
||||||
|
(let ((backend (gptel--make-openai
|
||||||
|
:name name
|
||||||
|
:host host
|
||||||
|
:header header
|
||||||
|
:key key
|
||||||
|
:models models
|
||||||
|
:protocol protocol
|
||||||
|
:endpoint endpoint
|
||||||
|
:stream stream
|
||||||
|
:url (if protocol
|
||||||
|
(concat protocol "://" host endpoint)
|
||||||
|
(concat host endpoint)))))
|
||||||
|
(prog1 backend
|
||||||
|
(setf (alist-get name gptel--known-backends
|
||||||
|
nil nil #'equal)
|
||||||
|
backend))))
|
||||||
|
|
||||||
|
;;; Azure
|
||||||
|
;;;###autoload
|
||||||
|
(cl-defun gptel-make-azure
|
||||||
|
(name &key host
|
||||||
|
(protocol "https")
|
||||||
|
(header (lambda () `(("api-key" . ,(gptel--get-api-key)))))
|
||||||
|
(key 'gptel-api-key)
|
||||||
|
models stream endpoint)
|
||||||
|
"Register an Azure backend for gptel with NAME.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
|
||||||
|
HOST is the API host.
|
||||||
|
|
||||||
|
MODELS is a list of available model names.
|
||||||
|
|
||||||
|
STREAM is a boolean to toggle streaming responses, defaults to
|
||||||
|
false.
|
||||||
|
|
||||||
|
PROTOCOL (optional) specifies the protocol, https by default.
|
||||||
|
|
||||||
|
ENDPOINT is the API endpoint for completions.
|
||||||
|
|
||||||
|
HEADER (optional) is for additional headers to send with each
|
||||||
|
request. It should be an alist or a function that retuns an
|
||||||
|
alist, like:
|
||||||
|
((\"Content-Type\" . \"application/json\"))
|
||||||
|
|
||||||
|
KEY (optional) is a variable whose value is the API key, or
|
||||||
|
function that returns the key."
|
||||||
|
(let ((backend (gptel--make-openai
|
||||||
|
:name name
|
||||||
|
:host host
|
||||||
|
:header header
|
||||||
|
:key key
|
||||||
|
:models models
|
||||||
|
:protocol protocol
|
||||||
|
:endpoint endpoint
|
||||||
|
:stream stream
|
||||||
|
:url (if protocol
|
||||||
|
(concat protocol "://" host endpoint)
|
||||||
|
(concat host endpoint)))))
|
||||||
|
(prog1 backend
|
||||||
|
(setf (alist-get name gptel--known-backends
|
||||||
|
nil nil #'equal)
|
||||||
|
backend))))
|
||||||
|
|
||||||
|
;; GPT4All
|
||||||
|
;;;###autoload
|
||||||
|
(defalias 'gptel-make-gpt4all 'gptel-make-openai
|
||||||
|
"Register a GPT4All backend for gptel with NAME.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
|
||||||
|
HOST is where GPT4All runs (with port), typically localhost:8491
|
||||||
|
|
||||||
|
MODELS is a list of available model names.
|
||||||
|
|
||||||
|
STREAM is a boolean to toggle streaming responses, defaults to
|
||||||
|
false.
|
||||||
|
|
||||||
|
PROTOCOL specifies the protocol, https by default.
|
||||||
|
|
||||||
|
ENDPOINT (optional) is the API endpoint for completions, defaults to
|
||||||
|
\"/api/v1/completions\"
|
||||||
|
|
||||||
|
HEADER (optional) is for additional headers to send with each
|
||||||
|
request. It should be an alist or a function that retuns an
|
||||||
|
alist, like:
|
||||||
|
((\"Content-Type\" . \"application/json\"))
|
||||||
|
|
||||||
|
KEY (optional) is a variable whose value is the API key, or
|
||||||
|
function that returns the key. This is typically not required for
|
||||||
|
local models like GPT4All.")
|
||||||
|
|
||||||
|
(provide 'gptel-openai)
|
||||||
|
;;; gptel-backends.el ends here
|
|
@ -116,23 +116,24 @@ which see."
|
||||||
gptel--system-message (max (- (window-width) 14) 20) nil nil t)))
|
gptel--system-message (max (- (window-width) 14) 20) nil nil t)))
|
||||||
("h" "Set directives for chat" gptel-system-prompt :transient t)]
|
("h" "Set directives for chat" gptel-system-prompt :transient t)]
|
||||||
[["Session Parameters"
|
[["Session Parameters"
|
||||||
|
(gptel--infix-provider)
|
||||||
|
;; (gptel--infix-model)
|
||||||
(gptel--infix-max-tokens)
|
(gptel--infix-max-tokens)
|
||||||
(gptel--infix-num-messages-to-send)
|
(gptel--infix-num-messages-to-send)
|
||||||
(gptel--infix-temperature)
|
(gptel--infix-temperature)]
|
||||||
(gptel--infix-model)]
|
|
||||||
["Prompt:"
|
["Prompt:"
|
||||||
("-r" "From minibuffer instead" "-r")
|
("p" "From minibuffer instead" "p")
|
||||||
("-i" "Replace/Delete prompt" "-i")
|
("i" "Replace/Delete prompt" "i")
|
||||||
"Response to:"
|
"Response to:"
|
||||||
("-m" "Minibuffer instead" "-m")
|
("m" "Minibuffer instead" "m")
|
||||||
("-n" "New session" "-n"
|
("n" "New session" "n"
|
||||||
:class transient-option
|
:class transient-option
|
||||||
:prompt "Name for new session: "
|
:prompt "Name for new session: "
|
||||||
:reader
|
:reader
|
||||||
(lambda (prompt _ history)
|
(lambda (prompt _ history)
|
||||||
(read-string
|
(read-string
|
||||||
prompt (generate-new-buffer-name "*ChatGPT*") history)))
|
prompt (generate-new-buffer-name "*ChatGPT*") history)))
|
||||||
("-e" "Existing session" "-e"
|
("e" "Existing session" "e"
|
||||||
:class transient-option
|
:class transient-option
|
||||||
:prompt "Existing session: "
|
:prompt "Existing session: "
|
||||||
:reader
|
:reader
|
||||||
|
@ -142,7 +143,7 @@ which see."
|
||||||
(lambda (buf) (and (buffer-local-value 'gptel-mode (get-buffer buf))
|
(lambda (buf) (and (buffer-local-value 'gptel-mode (get-buffer buf))
|
||||||
(not (equal (current-buffer) buf))))
|
(not (equal (current-buffer) buf))))
|
||||||
t nil history)))
|
t nil history)))
|
||||||
("-k" "Kill-ring" "-k")]
|
("k" "Kill-ring" "k")]
|
||||||
[:description gptel--refactor-or-rewrite
|
[:description gptel--refactor-or-rewrite
|
||||||
:if use-region-p
|
:if use-region-p
|
||||||
("r"
|
("r"
|
||||||
|
@ -245,7 +246,7 @@ include."
|
||||||
:description "Number of past messages to send"
|
:description "Number of past messages to send"
|
||||||
:class 'transient-lisp-variable
|
:class 'transient-lisp-variable
|
||||||
:variable 'gptel--num-messages-to-send
|
:variable 'gptel--num-messages-to-send
|
||||||
:key "n"
|
:key "-n"
|
||||||
:prompt "Number of past messages to include for context (leave empty for all): "
|
:prompt "Number of past messages to include for context (leave empty for all): "
|
||||||
:reader 'gptel--transient-read-variable)
|
:reader 'gptel--transient-read-variable)
|
||||||
|
|
||||||
|
@ -262,16 +263,61 @@ will get progressively longer!"
|
||||||
:description "Response length (tokens)"
|
:description "Response length (tokens)"
|
||||||
:class 'transient-lisp-variable
|
:class 'transient-lisp-variable
|
||||||
:variable 'gptel-max-tokens
|
:variable 'gptel-max-tokens
|
||||||
:key "<"
|
:key "-c"
|
||||||
:prompt "Response length in tokens (leave empty: default, 80-200: short, 200-500: long): "
|
:prompt "Response length in tokens (leave empty: default, 80-200: short, 200-500: long): "
|
||||||
:reader 'gptel--transient-read-variable)
|
:reader 'gptel--transient-read-variable)
|
||||||
|
|
||||||
|
(defclass gptel-provider-variable (transient-lisp-variable)
|
||||||
|
((model :initarg :model)
|
||||||
|
(model-value :initarg :model-value)
|
||||||
|
(always-read :initform t)
|
||||||
|
(set-value :initarg :set-value :initform #'set))
|
||||||
|
"Class used for gptel-backends.")
|
||||||
|
|
||||||
|
(cl-defmethod transient-format-value ((obj gptel-provider-variable))
|
||||||
|
(propertize (concat (gptel-backend-name (oref obj value)) ":"
|
||||||
|
(buffer-local-value (oref obj model) transient--original-buffer))
|
||||||
|
'face 'transient-value))
|
||||||
|
|
||||||
|
(cl-defmethod transient-infix-set ((obj gptel-provider-variable) value)
|
||||||
|
(pcase-let ((`(,backend-value ,model-value) value))
|
||||||
|
(funcall (oref obj set-value)
|
||||||
|
(oref obj variable)
|
||||||
|
(oset obj value backend-value))
|
||||||
|
(funcall (oref obj set-value)
|
||||||
|
(oref obj model)
|
||||||
|
(oset obj model-value model-value))))
|
||||||
|
|
||||||
|
(transient-define-infix gptel--infix-provider ()
|
||||||
|
"AI Provider for Chat."
|
||||||
|
:description "GPT Model: "
|
||||||
|
:class 'gptel-provider-variable
|
||||||
|
:prompt "Model provider: "
|
||||||
|
:variable 'gptel-backend
|
||||||
|
:model 'gptel-model
|
||||||
|
:key "-m"
|
||||||
|
:reader (lambda (prompt &rest _)
|
||||||
|
(let* ((backend-name
|
||||||
|
(if (<= (length gptel--known-backends) 1)
|
||||||
|
(caar gptel--known-backends)
|
||||||
|
(completing-read
|
||||||
|
prompt
|
||||||
|
(mapcar #'car gptel--known-backends))))
|
||||||
|
(backend (alist-get backend-name gptel--known-backends
|
||||||
|
nil nil #'equal))
|
||||||
|
(backend-models (gptel-backend-models backend))
|
||||||
|
(model-name (if (= (length backend-models) 1)
|
||||||
|
(car backend-models)
|
||||||
|
(completing-read
|
||||||
|
"Model: " backend-models))))
|
||||||
|
(list backend model-name))))
|
||||||
|
|
||||||
(transient-define-infix gptel--infix-model ()
|
(transient-define-infix gptel--infix-model ()
|
||||||
"AI Model for Chat."
|
"AI Model for Chat."
|
||||||
:description "GPT Model: "
|
:description "GPT Model: "
|
||||||
:class 'transient-lisp-variable
|
:class 'transient-lisp-variable
|
||||||
:variable 'gptel-model
|
:variable 'gptel-model
|
||||||
:key "m"
|
:key "-m"
|
||||||
:choices '("gpt-3.5-turbo" "gpt-3.5-turbo-16k" "gpt-4" "gpt-4-32k")
|
:choices '("gpt-3.5-turbo" "gpt-3.5-turbo-16k" "gpt-4" "gpt-4-32k")
|
||||||
:reader (lambda (prompt &rest _)
|
:reader (lambda (prompt &rest _)
|
||||||
(completing-read
|
(completing-read
|
||||||
|
@ -283,7 +329,7 @@ will get progressively longer!"
|
||||||
:description "Randomness (0 - 2.0)"
|
:description "Randomness (0 - 2.0)"
|
||||||
:class 'transient-lisp-variable
|
:class 'transient-lisp-variable
|
||||||
:variable 'gptel-temperature
|
:variable 'gptel-temperature
|
||||||
:key "t"
|
:key "-t"
|
||||||
:prompt "Set temperature (0.0-2.0, leave empty for default): "
|
:prompt "Set temperature (0.0-2.0, leave empty for default): "
|
||||||
:reader 'gptel--transient-read-variable)
|
:reader 'gptel--transient-read-variable)
|
||||||
|
|
||||||
|
@ -313,42 +359,43 @@ will get progressively longer!"
|
||||||
:description "Send prompt"
|
:description "Send prompt"
|
||||||
(interactive (list (transient-args transient-current-command)))
|
(interactive (list (transient-args transient-current-command)))
|
||||||
(let ((stream gptel-stream)
|
(let ((stream gptel-stream)
|
||||||
(in-place (and (member "-i" args) t))
|
(in-place (and (member "i" args) t))
|
||||||
(output-to-other-buffer-p)
|
(output-to-other-buffer-p)
|
||||||
|
(backend-name (gptel-backend-name gptel-backend))
|
||||||
(buffer) (position)
|
(buffer) (position)
|
||||||
(callback) (gptel-buffer-name)
|
(callback) (gptel-buffer-name)
|
||||||
(prompt
|
(prompt
|
||||||
(and (member "-r" args)
|
(and (member "p" args)
|
||||||
(read-string
|
(read-string
|
||||||
"Ask ChatGPT: "
|
(format "Ask %s: " (gptel-backend-name gptel-backend))
|
||||||
(apply #'buffer-substring-no-properties
|
(apply #'buffer-substring-no-properties
|
||||||
(if (use-region-p)
|
(if (use-region-p)
|
||||||
(list (region-beginning) (region-end))
|
(list (region-beginning) (region-end))
|
||||||
(list (line-beginning-position) (line-end-position))))))))
|
(list (line-beginning-position) (line-end-position))))))))
|
||||||
(cond
|
(cond
|
||||||
((member "-m" args)
|
((member "m" args)
|
||||||
(setq stream nil)
|
(setq stream nil)
|
||||||
(setq callback
|
(setq callback
|
||||||
(lambda (resp info)
|
(lambda (resp info)
|
||||||
(if resp
|
(if resp
|
||||||
(message "ChatGPT response: %s" resp)
|
(message "%s response: %s" backend-name resp)
|
||||||
(message "ChatGPT response error: %s" (plist-get info :status))))))
|
(message "%s response error: %s" backend-name (plist-get info :status))))))
|
||||||
((member "-k" args)
|
((member "k" args)
|
||||||
(setq stream nil)
|
(setq stream nil)
|
||||||
(setq callback
|
(setq callback
|
||||||
(lambda (resp info)
|
(lambda (resp info)
|
||||||
(if (not resp)
|
(if (not resp)
|
||||||
(message "ChatGPT response error: %s" (plist-get info :status))
|
(message "%s response error: %s" backend-name (plist-get info :status))
|
||||||
(kill-new resp)
|
(kill-new resp)
|
||||||
(message "ChatGPT response: copied to kill-ring.")))))
|
(message "%s response: copied to kill-ring." backend-name)))))
|
||||||
((setq gptel-buffer-name
|
((setq gptel-buffer-name
|
||||||
(cl-some (lambda (s) (and (string-prefix-p "-n" s)
|
(cl-some (lambda (s) (and (string-prefix-p "n" s)
|
||||||
(substring s 2)))
|
(substring s 1)))
|
||||||
args))
|
args))
|
||||||
(setq buffer
|
(setq buffer
|
||||||
(gptel gptel-buffer-name
|
(gptel gptel-buffer-name
|
||||||
(condition-case nil
|
(condition-case nil
|
||||||
(gptel--api-key)
|
(gptel--get-api-key)
|
||||||
((error user-error)
|
((error user-error)
|
||||||
(setq gptel-api-key
|
(setq gptel-api-key
|
||||||
(read-passwd "OpenAI API key: "))))
|
(read-passwd "OpenAI API key: "))))
|
||||||
|
@ -370,7 +417,7 @@ will get progressively longer!"
|
||||||
(setq position (point)))
|
(setq position (point)))
|
||||||
(setq output-to-other-buffer-p t))
|
(setq output-to-other-buffer-p t))
|
||||||
((setq gptel-buffer-name
|
((setq gptel-buffer-name
|
||||||
(cl-some (lambda (s) (and (string-prefix-p "-e" s)
|
(cl-some (lambda (s) (and (string-prefix-p "e" s)
|
||||||
(substring s 2)))
|
(substring s 2)))
|
||||||
args))
|
args))
|
||||||
(setq buffer (get-buffer gptel-buffer-name))
|
(setq buffer (get-buffer gptel-buffer-name))
|
||||||
|
|
146
gptel.el
146
gptel.el
|
@ -76,18 +76,20 @@
|
||||||
(require 'json)
|
(require 'json)
|
||||||
(require 'map)
|
(require 'map)
|
||||||
(require 'text-property-search)
|
(require 'text-property-search)
|
||||||
|
(require 'gptel-openai)
|
||||||
|
|
||||||
(defgroup gptel nil
|
(defgroup gptel nil
|
||||||
"Interact with ChatGPT from anywhere in Emacs."
|
"Interact with ChatGPT from anywhere in Emacs."
|
||||||
:group 'hypermedia)
|
:group 'hypermedia)
|
||||||
|
|
||||||
(defcustom gptel-host "api.openai.com"
|
;; (defcustom gptel-host "api.openai.com"
|
||||||
"The API host queried by gptel."
|
;; "The API host queried by gptel."
|
||||||
:group 'gptel
|
;; :group 'gptel
|
||||||
:type 'string)
|
;; :type 'string)
|
||||||
|
(make-obsolete-variable
|
||||||
(defvar gptel-protocol "https"
|
'gptel-host
|
||||||
"Protocol used to query `gptel-host'.")
|
"Use `gptel-make-openai' instead."
|
||||||
|
"0.5.0")
|
||||||
|
|
||||||
(defcustom gptel-proxy ""
|
(defcustom gptel-proxy ""
|
||||||
"Path to a proxy to use for gptel interactions.
|
"Path to a proxy to use for gptel interactions.
|
||||||
|
@ -257,7 +259,7 @@ will get progressively longer!"
|
||||||
(defcustom gptel-model "gpt-3.5-turbo"
|
(defcustom gptel-model "gpt-3.5-turbo"
|
||||||
"GPT Model for chat.
|
"GPT Model for chat.
|
||||||
|
|
||||||
The current options are
|
The current options for ChatGPT are
|
||||||
- \"gpt-3.5-turbo\"
|
- \"gpt-3.5-turbo\"
|
||||||
- \"gpt-3.5-turbo-16k\"
|
- \"gpt-3.5-turbo-16k\"
|
||||||
- \"gpt-4\" (experimental)
|
- \"gpt-4\" (experimental)
|
||||||
|
@ -287,20 +289,46 @@ To set the temperature for a chat session interactively call
|
||||||
:group 'gptel
|
:group 'gptel
|
||||||
:type 'number)
|
:type 'number)
|
||||||
|
|
||||||
|
(defvar gptel--known-backends nil
|
||||||
|
"Alist of LLM backends known to gptel.
|
||||||
|
|
||||||
|
This is an alist mapping user-provided names to backend structs,
|
||||||
|
see `gptel-backend'.
|
||||||
|
|
||||||
|
You can have more than one backend pointing to the same resource
|
||||||
|
with differing settings.")
|
||||||
|
|
||||||
|
(defvar gptel--openai
|
||||||
|
(gptel-make-openai
|
||||||
|
"ChatGPT"
|
||||||
|
:header (lambda () `(("Authorization" . ,(concat "Bearer " (gptel--get-api-key)))))
|
||||||
|
:key #'gptel--get-api-key
|
||||||
|
:stream t
|
||||||
|
:models '("gpt-3.5-turbo" "gpt-3.5-turbo-16k" "gpt-4" "gpt-4-32k")))
|
||||||
|
|
||||||
|
(defvar-local gptel-backend gptel--openai)
|
||||||
|
|
||||||
(defvar-local gptel--bounds nil)
|
(defvar-local gptel--bounds nil)
|
||||||
(put 'gptel--bounds 'safe-local-variable #'gptel--always)
|
(put 'gptel--bounds 'safe-local-variable #'gptel--always)
|
||||||
|
|
||||||
(defvar-local gptel--num-messages-to-send nil)
|
(defvar-local gptel--num-messages-to-send nil)
|
||||||
(put 'gptel--num-messages-to-send 'safe-local-variable #'gptel--always)
|
(put 'gptel--num-messages-to-send 'safe-local-variable #'gptel--always)
|
||||||
(defvar gptel--debug nil)
|
|
||||||
|
(defvar gptel--debug nil
|
||||||
|
"Enable printing debug messages.
|
||||||
|
|
||||||
|
Also shows the response buffer when making requests.")
|
||||||
|
|
||||||
(defun gptel-api-key-from-auth-source (&optional host user)
|
(defun gptel-api-key-from-auth-source (&optional host user)
|
||||||
"Lookup api key in the auth source.
|
"Lookup api key in the auth source.
|
||||||
By default, `gptel-host' is used as HOST and \"apikey\" as USER."
|
By default, the LLM host for the active backend is used as HOST,
|
||||||
(if-let ((secret (plist-get (car (auth-source-search
|
and \"apikey\" as USER."
|
||||||
:host (or host gptel-host)
|
(if-let ((secret
|
||||||
:user (or user "apikey")
|
(plist-get
|
||||||
:require '(:secret)))
|
(car (auth-source-search
|
||||||
|
:host (or host (gptel-backend-host gptel-backend))
|
||||||
|
:user (or user "apikey")
|
||||||
|
:require '(:secret)))
|
||||||
:secret)))
|
:secret)))
|
||||||
(if (functionp secret)
|
(if (functionp secret)
|
||||||
(encode-coding-string (funcall secret) 'utf-8)
|
(encode-coding-string (funcall secret) 'utf-8)
|
||||||
|
@ -308,7 +336,7 @@ By default, `gptel-host' is used as HOST and \"apikey\" as USER."
|
||||||
(user-error "No `gptel-api-key' found in the auth source")))
|
(user-error "No `gptel-api-key' found in the auth source")))
|
||||||
|
|
||||||
;; FIXME Should we utf-8 encode the api-key here?
|
;; FIXME Should we utf-8 encode the api-key here?
|
||||||
(defun gptel--api-key ()
|
(defun gptel--get-api-key ()
|
||||||
"Get api key from `gptel-api-key'."
|
"Get api key from `gptel-api-key'."
|
||||||
(pcase gptel-api-key
|
(pcase gptel-api-key
|
||||||
((pred stringp) gptel-api-key)
|
((pred stringp) gptel-api-key)
|
||||||
|
@ -336,7 +364,8 @@ Currently saving and restoring state is implemented only for
|
||||||
(progn
|
(progn
|
||||||
(when-let ((bounds (org-entry-get (point-min) "GPTEL_BOUNDS")))
|
(when-let ((bounds (org-entry-get (point-min) "GPTEL_BOUNDS")))
|
||||||
(mapc (pcase-lambda (`(,beg . ,end))
|
(mapc (pcase-lambda (`(,beg . ,end))
|
||||||
(put-text-property beg end 'gptel 'response))
|
(add-text-properties
|
||||||
|
beg end '(gptel response rear-nonsticky t)))
|
||||||
(read bounds))
|
(read bounds))
|
||||||
(message "gptel chat restored."))
|
(message "gptel chat restored."))
|
||||||
(when-let ((model (org-entry-get (point-min) "GPTEL_MODEL")))
|
(when-let ((model (org-entry-get (point-min) "GPTEL_MODEL")))
|
||||||
|
@ -431,8 +460,8 @@ opening the file."
|
||||||
(gptel--restore-state)
|
(gptel--restore-state)
|
||||||
(setq gptel--old-header-line header-line-format
|
(setq gptel--old-header-line header-line-format
|
||||||
header-line-format
|
header-line-format
|
||||||
(list (concat (propertize " " 'display '(space :align-to 0))
|
(list '(:eval (concat (propertize " " 'display '(space :align-to 0))
|
||||||
(format "%s" (buffer-name)))
|
(format "%s" (gptel-backend-name gptel-backend))))
|
||||||
(propertize " Ready" 'face 'success)
|
(propertize " Ready" 'face 'success)
|
||||||
'(:eval
|
'(:eval
|
||||||
(let* ((l1 (length gptel-model))
|
(let* ((l1 (length gptel-model))
|
||||||
|
@ -468,8 +497,8 @@ opening the file."
|
||||||
(cl-defun gptel-request
|
(cl-defun gptel-request
|
||||||
(&optional prompt &key callback
|
(&optional prompt &key callback
|
||||||
(buffer (current-buffer))
|
(buffer (current-buffer))
|
||||||
position context (stream nil)
|
position context
|
||||||
(in-place nil)
|
(stream nil) (in-place nil)
|
||||||
(system gptel--system-message))
|
(system gptel--system-message))
|
||||||
"Request a response from ChatGPT for PROMPT.
|
"Request a response from ChatGPT for PROMPT.
|
||||||
|
|
||||||
|
@ -581,7 +610,7 @@ instead."
|
||||||
(interactive "P")
|
(interactive "P")
|
||||||
(if (and arg (require 'gptel-transient nil t))
|
(if (and arg (require 'gptel-transient nil t))
|
||||||
(call-interactively #'gptel-menu)
|
(call-interactively #'gptel-menu)
|
||||||
(message "Querying ChatGPT...")
|
(message "Querying %s..." (gptel-backend-name gptel-backend))
|
||||||
(let* ((response-pt
|
(let* ((response-pt
|
||||||
(if (use-region-p)
|
(if (use-region-p)
|
||||||
(set-marker (make-marker) (region-end))
|
(set-marker (make-marker) (region-end))
|
||||||
|
@ -698,38 +727,24 @@ there."
|
||||||
(goto-char (point-max))))
|
(goto-char (point-max))))
|
||||||
(t (goto-char (or prompt-end (point-max)))))
|
(t (goto-char (or prompt-end (point-max)))))
|
||||||
(let ((max-entries (and gptel--num-messages-to-send
|
(let ((max-entries (and gptel--num-messages-to-send
|
||||||
(* 2 gptel--num-messages-to-send)))
|
(* 2 gptel--num-messages-to-send))))
|
||||||
(prop) (prompts))
|
(gptel--parse-buffer gptel-backend max-entries)))))
|
||||||
(while (and
|
|
||||||
(or (not max-entries) (>= max-entries 0))
|
|
||||||
(setq prop (text-property-search-backward
|
|
||||||
'gptel 'response
|
|
||||||
(when (get-char-property (max (point-min) (1- (point)))
|
|
||||||
'gptel)
|
|
||||||
t))))
|
|
||||||
(push (list :role (if (prop-match-value prop) "assistant" "user")
|
|
||||||
:content
|
|
||||||
(string-trim
|
|
||||||
(buffer-substring-no-properties (prop-match-beginning prop)
|
|
||||||
(prop-match-end prop))
|
|
||||||
"[*# \t\n\r]+"))
|
|
||||||
prompts)
|
|
||||||
(and max-entries (cl-decf max-entries)))
|
|
||||||
(cons (list :role "system"
|
|
||||||
:content gptel--system-message)
|
|
||||||
prompts)))))
|
|
||||||
|
|
||||||
(defun gptel--request-data (prompts)
|
(cl-defgeneric gptel--parse-buffer (backend max-entries)
|
||||||
"JSON encode PROMPTS for sending to ChatGPT."
|
"Parse the current buffer backwards from point and return a list
|
||||||
(let ((prompts-plist
|
of prompts.
|
||||||
`(:model ,gptel-model
|
|
||||||
:messages [,@prompts]
|
BACKEND is the LLM backend in use.
|
||||||
:stream ,(or (and gptel-stream gptel-use-curl) :json-false))))
|
|
||||||
(when gptel-temperature
|
MAX-ENTRIES is the number of queries/responses to include for
|
||||||
(plist-put prompts-plist :temperature gptel-temperature))
|
contexbt.")
|
||||||
(when gptel-max-tokens
|
|
||||||
(plist-put prompts-plist :max_tokens gptel-max-tokens))
|
(cl-defgeneric gptel--request-data (backend prompts)
|
||||||
prompts-plist))
|
"Generate a plist of all data for an LLM query.
|
||||||
|
|
||||||
|
BACKEND is the LLM backend in use.
|
||||||
|
|
||||||
|
PROMPTS is the plist of previous user queries and LLM responses.")
|
||||||
|
|
||||||
;; TODO: Use `run-hook-wrapped' with an accumulator instead to handle
|
;; TODO: Use `run-hook-wrapped' with an accumulator instead to handle
|
||||||
;; buffer-local hooks, etc.
|
;; buffer-local hooks, etc.
|
||||||
|
@ -773,13 +788,17 @@ the response is inserted into the current buffer after point."
|
||||||
(message-log-max nil)
|
(message-log-max nil)
|
||||||
(url-request-method "POST")
|
(url-request-method "POST")
|
||||||
(url-request-extra-headers
|
(url-request-extra-headers
|
||||||
`(("Content-Type" . "application/json")
|
(append '(("Content-Type" . "application/json"))
|
||||||
("Authorization" . ,(concat "Bearer " (gptel--api-key)))))
|
(when-let ((backend-header (gptel-backend-header gptel-backend)))
|
||||||
|
(if (functionp backend-header)
|
||||||
|
(funcall backend-header)
|
||||||
|
backend-header))))
|
||||||
(url-request-data
|
(url-request-data
|
||||||
(encode-coding-string
|
(encode-coding-string
|
||||||
(json-encode (gptel--request-data (plist-get info :prompt)))
|
(json-encode (gptel--request-data
|
||||||
|
gptel-backend (plist-get info :prompt)))
|
||||||
'utf-8)))
|
'utf-8)))
|
||||||
(url-retrieve (format "%s://%s/v1/chat/completions" gptel-protocol gptel-host)
|
(url-retrieve (gptel-backend-url gptel-backend)
|
||||||
(lambda (_)
|
(lambda (_)
|
||||||
(pcase-let ((`(,response ,http-msg ,error)
|
(pcase-let ((`(,response ,http-msg ,error)
|
||||||
(gptel--url-parse-response (current-buffer))))
|
(gptel--url-parse-response (current-buffer))))
|
||||||
|
@ -790,6 +809,16 @@ the response is inserted into the current buffer after point."
|
||||||
(kill-buffer)))
|
(kill-buffer)))
|
||||||
nil t nil)))
|
nil t nil)))
|
||||||
|
|
||||||
|
(cl-defgeneric gptel--parse-response (backend response proc-info)
|
||||||
|
"Response extractor for LLM requests.
|
||||||
|
|
||||||
|
BACKEND is the LLM backend in use.
|
||||||
|
|
||||||
|
RESPONSE is the parsed JSON of the response, as a plist.
|
||||||
|
|
||||||
|
PROC-INFO is a plist with process information and other context.
|
||||||
|
See `gptel-curl--get-response' for its contents.")
|
||||||
|
|
||||||
(defun gptel--url-parse-response (response-buffer)
|
(defun gptel--url-parse-response (response-buffer)
|
||||||
"Parse response in RESPONSE-BUFFER."
|
"Parse response in RESPONSE-BUFFER."
|
||||||
(when (buffer-live-p response-buffer)
|
(when (buffer-live-p response-buffer)
|
||||||
|
@ -809,7 +838,8 @@ the response is inserted into the current buffer after point."
|
||||||
(json-readtable-error 'json-read-error))))))
|
(json-readtable-error 'json-read-error))))))
|
||||||
(cond
|
(cond
|
||||||
((string-match-p "200 OK" http-msg)
|
((string-match-p "200 OK" http-msg)
|
||||||
(list (string-trim (map-nested-elt response '(:choices 0 :message :content)))
|
(list (string-trim (gptel--parse-response gptel-backend response
|
||||||
|
'(:buffer response-buffer)))
|
||||||
http-msg))
|
http-msg))
|
||||||
((plist-get response :error)
|
((plist-get response :error)
|
||||||
(let* ((error-plist (plist-get response :error))
|
(let* ((error-plist (plist-get response :error))
|
||||||
|
@ -837,7 +867,7 @@ buffer created or switched to."
|
||||||
(read-string "Session name: " (generate-new-buffer-name gptel-default-session))
|
(read-string "Session name: " (generate-new-buffer-name gptel-default-session))
|
||||||
gptel-default-session)
|
gptel-default-session)
|
||||||
(condition-case nil
|
(condition-case nil
|
||||||
(gptel--api-key)
|
(gptel--get-api-key)
|
||||||
((error user-error)
|
((error user-error)
|
||||||
(setq gptel-api-key
|
(setq gptel-api-key
|
||||||
(read-passwd "OpenAI API key: "))))
|
(read-passwd "OpenAI API key: "))))
|
||||||
|
|
Loading…
Add table
Reference in a new issue