diff --git a/gptel-anthropic.el b/gptel-anthropic.el index fa0aed0..113c4fb 100644 --- a/gptel-anthropic.el +++ b/gptel-anthropic.el @@ -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)))))) diff --git a/gptel-curl.el b/gptel-curl.el index 08d7feb..0c0198f 100644 --- a/gptel-curl.el +++ b/gptel-curl.el @@ -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")) diff --git a/gptel-gemini.el b/gptel-gemini.el index 9609d90..3102628 100644 --- a/gptel-gemini.el +++ b/gptel-gemini.el @@ -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)))) diff --git a/gptel-ollama.el b/gptel-ollama.el index f501e7b..5622136 100644 --- a/gptel-ollama.el +++ b/gptel-ollama.el @@ -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))))) diff --git a/gptel-openai.el b/gptel-openai.el index bf0825f..df2bbe7 100644 --- a/gptel-openai.el +++ b/gptel-openai.el @@ -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))) diff --git a/gptel.el b/gptel.el index 2ef17f7..51ce5fe 100644 --- a/gptel.el +++ b/gptel.el @@ -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)) diff --git a/test/gptel-org-test.el b/test/gptel-org-test.el index 13f8af3..858b4d6 100644 --- a/test/gptel-org-test.el +++ b/test/gptel-org-test.el @@ -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))))))