config

Personal configuration.
git clone git://code.dwrz.net/config
Log | Files | Refs

gptel.el (73991B)


      1 ;;; gptel.el --- Interact with ChatGPT or other LLMs     -*- lexical-binding: t; -*-
      2 
      3 ;; Copyright (C) 2023  Karthik Chikmagalur
      4 
      5 ;; Author: Karthik Chikmagalur <karthik.chikmagalur@gmail.com>
      6 ;; Package-Version: 20241112.624
      7 ;; Package-Revision: 4aa6b7ca79b1
      8 ;; Package-Requires: ((emacs "27.1") (transient "0.4.0") (compat "29.1.4.1"))
      9 ;; Keywords: convenience
     10 ;; URL: https://github.com/karthink/gptel
     11 
     12 ;; SPDX-License-Identifier: GPL-3.0-or-later
     13 
     14 ;; This program is free software; you can redistribute it and/or modify
     15 ;; it under the terms of the GNU General Public License as published by
     16 ;; the Free Software Foundation, either version 3 of the License, or
     17 ;; (at your option) any later version.
     18 
     19 ;; This program is distributed in the hope that it will be useful,
     20 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     21 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     22 ;; GNU General Public License for more details.
     23 
     24 ;; You should have received a copy of the GNU General Public License
     25 ;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
     26 
     27 ;; This file is NOT part of GNU Emacs.
     28 
     29 ;;; Commentary:
     30 
     31 ;; gptel is a simple Large Language Model chat client, with support for multiple
     32 ;; models and backends.
     33 ;;
     34 ;; It works in the spirit of Emacs, available at any time and in any buffer.
     35 ;;
     36 ;; gptel supports
     37 ;;
     38 ;; - The services ChatGPT, Azure, Gemini, Anthropic AI, Anyscale, Together.ai,
     39 ;;   Perplexity, Anyscale, OpenRouter, Groq, PrivateGPT, DeepSeek, Cerebras,
     40 ;;   Github Models and Kagi (FastGPT & Summarizer)
     41 ;; - Local models via Ollama, Llama.cpp, Llamafiles or GPT4All
     42 ;;
     43 ;;  Additionally, any LLM service (local or remote) that provides an
     44 ;;  OpenAI-compatible API is supported.
     45 ;;
     46 ;; Features:
     47 ;; - It’s async and fast, streams responses.
     48 ;; - Interact with LLMs from anywhere in Emacs (any buffer, shell, minibuffer,
     49 ;;   wherever)
     50 ;; - LLM responses are in Markdown or Org markup.
     51 ;; - Supports conversations and multiple independent sessions.
     52 ;; - Supports multi-modal models (send images, documents).
     53 ;; - Save chats as regular Markdown/Org/Text files and resume them later.
     54 ;; - You can go back and edit your previous prompts or LLM responses when
     55 ;;   continuing a conversation.  These will be fed back to the model.
     56 ;; - Redirect prompts and responses easily
     57 ;; - Rewrite, refactor or fill in regions in buffers
     58 ;; - Write your own commands for custom tasks with a simple API.
     59 ;;
     60 ;; Requirements for ChatGPT, Azure, Gemini or Kagi:
     61 ;;
     62 ;; - You need an appropriate API key.  Set the variable `gptel-api-key' to the
     63 ;;   key or to a function of no arguments that returns the key.  (It tries to
     64 ;;   use `auth-source' by default)
     65 ;;
     66 ;;   ChatGPT is configured out of the box.  For the other sources:
     67 ;;
     68 ;; - For Azure: define a gptel-backend with `gptel-make-azure', which see.
     69 ;; - For Gemini: define a gptel-backend with `gptel-make-gemini', which see.
     70 ;; - For Anthropic (Claude): define a gptel-backend with `gptel-make-anthropic',
     71 ;;   which see
     72 ;; - For Together.ai, Anyscale, Perplexity, Groq, OpenRouter, DeepSeek, Cerebras or
     73 ;;   Github Models: define a gptel-backend with `gptel-make-openai', which see.
     74 ;; - For PrivateGPT: define a backend with `gptel-make-privategpt', which see.
     75 ;; - For Kagi: define a gptel-backend with `gptel-make-kagi', which see.
     76 ;;
     77 ;; For local models using Ollama, Llama.cpp or GPT4All:
     78 ;;
     79 ;; - The model has to be running on an accessible address (or localhost)
     80 ;; - Define a gptel-backend with `gptel-make-ollama' or `gptel-make-gpt4all',
     81 ;;   which see.
     82 ;; - Llama.cpp or Llamafiles: Define a gptel-backend with `gptel-make-openai',
     83 ;;
     84 ;; Consult the package README for examples and more help with configuring
     85 ;; backends.
     86 ;;
     87 ;; Usage:
     88 ;;
     89 ;; gptel can be used in any buffer or in a dedicated chat buffer.  The
     90 ;; interaction model is simple: Type in a query and the response will be
     91 ;; inserted below.  You can continue the conversation by typing below the
     92 ;; response.
     93 ;;
     94 ;; To use this in any buffer:
     95 ;;
     96 ;; - Call `gptel-send' to send the buffer's text up to the cursor.  Select a
     97 ;;   region to send only the region.
     98 ;;
     99 ;; - You can select previous prompts and responses to continue the conversation.
    100 ;;
    101 ;; - Call `gptel-send' with a prefix argument to access a menu where you can set
    102 ;;   your backend, model and other parameters, or to redirect the
    103 ;;   prompt/response.
    104 ;;
    105 ;; To use this in a dedicated buffer:
    106 ;; 
    107 ;; - M-x gptel: Start a chat session
    108 ;;
    109 ;; - In the chat session: Press `C-c RET' (`gptel-send') to send your prompt.
    110 ;;   Use a prefix argument (`C-u C-c RET') to access a menu.  In this menu you
    111 ;;   can set chat parameters like the system directives, active backend or
    112 ;;   model, or choose to redirect the input or output elsewhere (such as to the
    113 ;;   kill ring).
    114 ;;
    115 ;; - You can save this buffer to a file.  When opening this file, turn on
    116 ;;   `gptel-mode' before editing it to restore the conversation state and
    117 ;;   continue chatting.
    118 ;;
    119 ;; - To include media files with your request, you can add them to the context
    120 ;;   (described next), or include them as links in Org or Markdown mode chat
    121 ;;   buffers.  Sending media is disabled by default, you can turn it on globally
    122 ;;   via `gptel-track-media', or locally in a chat buffer via the header line.
    123 ;; 
    124 ;; Include more context with requests:
    125 ;;
    126 ;; If you want to provide the LLM with more context, you can add arbitrary
    127 ;; regions, buffers or files to the query with `gptel-add'.  To add text or
    128 ;; media files, call `gptel-add' in Dired or use the dedicated `gptel-add-file'.
    129 ;;
    130 ;; You can also add context from gptel's menu instead (gptel-send with a prefix
    131 ;; arg), as well as examine or modify context.
    132 ;;
    133 ;; When context is available, gptel will include it with each LLM query.
    134 ;;
    135 ;; Rewrite/refactor interface
    136 ;;
    137 ;; In any buffer: with a region selected, you can rewrite prose, refactor code
    138 ;; or fill in the region.  Use gptel's menu (C-u M-x `gptel-send') to access
    139 ;; this feature.
    140 ;;
    141 ;; gptel in Org mode:
    142 ;;
    143 ;; gptel offers a few extra conveniences in Org mode.
    144 ;; - You can limit the conversation context to an Org heading with
    145 ;;   `gptel-org-set-topic'.
    146 ;;   
    147 ;; - You can have branching conversations in Org mode, where each hierarchical
    148 ;;   outline path through the document is a separate conversation branch.
    149 ;;   See the variable `gptel-org-branching-context'.
    150 ;;   
    151 ;; - You can declare the gptel model, backend, temperature, system message and
    152 ;;   other parameters as Org properties with the command
    153 ;;   `gptel-org-set-properties'.  gptel queries under the corresponding heading
    154 ;;   will always use these settings, allowing you to create mostly reproducible
    155 ;;   LLM chat notebooks.
    156 ;;
    157 ;; Finally, gptel offers a general purpose API for writing LLM ineractions
    158 ;; that suit your workflow, see `gptel-request'.
    159 
    160 ;;; Code:
    161 (declare-function markdown-mode "markdown-mode")
    162 (declare-function gptel-curl-get-response "gptel-curl")
    163 (declare-function gptel-menu "gptel-transient")
    164 (declare-function gptel-system-prompt "gptel-transient")
    165 (declare-function pulse-momentary-highlight-region "pulse")
    166 
    167 (declare-function ediff-make-cloned-buffer "ediff-util")
    168 (declare-function ediff-regions-internal "ediff")
    169 
    170 (declare-function gptel-org--create-prompt "gptel-org")
    171 (declare-function gptel-org-set-topic "gptel-org")
    172 (declare-function gptel-org--save-state "gptel-org")
    173 (declare-function gptel-org--restore-state "gptel-org")
    174 (declare-function gptel--stream-convert-markdown->org "gptel-org")
    175 (declare-function gptel--convert-markdown->org "gptel-org")
    176 (define-obsolete-function-alias
    177   'gptel-set-topic 'gptel-org-set-topic "0.7.5")
    178 
    179 (eval-when-compile
    180   (require 'subr-x)
    181   (require 'cl-lib))
    182 (require 'compat nil t)
    183 (require 'url)
    184 (require 'map)
    185 (require 'text-property-search)
    186 (require 'cl-generic)
    187 (require 'gptel-openai)
    188 
    189 (with-eval-after-load 'org
    190   (require 'gptel-org))
    191 
    192 
    193 ;;; User options
    194 
    195 (defgroup gptel nil
    196   "Interact with LLMs from anywhere in Emacs."
    197   :group 'hypermedia)
    198 
    199 ;; (defcustom gptel-host "api.openai.com"
    200 ;;   "The API host queried by gptel."
    201 ;;   :group 'gptel
    202 ;;   :type 'string)
    203 (make-obsolete-variable
    204  'gptel-host
    205  "Use `gptel-make-openai' instead."
    206  "0.5.0")
    207 
    208 (defcustom gptel-proxy ""
    209   "Path to a proxy to use for gptel interactions.
    210 Passed to curl via --proxy arg, for example \"proxy.yourorg.com:80\"
    211 Leave it empty if you don't use a proxy."
    212   :type 'string)
    213 
    214 (defcustom gptel-api-key #'gptel-api-key-from-auth-source
    215   "An API key (string) for the default LLM backend.
    216 
    217 OpenAI by default.
    218 
    219 Can also be a function of no arguments that returns an API
    220 key (more secure) for the active backend."
    221   :type '(choice
    222           (string :tag "API key")
    223           (function :tag "Function that returns the API key")))
    224 
    225 (defcustom gptel-stream t
    226   "Stream responses from the LLM as they are received.
    227 
    228 This option is ignored unless
    229 - the LLM backend supports streaming, and
    230 - Curl is in use (see `gptel-use-curl')
    231 
    232 When set to nil, Emacs waits for the full response and inserts it
    233 all at once.  This wait is asynchronous.
    234 
    235 \='tis a bit silly."
    236   :type 'boolean)
    237 (make-obsolete-variable 'gptel-playback 'gptel-stream "0.3.0")
    238 
    239 (defcustom gptel-use-curl (and (executable-find "curl") t)
    240   "Whether gptel should prefer Curl when available."
    241   :type 'boolean)
    242 
    243 (defcustom gptel-curl-file-size-threshold 130000
    244   "Size threshold for using file input with Curl.
    245 
    246 Specifies the size threshold for when to use a temporary file to pass data to
    247 Curl in GPTel queries.  If the size of the data to be sent exceeds this
    248 threshold, the data is written to a temporary file and passed to Curl using the
    249 `--data-binary' option with a file reference.  Otherwise, the data is passed
    250 directly as a command-line argument.
    251 
    252 The value is an integer representing the number of bytes.
    253 
    254 Adjusting this value may be necessary depending on the environment
    255 and the typical size of the data being sent in GPTel queries.
    256 A larger value may improve performance by avoiding the overhead of creating
    257 temporary files for small data payloads, while a smaller value may be needed
    258 if the command-line argument size is limited by the operating system."
    259   :type 'natnum)
    260 
    261 (defcustom gptel-response-filter-functions
    262   (list #'gptel--convert-org)
    263   "Abnormal hook for transforming the response from an LLM.
    264 
    265 This is used to format the response in some way, such as filling
    266 paragraphs, adding annotations or recording information in the
    267 response like links.
    268 
    269 Each function in this hook receives two arguments, the response
    270 string to transform and the LLM interaction buffer.  It
    271 should return the transformed string.
    272 
    273 NOTE: This is only used for non-streaming responses.  To
    274 transform streaming responses, use `gptel-post-stream-hook' and
    275 `gptel-post-response-functions'."
    276   :type 'hook)
    277 
    278 (defcustom gptel-pre-response-hook nil
    279   "Hook run before inserting the LLM response into the current buffer.
    280 
    281 This hook is called in the buffer where the LLM response will be
    282 inserted.
    283 
    284 Note: this hook only runs if the request succeeds."
    285   :type 'hook)
    286 
    287 (define-obsolete-variable-alias
    288   'gptel-post-response-hook 'gptel-post-response-functions
    289   "0.6.0"
    290   "Post-response functions are now called with two arguments: the
    291 start and end buffer positions of the response.")
    292 
    293 (defcustom gptel-post-response-functions nil
    294   "Abnormal hook run after inserting the LLM response into the current buffer.
    295 
    296 This hook is called in the buffer to which the LLM response is
    297 sent, and after the full response has been inserted.  Each
    298 function is called with two arguments: the response beginning and
    299 end positions.
    300 
    301 Note: this hook runs even if the request fails.  In this case the
    302 response beginning and end positions are both the cursor position
    303 at the time of the request."
    304   :type 'hook)
    305 
    306 ;; (defcustom gptel-pre-stream-insert-hook nil
    307 ;;   "Hook run before each insertion of the LLM's streaming response.
    308 
    309 ;; This hook is called in the buffer from which the prompt was sent
    310 ;; to the LLM, immediately before text insertion."
    311 ;;   :group 'gptel
    312 ;;   :type 'hook)
    313 
    314 (defcustom gptel-post-stream-hook nil
    315   "Hook run after each insertion of the LLM's streaming response.
    316 
    317 This hook is called in the buffer from which the prompt was sent
    318 to the LLM, and after a text insertion."
    319   :type 'hook)
    320 
    321 (defcustom gptel-save-state-hook nil
    322   "Hook run before gptel saves model parameters to a file.
    323 
    324 You can use this hook to store additional conversation state or
    325 model parameters to the chat buffer, or to modify the buffer in
    326 some other way."
    327   :type 'hook)
    328 
    329 (defcustom gptel-default-mode (if (fboundp 'markdown-mode)
    330 				  'markdown-mode
    331 				'text-mode)
    332   "The default major mode for dedicated chat buffers.
    333 
    334 If `markdown-mode' is available, it is used.  Otherwise gptel
    335 defaults to `text-mode'."
    336   :type 'function)
    337 
    338 ;; TODO: Handle `prog-mode' using the `comment-start' variable
    339 (defcustom gptel-prompt-prefix-alist
    340   '((markdown-mode . "### ")
    341     (org-mode . "*** ")
    342     (text-mode . "### "))
    343   "String used as a prefix to the query being sent to the LLM.
    344 
    345 This is meant for the user to distinguish between queries and
    346 responses, and is removed from the query before it is sent.
    347 
    348 This is an alist mapping major modes to the prefix strings.  This
    349 is only inserted in dedicated gptel buffers."
    350   :type '(alist :key-type symbol :value-type string))
    351 
    352 (defcustom gptel-response-prefix-alist
    353   '((markdown-mode . "")
    354     (org-mode . "")
    355     (text-mode . ""))
    356   "String inserted before the response from the LLM.
    357 
    358 This is meant for the user to distinguish between queries and
    359 responses.
    360 
    361 This is an alist mapping major modes to the reply prefix strings.  This
    362 is only inserted in dedicated gptel buffers before the AI's response."
    363   :type '(alist :key-type symbol :value-type string))
    364 
    365 (defcustom gptel-use-header-line t
    366   "Whether `gptel-mode' should use header-line for status information.
    367 
    368 When set to nil, use the mode line for (minimal) status
    369 information and the echo area for messages."
    370   :type 'boolean)
    371 
    372 (defcustom gptel-display-buffer-action '(pop-to-buffer)
    373   "The action used to display gptel chat buffers.
    374 
    375 The gptel buffer is displayed in a window using
    376 
    377   (display-buffer BUFFER gptel-display-buffer-action)
    378 
    379 The value of this option has the form (FUNCTION . ALIST),
    380 where FUNCTION is a function or a list of functions.  Each such
    381 function should accept two arguments: a buffer to display and an
    382 alist of the same form as ALIST.  See info node `(elisp)Choosing
    383 Window' for details."
    384   :type display-buffer--action-custom-type)
    385 
    386 (defcustom gptel-crowdsourced-prompts-file
    387   (let ((cache-dir (or (eval-when-compile
    388 			 (require 'xdg)
    389 			 (xdg-cache-home))
    390                        user-emacs-directory)))
    391     (expand-file-name "gptel-crowdsourced-prompts.csv" cache-dir))
    392   "File used to store crowdsourced system prompts.
    393 
    394 These are prompts cached from an online source (see
    395 `gptel--crowdsourced-prompts-url'), and can be set from the
    396 transient menu interface provided by `gptel-menu'."
    397   :type 'file)
    398 
    399 ;; Model and interaction parameters
    400 (defcustom gptel-directives
    401   '((default . "You are a large language model living in Emacs and a helpful assistant. Respond concisely.")
    402     (programming . "You are a large language model and a careful programmer. Provide code and only code as output without any additional text, prompt or note.")
    403     (writing . "You are a large language model and a writing assistant. Respond concisely.")
    404     (chat . "You are a large language model and a conversation partner. Respond concisely."))
    405   "System prompts (directives) for the LLM.
    406 
    407 These are system instructions sent at the beginning of each
    408 request to the LLM.
    409 
    410 Each entry in this alist maps a symbol naming the directive to
    411 the string that is sent.  To set the directive for a chat session
    412 interactively call `gptel-send' with a prefix argument."
    413   :safe #'always
    414   :type '(alist :key-type symbol :value-type string))
    415 
    416 (defvar gptel--system-message (alist-get 'default gptel-directives)
    417   "The system message used by gptel.")
    418 (put 'gptel--system-message 'safe-local-variable #'always)
    419 
    420 (defcustom gptel-max-tokens nil
    421   "Max tokens per response.
    422 
    423 This is roughly the number of words in the response.  100-300 is a
    424 reasonable range for short answers, 400 or more for longer
    425 responses.
    426 
    427 To set the target token count for a chat session interactively
    428 call `gptel-send' with a prefix argument."
    429   :safe #'always
    430   :type '(choice (natnum :tag "Specify Token count")
    431                  (const :tag "Default" nil)))
    432 
    433 (defcustom gptel-model 'gpt-4o-mini
    434   "GPT Model for chat.
    435 
    436 The name of the model, as a symbol.  This is the name as expected
    437 by the LLM provider's API.
    438 
    439 The current options for ChatGPT are
    440 - `gpt-3.5-turbo'
    441 - `gpt-3.5-turbo-16k'
    442 - `gpt-4o-mini'
    443 - `gpt-4'
    444 - `gpt-4o'
    445 - `gpt-4-turbo'
    446 - `gpt-4-turbo-preview'
    447 - `gpt-4-32k'
    448 - `gpt-4-1106-preview'
    449 
    450 To set the model for a chat session interactively call
    451 `gptel-send' with a prefix argument."
    452   :safe #'always
    453   :type '(choice
    454           (symbol :tag "Specify model name")
    455           (const :tag "GPT 4 omni mini" gpt-4o-mini)
    456           (const :tag "GPT 3.5 turbo" gpt-3.5-turbo)
    457           (const :tag "GPT 3.5 turbo 16k" gpt-3.5-turbo-16k)
    458           (const :tag "GPT 4" gpt-4)
    459           (const :tag "GPT 4 omni" gpt-4o)
    460           (const :tag "GPT 4 turbo" gpt-4-turbo)
    461           (const :tag "GPT 4 turbo (preview)" gpt-4-turbo-preview)
    462           (const :tag "GPT 4 32k" gpt-4-32k)
    463           (const :tag "GPT 4 1106 (preview)" gpt-4-1106-preview)))
    464 
    465 (defcustom gptel-temperature 1.0
    466   "\"Temperature\" of the LLM response.
    467 
    468 This is a number between 0.0 and 2.0 that controls the randomness
    469 of the response, with 2.0 being the most random.
    470 
    471 To set the temperature for a chat session interactively call
    472 `gptel-send' with a prefix argument."
    473   :safe #'always
    474   :type 'number)
    475 
    476 (defvar gptel--known-backends)
    477 
    478 (defconst gptel--openai-models
    479   '((gpt-4o
    480      :description "Advanced model for complex tasks; cheaper & faster than GPT-Turbo"
    481      :capabilities (media tool json url)
    482      :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp")
    483      :context-window 128
    484      :input-cost 2.50
    485      :output-cost 10
    486      :cutoff-date "2023-10")
    487     (gpt-4o-mini
    488      :description "Cheap model for fast tasks; cheaper & more capable than GPT-3.5 Turbo"
    489      :capabilities (media tool json url)
    490      :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp")
    491      :context-window 128
    492      :input-cost 0.15
    493      :output-cost 0.60
    494      :cutoff-date "2023-10")
    495     (gpt-4-turbo
    496      :description "Previous high-intelligence model"
    497      :capabilities (media tool url)
    498      :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp")
    499      :context-window 128
    500      :input-cost 10
    501      :output-cost 30
    502      :cutoff-date "2023-12")
    503     ;; points to gpt-4-0613
    504     (gpt-4
    505      :description "GPT-4 snapshot from June 2023 with improved function calling support"
    506      :context-window 8.192
    507      :input-cost 30
    508      :output-cost 60
    509      :cutoff-date "2023-09")
    510     (gpt-4-turbo-preview
    511      :description "Points to gpt-4-0125-preview"
    512      :context-window 128
    513      :input-cost 10
    514      :output-cost 30
    515      :cutoff-date "2023-12")
    516     (gpt-4-0125-preview
    517      :description "GPT-4 Turbo preview model intended to reduce cases of “laziness”"
    518      :context-window 128
    519      :input-cost 10
    520      :output-cost 30
    521      :cutoff-date "2023-12")
    522     (o1-preview
    523      :description "Reasoning model designed to solve hard problems across domains"
    524      :context-window 128
    525      :input-cost 15
    526      :output-cost 60
    527      :cutoff-date "2023-10"
    528      :capabilities (nosystem)
    529      :request-params (:stream :json-false))
    530     (o1-mini
    531      :description "Faster and cheaper reasoning model good at coding, math, and science"
    532      :context-window 128
    533      :input-cost 3
    534      :output-cost 12
    535      :cutoff-date "2023-10"
    536      :capabilities (nosystem)
    537      :request-params (:stream :json-false))
    538     ;; limited information available
    539     (gpt-4-32k
    540      :input-cost 60
    541      :output-cost 120)
    542     (gpt-4-1106-preview
    543      :description "Preview model with improved function calling support"
    544      :context-window 128
    545      :input-cost 10
    546      :output-cost 30
    547      :cutoff-date "2023-04")
    548     (gpt-3.5-turbo
    549      :description "More expensive & less capable than GPT-4o-mini; use that instead"
    550      :capabilities (tool)
    551      :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp")
    552      :context-window 16.358
    553      :input-cost 0.50
    554      :output-cost 1.50
    555      :cutoff-date "2021-09")
    556     (gpt-3.5-turbo-16k
    557      :description "More expensive & less capable than GPT-4o-mini; use that instead"
    558      :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp")
    559      :context-window 16.385
    560      :input-cost 3
    561      :output-cost 4
    562      :cutoff-date "2021-09"))
    563   "List of available OpenAI models and associated properties.
    564 Keys:
    565 
    566 - `:description': a brief description of the model.
    567 
    568 - `:capabilities': a list of capabilities supported by the model.
    569 
    570 - `:mime-types': a list of supported MIME types for media files.
    571 
    572 - `:context-window': the context window size, in thousands of tokens.
    573 
    574 - `:input-cost': the input cost, in US dollars per million tokens.
    575 
    576 - `:output-cost': the output cost, in US dollars per million tokens.
    577 
    578 - `:cutoff-date': the knowledge cutoff date.
    579 
    580 - `:request-params': a plist of additional request parameters to
    581   include when using this model.
    582 
    583 Information about the OpenAI models was obtained from the following
    584 sources:
    585 
    586 - <https://openai.com/pricing>
    587 - <https://platform.openai.com/docs/models>")
    588 
    589 (defvar gptel--openai
    590   (gptel-make-openai
    591       "ChatGPT"
    592     :key 'gptel-api-key
    593     :stream t
    594     :models gptel--openai-models))
    595 
    596 (defcustom gptel-backend gptel--openai
    597   "LLM backend to use.
    598 
    599 This is the default \"backend\", an object of type
    600 `gptel-backend' containing connection, authentication and model
    601 information.
    602 
    603 A backend for ChatGPT is pre-defined by gptel.  Backends for
    604 other LLM providers (local or remote) may be constructed using
    605 one of the available backend creation functions:
    606 - `gptel-make-openai'
    607 - `gptel-make-azure'
    608 - `gptel-make-ollama'
    609 - `gptel-make-gpt4all'
    610 - `gptel-make-gemini'
    611 See their documentation for more information and the package
    612 README for examples."
    613   :safe #'always
    614   :type `(choice
    615           (const :tag "ChatGPT" ,gptel--openai)
    616           (restricted-sexp :match-alternatives (gptel-backend-p 'nil)
    617 			   :tag "Other backend")))
    618 
    619 (defvar gptel-expert-commands nil
    620   "Whether experimental gptel options should be enabled.
    621 
    622 This opens up advanced options in `gptel-menu'.")
    623 
    624 (defvar-local gptel--bounds nil)
    625 (put 'gptel--bounds 'safe-local-variable #'always)
    626 
    627 (defvar gptel--num-messages-to-send nil)
    628 (put 'gptel--num-messages-to-send 'safe-local-variable #'always)
    629 
    630 (defcustom gptel-log-level nil
    631   "Logging level for gptel.
    632 
    633 This is one of nil or the symbols info and debug:
    634 
    635 nil: Don't log responses
    636 info: Log request and response bodies
    637 debug: Log request/response bodies, headers and all other
    638        connection settings.
    639 
    640 When non-nil, information is logged to `gptel--log-buffer-name',
    641 which see."
    642   :type '(choice
    643           (const :tag "No logging" nil)
    644           (const :tag "Limited" info)
    645           (const :tag "Full" debug)))
    646 (make-obsolete-variable
    647  'gptel--debug 'gptel-log-level "0.6.5")
    648 
    649 (defcustom gptel-track-response t
    650   "Distinguish between user messages and LLM responses.
    651 
    652 When creating a prompt to send to the LLM, gptel distinguishes
    653 between text entered by the user and past LLM responses.  This
    654 distinction is necessary for back-and-forth conversation with an
    655 LLM.
    656 
    657 In regular Emacs buffers you can turn this behavior off by
    658 setting `gptel-track-response' to nil.  All text, including
    659 past LLM responses, is then treated as user input when sending
    660 queries.
    661 
    662 This variable has no effect in dedicated chat buffers (buffers
    663 with `gptel-mode' enabled), where user prompts and responses are
    664 always handled separately."
    665   :type 'boolean)
    666 
    667 (defcustom gptel-track-media nil
    668   "Whether supported media in chat buffers should be sent.
    669 
    670 When the active `gptel-model' supports it, gptel can send images
    671 or other media from links in chat buffers to the LLM.  To use
    672 this, the following steps are required.
    673 
    674 1. `gptel-track-media' (this variable) should be non-nil
    675 
    676 2. The LLM should provide vision or document support.  Currently,
    677 only the OpenAI, Anthropic and Ollama APIs are supported.  See
    678 the documentation of `gptel-make-openai', `gptel-make-anthropic'
    679 and `gptel-make-ollama' resp. for details on how to specify media
    680 support for models.
    681 
    682 3. Only \"standalone\" links in chat buffers are considered.
    683 These are links on their own line with no surrounding text.
    684 Further:
    685 
    686 - In Org mode, only files or URLs of the form
    687   [[/path/to/media][bracket links]] and <angle/link/path>
    688   are sent.
    689 
    690 - In Markdown mode, only files or URLS of the form
    691   [bracket link](/path/to/media) and <angle/link/path>
    692   are sent.
    693 
    694 This option has no effect in non-chat buffers.  To include
    695 media (including images) more generally, use `gptel-add'."
    696   :type 'boolean)
    697 
    698 (defcustom gptel-use-context 'system
    699   "Where in the request to inject gptel's additional context.
    700 
    701 gptel always includes the active region or the buffer up to the
    702 cursor in the request to the LLM.  Additionally, you can add
    703 other buffers or their regions to the context with
    704 `gptel-add-context', or from gptel's menu.  This data will be
    705 sent with every request.
    706 
    707 This option controls whether and where this additional context is
    708 included in the request.
    709 
    710 Currently supported options are:
    711 
    712     nil     - Do not use the context.
    713     system  - Include the context with the system message.
    714     user    - Include the context with the user prompt."
    715   :group 'gptel
    716   :type '(choice
    717           (const :tag "Don't include context" nil)
    718           (const :tag "With system message" system)
    719           (const :tag "With user prompt" user)))
    720 
    721 (defvar-local gptel--old-header-line nil)
    722 
    723 (defvar gptel-context--alist nil
    724   "List of gptel's context sources.
    725 
    726 Each entry is of the form
    727  (buffer . (overlay1 overlay2 ...))
    728 or
    729  (\"path/to/file\").")
    730 
    731 
    732 ;;; Utility functions
    733 
    734 (defun gptel-api-key-from-auth-source (&optional host user)
    735   "Lookup api key in the auth source.
    736 By default, the LLM host for the active backend is used as HOST,
    737 and \"apikey\" as USER."
    738   (if-let ((secret
    739             (plist-get
    740              (car (auth-source-search
    741                    :host (or host (gptel-backend-host gptel-backend))
    742                    :user (or user "apikey")
    743                    :require '(:secret)))
    744                               :secret)))
    745       (if (functionp secret)
    746           (encode-coding-string (funcall secret) 'utf-8)
    747         secret)
    748     (user-error "No `gptel-api-key' found in the auth source")))
    749 
    750 ;; FIXME Should we utf-8 encode the api-key here?
    751 (defun gptel--get-api-key (&optional key)
    752   "Get api key from KEY, or from `gptel-api-key'."
    753   (when-let ((key-sym (or key (gptel-backend-key gptel-backend))))
    754     (cl-typecase key-sym
    755       (function (funcall key-sym))
    756       (string key-sym)
    757       (symbol (if-let ((val (symbol-value key-sym)))
    758                   (gptel--get-api-key
    759                    (symbol-value key-sym))
    760                 (error "`gptel-api-key' is not valid")))
    761       (t (error "`gptel-api-key' is not valid")))))
    762 
    763 (defsubst gptel--to-number (val)
    764   "Ensure VAL is a number."
    765   (cond
    766    ((numberp val) val)
    767    ((stringp val) (string-to-number val))
    768    ((error "%S cannot be converted to a number" val))))
    769 
    770 (defsubst gptel--to-string (s)
    771   "Convert S to a string, if possible."
    772   (cl-etypecase s
    773     (symbol (symbol-name s))
    774     (string s)
    775     (number (number-to-string s))))
    776 
    777 (defsubst gptel--intern (s)
    778   "Intern S, if possible."
    779   (cl-etypecase s
    780     (symbol s)
    781     (string (intern s))))
    782 
    783 (defun gptel--merge-plists (&rest plists)
    784   "Merge PLISTS, altering the first one.
    785 
    786 Later plists in the sequence take precedence over earlier ones."
    787   (let (;; (rtn (copy-sequence (pop plists)))
    788         (rtn (pop plists))
    789         p v ls)
    790     (while plists
    791       (setq ls (pop plists))
    792       (while ls
    793         (setq p (pop ls) v (pop ls))
    794         (setq rtn (plist-put rtn p v))))
    795     rtn))
    796 (defun gptel-auto-scroll ()
    797   "Scroll window if LLM response continues below viewport.
    798 
    799 Note: This will move the cursor."
    800   (when-let ((win (get-buffer-window (current-buffer) 'visible))
    801              ((not (pos-visible-in-window-p (point) win)))
    802              (scroll-error-top-bottom t))
    803     (condition-case nil
    804         (with-selected-window win
    805           (scroll-up-command))
    806       (error nil))))
    807 
    808 (defun gptel-beginning-of-response (&optional _ _ arg)
    809   "Move point to the beginning of the LLM response ARG times."
    810   (interactive "p")
    811   ;; FIXME: Only works for arg == 1
    812   (gptel-end-of-response nil nil (- (or arg 1))))
    813 
    814 (defun gptel-end-of-response (&optional _ _ arg)
    815   "Move point to the end of the LLM response ARG times."
    816   (interactive (list nil nil
    817                      (prefix-numeric-value current-prefix-arg)))
    818   (unless arg (setq arg 1))
    819   (let ((search (if (> arg 0)
    820                     #'text-property-search-forward
    821                   #'text-property-search-backward)))
    822     (dotimes (_ (abs arg))
    823       (funcall search 'gptel 'response t)
    824       (if (> arg 0)
    825           (when (looking-at (concat "\n\\{1,2\\}"
    826                                     (regexp-quote
    827                                      (gptel-prompt-prefix-string))
    828                                     "?"))
    829             (goto-char (match-end 0)))
    830         (when (looking-back (concat (regexp-quote
    831                                      (gptel-response-prefix-string))
    832                                     "?")
    833                             (point-min))
    834           (goto-char (match-beginning 0)))))))
    835 
    836 (defmacro gptel--at-word-end (&rest body)
    837   "Execute BODY at end of the current word or punctuation."
    838   `(save-excursion
    839      (skip-syntax-forward "w.")
    840      ,(macroexp-progn body)))
    841 
    842 (defun gptel-prompt-prefix-string ()
    843   "Prefix before user prompts in `gptel-mode'."
    844   (or (alist-get major-mode gptel-prompt-prefix-alist) ""))
    845 
    846 (defun gptel-response-prefix-string ()
    847   "Prefix before LLM responses in `gptel-mode'."
    848   (or (alist-get major-mode gptel-response-prefix-alist) ""))
    849 
    850 (defsubst gptel--trim-prefixes (s)
    851   "Remove prompt/response prefixes from string S."
    852   (string-trim s
    853    (format "[\t\r\n ]*\\(?:%s\\)?[\t\r\n ]*"
    854              (regexp-quote (gptel-prompt-prefix-string)))
    855    (format "[\t\r\n ]*\\(?:%s\\)?[\t\r\n ]*"
    856            (regexp-quote (gptel-response-prefix-string)))))
    857 
    858 (defsubst gptel--link-standalone-p (beg end)
    859   "Return non-nil if positions BEG and END are isolated.
    860 
    861 This means the extent from BEG to END is the only non-whitespace
    862 content on this line."
    863   (save-excursion
    864     (and (= beg (progn (goto-char beg) (beginning-of-line)
    865                        (skip-chars-forward "\t ")
    866                        (point)))
    867          (= end (progn (goto-char end) (end-of-line)
    868                        (skip-chars-backward "\t ")
    869                        (point))))))
    870 
    871 (defvar-local gptel--backend-name nil
    872   "Store to persist backend name across Emacs sessions.
    873 
    874 Note: Changing this variable does not affect gptel\\='s behavior
    875 in any way.")
    876 (put 'gptel--backend-name 'safe-local-variable #'always)
    877 
    878 ;;;; Model interface
    879 ;; NOTE: This interface would be simpler to implement as a defstruct.  But then
    880 ;; users cannot set `gptel-model' to a symbol/string directly, or we'd need
    881 ;; another map from these symbols to the actual model structs.
    882 
    883 (defsubst gptel--model-name (model)
    884   "Get name of gptel MODEL."
    885   (gptel--to-string model))
    886 
    887 (defsubst gptel--model-capabilities (model)
    888   "Get MODEL capabilities."
    889   (get model :capabilities))
    890 
    891 (defsubst gptel--model-mimes (model)
    892   "Get supported mime-types for MODEL."
    893   (get model :mime-types))
    894 
    895 (defsubst gptel--model-capable-p (cap &optional model)
    896   "Return non-nil if MODEL supports capability CAP."
    897   (memq cap (gptel--model-capabilities
    898              (or model gptel-model))))
    899 
    900 ;; TODO Handle model mime specifications like "image/*"
    901 (defsubst gptel--model-mime-capable-p (mime &optional model)
    902   "Return non nil if MODEL can understand MIME type."
    903   (car-safe (member mime (gptel--model-mimes
    904                         (or model gptel-model)))))
    905 
    906 (defsubst gptel--model-request-params (model)
    907   "Get model-specific request parameters for MODEL."
    908   (get model :request-params))
    909 
    910 ;;;; File handling
    911 (defun gptel--base64-encode (file)
    912   "Encode FILE as a base64 string.
    913 
    914 FILE is assumed to exist and be a regular file."
    915   (with-temp-buffer
    916     (insert-file-contents-literally file)
    917     (base64-encode-region (point-min) (point-max)
    918                           :no-line-break)
    919     (buffer-string)))
    920 
    921 ;;;; Response text recognition
    922 
    923 (defun gptel--get-buffer-bounds ()
    924   "Return the gptel response boundaries in the buffer as an alist."
    925   (save-excursion
    926     (save-restriction
    927       (widen)
    928       (goto-char (point-max))
    929       (let ((prop) (bounds))
    930         (while (setq prop (text-property-search-backward
    931                            'gptel 'response t))
    932           (push (cons (prop-match-beginning prop)
    933                       (prop-match-end prop))
    934                 bounds))
    935         bounds))))
    936 
    937 (defun gptel--get-bounds ()
    938   "Return the gptel response boundaries around point."
    939   (let (prop)
    940     (save-excursion
    941       (when (text-property-search-backward
    942              'gptel 'response t)
    943         (when (setq prop (text-property-search-forward
    944                           'gptel 'response t))
    945           (cons (prop-match-beginning prop)
    946                       (prop-match-end prop)))))))
    947 
    948 (defun gptel--in-response-p (&optional pt)
    949   "Check if position PT is inside a gptel response."
    950   (get-char-property (or pt (point)) 'gptel))
    951 
    952 (defun gptel--at-response-history-p (&optional pt)
    953   "Check if gptel response at position PT has variants."
    954   (get-char-property (or pt (point)) 'gptel-history))
    955 
    956 (defvar gptel--mode-description-alist
    957   '((js2-mode      . "Javascript")
    958     (sh-mode       . "Shell")
    959     (enh-ruby-mode . "Ruby")
    960     (yaml-mode     . "Yaml")
    961     (yaml-ts-mode  . "Yaml")
    962     (rustic-mode   . "Rust"))
    963   "Mapping from unconventionally named major modes to languages.
    964 
    965 This is used when generating system prompts for rewriting and
    966 when including context from these major modes.")
    967 
    968 (defun gptel--strip-mode-suffix (mode-sym)
    969   "Remove the -mode suffix from MODE-SYM.
    970 
    971 MODE-SYM is typically a major-mode symbol."
    972   (or (alist-get mode-sym gptel--mode-description-alist)
    973       (let ((mode-name (thread-last
    974                          (symbol-name mode-sym)
    975                          (string-remove-suffix "-mode")
    976                          (string-remove-suffix "-ts"))))
    977         (if (provided-mode-derived-p
    978              mode-sym 'prog-mode 'text-mode 'tex-mode)
    979             mode-name ""))))
    980 
    981 
    982 ;;; Logging
    983 
    984 (defconst gptel--log-buffer-name "*gptel-log*"
    985   "Log buffer for gptel.")
    986 
    987 (declare-function json-pretty-print "json")
    988 
    989 (defun gptel--log (data &optional type no-json)
    990   "Log DATA to `gptel--log-buffer-name'.
    991 
    992 TYPE is a label for data being logged.  DATA is assumed to be
    993 Valid JSON unless NO-JSON is t."
    994   (with-current-buffer (get-buffer-create gptel--log-buffer-name)
    995     (let ((p (goto-char (point-max))))
    996       (unless (bobp) (insert "\n"))
    997       (insert (format "{\"gptel\": \"%s\", " (or type "none"))
    998               (format-time-string "\"timestamp\": \"%Y-%m-%d %H:%M:%S\"}\n")
    999               data)
   1000       (unless no-json (ignore-errors (json-pretty-print p (point)))))))
   1001 
   1002 
   1003 ;;; Saving and restoring state
   1004 
   1005 (defun gptel--restore-state ()
   1006   "Restore gptel state when turning on `gptel-mode'."
   1007   (when (buffer-file-name)
   1008     (if (derived-mode-p 'org-mode)
   1009         (progn
   1010           (require 'gptel-org)
   1011           (gptel-org--restore-state))
   1012       (when gptel--bounds
   1013         (mapc (pcase-lambda (`(,beg . ,end))
   1014                 (put-text-property beg end 'gptel 'response))
   1015               gptel--bounds)
   1016         (message "gptel chat restored."))
   1017       (when gptel--backend-name
   1018         (if-let ((backend (alist-get
   1019                            gptel--backend-name gptel--known-backends
   1020                            nil nil #'equal)))
   1021             (setq-local gptel-backend backend)
   1022           (message
   1023            (substitute-command-keys
   1024             (concat
   1025              "Could not activate gptel backend \"%s\"!  "
   1026              "Switch backends with \\[universal-argument] \\[gptel-send]"
   1027              " before using gptel."))
   1028            gptel--backend-name))))))
   1029 
   1030 (defun gptel--save-state ()
   1031   "Write the gptel state to the buffer.
   1032 
   1033 This saves chat metadata when writing the buffer to disk.  To
   1034 restore a chat session, turn on `gptel-mode' after opening the
   1035 file."
   1036   (run-hooks 'gptel-save-state-hook)
   1037   (if (derived-mode-p 'org-mode)
   1038       (progn
   1039         (require 'gptel-org)
   1040         (gptel-org--save-state))
   1041     (let ((print-escape-newlines t))
   1042       (save-excursion
   1043         (save-restriction
   1044           (add-file-local-variable 'gptel-model gptel-model)
   1045           (add-file-local-variable 'gptel--backend-name
   1046                                    (gptel-backend-name gptel-backend))
   1047           (unless (equal (default-value 'gptel-temperature) gptel-temperature)
   1048             (add-file-local-variable 'gptel-temperature gptel-temperature))
   1049           (unless (string= (default-value 'gptel--system-message)
   1050                            gptel--system-message)
   1051             (add-file-local-variable 'gptel--system-message gptel--system-message))
   1052           (when gptel-max-tokens
   1053             (add-file-local-variable 'gptel-max-tokens gptel-max-tokens))
   1054           (when (natnump gptel--num-messages-to-send)
   1055             (add-file-local-variable 'gptel--num-messages-to-send
   1056                                      gptel--num-messages-to-send))
   1057           (add-file-local-variable 'gptel--bounds (gptel--get-buffer-bounds)))))))
   1058 
   1059 
   1060 ;;; Minor mode and UI
   1061 
   1062 ;; NOTE: It's not clear that this is the best strategy:
   1063 (add-to-list 'text-property-default-nonsticky '(gptel . t))
   1064 
   1065 ;;;###autoload
   1066 (define-minor-mode gptel-mode
   1067   "Minor mode for interacting with LLMs."
   1068   :lighter " GPT"
   1069   :keymap
   1070   (let ((map (make-sparse-keymap)))
   1071     (define-key map (kbd "C-c RET") #'gptel-send)
   1072     map)
   1073   (if gptel-mode
   1074       (progn
   1075         (unless (or (derived-mode-p 'org-mode 'markdown-mode)
   1076                     (eq major-mode 'text-mode))
   1077           (gptel-mode -1)
   1078           (user-error (format "`gptel-mode' is not supported in `%s'." major-mode)))
   1079         (add-hook 'before-save-hook #'gptel--save-state nil t)
   1080         (gptel--restore-state)
   1081         (if gptel-use-header-line
   1082           (setq gptel--old-header-line header-line-format
   1083                 header-line-format
   1084                 (list '(:eval (concat (propertize " " 'display '(space :align-to 0))
   1085                                (format "%s" (gptel-backend-name gptel-backend))))
   1086                       (propertize " Ready" 'face 'success)
   1087                       '(:eval
   1088                         (let* ((model (gptel--model-name gptel-model))
   1089                               (system
   1090                                (propertize
   1091                                 (buttonize
   1092                                  (format "[Prompt: %s]"
   1093                                   (or (car-safe (rassoc gptel--system-message gptel-directives))
   1094                                    (truncate-string-to-width gptel--system-message 15 nil nil t)))
   1095                                  (lambda (&rest _) (gptel-system-prompt)))
   1096                                 'mouse-face 'highlight
   1097                                 'help-echo "System message for session"))
   1098                               (context
   1099                                (and gptel-context--alist
   1100                                 (cl-loop for entry in gptel-context--alist
   1101                                  if (bufferp (car entry)) count it into bufs
   1102                                  else count (stringp (car entry)) into files
   1103                                  finally return
   1104                                  (propertize
   1105                                   (buttonize
   1106                                    (concat "[Context: "
   1107                                     (and (> bufs 0) (format "%d buf" bufs))
   1108                                     (and (> bufs 1) "s")
   1109                                     (and (> bufs 0) (> files 0) ", ")
   1110                                     (and (> files 0) (format "%d file" files))
   1111                                     (and (> files 1) "s")
   1112                                     "]")
   1113                                    (lambda (&rest _)
   1114                                      (require 'gptel-context)
   1115                                      (gptel-context--buffer-setup)))
   1116                                   'mouse-face 'highlight
   1117                                   'help-echo "Active gptel context"))))
   1118                               (toggle-track-media
   1119                                (lambda (&rest _)
   1120                                  (setq-local gptel-track-media
   1121                                   (not gptel-track-media))
   1122                                  (if gptel-track-media
   1123                                      (message
   1124                                       (concat
   1125                                        "Sending media from included links.  To include media, create "
   1126                                        "a \"standalone\" link in a paragraph by itself, separated from surrounding text."))
   1127                                    (message "Ignoring image links.  Only link text will be sent."))
   1128                                  (run-at-time 0 nil #'force-mode-line-update)))
   1129                               (track-media
   1130                                (and (gptel--model-capable-p 'media)
   1131                                 (if gptel-track-media
   1132                                     (propertize
   1133                                      (buttonize "[Sending media]" toggle-track-media)
   1134                                      'mouse-face 'highlight
   1135                                      'help-echo
   1136                                      "Sending media from standalone links/urls when supported.\nClick to toggle")
   1137                                   (propertize
   1138                                      (buttonize "[Ignoring media]" toggle-track-media)
   1139                                      'mouse-face 'highlight
   1140                                      'help-echo
   1141                                      "Ignoring images from standalone links/urls.\nClick to toggle")))))
   1142                          (concat
   1143                           (propertize
   1144                            " " 'display
   1145                            `(space :align-to (- right ,(+ 5 (length model) (length system)
   1146                                                         (length track-media) (length context)))))
   1147                           track-media (and context " ") context " " system " "
   1148                           (propertize
   1149                            (buttonize (concat "[" model "]")
   1150                             (lambda (&rest _) (gptel-menu)))
   1151                            'mouse-face 'highlight
   1152                            'help-echo "GPT model in use"))))))
   1153           (setq mode-line-process
   1154                 '(:eval (concat " "
   1155                          (buttonize (gptel--model-name gptel-model)
   1156                             (lambda (&rest _) (gptel-menu))))))))
   1157     (remove-hook 'before-save-hook #'gptel--save-state t)
   1158     (if gptel-use-header-line
   1159         (setq header-line-format gptel--old-header-line
   1160               gptel--old-header-line nil)
   1161       (setq mode-line-process nil))))
   1162 
   1163 (defun gptel--update-status (&optional msg face)
   1164   "Update status MSG in FACE."
   1165   (when gptel-mode
   1166     (if gptel-use-header-line
   1167         (and (consp header-line-format)
   1168            (setf (nth 1 header-line-format)
   1169                  (propertize msg 'face face)))
   1170       (if (member msg '(" Typing..." " Waiting..."))
   1171           (setq mode-line-process (propertize msg 'face face))
   1172         (setq mode-line-process
   1173               '(:eval (concat " "
   1174                        (buttonize (gptel--model-name gptel-model)
   1175                             (lambda (&rest _) (gptel-menu))))))
   1176         (message (propertize msg 'face face))))
   1177     (force-mode-line-update)))
   1178 
   1179 (declare-function gptel-context--wrap "gptel-context")
   1180 
   1181 
   1182 ;;; Send queries, handle responses
   1183 (cl-defun gptel-request
   1184     (&optional prompt &key callback
   1185                (buffer (current-buffer))
   1186                position context dry-run
   1187                (stream nil) (in-place nil)
   1188                (system gptel--system-message))
   1189   "Request a response from the `gptel-backend' for PROMPT.
   1190 
   1191 The request is asynchronous, the function immediately returns
   1192 with the data that was sent.
   1193 
   1194 Note: This function is not fully self-contained.  Consider
   1195 let-binding the parameters `gptel-backend' and `gptel-model'
   1196 around calls to it as required.
   1197 
   1198 If PROMPT is
   1199 - a string, it is used to create a full prompt suitable for
   1200   sending to the LLM.
   1201 - nil but region is active, the region contents are used.
   1202 - nil, the current buffer's contents up to (point) are used.
   1203   Previous responses from the LLM are identified as responses.
   1204 - A list of plists, it is used as is.
   1205 
   1206 Keyword arguments:
   1207 
   1208 CALLBACK, if supplied, is a function of two arguments, called
   1209 with the RESPONSE (a string) and INFO (a plist):
   1210 
   1211  (callback RESPONSE INFO)
   1212 
   1213 RESPONSE is nil if there was no response or an error.
   1214 
   1215 The INFO plist has (at least) the following keys:
   1216 :data         - The request data included with the query
   1217 :position     - marker at the point the request was sent, unless
   1218                 POSITION is specified.
   1219 :buffer       - The buffer current when the request was sent,
   1220                 unless BUFFER is specified.
   1221 :status       - Short string describing the result of the request
   1222 
   1223 Example of a callback that messages the user with the response
   1224 and info:
   1225 
   1226  (lambda (response info)
   1227   (if response
   1228       (let ((posn (marker-position (plist-get info :position)))
   1229             (buf  (buffer-name (plist-get info :buffer))))
   1230         (message \"Response for request from %S at %d: %s\"
   1231                  buf posn response))
   1232     (message \"gptel-request failed with message: %s\"
   1233              (plist-get info :status))))
   1234 
   1235 Or, for just the response:
   1236 
   1237  (lambda (response _)
   1238   ;; Do something with response
   1239   (message (rot13-string response)))
   1240 
   1241 If CALLBACK is omitted, the response is inserted at the point the
   1242 request was sent.
   1243 
   1244 BUFFER and POSITION are the buffer and position (integer or
   1245 marker) at which the response is inserted.  If a CALLBACK is
   1246 specified, no response is inserted and these arguments are
   1247 ignored, but they are still available in the INFO plist passed
   1248 to CALLBACK for you to use.
   1249 
   1250 BUFFER defaults to the current buffer, and POSITION to the value
   1251 of (point) or (region-end), depending on whether the region is
   1252 active.
   1253 
   1254 CONTEXT is any additional data needed for the callback to run. It
   1255 is included in the INFO argument to the callback.
   1256 
   1257 SYSTEM is the system message (chat directive) sent to the LLM. If
   1258 omitted, the value of `gptel--system-message' for the current
   1259 buffer is used.
   1260 
   1261 The following keywords are mainly for internal use:
   1262 
   1263 IN-PLACE is a boolean used by the default callback when inserting
   1264 the response to determine if delimiters are needed between the
   1265 prompt and the response.
   1266 
   1267 STREAM is a boolean that determines if the response should be
   1268 streamed, as in `gptel-stream'. Do not set this if you are
   1269 specifying a custom CALLBACK!
   1270 
   1271 If DRY-RUN is non-nil, construct and return the full
   1272 query data as usual, but do not send the request.
   1273 
   1274 Model parameters can be let-bound around calls to this function."
   1275   (declare (indent 1))
   1276   ;; TODO Remove this check in version 1.0
   1277   (gptel--sanitize-model)
   1278   (let* ((gptel--system-message
   1279           ;Add context chunks to system message if required
   1280           (if (and gptel-context--alist
   1281                    (eq gptel-use-context 'system)
   1282                    (not (gptel--model-capable-p 'nosystem)))
   1283               (gptel-context--wrap system)
   1284             system))
   1285          (gptel-stream stream)
   1286          (start-marker
   1287           (cond
   1288            ((null position)
   1289             (if (use-region-p)
   1290                 (set-marker (make-marker) (region-end))
   1291               (gptel--at-word-end (point-marker))))
   1292            ((markerp position) position)
   1293            ((integerp position)
   1294             (set-marker (make-marker) position buffer))))
   1295          (full-prompt
   1296           (cond
   1297            ((null prompt)
   1298             (gptel--create-prompt start-marker))
   1299            ((stringp prompt)
   1300             ;; FIXME Dear reader, welcome to Jank City:
   1301             (with-temp-buffer
   1302               (let ((gptel-model (buffer-local-value 'gptel-model buffer))
   1303                     (gptel-backend (buffer-local-value 'gptel-backend buffer)))
   1304                 (insert prompt)
   1305                 (gptel--create-prompt))))
   1306            ((consp prompt) prompt)))
   1307          (request-data (gptel--request-data gptel-backend full-prompt))
   1308          (info (list :data request-data
   1309                      :buffer buffer
   1310                      :position start-marker)))
   1311     ;; This context should not be confused with the context aggregation context!
   1312     (when context (plist-put info :context context))
   1313     (when in-place (plist-put info :in-place in-place))
   1314     (unless dry-run
   1315       (funcall (if gptel-use-curl
   1316                    #'gptel-curl-get-response #'gptel--url-get-response)
   1317                info callback))
   1318     request-data))
   1319 
   1320 ;; TODO: Handle multiple requests(#15). (Only one request from one buffer at a time?)
   1321 ;;;###autoload
   1322 (defun gptel-send (&optional arg)
   1323   "Submit this prompt to the current LLM backend.
   1324 
   1325 By default, the contents of the buffer up to the cursor position
   1326 are sent.  If the region is active, its contents are sent
   1327 instead.
   1328 
   1329 The response from the LLM is inserted below the cursor position
   1330 at the time of sending.  To change this behavior or model
   1331 parameters, use prefix arg ARG activate a transient menu with
   1332 more options instead.
   1333 
   1334 This command is asynchronous, you can continue to use Emacs while
   1335 waiting for the response."
   1336   (interactive "P")
   1337   (if (and arg (require 'gptel-transient nil t))
   1338       (call-interactively #'gptel-menu)
   1339   (message "Querying %s..." (gptel-backend-name gptel-backend))
   1340   (gptel--sanitize-model)
   1341   (gptel-request nil :stream gptel-stream)
   1342   (gptel--update-status " Waiting..." 'warning)))
   1343 
   1344 (declare-function json-pretty-print-buffer "json")
   1345 (defun gptel--inspect-query (request-data &optional arg)
   1346   "Show REQUEST-DATA, the full LLM query to be sent, in a buffer.
   1347 
   1348 This functions as a dry run of `gptel-send'.  If ARG is
   1349 the symbol json, show the encoded JSON query instead of the Lisp
   1350 structure gptel uses."
   1351   (with-current-buffer (get-buffer-create "*gptel-query*")
   1352     (let ((standard-output (current-buffer))
   1353           (inhibit-read-only t))
   1354       (buffer-disable-undo)
   1355       (erase-buffer)
   1356       (if (eq arg 'json)
   1357           (progn (fundamental-mode)
   1358                  (insert (gptel--json-encode request-data))
   1359                  (json-pretty-print-buffer))
   1360         (lisp-data-mode)
   1361         (prin1 request-data)
   1362         (pp-buffer))
   1363       (goto-char (point-min))
   1364       (view-mode 1)
   1365       (display-buffer (current-buffer) gptel-display-buffer-action))))
   1366 
   1367 (defun gptel--insert-response (response info)
   1368   "Insert the LLM RESPONSE into the gptel buffer.
   1369 
   1370 INFO is a plist containing information relevant to this buffer.
   1371 See `gptel--url-get-response' for details."
   1372   (let* ((status-str  (plist-get info :status))
   1373          (gptel-buffer (plist-get info :buffer))
   1374          (start-marker (plist-get info :position))
   1375          response-beg response-end)
   1376     ;; Handle read-only buffers
   1377     (when (with-current-buffer gptel-buffer
   1378             (or buffer-read-only
   1379                 (get-char-property start-marker 'read-only)))
   1380       (message "Buffer is read only, displaying reply in buffer \"*LLM response*\"")
   1381       (display-buffer
   1382        (with-current-buffer (get-buffer-create "*LLM response*")
   1383          (visual-line-mode 1)
   1384          (goto-char (point-max))
   1385          (move-marker start-marker (point) (current-buffer))
   1386          (current-buffer))
   1387        '((display-buffer-reuse-window
   1388           display-buffer-pop-up-window)
   1389          (reusable-frames . visible))))
   1390     ;; Insert response and status message/error message
   1391     (with-current-buffer gptel-buffer
   1392       (if response
   1393           (progn
   1394             (setq response (gptel--transform-response
   1395                                response gptel-buffer))
   1396             (save-excursion
   1397               (put-text-property
   1398                0 (length response) 'gptel 'response response)
   1399               (with-current-buffer (marker-buffer start-marker)
   1400                 (goto-char start-marker)
   1401                 (run-hooks 'gptel-pre-response-hook)
   1402                 (unless (or (bobp) (plist-get info :in-place))
   1403                   (insert "\n\n")
   1404                   (when gptel-mode
   1405                     (insert (gptel-response-prefix-string))))
   1406                 (setq response-beg (point)) ;Save response start position
   1407                 (insert response)
   1408                 (setq response-end (point))
   1409                 (pulse-momentary-highlight-region response-beg response-end)
   1410                 (when gptel-mode (insert "\n\n" (gptel-prompt-prefix-string)))) ;Save response end position
   1411               (when gptel-mode (gptel--update-status " Ready" 'success))))
   1412         (gptel--update-status
   1413          (format " Response Error: %s" status-str) 'error)
   1414         (message "gptel response error: (%s) %s"
   1415                  status-str (plist-get info :error))))
   1416     ;; Run hook in visible window to set window-point, BUG #269
   1417     (if-let ((gptel-window (get-buffer-window gptel-buffer 'visible)))
   1418         (with-selected-window gptel-window
   1419           (run-hook-with-args 'gptel-post-response-functions response-beg response-end))
   1420       (with-current-buffer gptel-buffer
   1421         (run-hook-with-args 'gptel-post-response-functions response-beg response-end)))))
   1422 
   1423 (defun gptel--create-prompt (&optional prompt-end)
   1424   "Return a full conversation prompt from the contents of this buffer.
   1425 
   1426 If `gptel--num-messages-to-send' is set, limit to that many
   1427 recent exchanges.
   1428 
   1429 If the region is active limit the prompt to the region contents
   1430 instead.
   1431 
   1432 If `gptel-context--alist' is non-nil and the additional
   1433 context needs to be included with the user prompt, add it.
   1434 
   1435 If PROMPT-END (a marker) is provided, end the prompt contents
   1436 there."
   1437   (save-excursion
   1438     (save-restriction
   1439       (let* ((max-entries (and gptel--num-messages-to-send
   1440                                (* 2 gptel--num-messages-to-send)))
   1441              (prompt-end (or prompt-end (point-max)))
   1442              (prompts
   1443               (cond
   1444                ((use-region-p)
   1445                 ;; Narrow to region
   1446                 (narrow-to-region (region-beginning) (region-end))
   1447                 (goto-char (point-max))
   1448                 (gptel--parse-buffer gptel-backend max-entries))
   1449                ((derived-mode-p 'org-mode)
   1450                 (require 'gptel-org)
   1451                 (goto-char prompt-end)
   1452                 (gptel-org--create-prompt prompt-end))
   1453                (t (goto-char prompt-end)
   1454                   (gptel--parse-buffer gptel-backend max-entries)))))
   1455         ;; NOTE: prompts is modified in place here
   1456         (when gptel-context--alist
   1457           ;; Inject context chunks into the last user prompt if required.
   1458           ;; This is also the fallback for when `gptel-use-context' is set to
   1459           ;; 'system but the model does not support system messages.
   1460           (when (and gptel-use-context
   1461                      (or (eq gptel-use-context 'user)
   1462                          (gptel--model-capable-p 'nosystem))
   1463                      (> (length prompts) 0)) ;FIXME context should be injected
   1464                                              ;even when there are no prompts
   1465             (gptel--wrap-user-prompt gptel-backend prompts))
   1466           ;; Inject media chunks into the first user prompt if required.  Media
   1467           ;; chunks are always included with the first user message,
   1468           ;; irrespective of the preference in `gptel-use-context'.  This is
   1469           ;; because media cannot be included (in general) with system messages.
   1470           (when (and gptel-use-context gptel-track-media
   1471                      (gptel--model-capable-p 'media))
   1472             (gptel--wrap-user-prompt gptel-backend prompts :media)))
   1473         prompts))))
   1474 
   1475 (cl-defgeneric gptel--parse-buffer (backend max-entries)
   1476   "Parse current buffer backwards from point and return a list of prompts.
   1477 
   1478 BACKEND is the LLM backend in use.
   1479 
   1480 MAX-ENTRIES is the number of queries/responses to include for
   1481 contexbt.")
   1482 
   1483 (cl-defgeneric gptel--parse-media-links (mode beg end)
   1484   "Find media links between BEG and END.
   1485 
   1486 MODE is the major-mode of the buffer.
   1487 
   1488 Returns a plist where each entry is of the form
   1489   (:text \"some text\")
   1490 or
   1491   (:media \"media uri or file path\")."
   1492   (ignore mode)                         ;byte-compiler
   1493   (list `(:text ,(buffer-substring beg end))))
   1494 
   1495 (defvar markdown-regex-link-inline)
   1496 (defvar markdown-regex-angle-uri)
   1497 (declare-function markdown-link-at-pos "markdown-mode")
   1498 (declare-function mailcap-file-name-to-mime-type "mailcap")
   1499 
   1500 (cl-defmethod gptel--parse-media-links ((_mode (eql 'markdown-mode)) beg end)
   1501   "Parse text and actionable links between BEG and END.
   1502 
   1503 Return a list of the form
   1504  ((:text \"some text\")
   1505   (:media \"/path/to/media.png\" :mime \"image/png\")
   1506   (:text \"More text\"))
   1507 for inclusion into the user prompt for the gptel request."
   1508   (require 'mailcap)                    ;FIXME Avoid this somehow
   1509   (let ((parts) (from-pt))
   1510     (save-excursion
   1511       (setq from-pt (goto-char beg))
   1512       (while (re-search-forward
   1513               (concat "\\(?:" markdown-regex-link-inline "\\|"
   1514                       markdown-regex-angle-uri "\\)")
   1515               end t)
   1516         (when-let* ((link-at-pt (markdown-link-at-pos (point)))
   1517                     ((gptel--link-standalone-p
   1518                       (car link-at-pt) (cadr link-at-pt)))
   1519                     (path (nth 3 link-at-pt))
   1520                     (path (string-remove-prefix "file://" path))
   1521                     (mime (mailcap-file-name-to-mime-type path))
   1522                     ((gptel--model-mime-capable-p mime)))
   1523           (cond
   1524            ((seq-some (lambda (p) (string-prefix-p p path))
   1525                       '("https:" "http:" "ftp:"))
   1526             ;; Collect text up to this image, and collect this image url
   1527             (when (gptel--model-capable-p 'url) ; FIXME This is not a good place
   1528                                                 ; to check for url capability!
   1529               (push (list :text (buffer-substring-no-properties from-pt (car link-at-pt)))
   1530                     parts)
   1531               (push (list :url path :mime mime) parts)
   1532               (setq from-pt (cadr link-at-pt))))
   1533            ((file-readable-p path)
   1534             ;; Collect text up to this image, and collect this image
   1535             (push (list :text (buffer-substring-no-properties from-pt (car link-at-pt)))
   1536                   parts)
   1537             (push (list :media path :mime mime) parts)
   1538             (setq from-pt (cadr link-at-pt)))))))
   1539     (unless (= from-pt end)
   1540       (push (list :text (buffer-substring-no-properties from-pt end)) parts))
   1541     (nreverse parts)))
   1542 
   1543 (cl-defgeneric gptel--wrap-user-prompt (backend _prompts)
   1544   "Wrap the last prompt in PROMPTS with gptel's context.
   1545 
   1546 PROMPTS is a structure as returned by `gptel--parse-buffer'.
   1547 Typically this is a list of plists.
   1548 
   1549 BACKEND is the gptel backend in use."
   1550   (display-warning
   1551    '(gptel context)
   1552    (format "Context support not implemented for backend %s, ignoring context"
   1553            (gptel-backend-name backend))))
   1554 
   1555 (cl-defgeneric gptel--request-data (backend prompts)
   1556   "Generate a plist of all data for an LLM query.
   1557 
   1558 BACKEND is the LLM backend in use.
   1559 
   1560 PROMPTS is the plist of previous user queries and LLM responses.")
   1561 
   1562 ;; TODO: Use `run-hook-wrapped' with an accumulator instead to handle
   1563 ;; buffer-local hooks, etc.
   1564 (defun gptel--transform-response (content-str buffer)
   1565   "Filter CONTENT-STR through `gptel-response-filter-functions`.
   1566 
   1567 BUFFER is passed along with CONTENT-STR to each function in this
   1568 hook."
   1569   (let ((filtered-str content-str))
   1570     (dolist (filter-func gptel-response-filter-functions filtered-str)
   1571       (condition-case nil
   1572           (when (functionp filter-func)
   1573             (setq filtered-str
   1574                   (funcall filter-func filtered-str buffer)))
   1575         (error
   1576          (display-warning '(gptel filter-functions)
   1577                           (format "Function %S returned an error"
   1578                                   filter-func)))))))
   1579 
   1580 (defun gptel--convert-org (content buffer)
   1581   "Transform CONTENT according to required major-mode.
   1582 
   1583 Currently only `org-mode' is handled.
   1584 
   1585 BUFFER is the LLM interaction buffer."
   1586   (if (with-current-buffer buffer (derived-mode-p 'org-mode))
   1587       (gptel--convert-markdown->org content)
   1588     content))
   1589 
   1590 (defun gptel--url-get-response (info &optional callback)
   1591   "Fetch response to prompt in INFO from the LLM.
   1592 
   1593 INFO is a plist with the following keys:
   1594 - :data     (the data being sent)
   1595 - :buffer   (the gptel buffer)
   1596 - :position (marker at which to insert the response).
   1597 
   1598 Call CALLBACK with the response and INFO afterwards.  If omitted
   1599 the response is inserted into the current buffer after point."
   1600   (let* ((inhibit-message t)
   1601          (message-log-max nil)
   1602          (backend gptel-backend)
   1603          (url-request-method "POST")
   1604          (url-request-extra-headers
   1605           (append '(("Content-Type" . "application/json"))
   1606                   (when-let ((header (gptel-backend-header gptel-backend)))
   1607                     (if (functionp header)
   1608                         (funcall header) header))))
   1609         (url-request-data
   1610          (encode-coding-string
   1611           (gptel--json-encode (plist-get info :data))
   1612           'utf-8)))
   1613     ;; why do these checks not occur inside of `gptel--log'?
   1614     (when gptel-log-level               ;logging
   1615       (when (eq gptel-log-level 'debug)
   1616         (gptel--log (gptel--json-encode
   1617                      (mapcar (lambda (pair) (cons (intern (car pair)) (cdr pair)))
   1618                              url-request-extra-headers))
   1619                     "request headers"))
   1620       (gptel--log url-request-data "request body"))
   1621     (url-retrieve (let ((backend-url (gptel-backend-url gptel-backend)))
   1622                     (if (functionp backend-url)
   1623                         (funcall backend-url) backend-url))
   1624                   (lambda (_)
   1625                     (pcase-let ((`(,response ,http-msg ,error)
   1626                                  (gptel--url-parse-response backend (current-buffer))))
   1627                       (plist-put info :status http-msg)
   1628                       (when error (plist-put info :error error))
   1629                       (funcall (or callback #'gptel--insert-response)
   1630                                response info)
   1631                       (kill-buffer)))
   1632                   nil t nil)))
   1633 
   1634 (cl-defgeneric gptel--parse-response (backend response proc-info)
   1635   "Response extractor for LLM requests.
   1636 
   1637 BACKEND is the LLM backend in use.
   1638 
   1639 RESPONSE is the parsed JSON of the response, as a plist.
   1640 
   1641 PROC-INFO is a plist with process information and other context.
   1642 See `gptel-curl--get-response' for its contents.")
   1643 
   1644 (defvar url-http-end-of-headers)
   1645 (defvar url-http-response-status)
   1646 (defun gptel--url-parse-response (backend response-buffer)
   1647   "Parse response from BACKEND in RESPONSE-BUFFER."
   1648   (when (buffer-live-p response-buffer)
   1649     (with-current-buffer response-buffer
   1650       (when gptel-log-level             ;logging
   1651         (save-excursion
   1652           (goto-char url-http-end-of-headers)
   1653           (when (eq gptel-log-level 'debug)
   1654             (gptel--log (gptel--json-encode (buffer-substring-no-properties (point-min) (point)))
   1655                         "response headers"))
   1656           (gptel--log (buffer-substring-no-properties (point) (point-max))
   1657                       "response body")))
   1658       (if-let* ((http-msg (string-trim (buffer-substring (line-beginning-position)
   1659                                                          (line-end-position))))
   1660                 (response (progn (goto-char url-http-end-of-headers)
   1661                                  (condition-case nil
   1662                                      (gptel--json-read)
   1663                                    (error 'json-read-error)))))
   1664           (cond
   1665             ;; FIXME Handle the case where HTTP 100 is followed by HTTP (not 200) BUG #194
   1666            ((or (memq url-http-response-status '(200 100))
   1667                 (string-match-p "\\(?:1\\|2\\)00 OK" http-msg))
   1668             (list (string-trim (gptel--parse-response backend response
   1669                                              `(:buffer ,response-buffer
   1670                                                :backend ,backend)))
   1671                    http-msg))
   1672            ((plist-get response :error)
   1673             (let* ((error-data (plist-get response :error))
   1674                    (error-msg (plist-get error-data :message))
   1675                    (error-type (plist-get error-data :type))
   1676                    (backend-name (gptel-backend-name backend)))
   1677               (if (stringp error-data)
   1678                   (progn
   1679 		    (message "%s error: (%s) %s" backend-name http-msg error-data)
   1680                     (setq error-msg (string-trim error-data)))
   1681                 (when (stringp error-msg)
   1682                   (message "%s error: (%s) %s" backend-name http-msg (string-trim error-msg)))
   1683                 (when error-type
   1684 		  (setq http-msg (concat "("  http-msg ") " (string-trim error-type)))))
   1685               (list nil (concat "(" http-msg ") " (or error-msg "")))))
   1686            ((eq response 'json-read-error)
   1687             (list nil (concat "(" http-msg ") Malformed JSON in response.") "json-read-error"))
   1688            (t (list nil (concat "(" http-msg ") Could not parse HTTP response.")
   1689                     "Could not parse HTTP response.")))
   1690         (list nil (concat "(" http-msg ") Could not parse HTTP response.")
   1691               "Could not parse HTTP response.")))))
   1692 
   1693 (cl-defun gptel--sanitize-model (&key (backend gptel-backend)
   1694                                       (model gptel-model)
   1695                                       (shoosh t))
   1696   "Check if MODEL is available in BACKEND, adjust accordingly.
   1697 
   1698 If SHOOSH is true, don't issue a warning."
   1699   (let ((available (gptel-backend-models backend)))
   1700     (when (stringp model)
   1701       (unless shoosh
   1702         (display-warning
   1703          'gptel
   1704          (format "`gptel-model' expects a symbol, found string \"%s\"
   1705    Resetting `gptel-model' to %s"
   1706                  model model)))
   1707       (setq gptel-model (gptel--intern model)
   1708             model gptel-model))
   1709     (unless (member model available)
   1710       (let ((fallback (car available)))
   1711         (unless shoosh
   1712           (display-warning
   1713            'gptel
   1714            (format (concat "Preferred `gptel-model' \"%s\" not"
   1715                            "supported in \"%s\", using \"%s\" instead")
   1716                    model (gptel-backend-name backend) fallback)))
   1717         (setq-local gptel-model fallback)))))
   1718 
   1719 ;;;###autoload
   1720 (defun gptel (name &optional _ initial interactivep)
   1721   "Switch to or start a chat session with NAME.
   1722 
   1723 Ask for API-KEY if `gptel-api-key' is unset.
   1724 
   1725 If region is active, use it as the INITIAL prompt.  Returns the
   1726 buffer created or switched to.
   1727 
   1728 INTERACTIVEP is t when gptel is called interactively."
   1729   (interactive
   1730    (let* ((backend (default-value 'gptel-backend))
   1731           (backend-name
   1732            (format "*%s*" (gptel-backend-name backend))))
   1733      (list (read-buffer
   1734             "Create or choose gptel buffer: "
   1735             backend-name nil                         ; DEFAULT and REQUIRE-MATCH
   1736             (lambda (b)                                   ; PREDICATE
   1737               ;; NOTE: buffer check is required (#450)
   1738               (and-let* ((buf (get-buffer (or (car-safe b) b))))
   1739                 (buffer-local-value 'gptel-mode buf))))
   1740            (condition-case nil
   1741                (gptel--get-api-key
   1742                 (gptel-backend-key backend))
   1743              ((error user-error)
   1744               (setq gptel-api-key
   1745                     (read-passwd
   1746                      (format "%s API key: " backend-name)))))
   1747            (and (use-region-p)
   1748                 (buffer-substring (region-beginning)
   1749                                   (region-end)))
   1750            t)))
   1751   (with-current-buffer (get-buffer-create name)
   1752     (cond ;Set major mode
   1753      ((eq major-mode gptel-default-mode))
   1754      ((eq gptel-default-mode 'text-mode)
   1755       (text-mode)
   1756       (visual-line-mode 1))
   1757      (t (funcall gptel-default-mode)))
   1758     (gptel--sanitize-model :backend (default-value 'gptel-backend)
   1759                            :model (default-value 'gptel-model)
   1760                            :shoosh nil)
   1761     (unless gptel-mode (gptel-mode 1))
   1762     (goto-char (point-max))
   1763     (skip-chars-backward "\t\r\n")
   1764     (if (bobp) (insert (or initial (gptel-prompt-prefix-string))))
   1765     (when interactivep
   1766       (display-buffer (current-buffer) gptel-display-buffer-action)
   1767       (message "Send your query with %s!"
   1768                (substitute-command-keys "\\[gptel-send]")))
   1769     (current-buffer)))
   1770 
   1771 
   1772 ;;; Response tweaking commands
   1773 
   1774 (defun gptel--attach-response-history (history &optional buf)
   1775   "Attach HISTORY to the next gptel response in buffer BUF.
   1776 
   1777 HISTORY is a list of strings typically containing text replaced
   1778 by gptel.  BUF is the current buffer if not specified.
   1779 
   1780 This is used to maintain variants of prompts or responses to diff
   1781 against if required."
   1782   (with-current-buffer (or buf (current-buffer))
   1783     (letrec ((gptel--attach-after
   1784               (lambda (b e)
   1785                 (put-text-property b e 'gptel-history
   1786                                    (append (ensure-list history)
   1787                                            (get-char-property (1- e) 'gptel-history)))
   1788                 (remove-hook 'gptel-post-response-functions
   1789                              gptel--attach-after 'local))))
   1790       (add-hook 'gptel-post-response-functions gptel--attach-after
   1791                 nil 'local))))
   1792 
   1793 (defun gptel--ediff (&optional arg bounds-func)
   1794   "Ediff response at point against previous gptel responses.
   1795 
   1796 If prefix ARG is non-nil, select the previous response to ediff
   1797 against interactively.
   1798 
   1799 If specified, use BOUNDS-FUNC to compute the bounds of the
   1800 response at point.  This can be used to include additional
   1801 context for the ediff session."
   1802   (interactive "P")
   1803   (when (gptel--at-response-history-p)
   1804     (pcase-let* ((`(,beg . ,end) (funcall (or bounds-func #'gptel--get-bounds)))
   1805                  (prev-response
   1806                   (if arg
   1807                       (completing-read "Choose response variant to diff against: "
   1808                                        (get-char-property (point) 'gptel-history)
   1809                                        nil t)
   1810                     (car-safe (get-char-property (point) 'gptel-history))))
   1811                  (buffer-mode major-mode)
   1812                  (bufname (buffer-name))
   1813                  (`(,new-buf ,new-beg ,new-end)
   1814                   (with-current-buffer
   1815                       (get-buffer-create (concat bufname "-PREVIOUS-*"))
   1816                     (let ((inhibit-read-only t))
   1817                       (erase-buffer)
   1818                       (delay-mode-hooks (funcall buffer-mode))
   1819                       (visual-line-mode)
   1820                       (insert prev-response)
   1821                       (goto-char (point-min))
   1822                       (list (current-buffer) (point-min) (point-max))))))
   1823       (unless prev-response (user-error "gptel response is additive: no changes to ediff"))
   1824       (require 'ediff)
   1825       (letrec ((cwc (current-window-configuration))
   1826                (gptel--ediff-restore
   1827                 (lambda ()
   1828                   (when (window-configuration-p cwc)
   1829                     (set-window-configuration cwc))
   1830                   (kill-buffer (get-buffer (concat bufname "-PREVIOUS-*")))
   1831                   (kill-buffer (get-buffer (concat bufname "-CURRENT-*")))
   1832                   (remove-hook 'ediff-quit-hook gptel--ediff-restore))))
   1833         (add-hook 'ediff-quit-hook gptel--ediff-restore)
   1834         (apply
   1835          #'ediff-regions-internal
   1836          (get-buffer (ediff-make-cloned-buffer (current-buffer) "-CURRENT-*"))
   1837          beg end new-buf new-beg new-end
   1838          nil
   1839          (list 'ediff-regions-wordwise 'word-wise nil)
   1840          ;; (if (transient-arg-value "-w" args)
   1841          ;;     (list 'ediff-regions-wordwise 'word-wise nil)
   1842          ;;   (list 'ediff-regions-linewise nil nil))
   1843          )))))
   1844 
   1845 (defun gptel--mark-response ()
   1846   "Mark gptel response at point, if any."
   1847   (interactive)
   1848   (unless (gptel--in-response-p) (user-error "No gptel response at point"))
   1849   (pcase-let ((`(,beg . ,end) (gptel--get-bounds)))
   1850     (goto-char beg) (push-mark) (goto-char end) (activate-mark)))
   1851 
   1852 (defun gptel--previous-variant (&optional arg)
   1853   "Switch to previous gptel-response at this point, if it exists."
   1854   (interactive "p")
   1855   (pcase-let* ((`(,beg . ,end) (gptel--get-bounds))
   1856                (history (get-char-property (point) 'gptel-history))
   1857                (alt-response (car-safe history))
   1858                (offset))
   1859     (unless (and history alt-response)
   1860       (user-error "No variant responses available"))
   1861     (if (> arg 0)
   1862         (setq history (append (cdr history)
   1863                               (list (buffer-substring-no-properties beg end))))
   1864       (setq
   1865        alt-response (car (last history))
   1866        history (cons (buffer-substring-no-properties beg end)
   1867                      (nbutlast history))))
   1868     (add-text-properties
   1869              0 (length alt-response)
   1870              `(gptel response gptel-history ,history)
   1871              alt-response)
   1872     (setq offset (min (- (point) beg) (1- (length alt-response))))
   1873     (delete-region beg end)
   1874     (insert alt-response)
   1875     (goto-char (+ beg offset))
   1876     (pulse-momentary-highlight-region beg (+ beg (length alt-response)))))
   1877 
   1878 (defun gptel--next-variant (&optional arg)
   1879   "Switch to next gptel-response at this point, if it exists."
   1880   (interactive "p")
   1881   (gptel--previous-variant (- arg)))
   1882 
   1883 (provide 'gptel)
   1884 ;;; gptel.el ends here
   1885 
   1886 ;; Local Variables:
   1887 ;; bug-reference-url-format: "https://github.com/karthink/gptel/issues/%s"
   1888 ;; End: