branch: elpa/gptel commit b60589fa24b9f3e0ad07b90ce5562e07d0ef1400 Author: kiennq <kien.n.qu...@gmail.com> Commit: GitHub <nore...@github.com>
gptel-gh: Add support for Github Copilot Chat (#767) * gptel-gh.el: (gptel--gh, gptel-make-gh-copilot, gptel--gh-uuid, gptel--gh-token-file, gptel--gh-github-token-file, gptel--gh-auth-common-headers, gptel--gh-client-id, gptel--gh-machine-id, gptel--gh-login, gptel--gh-renew-token, gptel--gh-auth, gptel--gh-restore, gptel--gh-save, gptel--gh-models): Add support for GitHub Copilot Chat. * gptel.el (gptel--url-retrieve): Add a general purpose, synchronous HTTP fetcher for auxiliary tasks. This is used for authentication in the GitHub Copilot chat backend, but will be used in the future to handle fetching a list of updated models for other backends, among other tasks. * README.org (GitHub CopilotChat): Add Github CopilotChat section. --- README.org | 24 +++++ gptel-gh.el | 307 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ gptel.el | 13 +++ 3 files changed, 344 insertions(+) diff --git a/README.org b/README.org index 79723a2f75..04871bb890 100644 --- a/README.org +++ b/README.org @@ -32,6 +32,7 @@ gptel is a simple Large Language Model chat client for Emacs, with support for m | Github Models | ✓ | [[https://github.com/settings/tokens][Token]] | | Novita AI | ✓ | [[https://novita.ai/model-api/product/llm-api?utm_source=github_gptel&utm_medium=github_readme&utm_campaign=link][Token]] | | xAI | ✓ | [[https://console.x.ai?utm_source=github_gptel&utm_medium=github_readme&utm_campaign=link][API key]] | +| Github CopilotChat | ✓ | Github account | #+html: </div> *General usage*: ([[https://www.youtube.com/watch?v=bsRnh_brggM][YouTube Demo]]) @@ -113,6 +114,7 @@ gptel uses Curl if available, but falls back to the built-in url-retrieve to wor - [[#github-models][Github Models]] - [[#novita-ai][Novita AI]] - [[#xai][xAI]] + - [[#github-copilotchat][Github CopilotChat]] - [[#usage][Usage]] - [[#in-any-buffer][In any buffer:]] - [[#in-a-dedicated-chat-buffer][In a dedicated chat buffer:]] @@ -895,6 +897,28 @@ The above code makes the backend available to select. If you want it to be the #+html: </details> +#+html: </details> +#+html: <details><summary> +**** Github CopilotChat +#+html: </summary> + +Register a backend with +#+begin_src emacs-lisp +(gptel-make-gh-copilot "Copilot") +#+end_src + +You will be informed to login into =Github= as required. +You can pick this backend from the menu when using gptel (see [[#usage][Usage]]). + +***** (Optional) Set as the default gptel backend + +The above code makes the backend available to select. If you want it to be the default backend for gptel, you can set this as the value of =gptel-backend=. Use this instead of the above. +#+begin_src emacs-lisp +;; OPTIONAL configuration +(setq gptel-model 'claude-3.7-sonnet + gptel-backend (gptel-make-gh-copilot "Copilot")) +#+end_src + ** Usage gptel provides a few powerful, general purpose and flexible commands. You can dynamically tweak their behavior to the needs of your task with /directives/, redirection options and more. There is a [[https://www.youtube.com/watch?v=bsRnh_brggM][video demo]] showing various uses of gptel -- but =gptel-send= might be all you need. diff --git a/gptel-gh.el b/gptel-gh.el new file mode 100644 index 0000000000..3069ca7f69 --- /dev/null +++ b/gptel-gh.el @@ -0,0 +1,307 @@ +;;; gptel-gh.el --- Github Copilot AI suppport for gptel -*- lexical-binding: t; -*- + +;; 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 Github Copilot API to gptel + +;;; Code: + +(eval-when-compile + (require 'cl-lib)) +(require 'map) +(require 'gptel) +(require 'browse-url) + +;;; Github Copilot +(defconst gptel--gh-models + '((gpt-4o-2024-11-20 + :description + "Advanced model for complex tasks; cheaper & faster than GPT-Turbo" + :capabilities (media tool-use json url) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 128 :input-cost 2.5 :output-cost 10 :cutoff-date "2023-10") + (gpt-4.5-preview + :description "Largest and most capable GPT model to date" + :capabilities (tool-use url) + :context-window 128 + :input-cost 75 + :output-cost 150 + :cutoff-date "2023-10") + (o1 + :description "Reasoning model designed to solve hard problems across domains" + :capabilities (reasoning) + :context-window 200 + :input-cost 15 + :output-cost 60 + :cutoff-date "2023-10" + :request-params (:stream :json-false)) + (o3-mini + :description "High intelligence at the same cost and latency targets of o1-mini" + :context-window 200 + :input-cost 3 + :output-cost 12 + :cutoff-date "2023-10" + :capabilities (reasoning) + :request-params (:stream :json-false)) + (claude-3.5-sonnet + :description "Highest level of intelligence and capability" + :capabilities (tool-use cache) + :context-window 200 + :input-cost 3 + :output-cost 15 + :cutoff-date "2024-04") + (claude-3.7-sonnet + :description "Hybrid model capable of standard thinking and extended thinking modes" + :capabilities (tool-use cache) + :context-window 200 + :input-cost 3 + :output-cost 15 + :cutoff-date "2025-02") + (claude-3.7-sonnet-thought + :description "Claude 3.7 Sonnet Thinking" + :capabilities (tool-use cache) + :context-window 200 + :input-cost 3 + :output-cost 15 + :cutoff-date "2025-02") + (gemini-2.0-flash-001 + :description "Next gen, high speed, multimodal for a diverse variety of tasks" + :capabilities (tool-use json) + :context-window 1000 + :input-cost 0.10 + :output-cost 0.40 + :cutoff-date "2024-08"))) + +(cl-defstruct (gptel--gh (:include gptel-openai) + (:copier nil) + (:constructor gptel--make-gh)) + token github-token sessionid machineid) + +(defcustom gptel-gh-github-token-file (expand-file-name ".cache/copilot-chat/github-token" + user-emacs-directory) + "File where the GitHub token is stored." + :type 'string + :group 'gptel) + +(defcustom gptel-gh-token-file (expand-file-name ".cache/copilot-chat/token" + user-emacs-directory) + "File where the chat token is cached." + :type 'string + :group 'gptel) + +(defconst gptel--gh-auth-common-headers + `(("editor-plugin-version" . "gptel/*") + ("editor-version" . ,(concat "emacs/" emacs-version)))) + +(defconst gptel--gh-client-id "Iv1.b507a08c87ecfe98") + +;; https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random) +(defun gptel--gh-uuid () + "Generate a UUID v4-1." + (format "%04x%04x-%04x-4%03x-8%03x-%04x%04x%04x" + (random #x10000) (random #x10000) + (random #x10000) + (random #x1000) + (random #x1000) + (random #x10000) (random #x10000) (random #x10000))) + +(defun gptel--gh-machine-id () + "Generate a machine ID." + (let ((hex-chars "0123456789abcdef") + (length 65) + hex) + (dotimes (_ length) + (setq hex (nconc hex (list (aref hex-chars (random 16)))))) + (apply #'string hex))) + +(defun gptel--gh-restore (file) + "Restore saved object from FILE." + (when (file-exists-p file) + ;; We set the coding system to `utf-8-auto-dos' when reading so that + ;; files with CR EOL can still be read properly + (let ((coding-system-for-read 'utf-8-auto-dos)) + (with-temp-buffer + (set-buffer-multibyte nil) + (insert-file-contents-literally file) + (goto-char (point-min)) + (read (current-buffer)))))) + +(defun gptel--gh-save (file obj) + "Save OBJ to FILE." + (let ((print-length nil) + (print-level nil) + (coding-system-for-write 'utf-8-unix)) + (make-directory (file-name-directory file) t) + (write-region (prin1-to-string obj) nil file nil :silent) + obj)) + +(defun gptel--gh-login() + "Manage github login." + (pcase-let (((map :device_code :user_code :verification_uri) + (gptel--url-retrieve + "https://github.com/login/device/code" + :method 'post + :headers gptel--gh-auth-common-headers + :data `( :client_id ,gptel--gh-client-id + :scope "read:user")))) + (gui-set-selection 'CLIPBOARD user_code) + (read-from-minibuffer + (format "Your one-time code %s is copied. \ +Press ENTER to open GitHub in your browser. \ +If your browser does not open automatically, browse to %s." + user_code verification_uri)) + (browse-url verification_uri) + (read-from-minibuffer "Press ENTER after authorizing.") + (thread-last + (plist-get + (gptel--url-retrieve + "https://github.com/login/oauth/access_token" + :method 'post + :headers gptel--gh-auth-common-headers + :data `( :client_id ,gptel--gh-client-id + :device_code ,device_code + :grant_type "urn:ietf:params:oauth:grant-type:device_code")) + :access_token) + (gptel--gh-save gptel-gh-github-token-file) + (setf (gptel--gh-github-token gptel-backend))))) + +(defun gptel--gh-renew-token () + "Renew session token." + (let ((token + (gptel--url-retrieve + "https://api.github.com/copilot_internal/v2/token" + :method 'get + :headers `(("authorization" + . ,(format "token %s" (gptel--gh-github-token gptel-backend))) + ,@gptel--gh-auth-common-headers)))) + (if (not (plist-get token :token)) + (progn + (setf (gptel--gh-github-token gptel-backend) nil) + (user-error "Error: You might not have access to Github Copilot Chat!")) + (thread-last + (gptel--gh-save gptel-gh-token-file token) + (setf (gptel--gh-token gptel-backend)))))) + +(defun gptel--gh-auth () + "Authenticate with GitHub Copilot API. + +We first need github authorization (github token). +Then we need a session token." + (unless (gptel--gh-github-token gptel-backend) + (let ((token (gptel--gh-restore gptel-gh-github-token-file))) + (if token + (setf (gptel--gh-github-token gptel-backend) token) + (gptel--gh-login)))) + + (when (null (gptel--gh-token gptel-backend)) + ;; try to load token from `gptel-gh-token-file' + (setf (gptel--gh-token gptel-backend) + (gptel--gh-restore gptel-gh-token-file))) + + (pcase-let (((map :token :expires_at) + (gptel--gh-token gptel-backend))) + (when (or (null token) + (and expires_at + (> (round (float-time (current-time))) + expires_at))) + (gptel--gh-renew-token)))) + +;;;###autoload +(cl-defun gptel-make-gh-copilot + (name &key curl-args request-params + (header (lambda () + (gptel--gh-auth) + `(("openai-intent" . "conversation-panel") + ("authorization" . ,(concat "Bearer " + (plist-get (gptel--gh-token gptel-backend) :token))) + ("x-request-id" . ,(gptel--gh-uuid)) + ("vscode-sessionid" . ,(or (gptel--gh-sessionid gptel-backend) "")) + ("vscode-machineid" . ,(or (gptel--gh-machineid gptel-backend) "")) + ("copilot-vision-request" . "true") + ("copilot-integration-id" . "vscode-chat")))) + (host "api.githubcopilot.com") + (protocol "https") + (endpoint "/chat/completions") + (stream t) + (models gptel--gh-models)) + "Register a Github Copilot chat backend for gptel with NAME. + +Keyword arguments: + +CURL-ARGS (optional) is a list of additional Curl arguments. + +HOST (optional) is the API host, typically \"api.githubcopilot.com\". + +MODELS is a list of available model names, as symbols. +Additionally, you can specify supported LLM capabilities like +vision or tool-use by appending a plist to the model with more +information, in the form + + (model-name . plist) + +For a list of currently recognized plist keys, see +`gptel--openai-models'. An example of a model specification +including both kinds of specs: + +:models +\\='(gpt-3.5-turbo ;Simple specs + gpt-4-turbo + (gpt-4o-mini ;Full spec + :description + \"Affordable and intelligent small model for lightweight tasks\" + :capabilities (media tool json url) + :mime-types + (\"image/jpeg\" \"image/png\" \"image/gif\" \"image/webp\"))) + +Defaults to a list of models supported by GitHub Copilot. + +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 +\"/chat/completions\". + +HEADER (optional) is for additional headers to send with each +request. It should be an alist or a function that returns an +alist, like: + ((\"Content-Type\" . \"application/json\")) + +Defaults to headers required by GitHub Copilot. + +REQUEST-PARAMS (optional) is a plist of additional HTTP request +parameters (as plist keys) and values supported by the API. Use +these to set parameters that gptel does not provide user options +for." + (declare (indent 1)) + (let ((backend (gptel--make-gh + :name name + :host host + :header header + :models (gptel--process-models models) + :protocol protocol + :endpoint endpoint + :stream stream + :request-params request-params + :curl-args curl-args + :url (concat protocol "://" host endpoint) + :machineid (gptel--gh-machine-id)))) + (setf (alist-get name gptel--known-backends nil nil #'equal) backend) + backend)) + +(provide 'gptel-gh) +;;; gptel-gh.el ends here diff --git a/gptel.el b/gptel.el index 69bc000fee..b7f8c9abf6 100644 --- a/gptel.el +++ b/gptel.el @@ -912,6 +912,19 @@ Later plists in the sequence take precedence over earlier ones." (setq rtn (plist-put rtn p v)))) rtn)) +(cl-defun gptel--url-retrieve (url &key method data headers) + "Retrieve URL synchronously with METHOD, DATA and HEADERS." + (declare (indent 1)) + (let ((url-request-method (if (eq method'post) "POST" "GET")) + (url-request-data (encode-coding-string (gptel--json-encode data) 'utf-8)) + (url-mime-accept-string "application/json") + (url-request-extra-headers + `(("content-type" . "application/json") + ,@headers))) + (with-current-buffer (url-retrieve-synchronously url 'silent) + (goto-char url-http-end-of-headers) + (gptel--json-read)))) + (defun gptel-auto-scroll () "Scroll window if LLM response continues below viewport.