gptel: Use libjansson support if available

Using the libjansson JSON parser gives us a modest boost in speed.
It's not as significant a speedup as it is for LSP clients since
our jSON payloads are smaller and less frequent -- but we might as
well use it.

* gptel.el (gptel--json-read, gptel--json-encode,
gptel--url-get-response, gptel--parse-response): Define macros to
use the libjansson-supported `json-parse-buffer` and
`json-serialize`.  Replace use of `json-encode` and `json-read`
appropriately.

* gptel-openai.el: (gptel-curl--parse-stream) : Use
`gptel--json-read` instead of `json-read`.

* gptel-ollama.el (gptel-curl--parse-stream): Use
`gptel--json-read` instead of `json-read`.

* gptel-gemini.el (gptel-curl--parse-stream): Use
`gptel--json-read` instead of `json-read`.

* gptel-curl.el (gptel-curl--get-args, gptel-curl--get-response,
gptel-curl--log-response, gptel-curl--stream-cleanup,
gptel-curl--parse-response): Use `gptel--json-read` and
`gptel--json-encode` in place of the json.el versions.

* gptel-anthropic.el (gptel-curl--parse-stream): Use
`gptel--json-read` instead of `json-read`.

* test/gptel-org-test.el: Use `gptel--json-read`.
This commit is contained in:
Karthik Chikmagalur 2024-03-13 01:53:06 -07:00
parent fbb0ee29c4
commit f58ad9435c
7 changed files with 62 additions and 39 deletions

View file

