;;; gptel-curl.el --- Curl support for GPTel -*- lexical-binding: t; -*- ;; Copyright (C) 2023 Karthik Chikmagalur ;; Author: Karthik Chikmagalur;; ;; Keywords: convenience ;; SPDX-License-Identifier: GPL-3.0-or-later ;; 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 . ;;; Commentary: ;; Curl support for GPTel. Utility functions. ;;; Code: (require 'gptel) (eval-when-compile (require 'subr-x)) (require 'map) (require 'json) (defvar gptel-curl--process-alist nil "Alist of active GPTel curl requests.") (defun gptel-curl--get-args (prompts token) "Produce list of arguments for calling Curl. PROMPTS is the data to send, TOKEN is a unique identifier." (let* ((args (list "--location" "--silent" "--compressed" "--disable")) (url "https://api.openai.com/v1/chat/completions") (data (encode-coding-string (json-encode (gptel--request-data prompts)) 'utf-8)) (headers `(("Content-Type" . "application/json") ("Authorization" . ,(concat "Bearer " (gptel--api-key)))))) (push (format "-X%s" "POST") args) (push (format "-w(%s . %%{size_header})" token) args) ;; (push (format "--keepalive-time %s" 240) args) (push (format "-m%s" 60) args) (push "-D-" args) (pcase-dolist (`(,key . ,val) headers) (push (format "-H%s: %s" key val) args)) (push (format "-d%s" data) args) (nreverse (cons url args)))) ;;;###autoload (defun gptel-curl-get-response (info &optional callback) "Retrieve response to prompt in INFO. INFO is a plist with the following keys: - :prompt (the prompt being sent) - :gptel-buffer (the gptel buffer) - :insert-marker (marker at which to insert the response). Call CALLBACK with the response and INFO afterwards. If omitted the response is inserted into the current buffer after point." (with-current-buffer (generate-new-buffer "*gptel-curl*") (let* ((token (md5 (format "%s%s%s%s" (random) (emacs-pid) (user-full-name) (recent-keys)))) (args (gptel-curl--get-args (plist-get info :prompt) token)) (process (apply #'start-process "gptel-curl" (current-buffer) "curl" args))) (set-process-query-on-exit-flag process nil) (setf (alist-get process gptel-curl--process-alist) (nconc (list :token token :callback (or callback #'gptel--insert-response)) info)) (set-process-sentinel process #'gptel-curl--sentinel)))) (defun gptel-curl--sentinel (process status) "Process sentinel for GPTel curl requests. PROCESS and STATUS are process parameters." (let ((proc-buf (process-buffer process))) (when gptel--debug (with-current-buffer proc-buf (clone-buffer "*gptel-error*" 'show))) (if-let* (((eq (process-status process) 'exit)) (proc-info (alist-get process gptel-curl--process-alist)) (proc-token (plist-get proc-info :token)) (proc-callback (plist-get proc-info :callback)) (response (gptel-curl--parse-response proc-buf proc-token))) (funcall proc-callback response proc-info) ;; Failed (funcall proc-callback (list :content nil :status status) proc-info)) (setf (alist-get process gptel-curl--process-alist nil 'remove) nil) (kill-buffer proc-buf))) (defun gptel-curl--parse-response (buf token) "Parse the buffer BUF with curl's response. TOKEN is used to disambiguate multiple requests in a single buffer." (with-current-buffer buf (progn (goto-char (point-max)) (search-backward token) (backward-char) (pcase-let* ((`(,_ . ,header-size) (read (current-buffer)))) ;; (if (search-backward token nil t) ;; (search-forward ")" nil t) ;; (goto-char (point-min))) (goto-char (point-min)) (if-let* ((http-msg (buffer-substring (line-beginning-position) (line-end-position))) (http-status (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))))) (cond ((equal http-status "200") (list :content (string-trim (map-nested-elt response '(:choices 0 :message :content))) :status http-msg)) ((plist-get response :error) (let* ((error-plist (plist-get response :error)) (error-msg (plist-get error-plist :message)) (error-type (plist-get error-plist :type))) (message "ChatGPT error: %s" error-msg) (list :content nil :status (concat http-msg ": " error-type)))) ((eq response 'json-read-error) (message "ChatGPT error: Malformed JSON in response.") (list :content nil :status (concat http-msg ": Malformed JSON in response."))) (t (message "ChatGPT error: Could not parse HTTP response.") (list :content nil :status (concat http-msg ": Could not parse HTTP response.")))) (message "ChatGPT error: Could not parse HTTP response.") (list :content nil :status (concat http-msg ": Could not parse HTTP response."))))))) (provide 'gptel-curl) ;;; gptel-curl.el ends here