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

View file

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

View file

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

View file

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

View file

@ -44,6 +44,29 @@
(declare-function gptel-prompt-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
(cl-defstruct
(gptel-backend (:constructor gptel--make-backend)
@ -57,13 +80,12 @@
(:include gptel-backend)))
(cl-defmethod gptel-curl--parse-stream ((_backend gptel-openai) _info)
(let* ((json-object-type 'plist)
(content-strs))
(let* ((content-strs))
(condition-case nil
(while (re-search-forward "^data:" nil t)
(save-match-data
(unless (looking-at " *\\[DONE\\]")
(when-let* ((response (json-read))
(when-let* ((response (gptel--json-read))
(delta (map-nested-elt
response '(:choices 0 :delta)))
(content (plist-get delta :content)))

View file

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

View file

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