@ -32,7 +32,7 @@
(declare-function prop-match-value "text-property-search") (declare-function prop-match-value "text-property-search")
(declare-function text-property-search-backward "text-property-search") (declare-function text-property-search-backward "text-property-search")
(declare-function json-read "json") (declare-function json-read "json" ())
;;; Anthropic (Messages API) ;;; Anthropic (Messages API)
(cl-defstruct (gptel-anthropic (:constructor gptel--make-anthropic) (cl-defstruct (gptel-anthropic (:constructor gptel--make-anthropic)
@ -40,8 +40,7 @@
(:include gptel-backend))) (:include gptel-backend)))
(cl-defmethod gptel-curl--parse-stream ((_backend gptel-anthropic) _info) (cl-defmethod gptel-curl--parse-stream ((_backend gptel-anthropic) _info)
(let* ((json-object-type 'plist) (let* ((content-strs)
(content-strs)
(pt (point))) (pt (point)))
(condition-case nil (condition-case nil
(while (re-search-forward "^event: " nil t) (while (re-search-forward "^event: " nil t)
@ -50,7 +49,7 @@
((looking-at "content_block_\\(?:start\\|delta\\|stop\\)") ((looking-at "content_block_\\(?:start\\|delta\\|stop\\)")
(save-match-data (save-match-data
(forward-line 1) (forward-char 5) (forward-line 1) (forward-char 5)
(when-let* ((response (json-read)) (when-let* ((response (gptel--json-read))
(content (map-nested-elt (content (map-nested-elt
response '(:delta :text)))) response '(:delta :text))))
(push content content-strs)))))) (push content content-strs))))))

View file

@ -32,7 +32,9 @@
(require 'cl-lib) (require 'cl-lib)
(require 'subr-x)) (require 'subr-x))
(require 'map) (require 'map)
(require 'json)
(declare-function json-read "json" ())
(defvar json-object-type)
(defconst gptel-curl--common-args (defconst gptel-curl--common-args
(if (memq system-type '(windows-nt ms-dos)) (if (memq system-type '(windows-nt ms-dos))
@ -53,7 +55,7 @@ PROMPTS is the data to send, TOKEN is a unique identifier."
(if (functionp backend-url) (if (functionp backend-url)
(funcall backend-url) backend-url))) (funcall backend-url) backend-url)))
(data (encode-coding-string (data (encode-coding-string
(json-encode (gptel--request-data gptel-backend prompts)) (gptel--json-encode (gptel--request-data gptel-backend prompts))
'utf-8)) 'utf-8))
(headers (headers
(append '(("Content-Type" . "application/json")) (append '(("Content-Type" . "application/json"))
@ -62,7 +64,7 @@ PROMPTS is the data to send, TOKEN is a unique identifier."
(funcall header) header))))) (funcall header) header)))))
(when gptel-log-level (when gptel-log-level
(when (eq gptel-log-level 'debug) (when (eq gptel-log-level 'debug)
(gptel--log (json-encode headers) "request headers")) (gptel--log (gptel--json-encode headers) "request headers"))
(gptel--log data "request body")) (gptel--log data "request body"))
(append (append
gptel-curl--common-args gptel-curl--common-args
@ -108,7 +110,7 @@ the response is inserted into the current buffer after point."
(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 (eq gptel-log-level 'debug) (when (eq gptel-log-level 'debug)
(gptel--log (json-encode (cons "curl" args)) (gptel--log (gptel--json-encode (cons "curl" args))
"request Curl command")) "request Curl command"))
(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)
@ -154,7 +156,7 @@ PROC-INFO is the plist containing process metadata."
(goto-char (point-min)) (goto-char (point-min))
(when (re-search-forward " ?\n ?\n" nil t) (when (re-search-forward " ?\n ?\n" nil t)
(when (eq gptel-log-level 'debug) (when (eq gptel-log-level 'debug)
(gptel--log (json-encode-string (gptel--log (gptel--json-encode
(buffer-substring-no-properties (buffer-substring-no-properties
(point-min) (1- (point)))) (point-min) (1- (point))))
"response headers")) "response headers"))
@ -216,10 +218,9 @@ PROCESS and _STATUS are process parameters."
(search-backward (plist-get info :token)) (search-backward (plist-get info :token))
(backward-char) (backward-char)
(pcase-let* ((`(,_ . ,header-size) (read (current-buffer))) (pcase-let* ((`(,_ . ,header-size) (read (current-buffer)))
(json-object-type 'plist)
(response (progn (goto-char header-size) (response (progn (goto-char header-size)
(condition-case nil (json-read) (condition-case nil (gptel--json-read)
(json-readtable-error 'json-read-error)))) (error 'json-read-error))))
(error-data (plist-get response :error))) (error-data (plist-get response :error)))
(cond (cond
(error-data (error-data
@ -379,11 +380,10 @@ PROC-INFO is a plist with contextual information."
(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)
(response (progn (goto-char header-size) (response (progn (goto-char header-size)
(condition-case nil (condition-case nil
(json-read) (gptel--json-read)
(json-readtable-error 'json-read-error))))) (error 'json-read-error)))))
(cond (cond
;; FIXME Handle the case where HTTP 100 is followed by HTTP (not 200) BUG #194 ;; FIXME Handle the case where HTTP 100 is followed by HTTP (not 200) BUG #194
((member http-status '("200" "100")) ((member http-status '("200" "100"))

View file

@ -30,6 +30,7 @@
(declare-function prop-match-value "text-property-search") (declare-function prop-match-value "text-property-search")
(declare-function text-property-search-backward "text-property-search") (declare-function text-property-search-backward "text-property-search")
(declare-function json-read "json") (declare-function json-read "json")
(defvar json-object-type)
;;; Gemini ;;; Gemini
(cl-defstruct (cl-defstruct
@ -38,15 +39,14 @@
(:include gptel-backend))) (:include gptel-backend)))
(cl-defmethod gptel-curl--parse-stream ((_backend gptel-gemini) _info) (cl-defmethod gptel-curl--parse-stream ((_backend gptel-gemini) _info)
(let* ((json-object-type 'plist) (let* ((content-strs))
(content-strs))
(condition-case nil (condition-case nil
;; while-let is Emacs 29.1+ only ;; while-let is Emacs 29.1+ only
(while (prog1 (search-forward "{" nil t) (while (prog1 (search-forward "{" nil t)
(backward-char 1)) (backward-char 1))
(save-match-data (save-match-data
(when-let* (when-let*
((response (json-read)) ((response (gptel--json-read))
(text (map-nested-elt (text (map-nested-elt
response '(:candidates 0 :content :parts 0 :text)))) response '(:candidates 0 :content :parts 0 :text))))
(push text content-strs)))) (push text content-strs))))

View file

@ -26,6 +26,9 @@
(require 'gptel) (require 'gptel)
(require 'cl-generic) (require 'cl-generic)
(declare-function json-read "json" ())
(defvar json-object-type)
;;; Ollama ;;; Ollama
(cl-defstruct (gptel-ollama (:constructor gptel--make-ollama) (cl-defstruct (gptel-ollama (:constructor gptel--make-ollama)
(:copier nil) (:copier nil)
@ -42,15 +45,14 @@ Ollama models.")
(when (bobp) (when (bobp)
(re-search-forward "^{") (re-search-forward "^{")
(forward-line 0)) (forward-line 0))
(let* ((json-object-type 'plist) (let* ((content-strs)
(content-strs)
(content)) (content))
(condition-case nil (condition-case nil
(while (setq content (json-read)) (while (setq content (gptel--json-read))
(let ((done (map-elt content :done)) (let ((done (map-elt content :done))
(response (map-elt content :response))) (response (map-elt content :response)))
(push response content-strs) (push response content-strs)
(unless (eq done json-false) (unless (eq done :json-false)
(with-current-buffer (plist-get info :buffer) (with-current-buffer (plist-get info :buffer)
(setq gptel--ollama-context (map-elt content :context))) (setq gptel--ollama-context (map-elt content :context)))
(goto-char (point-max))))) (goto-char (point-max)))))

View file

@ -44,6 +44,29 @@
(declare-function gptel-prompt-prefix-string "gptel") (declare-function gptel-prompt-prefix-string "gptel")
(declare-function gptel-response-prefix-string "gptel") (declare-function gptel-response-prefix-string "gptel")
(defmacro gptel--json-read ()
(if (fboundp 'json-parse-buffer)
`(json-parse-buffer
:object-type 'plist
:null-object nil
:false-object :json-false)
(require 'json)
(defvar json-object-type)
(declare-function json-read "json" ())
`(let ((json-object-type 'plist))
gptel--json-read)))
(defmacro gptel--json-encode (object)
(if (fboundp 'json-serialize)
`(json-serialize ,object
:null-object nil
:false-object :json-false)
(require 'json)
(defvar json-false)
(declare-function json-encode "json" (object))
`(let ((json-false :json-false))
(json-encode ,object))))
;;; Common backend struct for LLM support ;;; Common backend struct for LLM support
(cl-defstruct (cl-defstruct
(gptel-backend (:constructor gptel--make-backend) (gptel-backend (:constructor gptel--make-backend)
@ -57,13 +80,12 @@
(:include gptel-backend))) (:include gptel-backend)))
(cl-defmethod gptel-curl--parse-stream ((_backend gptel-openai) _info) (cl-defmethod gptel-curl--parse-stream ((_backend gptel-openai) _info)
(let* ((json-object-type 'plist) (let* ((content-strs))
(content-strs))
(condition-case nil (condition-case nil
(while (re-search-forward "^data:" nil t) (while (re-search-forward "^data:" nil t)
(save-match-data (save-match-data
(unless (looking-at " *\\[DONE\\]") (unless (looking-at " *\\[DONE\\]")
(when-let* ((response (json-read)) (when-let* ((response (gptel--json-read))
(delta (map-nested-elt (delta (map-nested-elt
response '(:choices 0 :delta))) response '(:choices 0 :delta)))
(content (plist-get delta :content))) (content (plist-get delta :content)))

View file

@ -128,7 +128,6 @@
(require 'cl-lib)) (require 'cl-lib))
(require 'compat nil t) (require 'compat nil t)
(require 'url) (require 'url)
(require 'json)
(require 'map) (require 'map)
(require 'text-property-search) (require 'text-property-search)
(require 'cl-generic) (require 'cl-generic)
@ -615,6 +614,8 @@ in any way.")
(defconst gptel--log-buffer-name "*gptel-log*" (defconst gptel--log-buffer-name "*gptel-log*"
"Log buffer for gptel.") "Log buffer for gptel.")
(declare-function json-pretty-print "json")
(defun gptel--log (data &optional type no-json) (defun gptel--log (data &optional type no-json)
"Log DATA to `gptel--log-buffer-name'. "Log DATA to `gptel--log-buffer-name'.
@ -1129,12 +1130,12 @@ the response is inserted into the current buffer after point."
(funcall header) header)))) (funcall header) header))))
(url-request-data (url-request-data
(encode-coding-string (encode-coding-string
(json-encode (gptel--request-data (gptel--json-encode (gptel--request-data
gptel-backend (plist-get info :prompt))) gptel-backend (plist-get info :prompt)))
'utf-8))) 'utf-8)))
(when gptel-log-level ;logging (when gptel-log-level ;logging
(when (eq gptel-log-level 'debug) (when (eq gptel-log-level 'debug)
(gptel--log (json-encode url-request-extra-headers) "request headers")) (gptel--log (gptel--json-encode url-request-extra-headers) "request headers"))
(gptel--log url-request-data "request body")) (gptel--log url-request-data "request body"))
(url-retrieve (let ((backend-url (gptel-backend-url gptel-backend))) (url-retrieve (let ((backend-url (gptel-backend-url gptel-backend)))
(if (functionp backend-url) (if (functionp backend-url)
@ -1169,20 +1170,16 @@ See `gptel-curl--get-response' for its contents.")
(save-excursion (save-excursion
(goto-char url-http-end-of-headers) (goto-char url-http-end-of-headers)
(when (eq gptel-log-level 'debug) (when (eq gptel-log-level 'debug)
(gptel--log (json-encode (buffer-substring-no-properties (point-min) (point))) (gptel--log (gptel--json-encode (buffer-substring-no-properties (point-min) (point)))
"response headers")) "response headers"))
(gptel--log (buffer-substring-no-properties (point) (point-max)) (gptel--log (buffer-substring-no-properties (point) (point-max))
"response body"))) "response body")))
(if-let* ((http-msg (string-trim (buffer-substring (line-beginning-position) (if-let* ((http-msg (string-trim (buffer-substring (line-beginning-position)
(line-end-position)))) (line-end-position))))
(json-object-type 'plist)
(response (progn (goto-char url-http-end-of-headers) (response (progn (goto-char url-http-end-of-headers)
(let ((json-str (decode-coding-string (condition-case nil
(buffer-substring-no-properties (point) (point-max)) (gptel--json-read)
'utf-8))) (error 'json-read-error)))))
(condition-case nil
(json-read-from-string json-str)
(json-readtable-error 'json-read-error))))))
(cond (cond
;; FIXME Handle the case where HTTP 100 is followed by HTTP (not 200) BUG #194 ;; FIXME Handle the case where HTTP 100 is followed by HTTP (not 200) BUG #194
((or (memq url-http-response-status '(200 100)) ((or (memq url-http-response-status '(200 100))

View file

@ -3,6 +3,9 @@
(require 'gptel) (require 'gptel)
(require 'cl-generic) (require 'cl-generic)
(declare-function json-read "json" ())
(defvar json-object-type)
;;; Methods for collecting data from HTTP logs ;;; Methods for collecting data from HTTP logs
(cl-defgeneric gptel-test--read-response (backend &optional from to)) (cl-defgeneric gptel-test--read-response (backend &optional from to))
(cl-defmethod gptel-test--read-response ((_backend gptel-openai) &optional from to) (cl-defmethod gptel-test--read-response ((_backend gptel-openai) &optional from to)
@ -11,16 +14,16 @@
(save-restriction (save-restriction
(narrow-to-region from to) (narrow-to-region from to)
(goto-char from) (goto-char from)
(let ((strs) (json-object-type 'plist)) (let ((strs))
(while (re-search-forward "^data: *" nil t) (while (re-search-forward "^data: *" nil t)
;; (forward-char) ;; (forward-char)
(condition-case-unless-debug err (condition-case-unless-debug err
(thread-first (thread-first
(json-parse-buffer :object-type 'plist) (gptel--json-read :object-type 'plist)
;; (json-read) ;; (json-read)
(map-nested-elt '(:choices 0 :delta :content)) (map-nested-elt '(:choices 0 :delta :content))
(push strs)) (push strs))
(json-readtable-error strs) (error strs)
(:success strs))) (:success strs)))
(setq strs (delq nil (nreverse strs)))))) (setq strs (delq nil (nreverse strs))))))