From e20eb7a1f5de05712f841ad04eb2670fa2f765b1 Mon Sep 17 00:00:00 2001 From: Andreas Gerler Date: Sun, 26 Apr 2026 19:53:40 +0200 Subject: [PATCH] update gptel --- elpa/gptel-20260425.846/README-elpa | 159 +++ .../gptel-anthropic.el | 100 +- .../gptel-autoloads.el | 180 ++- .../gptel-bedrock.el | 102 +- .../gptel-context.el | 21 +- .../gptel-gemini.el | 123 +- .../gptel-gh.el | 320 +++-- .../gptel-integrations.el | 64 +- .../gptel-kagi.el | 15 +- .../gptel-ollama.el | 70 +- .../gptel-openai-extras.el | 106 +- .../gptel-openai-responses.el | 602 ++++++++++ .../gptel-openai.el | 517 +++++--- .../gptel-org.el | 199 +-- .../gptel-pkg.el | 6 +- .../gptel-request.el | 1069 ++++++++++------- .../gptel-rewrite.el | 147 ++- .../gptel-transient.el | 300 ++--- .../gptel.el | 1019 ++++++++++++---- 19 files changed, 3732 insertions(+), 1387 deletions(-) create mode 100644 elpa/gptel-20260425.846/README-elpa rename elpa/{gptel-20260123.739 => gptel-20260425.846}/gptel-anthropic.el (88%) rename elpa/{gptel-20260123.739 => gptel-20260425.846}/gptel-autoloads.el (67%) rename elpa/{gptel-20260123.739 => gptel-20260425.846}/gptel-bedrock.el (88%) rename elpa/{gptel-20260123.739 => gptel-20260425.846}/gptel-context.el (98%) rename elpa/{gptel-20260123.739 => gptel-20260425.846}/gptel-gemini.el (83%) rename elpa/{gptel-20260123.739 => gptel-20260425.846}/gptel-gh.el (62%) rename elpa/{gptel-20260123.739 => gptel-20260425.846}/gptel-integrations.el (82%) rename elpa/{gptel-20260123.739 => gptel-20260425.846}/gptel-kagi.el (94%) rename elpa/{gptel-20260123.739 => gptel-20260425.846}/gptel-ollama.el (84%) rename elpa/{gptel-20260123.739 => gptel-20260425.846}/gptel-openai-extras.el (86%) create mode 100644 elpa/gptel-20260425.846/gptel-openai-responses.el rename elpa/{gptel-20260123.739 => gptel-20260425.846}/gptel-openai.el (62%) rename elpa/{gptel-20260123.739 => gptel-20260425.846}/gptel-org.el (84%) rename elpa/{gptel-20260123.739 => gptel-20260425.846}/gptel-pkg.el (77%) rename elpa/{gptel-20260123.739 => gptel-20260425.846}/gptel-request.el (78%) rename elpa/{gptel-20260123.739 => gptel-20260425.846}/gptel-rewrite.el (86%) rename elpa/{gptel-20260123.739 => gptel-20260425.846}/gptel-transient.el (91%) rename elpa/{gptel-20260123.739 => gptel-20260425.846}/gptel.el (71%) diff --git a/elpa/gptel-20260425.846/README-elpa b/elpa/gptel-20260425.846/README-elpa new file mode 100644 index 00000000..8b939621 --- /dev/null +++ b/elpa/gptel-20260425.846/README-elpa @@ -0,0 +1,159 @@ +gptel is a simple Large Language Model chat client, with support for multiple +models and backends. + +It works in the spirit of Emacs, available at any time and in any buffer. + +gptel supports: + +- The services ChatGPT, Azure, Gemini, Anthropic AI, Together.ai, Perplexity, + AI/ML API, Anyscale, OpenRouter, Groq, PrivateGPT, DeepSeek, Cerebras, Github Models, + GitHub Copilot chat, AWS Bedrock, Novita AI, xAI, Sambanova, Mistral Le + Chat and Kagi (FastGPT & Summarizer). +- Local models via Ollama, Llama.cpp, Llamafiles or GPT4All + +Additionally, any LLM service (local or remote) that provides an +OpenAI-compatible API is supported. + +Features: + +- Interact with LLMs from anywhere in Emacs (any buffer, shell, minibuffer, + wherever). +- LLM responses are in Markdown or Org markup. +- Supports conversations and multiple independent sessions. +- Supports tool-use to equip LLMs with agentic capabilities. +- Supports Model Context Protocol (MCP) integration using the mcp.el package. +- Supports multi-modal models (send images, documents). +- Supports "reasoning" content in LLM responses. +- Save chats as regular Markdown/Org/Text files and resume them later. +- You can go back and edit your previous prompts or LLM responses when + continuing a conversation. These will be fed back to the model. +- Redirect prompts and responses easily +- Rewrite, refactor or fill in regions in buffers. +- Write your own commands for custom tasks with a simple API. + +Requirements for ChatGPT, Azure, Gemini or Kagi: + +- You need an appropriate API key. Set the variable `gptel-api-key' to the + key or to a function of no arguments that returns the key. (It tries to + use `auth-source' by default) + +ChatGPT is configured out of the box. For the other sources: + +- For Azure: define a gptel-backend with `gptel-make-azure'. +- For Gemini: define a gptel-backend with `gptel-make-gemini'. +- For Anthropic (Claude): define a gptel-backend with `gptel-make-anthropic'. +- For AI/ML API, Together.ai, Anyscale, Groq, OpenRouter, DeepSeek, Cerebras + or Github Models: define a gptel-backend with `gptel-make-openai'. +- For PrivateGPT: define a backend with `gptel-make-privategpt'. +- For Perplexity: define a backend with `gptel-make-perplexity'. +- For Deepseek: define a backend with `gptel-make-deepseek'. +- For Kagi: define a gptel-backend with `gptel-make-kagi'. + +For local models using Ollama, Llama.cpp or GPT4All: + +- The model has to be running on an accessible address (or localhost) +- Define a gptel-backend with `gptel-make-ollama' or `gptel-make-gpt4all'. +- Llama.cpp or Llamafiles: Define a gptel-backend with `gptel-make-openai'. + +Consult the package README for examples and more help with configuring +backends. + +Usage: + +gptel can be used in any buffer or in a dedicated chat buffer. The +interaction model is simple: Type in a query and the response will be +inserted below. You can continue the conversation by typing below the +response. + +To use this in any buffer: + +- Call `gptel-send' to send the buffer's text up to the cursor. Select a + region to send only the region. + +- You can select previous prompts and responses to continue the conversation. + +- Call `gptel-send' with a prefix argument to access a menu where you can set + your backend, model and other parameters, or to redirect the + prompt/response. + +To use this in a dedicated buffer: + +- M-x gptel: Start a chat session. + +- In the chat session: Press `C-c RET' (`gptel-send') to send your prompt. + Use a prefix argument (`C-u C-c RET') to access a menu. In this menu you + can set chat parameters like the system directives, active backend or + model, or choose to redirect the input or output elsewhere (such as to the + kill ring or the echo area). + +- You can save this buffer to a file. When opening this file, turn on + `gptel-mode' before editing it to restore the conversation state and + continue chatting. + +- To include media files with your request, you can add them to the context + (described next), or include them as links in Org or Markdown mode chat + buffers. Sending media is disabled by default, you can turn it on globally + via `gptel-track-media', or locally in a chat buffer via the header line. + +Include more context with requests: + +If you want to provide the LLM with more context, you can add arbitrary +regions, buffers, files or directories to the query with `gptel-add'. To add +text or media files, call `gptel-add' in Dired or use the dedicated +`gptel-add-file'. + +You can also add context from gptel's menu instead (`gptel-send' with a +prefix arg), as well as examine or modify context. + +When context is available, gptel will include it with each LLM query. + +LLM Tool use: + +gptel supports "tool calling" behavior, where LLMs can specify arguments with +which to call provided "tools" (elisp functions). The results of running the +tools are fed back to the LLM, giving it capabilities and knowledge beyond +what is available out of the box. For example, tools can perform web +searches or API lookups, modify files and directories, and so on. + +Tools can be specified via `gptel-make-tool', or obtained from other +repositories, or from Model Context Protocol (MCP) servers using the mcp.el +package. See the README for details. + +Tools can be included with LLM queries using gptel's menu, or from +`gptel-tools'. + +Rewrite interface + +In any buffer: with a region selected, you can rewrite prose, refactor code +or fill in the region. This is accessible via `gptel-rewrite', and also from +the `gptel-send' menu. + +Presets + +Define a bundle of configuration (model, backend, system message, tools etc) +as a "preset" that can be applied together, making it easy to switch between +tasks in gptel. Presets can be saved and applied from gptel's transient +menu. You can also include a cookie of the form "@preset-name" in the prompt +to send a request with a preset applied. This feature works everywhere, but +preset cookies are also fontified in chat buffers. + +gptel in Org mode: + +gptel offers a few extra conveniences in Org mode: + +- You can limit the conversation context to an Org heading with + `gptel-org-set-topic'. + +- You can have branching conversations in Org mode, where each hierarchical + outline path through the document is a separate conversation branch. + See the variable `gptel-org-branching-context'. + +- You can declare the gptel model, backend, temperature, system message and + other parameters as Org properties with the command + `gptel-org-set-properties'. gptel queries under the corresponding heading + will always use these settings, allowing you to create mostly reproducible + LLM chat notebooks. + +Finally, gptel offers a general purpose API for writing LLM ineractions that +suit your workflow. See `gptel-request', and `gptel-fsm' for more advanced +usage. diff --git a/elpa/gptel-20260123.739/gptel-anthropic.el b/elpa/gptel-20260425.846/gptel-anthropic.el similarity index 88% rename from elpa/gptel-20260123.739/gptel-anthropic.el rename to elpa/gptel-20260425.846/gptel-anthropic.el index 6ef77ba4..51afa353 100644 --- a/elpa/gptel-20260123.739/gptel-anthropic.el +++ b/elpa/gptel-20260425.846/gptel-anthropic.el @@ -25,7 +25,7 @@ (require 'cl-generic) (require 'cl-lib) (require 'map) -(require 'gptel) +(eval-and-compile (require 'gptel-request)) (defvar json-object-type) @@ -40,6 +40,22 @@ (:copier nil) (:include gptel-backend))) +(defun gptel--anthropic-update-tokens (usage info) + "Update token usage information from USAGE. +USAGE is part of the response, INFO is the request plist." + (when usage + (let* ((tokens (plist-get info :tokens)) + (input (+ (or (plist-get usage :input_tokens) 0) + (or (plist-get tokens :input) 0))) + (output (+ (or (plist-get usage :output_tokens) 0) + (or (plist-get tokens :output) 0))) + (cached (+ (or (plist-get usage :cache_read_input_tokens) 0) + (or (plist-get tokens :cached) 0))) + (cache (+ (or (plist-get usage :cache_creation_input_tokens) 0) + (or (plist-get tokens :cache) 0)))) + (list :input input :output output + :cache cache :cached cached)))) + ;; NOTE the crucial difference between ;; - (push val (plist-get info :key)) and ;; (plist-put (plist-get info :key) (cons val ...)) @@ -94,7 +110,8 @@ information if the stream contains it. Not my best work, I know." ("text" (push (plist-get cblock :text) content-strs)) ("tool_use" (plist-put info :tool-use (cons (list :id (plist-get cblock :id) - :name (plist-get cblock :name)) + :name (plist-get cblock :name) + :input nil) ;ensure :input key is always present (plist-get info :tool-use)))) ("thinking" (plist-put info :reasoning (plist-get cblock :thinking)) (plist-put info :reasoning-block 'in))))) @@ -146,8 +163,9 @@ information if the stream contains it. Not my best work, I know." (plist-put tool-call :input nil) (plist-put tool-call :id (plist-get tool-call :id))) tool-use)) - (plist-put info :output-tokens - (map-nested-elt response '(:usage :output_tokens))) + ;; Capture token usage + (plist-put info :tokens (gptel--anthropic-update-tokens + (plist-get response :usage) info)) (plist-put info :stop-reason (map-nested-elt response '(:delta :stop_reason))))))) (error (goto-char pt))) @@ -166,8 +184,8 @@ information if the stream contains it. Not my best work, I know." Mutate state INFO with response metadata." (plist-put info :stop-reason (plist-get response :stop_reason)) - (plist-put info :output-tokens - (map-nested-elt response '(:usage :output_tokens))) + (plist-put info :tokens (gptel--anthropic-update-tokens + (plist-get response :usage) info)) (cl-loop with content = (plist-get response :content) for cblock across content @@ -322,6 +340,41 @@ TOOL-USE is a list of plists containing tool names, arguments and call results." ;; NOTE: No `gptel--inject-prompt' method required for gptel-anthropic, since ;; this is handled by its defgeneric implementation +(cl-defmethod gptel--inject-tool-call ((_backend gptel-anthropic) data tool-call new-call) + "Replace TOOL-CALL in query DATA with NEW-CALL. + +BACKEND is the `gptel-backend'. See the generic function documentation +for details. This implementation handles the Anthropic API." + ;; FIXME: We currently assume that the tool call being modified is in the last + ;; position in the messages array. + (if-let* ((messages (plist-get data :messages)) + (entry (aref messages (1- (length messages)))) + (contents (plist-get entry :content)) + (id (plist-get tool-call :id)) + (indexed-call + (cl-loop for chunk across contents + for i upfrom 0 + if (equal (plist-get chunk :id) id) + return (cons i chunk) + finally return nil)) + (index (car indexed-call)) + (call (cdr indexed-call))) + (if (null new-call) + (if (= (length contents) 1) + (plist-put data :messages (substring messages nil -1)) + (plist-put entry :content + (vconcat (substring contents 0 index) + (substring contents (1+ index))))) + (when-let* ((args (plist-get new-call :args))) + (plist-put call :input args)) + (when-let* ((name (plist-get new-call :name))) + (plist-put call :name name))) + (display-warning + '(gptel tool-call) + (format "Could not inject updated tool-call arguments for tool call %s, %s" + (plist-get tool-call :name) + (truncate-string-to-width (prin1-to-string new-call) 50 nil nil t))))) + ;; TODO: Remove these functions (#792) (defun gptel--anthropic-format-tool-id (tool-id) (unless tool-id @@ -514,7 +567,15 @@ Media files, if present, are placed in `gptel-context'." ;; (plist-get (car (last prompts)) :content))) (defconst gptel--anthropic-models - '((claude-sonnet-4-5-20250929 + '((claude-sonnet-4-6 + :description "The best combination of speed and intelligence" + :capabilities (media tool-use cache) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp" "application/pdf") + :context-window 1000 + :input-cost 3 + :output-cost 15 + :cutoff-date "2025-08") + (claude-sonnet-4-5-20250929 :description "High-performance model with exceptional reasoning and efficiency" :capabilities (media tool-use cache) :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp" "application/pdf") @@ -538,6 +599,22 @@ Media files, if present, are placed in `gptel-context'." :input-cost 3 :output-cost 15 :cutoff-date "2025-03") + (claude-opus-4-7 + :description "Most capable model for complex reasoning and advanced coding" + :capabilities (media tool-use cache) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp" "application/pdf") + :context-window 1000 + :input-cost 5 + :output-cost 25 + :cutoff-date "2026-01") + (claude-opus-4-6 + :description "Most capable model for complex reasoning and advanced coding" + :capabilities (media tool-use cache) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp" "application/pdf") + :context-window 200 + :input-cost 5 + :output-cost 25 + :cutoff-date "2025-08") (claude-opus-4-5-20251101 :description "Most capable model for complex reasoning and advanced coding" :capabilities (media tool-use cache) @@ -637,10 +714,10 @@ URL `https://docs.anthropic.com/en/docs/about-claude/models#model-comparison-tab (cl-defun gptel-make-anthropic (name &key curl-args stream key request-params (header - (lambda () (when-let* ((key (gptel--get-api-key))) - `(("x-api-key" . ,key) - ("anthropic-version" . "2023-06-01") - ("anthropic-beta" . "extended-cache-ttl-2025-04-11"))))) + (lambda (_info) (when-let* ((key (gptel--get-api-key))) + `(("x-api-key" . ,key) + ("anthropic-version" . "2023-06-01") + ("anthropic-beta" . "extended-cache-ttl-2025-04-11"))))) (models gptel--anthropic-models) (host "api.anthropic.com") (protocol "https") @@ -719,4 +796,3 @@ for." ;; Local Variables: ;; byte-compile-warnings: (not docstrings) ;; End: - diff --git a/elpa/gptel-20260123.739/gptel-autoloads.el b/elpa/gptel-20260425.846/gptel-autoloads.el similarity index 67% rename from elpa/gptel-20260123.739/gptel-autoloads.el rename to elpa/gptel-20260425.846/gptel-autoloads.el index d01e3cb8..f7b2df4b 100644 --- a/elpa/gptel-20260123.739/gptel-autoloads.el +++ b/elpa/gptel-20260425.846/gptel-autoloads.el @@ -11,8 +11,8 @@ ;;; Generated autoloads from gptel.el -(autoload 'gptel-mode "gptel" "\ -Minor mode for interacting with LLMs. +(autoload 'gptel-mode "gptel" +"Minor mode for interacting with LLMs. This is a minor mode. If called interactively, toggle the `GPTel mode' mode. If the prefix argument is positive, enable the mode, and if it is @@ -29,8 +29,8 @@ The mode's hook is called both when the mode is enabled and when it is disabled. (fn &optional ARG)" t) -(autoload 'gptel-send "gptel" "\ -Submit this prompt to the current LLM backend. +(autoload 'gptel-send "gptel" +"Submit this prompt to the current LLM backend. By default, the contents of the buffer up to the cursor position are sent. If the region is active, its contents are sent @@ -45,8 +45,8 @@ This command is asynchronous, you can continue to use Emacs while waiting for the response. (fn &optional ARG)" t) -(autoload 'gptel "gptel" "\ -Switch to or start a chat session with NAME. +(autoload 'gptel "gptel" +"Switch to or start a chat session with NAME. Ask for API-KEY if `gptel-api-key' is unset. @@ -61,8 +61,8 @@ INTERACTIVEP is t when gptel is called interactively. ;;; Generated autoloads from gptel-anthropic.el -(autoload 'gptel-make-anthropic "gptel-anthropic" "\ -Register an Anthropic API-compatible backend for gptel with NAME. +(autoload 'gptel-make-anthropic "gptel-anthropic" +"Register an Anthropic API-compatible backend for gptel with NAME. Keyword arguments: @@ -111,15 +111,15 @@ parameters (as plist keys) and values supported by the API. Use these to set parameters that gptel does not provide user options for. -(fn NAME &key CURL-ARGS STREAM KEY REQUEST-PARAMS (HEADER (lambda nil (when-let* ((key (gptel--get-api-key))) \\=`((\"x-api-key\" \\=\\, key) (\"anthropic-version\" . \"2023-06-01\") (\"anthropic-beta\" . \"extended-cache-ttl-2025-04-11\"))))) (MODELS gptel--anthropic-models) (HOST \"api.anthropic.com\") (PROTOCOL \"https\") (ENDPOINT \"/v1/messages\"))") +(fn NAME &key CURL-ARGS STREAM KEY REQUEST-PARAMS (HEADER (lambda (_info) (when-let* ((key (gptel--get-api-key))) \\=`((\"x-api-key\" \\=\\, key) (\"anthropic-version\" . \"2023-06-01\") (\"anthropic-beta\" . \"extended-cache-ttl-2025-04-11\"))))) (MODELS gptel--anthropic-models) (HOST \"api.anthropic.com\") (PROTOCOL \"https\") (ENDPOINT \"/v1/messages\"))") (function-put 'gptel-make-anthropic 'lisp-indent-function 1) (register-definition-prefixes "gptel-anthropic" '("gptel--anthropic-")) ;;; Generated autoloads from gptel-bedrock.el -(autoload 'gptel-make-bedrock "gptel-bedrock" "\ -Register an AWS Bedrock backend for gptel with NAME. +(autoload 'gptel-make-bedrock "gptel-bedrock" +"Register an AWS Bedrock backend for gptel with NAME. Keyword arguments: @@ -142,14 +142,14 @@ parameters (as plist keys) and values supported by the API. (autoload 'gptel-add "gptel-context" "Add/remove regions or buffers from gptel's context." t) (autoload 'gptel-add-file "gptel-context" "Add files to gptel's context." t) -(autoload 'gptel-context--wrap "gptel-context" "\ -Add request context to DATA-BUF and run CALLBACK. +(autoload 'gptel-context--wrap "gptel-context" +"Add request context to DATA-BUF and run CALLBACK. DATA-BUF is the buffer where the request prompt is constructed. (fn CALLBACK DATA-BUF)") -(autoload 'gptel-context--collect "gptel-context" "\ -Get the list of all active context sources from CONTEXT-ALIST. +(autoload 'gptel-context--collect "gptel-context" +"Get the list of all active context sources from CONTEXT-ALIST. CONTEXT-ALIST defaults to the current value of `gptel-context'. @@ -161,8 +161,8 @@ Ignore overlays, buffers and files that are not live or readable. ;;; Generated autoloads from gptel-gemini.el -(autoload 'gptel-make-gemini "gptel-gemini" "\ -Register a Gemini backend for gptel with NAME. +(autoload 'gptel-make-gemini "gptel-gemini" +"Register a Gemini backend for gptel with NAME. Keyword arguments: @@ -214,15 +214,15 @@ parameters (as plist keys) and values supported by the API. Use these to set parameters that gptel does not provide user options for. -(fn NAME &key CURL-ARGS HEADER KEY REQUEST-PARAMS (STREAM nil) (HOST \"generativelanguage.googleapis.com\") (PROTOCOL \"https\") (MODELS gptel--gemini-models) (ENDPOINT \"/v1beta/models\"))") +(fn NAME &key CURL-ARGS KEY REQUEST-PARAMS (STREAM nil) (HEADER (lambda (_info) (when-let* ((key (gptel--get-api-key))) \\=`((\"X-goog-api-key\" \\=\\, key))))) (HOST \"generativelanguage.googleapis.com\") (PROTOCOL \"https\") (MODELS gptel--gemini-models) (ENDPOINT \"/v1beta/models\"))") (function-put 'gptel-make-gemini 'lisp-indent-function 1) (register-definition-prefixes "gptel-gemini" '("gptel--gemini-")) ;;; Generated autoloads from gptel-gh.el -(autoload 'gptel-make-gh-copilot "gptel-gh" "\ -Register a Github Copilot chat backend for gptel with NAME. +(autoload 'gptel-make-gh-copilot "gptel-gh" +"Register a Github Copilot chat backend for gptel with NAME. Keyword arguments: @@ -273,20 +273,20 @@ parameters (as plist keys) and values supported by the API. Use these to set parameters that gptel does not provide user options for. -(fn NAME &key CURL-ARGS REQUEST-PARAMS (HEADER (lambda nil (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) \"\")) ,@(when (and gptel-track-media (gptel--model-capable-p \\='media)) \\=`((\"copilot-vision-request\" . \"true\"))) (\"copilot-integration-id\" . \"vscode-chat\")))) (HOST \"api.githubcopilot.com\") (PROTOCOL \"https\") (ENDPOINT \"/chat/completions\") (STREAM t) (MODELS gptel--gh-models))") +(fn NAME &key CURL-ARGS REQUEST-PARAMS (HEADER (lambda (info) (gptel--gh-auth) \\=`((\"openai-intent\" . \"conversation-panel\") (\"authorization\" \\=\\, (concat \"Bearer \" (plist-get (gptel--gh-token gptel-backend) :token))) (\"x-initiator\" \\=\\, (or (plist-get info :gh-initiator) (prog1 \"user\" (plist-put info :gh-initiator \"agent\")))) (\"x-request-id\" \\=\\, (gptel--gh-uuid)) (\"vscode-sessionid\" \\=\\, (or (gptel--gh-sessionid gptel-backend) \"\")) (\"vscode-machineid\" \\=\\, (or (gptel--gh-machineid gptel-backend) \"\")) ,@(when (and gptel-track-media (gptel--model-capable-p \\='media)) \\=`((\"copilot-vision-request\" . \"true\"))) (\"copilot-integration-id\" . \"vscode-chat\")))) (HOST \"api.githubcopilot.com\") (PROTOCOL \"https\") (ENDPOINT \"/chat/completions\") (STREAM t) (MODELS gptel--gh-models))") (function-put 'gptel-make-gh-copilot 'lisp-indent-function 1) (register-definition-prefixes "gptel-gh" '("gptel-")) ;;; Generated autoloads from gptel-integrations.el -(register-definition-prefixes "gptel-integrations" '("gptel-mcp-")) +(register-definition-prefixes "gptel-integrations" '("gptel-")) ;;; Generated autoloads from gptel-kagi.el -(autoload 'gptel-make-kagi "gptel-kagi" "\ -Register a Kagi FastGPT backend for gptel with NAME. +(autoload 'gptel-make-kagi "gptel-kagi" +"Register a Kagi FastGPT backend for gptel with NAME. Keyword arguments: @@ -317,14 +317,14 @@ Example: (gptel-make-kagi \"Kagi\" :key my-kagi-key) -(fn NAME &key CURL-ARGS STREAM KEY (HOST \"kagi.com\") (HEADER (lambda nil \\=`((\"Authorization\" \\=\\, (concat \"Bot \" (gptel--get-api-key)))))) (MODELS \\='((fastgpt :capabilities (nosystem)) (summarize:cecil :capabilities (nosystem)) (summarize:agnes :capabilities (nosystem)) (summarize:daphne :capabilities (nosystem)) (summarize:muriel :capabilities (nosystem)))) (PROTOCOL \"https\") (ENDPOINT \"/api/v0/\"))") +(fn NAME &key CURL-ARGS STREAM KEY (HOST \"kagi.com\") (HEADER (lambda (_info) \\=`((\"Authorization\" \\=\\, (concat \"Bot \" (gptel--get-api-key)))))) (MODELS \\='((fastgpt :capabilities (nosystem)) (summarize:cecil :capabilities (nosystem)) (summarize:agnes :capabilities (nosystem)) (summarize:daphne :capabilities (nosystem)) (summarize:muriel :capabilities (nosystem)))) (PROTOCOL \"https\") (ENDPOINT \"/api/v0/\"))") (function-put 'gptel-make-kagi 'lisp-indent-function 1) ;;; Generated autoloads from gptel-ollama.el -(autoload 'gptel-make-ollama "gptel-ollama" "\ -Register an Ollama backend for gptel with NAME. +(autoload 'gptel-make-ollama "gptel-ollama" +"Register an Ollama backend for gptel with NAME. Keyword arguments: @@ -391,8 +391,8 @@ Example: ;;; Generated autoloads from gptel-openai.el -(autoload 'gptel-make-openai "gptel-openai" "\ -Register an OpenAI API-compatible backend for gptel with NAME. +(autoload 'gptel-make-openai "gptel-openai" +"Register an OpenAI API-compatible backend for gptel with NAME. Keyword arguments: @@ -442,10 +442,10 @@ parameters (as plist keys) and values supported by the API. Use these to set parameters that gptel does not provide user options for. -(fn NAME &key CURL-ARGS MODELS STREAM KEY REQUEST-PARAMS (HEADER (lambda nil (when-let* ((key (gptel--get-api-key))) \\=`((\"Authorization\" \\=\\, (concat \"Bearer \" key)))))) (HOST \"api.openai.com\") (PROTOCOL \"https\") (ENDPOINT \"/v1/chat/completions\"))") +(fn NAME &key CURL-ARGS (MODELS gptel--openai-models) STREAM KEY REQUEST-PARAMS (HEADER (lambda (_info) (when-let* ((key (gptel--get-api-key))) \\=`((\"Authorization\" \\=\\, (concat \"Bearer \" key)))))) (HOST \"api.openai.com\") (PROTOCOL \"https\") ENDPOINT)") (function-put 'gptel-make-openai 'lisp-indent-function 1) -(autoload 'gptel-make-azure "gptel-openai" "\ -Register an Azure backend for gptel with NAME. +(autoload 'gptel-make-azure "gptel-openai" +"Register an Azure backend for gptel with NAME. Keyword arguments: @@ -487,10 +487,10 @@ Example: :stream t :models \\='(gpt-3.5-turbo gpt-4)) -(fn NAME &key CURL-ARGS HOST (PROTOCOL \"https\") (HEADER (lambda nil \\=`((\"api-key\" \\=\\, (gptel--get-api-key))))) (KEY \\='gptel-api-key) MODELS STREAM ENDPOINT REQUEST-PARAMS)") +(fn NAME &key CURL-ARGS HOST (PROTOCOL \"https\") (HEADER (lambda (_info) \\=`((\"api-key\" \\=\\, (gptel--get-api-key))))) (KEY \\='gptel-api-key) MODELS STREAM ENDPOINT REQUEST-PARAMS)") (function-put 'gptel-make-azure 'lisp-indent-function 1) -(defalias 'gptel-make-gpt4all 'gptel-make-openai "\ -Register a GPT4All backend for gptel with NAME. +(defalias 'gptel-make-gpt4all 'gptel-make-openai +"Register a GPT4All backend for gptel with NAME. Keyword arguments: @@ -530,13 +530,13 @@ Example: :protocol \"http\" :host \"localhost:4891\" :models \\='(mistral-7b-openorca.Q4_0.gguf))") -(register-definition-prefixes "gptel-openai" '("gptel-")) +(register-definition-prefixes "gptel-openai" '("gptel--openai-")) ;;; Generated autoloads from gptel-openai-extras.el -(autoload 'gptel-make-privategpt "gptel-openai-extras" "\ -Register an Privategpt API-compatible backend for gptel with NAME. +(autoload 'gptel-make-privategpt "gptel-openai-extras" +"Register an Privategpt API-compatible backend for gptel with NAME. Keyword arguments: @@ -570,10 +570,10 @@ parameters (as plist keys) and values supported by the API. Use these to set parameters that gptel does not provide user options for. -(fn NAME &key CURL-ARGS STREAM KEY REQUEST-PARAMS (HEADER (lambda nil (when-let* ((key (gptel--get-api-key))) \\=`((\"Authorization\" \\=\\, (concat \"Bearer \" key)))))) (HOST \"localhost:8001\") (PROTOCOL \"http\") (MODELS \\='(private-gpt)) (ENDPOINT \"/v1/chat/completions\") (CONTEXT t) (SOURCES t))") +(fn NAME &key CURL-ARGS STREAM KEY REQUEST-PARAMS (HEADER (lambda (_info) (when-let* ((key (gptel--get-api-key))) \\=`((\"Authorization\" \\=\\, (concat \"Bearer \" key)))))) (HOST \"localhost:8001\") (PROTOCOL \"http\") (MODELS \\='(private-gpt)) (ENDPOINT \"/v1/chat/completions\") (CONTEXT t) (SOURCES t))") (function-put 'gptel-make-privategpt 'lisp-indent-function 1) -(autoload 'gptel-make-perplexity "gptel-openai-extras" "\ -Register a Perplexity backend for gptel with NAME. +(autoload 'gptel-make-perplexity "gptel-openai-extras" +"Register a Perplexity backend for gptel with NAME. Keyword arguments: @@ -599,17 +599,17 @@ returns the key. REQUEST-PARAMS (optional) is a plist of additional HTTP request parameters. -(fn NAME &key CURL-ARGS STREAM KEY (HEADER (lambda nil (when-let* ((key (gptel--get-api-key))) \\=`((\"Authorization\" \\=\\, (concat \"Bearer \" key)))))) (HOST \"api.perplexity.ai\") (PROTOCOL \"https\") (MODELS \\='(sonar sonar-pro sonar-reasoning sonar-reasoning-pro sonar-deep-research)) (ENDPOINT \"/chat/completions\") REQUEST-PARAMS)") +(fn NAME &key CURL-ARGS STREAM KEY (HEADER (lambda (_info) (when-let* ((key (gptel--get-api-key))) \\=`((\"Authorization\" \\=\\, (concat \"Bearer \" key)))))) (HOST \"api.perplexity.ai\") (PROTOCOL \"https\") (MODELS \\='(sonar sonar-pro sonar-reasoning sonar-reasoning-pro sonar-deep-research)) (ENDPOINT \"/chat/completions\") REQUEST-PARAMS)") (function-put 'gptel-make-perplexity 'lisp-indent-function 1) -(autoload 'gptel-make-deepseek "gptel-openai-extras" "\ -Register a DeepSeek backend for gptel with NAME. +(autoload 'gptel-make-deepseek "gptel-openai-extras" +"Register a DeepSeek backend for gptel with NAME. For the meanings of the keyword arguments, see `gptel-make-openai'. -(fn NAME &key CURL-ARGS STREAM KEY REQUEST-PARAMS (HEADER (lambda nil (when-let* ((key (gptel--get-api-key))) \\=`((\"Authorization\" \\=\\, (concat \"Bearer \" key)))))) (HOST \"api.deepseek.com\") (PROTOCOL \"https\") (ENDPOINT \"/v1/chat/completions\") (MODELS \\='((deepseek-reasoner :capabilities (tool reasoning) :context-window 128 :input-cost 0.56 :output-cost 1.68) (deepseek-chat :capabilities (tool) :context-window 128 :input-cost 0.56 :output-cost 1.68))))") +(fn NAME &key CURL-ARGS STREAM KEY REQUEST-PARAMS (HEADER (lambda (_info) (when-let* ((key (gptel--get-api-key))) \\=`((\"Authorization\" \\=\\, (concat \"Bearer \" key)))))) (HOST \"api.deepseek.com\") (PROTOCOL \"https\") (ENDPOINT \"/v1/chat/completions\") (MODELS \\='((deepseek-reasoner :capabilities (tool reasoning) :context-window 128 :input-cost 0.28 :output-cost 0.42) (deepseek-chat :capabilities (tool) :context-window 128 :input-cost 0.28 :output-cost 0.42))))") (function-put 'gptel-make-deepseek 'lisp-indent-function 1) -(autoload 'gptel-make-xai "gptel-openai-extras" "\ -Register an xAI backend for gptel with NAME. +(autoload 'gptel-make-xai "gptel-openai-extras" +"Register an xAI backend for gptel with NAME. Keyword arguments: @@ -622,22 +622,86 @@ false. The other keyword arguments are all optional. For their meanings see `gptel-make-openai'. -(fn NAME &key CURL-ARGS STREAM KEY REQUEST-PARAMS (HEADER (lambda nil (when-let* ((key (gptel--get-api-key))) \\=`((\"Authorization\" \\=\\, (concat \"Bearer \" key)))))) (HOST \"api.x.ai\") (PROTOCOL \"https\") (ENDPOINT \"/v1/chat/completions\") (MODELS \\='((grok-4 :description \"Grok Flagship model\" :capabilities (tool-use json reasoning) :context-window 256 :input-cost 3 :output-cost 15) (grok-code-fast-1 :description \"Fast reasoning model for agentic coding\" :capabilities (tool-use json reasoning) :context-window 256 :input-cost 0.2 :output-cost 1.5) (grok-3 :description \"Grok 3\" :capabilities (tool-use json reasoning) :context-window 131 :input-cost 3 :output-cost 15) (grok-3-fast :description \"Faster Grok 3\" :capabilities (tool-use json reasoning) :context-window 131 :input-cost 5 :output-cost 25) (grok-3-mini :description \"Mini Grok 3\" :capabilities (tool-use json reasoning) :context-window 131 :input-cost 0.3 :output-cost 0.5) (grok-3-mini-fast :description \"Faster mini Grok 3\" :capabilities (tool-use json reasoning) :context-window 131072 :input-cost 0.6 :output-cost 4) (grok-2-vision-1212 :description \"Grok 2 Vision\" :capabilities (tool-use json media) :mime-types (\"image/jpeg\" \"image/png\" \"image/gif\" \"image/webp\") :context-window 32768 :input-cost 2 :output-cost 10))))") +(fn NAME &key CURL-ARGS STREAM KEY REQUEST-PARAMS (HEADER (lambda (_info) (when-let* ((key (gptel--get-api-key))) \\=`((\"Authorization\" \\=\\, (concat \"Bearer \" key)))))) (HOST \"api.x.ai\") (PROTOCOL \"https\") (ENDPOINT \"/v1/chat/completions\") (MODELS \\='((grok-4-1-fast-reasoning :description \"Fast tool-calling model\" :capabilities (tool-use json reasoning) :context-window 2000 :input-cost 0.2 :output-cost 0.5) (grok-4-1-fast-non-reasoning :description \"Fast tool-calling model (non-reasoning)\" :capabilities (tool-use json) :context-window 2000 :input-cost 0.2 :output-cost 0.5) (grok-code-fast-1 :description \"Fast reasoning model for agentic coding\" :capabilities (tool-use json reasoning) :context-window 256 :input-cost 0.2 :output-cost 1.5) (grok-4-fast-reasoning :description \"Fast tool-calling model\" :capabilities (tool-use json reasoning) :context-window 2000 :input-cost 0.2 :output-cost 0.5) (grok-4-fast-non-reasoning :description \"Fast tool-calling model (non-reasoning)\" :capabilities (tool-use json) :context-window 2000 :input-cost 0.2 :output-cost 0.5) (grok-4 :description \"Grok Flagship model\" :capabilities (tool-use json reasoning) :context-window 256 :input-cost 3 :output-cost 15) (grok-3-mini :description \"Mini Grok 3\" :capabilities (tool-use json reasoning) :context-window 131 :input-cost 0.3 :output-cost 0.5) (grok-3 :description \"Grok 3\" :capabilities (tool-use json reasoning) :context-window 131 :input-cost 3 :output-cost 15) (grok-2-vision-1212 :description \"Grok 2 Vision\" :capabilities (tool-use json media) :mime-types (\"image/jpeg\" \"image/png\" \"image/gif\" \"image/webp\") :context-window 32 :input-cost 2 :output-cost 10))))") (function-put 'gptel-make-xai 'lisp-indent-function 1) (register-definition-prefixes "gptel-openai-extras" '("gptel--p")) + +;;; Generated autoloads from gptel-openai-responses.el + +(autoload 'gptel-make-openai-responses "gptel-openai-responses" +"Register an OpenAI Responses API backend for gptel with NAME. + +The Responses API is OpenAI's new API for agentic applications that +provides built-in tools like web search, code interpreter, and file +search. + +Keyword arguments: + +CURL-ARGS (optional) is a list of additional Curl arguments. + +HOST (optional) is the API host, typically \"api.openai.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'. + +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 +\"/v1/responses\". + +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\")) + +KEY (optional) is a variable whose value is the API key, or +function that returns the key. + +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. + +Example: +------- + + (gptel-make-openai-responses + \"OpenAI-Responses\" + :stream t + :models '((gpt-4o + :capabilities (media tool-use json url responses-api) + :mime-types (\"image/jpeg\" \"image/png\" \"image/gif\" \"image/webp\")) + (gpt-4o-mini + :capabilities (media tool-use json url responses-api) + :mime-types (\"image/jpeg\" \"image/png\" \"image/gif\" \"image/webp\")))) + +(fn NAME &key CURL-ARGS (MODELS gptel--openai-models) STREAM KEY REQUEST-PARAMS (HEADER (lambda (_info) (when-let* ((key (gptel--get-api-key))) \\=`((\"Authorization\" \\=\\, (concat \"Bearer \" key)))))) (HOST \"api.openai.com\") (PROTOCOL \"https\") (ENDPOINT \"/v1/responses\"))") +(function-put 'gptel-make-openai-responses 'lisp-indent-function 1) +(register-definition-prefixes "gptel-openai-responses" '("gptel--openai-responses-")) + ;;; Generated autoloads from gptel-org.el -(autoload 'gptel--convert-markdown->org "gptel-org" "\ -Convert string STR from markdown to org markup. +(autoload 'gptel--convert-markdown->org "gptel-org" +"Convert string STR from markdown to org markup. This is a very basic converter that handles only a few markup elements. (fn STR)") -(autoload 'gptel--stream-convert-markdown->org "gptel-org" "\ -Return a Markdown to Org converter. +(autoload 'gptel--stream-convert-markdown->org "gptel-org" +"Return a Markdown to Org converter. This function parses a stream of Markdown text to Org continuously when it is called with successive chunks of the @@ -652,8 +716,8 @@ cleaning up after. ;;; Generated autoloads from gptel-request.el -(autoload 'gptel-curl-get-response "gptel-request" "\ -Fetch response to prompt in state FSM from the LLM using Curl. +(autoload 'gptel-curl-get-response "gptel-request" +"Fetch response to prompt in state FSM from the LLM using Curl. FSM is the state machine driving this request. @@ -682,6 +746,14 @@ the response is inserted into the current buffer after point. (autoload 'gptel-menu "gptel-transient" nil t) (autoload 'gptel-system-prompt "gptel-transient" nil t) +(autoload 'gptel-preset "gptel-transient" +"Load gptel PRESET with SETTER. + +Interactively, query for PRESET, allow the preset scope to be set +dynamically, and offer to save the current gptel settings as a new or +existing preset, as well. + +(fn PRESET &optional SETTER)" t) (autoload 'gptel-tools "gptel-transient" nil t) (register-definition-prefixes "gptel-transient" '("gptel-")) diff --git a/elpa/gptel-20260123.739/gptel-bedrock.el b/elpa/gptel-20260425.846/gptel-bedrock.el similarity index 88% rename from elpa/gptel-20260123.739/gptel-bedrock.el rename to elpa/gptel-20260425.846/gptel-bedrock.el index 0ccbd5cf..5328fdd9 100644 --- a/elpa/gptel-20260123.739/gptel-bedrock.el +++ b/elpa/gptel-20260425.846/gptel-bedrock.el @@ -27,8 +27,8 @@ ;;; Code: (require 'cl-lib) (require 'map) -(require 'gptel) (require 'mail-parse) +(eval-and-compile (require 'gptel-request)) (declare-function gptel-context--collect-media "gptel-context") (declare-function gptel-context--wrap "gptel-context") @@ -38,6 +38,26 @@ (:include gptel-backend)) model-region) +(defun gptel--bedrock-update-tokens (usage info) + "Update token usage information from USAGE. +USAGE is part of the response, INFO is the request plist. + +This function accumulates token counts across multiple turns in a +multi-turn request (e.g., during tool use where results are fed +back to the LLM)." + (when usage + (let* ((tokens (plist-get info :tokens)) + (input (+ (or (plist-get usage :inputTokens) 0) + (or (plist-get tokens :input) 0))) + (output (+ (or (plist-get usage :outputTokens) 0) + (or (plist-get tokens :output) 0))) + (cached (+ (or (plist-get usage :cacheReadInputTokens) 0) + (or (plist-get tokens :cached) 0))) + (cache (+ (or (plist-get usage :cacheWriteInputTokens) 0) + (or (plist-get tokens :cache) 0)))) + (list :input input :output output + :cache cache :cached cached)))) + (defconst gptel-bedrock--prompt-type ;; For documentation purposes only -- this describes the type of prompt objects that get passed ;; around. https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Message.html @@ -96,10 +116,8 @@ TOOLS is a list of `gptel-tool' structs, which see." Mutate state INFO with response metadata." (plist-put info :stop-reason (plist-get response :stopReason)) - (plist-put info :input-tokens - (map-nested-elt response '(:usage :inputTokens))) - (plist-put info :output-tokens - (map-nested-elt response '(:usage :outputTokens))) + (plist-put info :tokens (gptel--bedrock-update-tokens + (plist-get response :usage) info)) (let* ((message (map-nested-elt response '(:output :message))) (content-strs (thread-last (plist-get message :content) @@ -180,8 +198,8 @@ INFO is a plist containing the request context." (push event (car acc-cell))) (pcase event-type ("metadata" - (plist-put info :input-tokens (map-nested-elt event '(:payload :usage :inputTokens))) - (plist-put info :output-tokens (map-nested-elt event '(:payload :usage :outputTokens)))) + (plist-put info :tokens (gptel--bedrock-update-tokens + (map-nested-elt event '(:payload :usage)) info))) ("contentBlockDelta" (when-let ((delta-text (map-nested-elt event '(:payload :delta :text)))) (push delta-text strings))) @@ -352,16 +370,19 @@ received." (start (car block-events)) (deltas (cdr block-events))) (when-let ((tool-use (map-nested-elt start '(:payload :start :toolUse)))) - (let ((id (plist-get tool-use :toolUseId)) - (name (plist-get tool-use :name)) - (input (gptel--json-read-string - (mapconcat - (lambda (delta) (map-nested-elt delta '(:payload :delta :toolUse :input))) - deltas)))) + (let* ((id (plist-get tool-use :toolUseId)) + (name (plist-get tool-use :name)) + (input-str + (mapconcat (lambda (delta) (map-nested-elt + delta '(:payload :delta :toolUse :input))) + deltas)) + (input (unless (string-blank-p input-str) + (gptel--json-read-string input-str)))) (push (list :toolUse (list :input input :name name :toolUseId id)) contents))) - (when-let ((texts (delq nil (mapcar (lambda (d) (map-nested-elt d '(:payload :delta :text))) deltas)))) + (when-let ((texts (delq nil (mapcar (lambda (d) (map-nested-elt d '(:payload :delta :text))) + deltas)))) (push (list :text (apply #'concat texts)) contents)) ;; Currently we discard any reasoning content but this would be the spot to handle it )) @@ -458,6 +479,44 @@ The output is a vector of entries in Bedrock API format." ;; gptel--inject-prompt not needed since the default implementation works here +(cl-defmethod gptel--inject-tool-call ((_backend gptel-bedrock) data tool-call new-call) + "Replace TOOL-CALL in query DATA with NEW-CALL. + +BACKEND is the `gptel-backend'. See the generic function documentation +for details. This implementation handles the AWS Bedrock API." + ;; FIXME: We currently assume that the tool call being modified is in the last + ;; position in the messages array. + (if-let* ((messages (plist-get data :messages)) + (entry (aref messages (1- (length messages)))) + (contents (plist-get entry :content)) + (id (plist-get tool-call :id)) + (indexed-call + (cl-loop for chunk across contents + for i upfrom 0 + for tool-use = (plist-get chunk :toolUse) + if (and tool-use (equal (plist-get tool-use :toolUseId) id)) + return (list i chunk tool-use) + finally return nil)) + (index (nth 0 indexed-call)) + (chunk (nth 1 indexed-call)) + (call (nth 2 indexed-call))) + (if (null new-call) + (if (= (length contents) 1) + (plist-put data :messages (substring messages nil -1)) + (plist-put entry :content + (vconcat (substring contents 0 index) + (substring contents (1+ index))))) + (when-let* ((args (plist-get new-call :args))) + (setq call (plist-put call :input args))) + (when-let* ((name (plist-get new-call :name))) + (setq call (plist-put call :name name))) + (plist-put chunk :toolUse call)) + (display-warning + '(gptel tool-call) + (format "Could not inject updated tool-call arguments for tool call %s, %s" + (plist-get tool-call :name) + (truncate-string-to-width (prin1-to-string new-call) 50 nil nil t))))) + (cl-defmethod gptel--parse-tool-results ((_backend gptel-bedrock) tool-use-requests) "Return a backend-appropriate prompt containing tool call results. @@ -478,6 +537,12 @@ conversation." (defvar gptel-bedrock--aws-profile-cache nil "Cache for AWS profile credentials in the form of (PROFILE . CREDS).") +(defvar gptel-bedrock-aws-cli-command (executable-find "aws") + "Path to the AWS CLI command. + +Can be customized to use a specific AWS CLI installation, +e.g. \"/usr/local/bin/aws\".") + (defun gptel-bedrock--fetch-aws-profile-credentials (profile &optional clear-cache) "Fetch & cache AWS credentials for PROFILE using aws-cli. @@ -491,7 +556,8 @@ Non-nil CLEAR-CACHE will refresh credentials." (or (and (not clear-cache) (cdr cell)) (setf (cdr cell) (with-temp-buffer - (unless (zerop (apply #'call-process "aws" nil t nil "configure" "export-credentials" + (unless (zerop (apply #'call-process gptel-bedrock-aws-cli-command + nil t nil "configure" "export-credentials" (unless (eql profile :static) (list (format "--profile=%s" profile))))) (user-error "Failed to get AWS credentials from profile")) (json-parse-string (buffer-string))))))) @@ -533,7 +599,9 @@ Convenient to use with `cl-multiple-value-bind'" (defvar gptel-bedrock--model-ids ;; https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html - '((claude-sonnet-4-5-20250929 . "anthropic.claude-sonnet-4-5-20250929-v1:0") + '((claude-sonnet-4-6 . "anthropic.claude-sonnet-4-6") + (claude-opus-4-6 . "anthropic.claude-opus-4-6-v1") + (claude-sonnet-4-5-20250929 . "anthropic.claude-sonnet-4-5-20250929-v1:0") (claude-haiku-4-5-20251001 . "anthropic.claude-haiku-4-5-20251001-v1:0") (claude-opus-4-5-20251101 . "anthropic.claude-opus-4-5-20251101-v1:0") (claude-opus-4-1-20250805 . "anthropic.claude-opus-4-1-20250805-v1:0") @@ -651,7 +719,7 @@ parameters (as plist keys) and values supported by the API." :curl-args (lambda () (append curl-args (gptel-bedrock--curl-args region aws-profile aws-bearer-token))) :request-params request-params :url - (lambda () + (lambda (_info) (concat protocol "://" host "/model/" (gptel-bedrock--get-model-id gptel-model model-region) "/" (if stream "converse-stream" "converse"))))))) diff --git a/elpa/gptel-20260123.739/gptel-context.el b/elpa/gptel-20260425.846/gptel-context.el similarity index 98% rename from elpa/gptel-20260123.739/gptel-context.el rename to elpa/gptel-20260425.846/gptel-context.el index f243ac50..b89293c3 100644 --- a/elpa/gptel-20260123.739/gptel-context.el +++ b/elpa/gptel-20260425.846/gptel-context.el @@ -27,7 +27,7 @@ ;;; Code: -(require 'gptel-request) +(eval-and-compile (require 'gptel-request)) (require 'cl-lib) (require 'project) @@ -516,24 +516,20 @@ Ignore overlays, buffers and files that are not live or readable." CONTEXT-DATA is a plist with keys :overlays, :lines and :bounds. Returns a sorted list of (START . END) position pairs." - (let (regions) + (let ((regions ; Collect bounds (already in position format) + (when-let* ((bounds (plist-get context-data :bounds))) + (if (consp (car bounds)) bounds (list bounds))))) (with-current-buffer buffer (without-restriction ;; Collect overlays (dolist (ov (plist-get context-data :overlays)) (when (overlay-start ov) (push (cons (overlay-start ov) (overlay-end ov)) - regions))) - ;; Collect bounds (already in position format) - (when-let* ((bounds (plist-get context-data :bounds))) - (if (consp (car bounds)) - (setq regions (nconc regions bounds)) ;((start1 . end1) (start2 . end2) ...) - (push bounds regions))) ;(start1 . end1) + regions))) ;(start1 . end1) ;; Collect lines (convert line numbers to positions) (when-let* ((line-bounds (plist-get context-data :lines))) - ;; Convert singleton (start1 . end1) to ((start1 . end1)) - (unless (consp (car line-bounds)) (setq line-bounds (list line-bounds))) - (dolist (pair line-bounds) + (dolist (pair (if (consp (car line-bounds)) ;Handle single (BEG . END) + line-bounds (list line-bounds))) (push (cons (progn (goto-char (point-min)) (forward-line (1- (car pair))) (point)) @@ -543,7 +539,8 @@ Returns a sorted list of (START . END) position pairs." regions))))) ;; TODO: Update sort for Emacs 28+ calling convention ;; Sort by start position. - (sort regions (lambda (a b) (< (car a) (car b)))))) + ;; NOTE: This can modify `:bounds' of `context-data' by side-effect! + (sort regions #'car-less-than-car))) (defun gptel-context--insert-buffer-string (buffer context-data &optional header) "Insert at point a context string from CONTEXT-DATA in BUFFER. diff --git a/elpa/gptel-20260123.739/gptel-gemini.el b/elpa/gptel-20260425.846/gptel-gemini.el similarity index 83% rename from elpa/gptel-20260123.739/gptel-gemini.el rename to elpa/gptel-20260425.846/gptel-gemini.el index 7af66816..83410523 100644 --- a/elpa/gptel-20260123.739/gptel-gemini.el +++ b/elpa/gptel-20260425.846/gptel-gemini.el @@ -22,16 +22,17 @@ ;; This file adds support for the Gemini API to gptel ;;; Code: -(require 'gptel) (require 'cl-generic) (require 'map) -(eval-when-compile (require 'cl-lib)) +(require 'cl-lib) +(eval-and-compile (require 'gptel-request)) (declare-function prop-match-value "text-property-search") (declare-function text-property-search-backward "text-property-search") (declare-function json-read "json") (declare-function gptel-context--wrap "gptel-context") (declare-function gptel-context--collect-media "gptel-context") + (defvar json-object-type) ;;; Gemini @@ -40,6 +41,18 @@ (:copier nil) (:include gptel-backend))) +(defun gptel--gemini-update-tokens (usage info) + "Update token usage information from USAGE. +USAGE is part of the response, INFO is the request plist." + (when usage + (let* ((tokens (plist-get info :tokens)) + (input (+ (plist-get usage :promptTokenCount) + (or (plist-get tokens :input) 0))) + (output (+ (- (plist-get usage :totalTokenCount) + (plist-get usage :promptTokenCount)) + (or (plist-get tokens :output) 0)))) + (list :input input :output output)))) + ;; TODO: Using alt=sse in the query url generates an OpenAI style streaming ;; response, with more immediate updates. Maybe we should switch to that and ;; rewrite the stream parser? @@ -72,11 +85,13 @@ Mutate state INFO with response metadata. If INCLUDE-TEXT is non-nil, include response text in the prompts list." (let* ((cand0 (map-nested-elt response '(:candidates 0))) - (parts (map-nested-elt cand0 '(:content :parts)))) - (plist-put info :stop-reason (plist-get cand0 :finishReason)) - (plist-put info :output-tokens - (map-nested-elt - response '(:usageMetadata :candidatesTokenCount))) + (parts (map-nested-elt cand0 '(:content :parts))) + (stop-reason (plist-get cand0 :finishReason))) + (when stop-reason + (plist-put info :stop-reason stop-reason) + (when (string= stop-reason "STOP") + (plist-put info :tokens (gptel--gemini-update-tokens + (plist-get response :usageMetadata) info)))) (cl-loop for part across parts for tx = (plist-get part :text) @@ -137,7 +152,7 @@ list." (when gptel-temperature (setq params (plist-put params - :temperature (max gptel-temperature 1.0)))) + :temperature (max 0.0 gptel-temperature)))) (when gptel-max-tokens (setq params (plist-put params @@ -240,12 +255,58 @@ TOOLS is a list of `gptel-tool' structs, which see." (:name ,name :content ,result))))) tool-use)))) -(cl-defmethod gptel--inject-prompt ((_backend gptel-gemini) data new-prompt &optional _position) - "Append NEW-PROMPT to existing prompts in query DATA. +(cl-defmethod gptel--inject-prompt ((_backend gptel-gemini) data new-prompt &optional position) + "Inject NEW-PROMPT into existing prompts in query DATA. See generic implementation for full documentation." + (when (keywordp (car-safe new-prompt)) ;Is new-prompt one or many? + (setq new-prompt (list new-prompt))) (let ((prompts (plist-get data :contents))) - (plist-put data :contents (vconcat prompts (list new-prompt))))) + (pcase position + ('nil (plist-put data :contents (vconcat prompts new-prompt))) + ((pred integerp) + (when (< position 0) (setq position (+ (length prompts) position))) + (plist-put data :contents (vconcat (substring prompts 0 position) + new-prompt + (substring prompts position))))))) + +(cl-defmethod gptel--inject-tool-call ((_backend gptel-gemini) data tool-call new-call) + "Replace TOOL-CALL in query DATA with NEW-CALL. + +BACKEND is the `gptel-backend'. See the generic function documentation +for details. This implementation handles the Gemini API." + ;; FIXME: We currently assume that the tool call being modified is in the last + ;; position in the messages array. + (if-let* ((messages (plist-get data :contents)) + (entry (aref messages (1- (length messages)))) + (parts (plist-get entry :parts)) + (indexed-call + (cl-loop with name = (plist-get tool-call :name) + with old-args = (plist-get tool-call :args) + for chunk across parts + for i upfrom 0 + if (plist-get chunk :functionCall) + if (and (equal (plist-get it :name) name) + (equal (plist-get it :args) old-args)) + return (cons i (plist-get chunk :functionCall)) end + finally return nil)) + (index (car indexed-call)) + (call (cdr indexed-call))) + (if (null new-call) + (if (= (length parts) 1) + (plist-put data :contents (substring messages nil -1)) + (plist-put entry :parts + (vconcat (substring parts 0 index) + (substring parts (1+ index))))) + (when-let* ((args (plist-get new-call :args))) + (plist-put call :args args)) + (when-let* ((name (plist-get new-call :name))) + (plist-put call :name name))) + (display-warning + '(gptel tool-call) + (format "Could not inject updated tool-call arguments for tool call %s, %s" + (plist-get tool-call :name) + (truncate-string-to-width (prin1-to-string new-call) 50 nil nil t))))) (cl-defmethod gptel--parse-list ((backend gptel-gemini) prompt-list) (if (consp (car prompt-list)) @@ -404,7 +465,7 @@ Media files, if present, are placed in `gptel-context'." :input-cost 0.10 :output-cost 0.40 :cutoff-date "2025-01") - (gemini-3-pro-preview + (gemini-3.1-pro-preview :description "Most intelligent Gemini model with SOTA reasoning and multimodal understanding" :capabilities (tool-use json media audio video) :mime-types ("image/png" "image/jpeg" "image/webp" "image/heic" "image/heif" @@ -415,6 +476,28 @@ Media files, if present, are placed in `gptel-context'." :input-cost 2.0 ; 4.0 for >200k tokens :output-cost 12.00 ; 18.0 for >200k tokens :cutoff-date "2025-01") + (gemini-3.1-flash-lite-preview + :description "Nost cost-efficient multimodal Gemini model, offering the fastest performance for high-frequency, lightweight tasks" + :capabilities (tool-use json media audio video) + :mime-types ("image/png" "image/jpeg" "image/webp" "image/heic" "image/heif" + "application/pdf" "text/plain" "text/csv" "text/html" + "audio/mpeg" "audio/wav" "audio/ogg" "audio/flac" "audio/aac" "audio/mp3" + "video/mp4" "video/mpeg" "video/avi" "video/quicktime" "video/webm") + :context-window 1048 + :input-cost 0.25 + :output-cost 1.50 + :cutoff-date "2025-01") + (gemini-3-pro-preview + :description "DEPRECATED: The model will be shut down on March 9, 2026. Please use gemini-3.1-pro-preview instead" + :capabilities (tool-use json media audio video) + :mime-types ("image/png" "image/jpeg" "image/webp" "image/heic" "image/heif" + "application/pdf" "text/plain" "text/csv" "text/html" + "audio/mpeg" "audio/wav" "audio/ogg" "audio/flac" "audio/aac" "audio/mp3" + "video/mp4" "video/mpeg" "video/avi" "video/quicktime" "video/webm") + :context-window 1048 ; 65536 output token limit + :input-cost 2.0 ; 4.0 for >200k tokens + :output-cost 12.00 ; 18.0 for >200k tokens + :cutoff-date "2025-01") (gemini-3-flash-preview :description "Most intelligent Gemini model built for speed" :capabilities (tool-use json media audio video) @@ -544,8 +627,11 @@ source: ;;;###autoload (cl-defun gptel-make-gemini - (name &key curl-args header key request-params + (name &key curl-args key request-params (stream nil) + (header + (lambda (_info) (when-let* ((key (gptel--get-api-key))) + `(("X-goog-api-key" . ,key))))) (host "generativelanguage.googleapis.com") (protocol "https") (models gptel--gemini-models) @@ -614,18 +700,17 @@ for." :stream stream :request-params request-params :key key - :url (lambda () + :url (lambda (_info) (let ((method (if (and stream gptel-use-curl gptel-stream) "streamGenerateContent" "generateContent"))) - (format "%s://%s%s/%s:%s?key=%s" + (format "%s://%s%s/%s:%s" protocol host endpoint gptel-model - method - (gptel--get-api-key))))))) + method)))))) (prog1 backend (setf (alist-get name gptel--known-backends nil nil #'equal) @@ -633,3 +718,7 @@ for." (provide 'gptel-gemini) ;;; gptel-gemini.el ends here + +;; Local Variables: +;; byte-compile-warnings: (not docstrings) +;; End: diff --git a/elpa/gptel-20260123.739/gptel-gh.el b/elpa/gptel-20260425.846/gptel-gh.el similarity index 62% rename from elpa/gptel-20260123.739/gptel-gh.el rename to elpa/gptel-20260425.846/gptel-gh.el index 5136bcf3..e9965f60 100644 --- a/elpa/gptel-20260123.739/gptel-gh.el +++ b/elpa/gptel-20260425.846/gptel-gh.el @@ -19,125 +19,178 @@ ;;; Code: -(eval-when-compile - (require 'cl-lib)) +(require 'cl-lib) (require 'map) -(require 'gptel) +(eval-and-compile + (require 'gptel-request) + (require 'gptel-openai) + (require 'gptel-openai-responses)) (require 'browse-url) ;;; Github Copilot (defconst gptel--gh-models - '((gpt-4o - :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 0 :output-cost 0 :cutoff-date "2023-10") - (gpt-4.1 + '((gpt-4.1 :description "Flagship model for complex tasks" :capabilities (media tool-use json url) :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") - :context-window 128 + :context-window 111 :input-cost 0 :output-cost 0 - :cutoff-date "2024-05") - (gpt-41-copilot - :description "Flagship model for complex tasks" + :cutoff-date "2024-04") + (gpt-4o + :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 200 + :context-window 64 :input-cost 0 :output-cost 0 - :cutoff-date "2024-05") - (gpt-5 + :cutoff-date "2023-09") + (gpt-5-mini + :description "Faster, more cost-efficient version of GPT-5" + :capabilities (media tool-use json url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 128 + :input-cost 0 + :output-cost 0 + :cutoff-date "2024-06") + (gpt-5.1 + :description "The best model for coding and agentic tasks" + :capabilities (media tool-use json url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 128 + :input-cost 1 + :output-cost 1 + :cutoff-date "2024-09") + (gpt-5.1-codex :description "Flagship model for coding, reasoning, and agentic tasks across domains" - :capabilities (media tool-use json url) + :capabilities (media tool-use json url responses-api) :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") - :context-window 264 + :context-window 128 :input-cost 1 :output-cost 1 :cutoff-date "2024-09") (gpt-5.1-codex-max :description "Flagship model for coding, reasoning, and agentic tasks across domains" - :capabilities (media tool-use json url) + :capabilities (media tool-use json url responses-api) :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") - :context-window 400 + :context-window 128 :input-cost 1 :output-cost 1 :cutoff-date "2024-09") - (gpt-5-mini - :description "Faster, more cost-efficient version of GPT-5" - :capabilities (media tool-use json url) + (gpt-5.1-codex-mini + :description "Flagship model for coding, reasoning, and agentic tasks across domains" + :capabilities (media tool-use json url responses-api) :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") - :context-window 264 - :input-cost 0 - :output-cost 0 + :context-window 128 + :input-cost 1 + :output-cost 1 :cutoff-date "2024-09") - (gpt-5.1 + (gpt-5.2 :description "The best model for coding and agentic tasks" - :capabilities (media tool-use json url) + :capabilities (media tool-use json url responses-api) :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") - :context-window 400 + :context-window 128 :input-cost 1 :output-cost 1 - :cutoff-date "2024-09") - (gpt-5.2 + :cutoff-date "2025-08") + (gpt-5.2-codex :description "The best model for coding and agentic tasks" - :capabilities (media tool-use json url) + :capabilities (media tool-use json url responses-api) :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") - :context-window 400 - :input-cost 1.75 - :output-cost 14 + :context-window 272 + :input-cost 1 + :output-cost 1 :cutoff-date "2025-08") - (claude-sonnet-4 - :description "High-performance model with exceptional reasoning and efficiency" - :capabilities (media tool-use cache) - :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp" "application/pdf") - :context-window 216 + (gpt-5.3-codex + :description "The most capable agentic coding model to date" + :capabilities (media tool-use json url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 400 :input-cost 1 :output-cost 1 - :cutoff-date "2025-03") - (claude-sonnet-4.5 - :description "High-performance model with exceptional reasoning and efficiency" - :capabilities (media tool-use cache) - :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp" "application/pdf") - :context-window 144 + :cutoff-date "2025-08") + (gpt-5.4 + :description "Best intelligence at scale for agentic, coding, and professional workflows" + :capabilities (media tool-use json url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 400 :input-cost 1 :output-cost 1 - :cutoff-date "2025-07") + :cutoff-date "2025-08") + (gpt-5.4-mini + :description "Strongest mini model yet for coding, computer use, and subagent" + :capabilities (media tool-use json url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 400 + :input-cost 0.33 + :output-cost 0.33 + :cutoff-date "2025-08") (claude-haiku-4.5 :description "Near-frontier intelligence at blazing speeds with extended thinking" :capabilities (media tool-use cache) :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp" "application/pdf") - :context-window 144 + :context-window 128 :input-cost 0.33 :output-cost 0.33 :cutoff-date "2025-02") - (claude-opus-41 + (claude-opus-4.5 :description "Most capable model for complex reasoning and advanced coding" :capabilities (media tool-use cache) :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp" "application/pdf") - :context-window 80 - :input-cost 10 - :output-cost 10 + :context-window 128 + :input-cost 3 + :output-cost 3 :cutoff-date "2025-03") - (claude-opus-4.5 + (claude-opus-4.6 + :description "Most capable model for complex reasoning and advanced coding" + :capabilities (media tool-use cache) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp" "application/pdf") + :context-window 128 + :input-cost 3 + :output-cost 3 + :cutoff-date "2025-03") + (claude-opus-4.7 :description "Most capable model for complex reasoning and advanced coding" :capabilities (media tool-use cache) :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp" "application/pdf") - :context-window 144 + :context-window 128 :input-cost 3 :output-cost 3 - :cutoff-date "2025-05") + :cutoff-date "2025-03") + (claude-sonnet-4 + :description "High-performance model with exceptional reasoning and efficiency" + :capabilities (media tool-use cache) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp" "application/pdf") + :context-window 128 + :input-cost 1 + :output-cost 1 + :cutoff-date "2025-03") + (claude-sonnet-4.5 + :description "High-performance model with exceptional reasoning and efficiency" + :capabilities (media tool-use cache) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp" "application/pdf") + :context-window 128 + :input-cost 1 + :output-cost 1 + :cutoff-date "2025-03") + (claude-sonnet-4.6 + :description "High-performance model with exceptional reasoning and efficiency" + :capabilities (media tool-use cache) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp" "application/pdf") + :context-window 128 + :input-cost 1 + :output-cost 1 + :cutoff-date "2025-03") (gemini-2.5-pro :description "Next gen, high speed, multimodal for a diverse variety of tasks" :capabilities (tool-use json media) :mime-types ("image/png" "image/jpeg" "image/webp" "image/heic" "image/heif" "application/pdf" "text/plain" "text/csv" "text/html") - :context-window 128 + :context-window 109 :input-cost 1 :output-cost 1 - :cutoff-date "2024-08") + :cutoff-date "2025-01") (gemini-3-flash-preview :description "Most intelligent Gemini model built for speed" :capabilities (tool-use json media audio video) @@ -145,7 +198,7 @@ "application/pdf" "text/plain" "text/csv" "text/html" "audio/mpeg" "audio/wav" "audio/ogg" "audio/flac" "audio/aac" "audio/mp3" "video/mp4" "video/mpeg" "video/avi" "video/quicktime" "video/webm") - :context-window 128 + :context-window 109 :input-cost 0.33 :output-cost 0.33 :cutoff-date "2025-01") @@ -156,21 +209,33 @@ "application/pdf" "text/plain" "text/csv" "text/html" "audio/mpeg" "audio/wav" "audio/ogg" "audio/flac" "audio/aac" "audio/mp3" "video/mp4" "video/mpeg" "video/avi" "video/quicktime" "video/webm") - :context-window 128 + :context-window 109 + :input-cost 1 + :output-cost 1 + :cutoff-date "2025-01") + (gemini-3.1-pro-preview + :description "Most intelligent Gemini model with SOTA reasoning and multimodal understanding" + :capabilities (tool-use json media audio video) + :mime-types ("image/png" "image/jpeg" "image/webp" "image/heic" "image/heif" + "application/pdf" "text/plain" "text/csv" "text/html" + "audio/mpeg" "audio/wav" "audio/ogg" "audio/flac" "audio/aac" "audio/mp3" + "video/mp4" "video/mpeg" "video/avi" "video/quicktime" "video/webm") + :context-window 109 :input-cost 1 :output-cost 1 :cutoff-date "2025-01") (grok-code-fast-1 :description "Fast reasoning model for agentic coding" :capabilities '(tool-use json reasoning) - :context-window 128 - :input-cost 0.2 - :output-cost 1.5))) + :context-window 109 + :input-cost 0.25 + :output-cost 1.5 + :cutoff-date "2025-08"))) (cl-defstruct (gptel--gh (:include gptel-openai) (:copier nil) (:constructor gptel--make-gh)) - token github-token sessionid machineid) + token github-token sessionid machineid responses-backend) (defcustom gptel-gh-github-token-file (expand-file-name ".cache/copilot-chat/github-token" user-emacs-directory) @@ -341,21 +406,85 @@ Then we need a session token." expires_at))) (gptel--gh-renew-token)))) +(cl-defmethod gptel-curl--parse-stream ((backend gptel--gh) info) + (let ((model (plist-get info :model))) + (if (gptel--model-capable-p 'responses-api model) + ;; Defer to gptel-openai-responses backend + (gptel-curl--parse-stream + (gptel--gh-responses-backend backend) info) + (cl-call-next-method)))) + +(cl-defmethod gptel--parse-response ((backend gptel--gh) response info) + (let ((model (plist-get info :model))) + (if (gptel--model-capable-p 'responses-api model) + ;; Defer to gptel-openai-responses backend + (gptel--parse-response + (gptel--gh-responses-backend backend) response info) + (cl-call-next-method)))) + +(cl-defmethod gptel--request-data ((backend gptel--gh) prompts) + (if (gptel--model-capable-p 'responses-api gptel-model) + (gptel--request-data (gptel--gh-responses-backend backend) prompts) + (cl-call-next-method))) + +(cl-defmethod gptel--parse-schema ((backend gptel--gh) schema) + (if (gptel--model-capable-p 'responses-api gptel-model) + (gptel--parse-schema (gptel--gh-responses-backend backend) schema) + (cl-call-next-method))) + +(cl-defmethod gptel--parse-tools ((backend gptel--gh) tools) + (if (gptel--model-capable-p 'responses-api gptel-model) + (gptel--parse-tools (gptel--gh-responses-backend backend) tools) + (cl-call-next-method))) + +(cl-defmethod gptel--inject-tool-call ((backend gptel--gh) data tool-call new-call) + (if (gptel--model-capable-p 'responses-api gptel-model) + (gptel--inject-tool-call (gptel--gh-responses-backend backend) data tool-call new-call) + (cl-call-next-method))) + +(cl-defmethod gptel--parse-tool-results ((backend gptel--gh) tool-use) + (if (gptel--model-capable-p 'responses-api gptel-model) + (gptel--parse-tool-results (gptel--gh-responses-backend backend) tool-use) + (cl-call-next-method))) + +(cl-defmethod gptel--inject-prompt ((backend gptel--gh) data new-prompt &optional position) + (if (gptel--model-capable-p 'responses-api gptel-model) + (gptel--inject-prompt (gptel--gh-responses-backend backend) data new-prompt position) + (cl-call-next-method))) + +(cl-defmethod gptel--parse-list ((backend gptel--gh) prompt-list) + (if (gptel--model-capable-p 'responses-api gptel-model) + (gptel--parse-list (gptel--gh-responses-backend backend) prompt-list) + (cl-call-next-method))) + +(cl-defmethod gptel--parse-buffer ((backend gptel--gh) &optional max-entries) + (if (gptel--model-capable-p 'responses-api gptel-model) + (gptel--parse-buffer (gptel--gh-responses-backend backend) max-entries) + (cl-call-next-method))) + +(cl-defmethod gptel--inject-media ((backend gptel--gh) prompts) + (if (gptel--model-capable-p 'responses-api gptel-model) + (gptel--inject-media (gptel--gh-responses-backend backend) prompts) + (cl-call-next-method))) + ;;;###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) "")) - ,@(when (and gptel-track-media - (gptel--model-capable-p 'media)) - `(("copilot-vision-request" . "true"))) - ("copilot-integration-id" . "vscode-chat")))) + (header + (lambda (info) (gptel--gh-auth) + `(("openai-intent" . "conversation-panel") + ("authorization" . ,( concat "Bearer " + (plist-get (gptel--gh-token gptel-backend) :token))) + ("x-initiator" . ,(or (plist-get info :gh-initiator) ;tool call return turn + (prog1 "user" ;user turn + (plist-put info :gh-initiator "agent")))) + ("x-request-id" . ,(gptel--gh-uuid)) + ("vscode-sessionid" . ,(or (gptel--gh-sessionid gptel-backend) "")) + ("vscode-machineid" . ,(or (gptel--gh-machineid gptel-backend) "")) + ,@(when (and gptel-track-media + (gptel--model-capable-p 'media)) + `(("copilot-vision-request" . "true"))) + ("copilot-integration-id" . "vscode-chat")))) (host "api.githubcopilot.com") (protocol "https") (endpoint "/chat/completions") @@ -412,18 +541,33 @@ 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)))) + (let* ((url (lambda (_info) + (concat protocol "://" host + (if (gptel--model-capable-p 'responses-api gptel-model) + "/v1/responses" endpoint)))) + (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 url + :machineid (gptel--gh-machine-id) + :responses-backend + (gptel--make-openai-responses + :name name + :host host + :header header + :protocol protocol + :endpoint "/v1/responses" + :stream stream + :request-params request-params + :curl-args curl-args + :url url)))) (setf (alist-get name gptel--known-backends nil nil #'equal) backend) backend)) diff --git a/elpa/gptel-20260123.739/gptel-integrations.el b/elpa/gptel-20260425.846/gptel-integrations.el similarity index 82% rename from elpa/gptel-20260123.739/gptel-integrations.el rename to elpa/gptel-20260425.846/gptel-integrations.el index 65b7de38..621345d0 100644 --- a/elpa/gptel-20260123.739/gptel-integrations.el +++ b/elpa/gptel-20260425.846/gptel-integrations.el @@ -36,6 +36,66 @@ (require 'cl-lib) (eval-when-compile (require 'transient)) +;;;;; Vterm integration +;; Insertion and deletion is tricky in Vterm buffers. Try to ensure that +;; gptel-send still works there. TODO: Insertion works, but insertion-in-place +;; is flaky and fails depending on how well Vterm's prompt tracking works, as +;; well as on the presence of "virtual text" in the prompt. +(declare-function vterm-copy-mode "vterm") +(declare-function vterm-delete-region "vterm") +(declare-function vterm-goto-char "vterm") +(declare-function vterm-reset-cursor-point "vterm") +(declare-function vterm-cursor-in-command-buffer-p "vterm") +(declare-function vterm-send-key "vterm") +(declare-function vterm-insert "vterm") +(defvar vterm-copy-mode) + +(defun gptel--vterm-delete () + "Try to delete the region or Vterm prompt text. + +Intended for use in Vterm buffers with the \"respond-in-place\" option +of `gptel-send'." + (if (use-region-p) + (let ((beg (region-beginning)) ; Clear region + (end (region-end))) + (vterm-copy-mode -1) + (condition-case nil + ;; Preferred solution, fails if the prompt is part of region + (vterm-delete-region beg end) + (buffer-read-only + (when (vterm-goto-char end) ;HACK Try to clear characters one by one + (let ((prev-pt (1- end))) + (while (and (>= (vterm-reset-cursor-point) beg) + (/= (point) prev-pt) + (vterm-cursor-in-command-buffer-p)) + (setq prev-pt (point)) + (vterm-send-key "" nil t nil t))))))) + (let ((prev-pt 0)) ; Clear to prompt + (while (and (/= (vterm-reset-cursor-point) prev-pt) + (vterm-cursor-in-command-buffer-p)) + (setq prev-pt (point)) + (vterm-send-key "" nil t nil t))))) + +(defun gptel--vterm-pre-insert (info) + "Set up insertion into Vterm buffers for `gptel-send'. + +INFO is the query information for the active request." + (let ((start-marker (plist-get info :position)) + (hold-buffer (gptel--temp-buffer " *gptel-vterm-redirect*"))) + (plist-put info :vterm-marker (copy-marker start-marker t)) + (with-current-buffer hold-buffer + (move-marker start-marker (point-min) hold-buffer) + ;; We collect text elsewhere and copy it into the Vterm buffer at the end + (add-hook 'gptel-post-response-functions + (lambda (beg end) + (let ((response (buffer-substring-no-properties beg end))) + (with-current-buffer (plist-get info :buffer) + (goto-char (plist-get info :vterm-marker)) + (when vterm-copy-mode (vterm-copy-mode -1)) + (vterm-insert response))) + (kill-buffer (current-buffer))) + 90 t)))) + ;;;; MCP integration - requires the mcp package (declare-function mcp-hub-get-all-tool "mcp-hub") (declare-function mcp-hub-get-servers "mcp-hub") @@ -100,10 +160,10 @@ not a function and non-nil, start SERVERS synchronously." (lambda (&optional server-names) "Register and add tools from servers. Report failures." (let ((tools (gptel-mcp--get-tools - (or server-names (mapcar #'car inactive-servers)))) + (or server-names (mapcar #'car servers)))) (now-active (cl-remove-if-not server-active-p mcp-hub-servers))) (mapc (lambda (tool) (apply #'gptel-make-tool tool)) tools) - (gptel-mcp--activate-tools tools) + (when tools (gptel-mcp--activate-tools tools)) (if-let* ((failed (cl-set-difference inactive-servers now-active :test #'equal))) (progn diff --git a/elpa/gptel-20260123.739/gptel-kagi.el b/elpa/gptel-20260425.846/gptel-kagi.el similarity index 94% rename from elpa/gptel-20260123.739/gptel-kagi.el rename to elpa/gptel-20260425.846/gptel-kagi.el index 97a8d9a7..d27188d4 100644 --- a/elpa/gptel-20260123.739/gptel-kagi.el +++ b/elpa/gptel-20260425.846/gptel-kagi.el @@ -23,10 +23,9 @@ ;; This file adds support for the Kagi FastGPT LLM API to gptel ;;; Code: -(require 'gptel) (require 'cl-generic) -(eval-when-compile - (require 'cl-lib)) +(eval-when-compile (require 'cl-lib)) +(eval-and-compile (require 'gptel-request)) (declare-function gptel-context--wrap "gptel-context") @@ -38,7 +37,10 @@ (cl-defmethod gptel--parse-response ((_backend gptel-kagi) response info) (let* ((data (plist-get response :data)) (output (plist-get data :output)) - (references (plist-get data :references))) + (references (plist-get data :references)) + (tokens (plist-get data :tokens))) + (when tokens + (plist-put info :tokens (list :input tokens :output tokens))) (if (eq references :null) (setq references nil)) (if (eq output :null) (setq output nil)) (when references @@ -121,7 +123,8 @@ (cl-defun gptel-make-kagi (name &key curl-args stream key (host "kagi.com") - (header (lambda () `(("Authorization" . ,(concat "Bot " (gptel--get-api-key)))))) + (header (lambda (_info) + `(("Authorization" . ,(concat "Bot " (gptel--get-api-key)))))) (models '((fastgpt :capabilities (nosystem)) (summarize:cecil :capabilities (nosystem)) (summarize:agnes :capabilities (nosystem)) @@ -171,7 +174,7 @@ Example: :protocol protocol :endpoint endpoint :url - (lambda () + (lambda (_info) (concat protocol "://" host endpoint (if (equal gptel-model 'fastgpt) "fastgpt" "summarize")))))) diff --git a/elpa/gptel-20260123.739/gptel-ollama.el b/elpa/gptel-20260425.846/gptel-ollama.el similarity index 84% rename from elpa/gptel-20260123.739/gptel-ollama.el rename to elpa/gptel-20260425.846/gptel-ollama.el index abecfb6c..63d5a9cc 100644 --- a/elpa/gptel-20260123.739/gptel-ollama.el +++ b/elpa/gptel-20260425.846/gptel-ollama.el @@ -23,12 +23,13 @@ ;; This file adds support for the Ollama LLM API to gptel ;;; Code: -(require 'gptel) (require 'cl-generic) +(eval-and-compile (require 'gptel-request)) (declare-function json-read "json" ()) (declare-function gptel-context--wrap "gptel-context") (declare-function gptel-context--collect-media "gptel-context") + (defvar json-object-type) ;;; Ollama @@ -36,14 +37,20 @@ (:copier nil) (:include gptel-backend))) -;; FIXME(fsm) Remove this variable -(defvar-local gptel--ollama-token-count 0 - "Token count for ollama conversations. - -This variable holds the total token count for conversations with -Ollama models. - -Intended for internal use only.") +(defun gptel--ollama-update-tokens (usage info) + "Update token usage information from USAGE. +USAGE is part of the response, INFO is the request plist. + +This function accumulates token counts across multiple turns in a +multi-turn request (e.g., during tool use where results are fed +back to the LLM)." + (when usage + (let* ((tokens (plist-get info :tokens)) + (input (+ (or (plist-get usage :prompt_eval_count) 0) + (or (plist-get tokens :input) 0))) + (output (+ (or (plist-get usage :eval_count) 0) + (or (plist-get tokens :output) 0)))) + (list :input input :output output)))) (cl-defmethod gptel-curl--parse-stream ((_backend gptel-ollama) info) "Parse response stream for the Ollama API." @@ -78,10 +85,7 @@ Intended for internal use only.") (plist-put info :reasoning-block t) (plist-put info :reasoning-block nil))) (unless (eq done :json-false) - (with-current-buffer (plist-get info :buffer) - (cl-incf gptel--ollama-token-count - (+ (or (map-elt content :prompt_eval_count) 0) - (or (map-elt content :eval_count) 0)))) + (plist-put info :tokens (gptel--ollama-update-tokens content info)) (goto-char (point-max))))) (error (goto-char pt))) (apply #'concat (nreverse content-strs)))) @@ -91,7 +95,7 @@ Intended for internal use only.") Store response metadata in state INFO." (plist-put info :stop-reason (plist-get response :done_reason)) - (plist-put info :output-tokens (plist-get response :eval_count)) + (plist-put info :tokens (gptel--ollama-update-tokens response info)) (let* ((message (plist-get response :message)) (reasoning (plist-get message :thinking)) (content (plist-get message :content))) @@ -160,6 +164,44 @@ Store response metadata in state INFO." ;; NOTE: No `gptel--inject-prompt' method required for gptel-ollama, since this is ;; handled by its defgeneric implementation +(cl-defmethod gptel--inject-tool-call ((_backend gptel-ollama) data tool-call new-call) + "Replace TOOL-CALL in query DATA with NEW-CALL. + +BACKEND is the `gptel-backend'. See the generic function documentation +for details. This implementation handles the Ollama API." + ;; FIXME: We currently assume that the tool call being modified is in the last + ;; position in the messages array. + (if-let* ((messages (plist-get data :messages)) + (entry (aref messages (1- (length messages)))) + (calls (plist-get entry :tool_calls)) + (indexed-call + (cl-loop with name = (plist-get tool-call :name) + with old-args = (plist-get tool-call :args) + for chunk across calls + for i upfrom 0 + for function = (plist-get chunk :function) + if (and (equal (plist-get function :name) name) + (equal (plist-get function :arguments) old-args)) + return (cons i function) end + finally return nil)) + (index (car indexed-call)) + (call (cdr indexed-call))) + (if (null new-call) + (if (= (length calls) 1) + (plist-put data :messages (substring messages nil -1)) + (plist-put entry :tool_calls + (vconcat (substring calls 0 index) + (substring calls (1+ index))))) + (when-let* ((args (plist-get new-call :args))) + (plist-put call :arguments args)) + (when-let* ((name (plist-get new-call :name))) + (plist-put call :name name))) + (display-warning + '(gptel tool-call) + (format "Could not inject updated tool-call arguments for tool call %s, %s" + (plist-get tool-call :name) + (truncate-string-to-width (prin1-to-string new-call) 50 nil nil t))))) + (cl-defmethod gptel--parse-list ((backend gptel-ollama) prompt-list) (if (consp (car prompt-list)) (let ((full-prompt)) ; Advanced format, list of lists diff --git a/elpa/gptel-20260123.739/gptel-openai-extras.el b/elpa/gptel-20260425.846/gptel-openai-extras.el similarity index 86% rename from elpa/gptel-20260123.739/gptel-openai-extras.el rename to elpa/gptel-20260425.846/gptel-openai-extras.el index f8ed4d38..b5fad51f 100644 --- a/elpa/gptel-20260123.739/gptel-openai-extras.el +++ b/elpa/gptel-20260425.846/gptel-openai-extras.el @@ -24,10 +24,11 @@ ;;; Code: (require 'cl-generic) -(eval-when-compile - (require 'cl-lib)) +(eval-when-compile (require 'cl-lib)) (require 'map) -(require 'gptel) +(eval-and-compile + (require 'gptel-request) + (require 'gptel-openai)) (defvar json-object-type) @@ -111,8 +112,9 @@ (cl-defun gptel-make-privategpt (name &key curl-args stream key request-params (header - (lambda () (when-let* ((key (gptel--get-api-key))) - `(("Authorization" . ,(concat "Bearer " key)))))) + (lambda (_info) + (when-let* ((key (gptel--get-api-key))) + `(("Authorization" . ,(concat "Bearer " key)))))) (host "localhost:8001") (protocol "http") (models '(private-gpt)) @@ -189,7 +191,7 @@ for." (cl-defmethod gptel--parse-response ((_backend gptel-perplexity) response _info) "Parse Perplexity response RESPONSE." - (let ((response-string (map-nested-elt response '(:choices 0 :message :content))) + (let ((response-string (cl-call-next-method)) (citations-string (when-let* ((citations (map-elt response :citations))) (gptel--perplexity-parse-citations citations)))) (concat response-string citations-string))) @@ -203,7 +205,7 @@ the response." (unless (plist-get info :citations) (save-excursion (goto-char (point-max)) - (when (search-backward (plist-get info :token) + (when (search-backward (plist-get info :uuid) (line-beginning-position) t) (forward-line 0) (when (re-search-backward "^data: " nil t) @@ -220,8 +222,9 @@ the response." (cl-defun gptel-make-perplexity (name &key curl-args stream key (header - (lambda () (when-let* ((key (gptel--get-api-key))) - `(("Authorization" . ,(concat "Bearer " key)))))) + (lambda (_info) + (when-let* ((key (gptel--get-api-key))) + `(("Authorization" . ,(concat "Bearer " key)))))) (host "api.perplexity.ai") (protocol "https") ;; https://docs.perplexity.ai/guides/model-cards @@ -291,30 +294,34 @@ The Deepseek API requires strictly alternating roles (user/assistant) in message (rest (cdr index))) (when (and p2 (equal (plist-get p1 :role) (plist-get p2 :role))) - (setf (plist-get p1 :content) - (concat (plist-get p1 :content) "\n" - (plist-get p2 :content))) - (setcdr index (cdr rest))) + ;; Blocks to be merged must both be text blocks + ;; and not tool calls + (when-let* ((content1 (plist-get p1 :content)) + (content2 (plist-get p2 :content))) + (plist-put p1 :content + (concat content1 "\n" content2)) + (setcdr index (cdr rest)))) (setq index (cdr index))))))) ;;;###autoload (cl-defun gptel-make-deepseek (name &key curl-args stream key request-params - (header (lambda () (when-let* ((key (gptel--get-api-key))) - `(("Authorization" . ,(concat "Bearer " key)))))) + (header (lambda (_info) + (when-let* ((key (gptel--get-api-key))) + `(("Authorization" . ,(concat "Bearer " key)))))) (host "api.deepseek.com") (protocol "https") (endpoint "/v1/chat/completions") (models '((deepseek-reasoner :capabilities (tool reasoning) :context-window 128 - :input-cost 0.56 - :output-cost 1.68) + :input-cost 0.28 + :output-cost 0.42) (deepseek-chat :capabilities (tool) :context-window 128 - :input-cost 0.56 - :output-cost 1.68)))) + :input-cost 0.28 + :output-cost 0.42)))) "Register a DeepSeek backend for gptel with NAME. For the meanings of the keyword arguments, see `gptel-make-openai'." @@ -338,18 +345,26 @@ For the meanings of the keyword arguments, see `gptel-make-openai'." ;;;###autoload (cl-defun gptel-make-xai (name &key curl-args stream key request-params - (header (lambda () (when-let* ((key (gptel--get-api-key))) - `(("Authorization" . ,(concat "Bearer " key)))))) + (header (lambda (_info) + (when-let* ((key (gptel--get-api-key))) + `(("Authorization" . ,(concat "Bearer " key)))))) (host "api.x.ai") (protocol "https") (endpoint "/v1/chat/completions") (models - '((grok-4 - :description "Grok Flagship model" + '((grok-4-1-fast-reasoning + :description "Fast tool-calling model" :capabilities (tool-use json reasoning) - :context-window 256 - :input-cost 3 - :output-cost 15) + :context-window 2000 + :input-cost 0.2 + :output-cost 0.5) + + (grok-4-1-fast-non-reasoning + :description "Fast tool-calling model (non-reasoning)" + :capabilities (tool-use json) + :context-window 2000 + :input-cost 0.2 + :output-cost 0.5) (grok-code-fast-1 :description "Fast reasoning model for agentic coding" @@ -358,19 +373,26 @@ For the meanings of the keyword arguments, see `gptel-make-openai'." :input-cost 0.2 :output-cost 1.5) - (grok-3 - :description "Grok 3" + (grok-4-fast-reasoning + :description "Fast tool-calling model" :capabilities (tool-use json reasoning) - :context-window 131 - :input-cost 3 - :output-cost 15) + :context-window 2000 + :input-cost 0.2 + :output-cost 0.5) + + (grok-4-fast-non-reasoning + :description "Fast tool-calling model (non-reasoning)" + :capabilities (tool-use json) + :context-window 2000 + :input-cost 0.2 + :output-cost 0.5) - (grok-3-fast - :description "Faster Grok 3" + (grok-4 + :description "Grok Flagship model" :capabilities (tool-use json reasoning) - :context-window 131 - :input-cost 5 - :output-cost 25) + :context-window 256 + :input-cost 3 + :output-cost 15) (grok-3-mini :description "Mini Grok 3" @@ -379,18 +401,18 @@ For the meanings of the keyword arguments, see `gptel-make-openai'." :input-cost 0.3 :output-cost 0.5) - (grok-3-mini-fast - :description "Faster mini Grok 3" + (grok-3 + :description "Grok 3" :capabilities (tool-use json reasoning) - :context-window 131072 - :input-cost 0.6 - :output-cost 4) + :context-window 131 + :input-cost 3 + :output-cost 15) (grok-2-vision-1212 :description "Grok 2 Vision" :capabilities (tool-use json media) :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") - :context-window 32768 + :context-window 32 :input-cost 2 :output-cost 10)))) "Register an xAI backend for gptel with NAME. diff --git a/elpa/gptel-20260425.846/gptel-openai-responses.el b/elpa/gptel-20260425.846/gptel-openai-responses.el new file mode 100644 index 00000000..e2f73aa4 --- /dev/null +++ b/elpa/gptel-20260425.846/gptel-openai-responses.el @@ -0,0 +1,602 @@ +;;; gptel-openai-responses.el --- Responses API support for gptel -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Karthik Chikmagalur + +;; Author: Karthik Chikmagalur +;; Keywords: comm + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; + +;;; Code: +(require 'cl-generic) +(require 'cl-lib) +(require 'map) +(require 'gptel-request) +(require 'gptel-openai) + +(defvar gptel-mode) +(declare-function gptel-context--collect-media "gptel-context") + +;;; OpenAI Responses +(defun gptel--openai-responses-update-tokens (usage info) + "Update token usage information from USAGE. +USAGE is part of the response, INFO is the request plist." + (when usage + (let* ((tokens (plist-get info :tokens)) + (input (+ (or (plist-get usage :input_tokens) 0) + (or (plist-get tokens :input) 0))) + (output (+ (or (plist-get usage :output_tokens) 0) + (or (plist-get tokens :output) 0))) + (cached (+ (or (map-nested-elt + usage '(:input_tokens_details :cached_tokens)) + 0) + (or (plist-get tokens :cached) 0)))) + (list :input input :output output :cached cached)))) + +(cl-defmethod gptel-curl--parse-stream ((_backend gptel-openai-responses) info) + "Parse an OpenAI Responses API data stream. + +Return the text response accumulated since the last call to this +function. Additionally, mutate state INFO to add tool-use +information if the stream contains it." + (let ((content-strs) wait) + (condition-case nil + (while (and (not wait) (re-search-forward "^event: *\\(.+\\)" nil t)) + (let ((event-type (match-string 1)) data) + (forward-line 1) + (if (not (looking-at "data:" t)) + (progn (goto-char (match-beginning 0)) ;not enough data, reset + (setq wait t)) + (forward-char 5) + (setq data (gptel--json-read)) + (pcase event-type + ;; Text content delta + ("response.output_text.delta" + (when-let* ((delta (plist-get data :delta)) + ((not (string-empty-p delta)))) + (push delta content-strs))) + ;; Function call arguments delta + ("response.function_call_arguments.delta" + (when-let* ((delta (plist-get data :delta))) + (plist-put info :partial_json + (cons delta (plist-get info :partial_json))))) + ;; Function call completed (user-defined tools) + ("response.output_item.done" + (when-let* ((item (plist-get data :item)) + ((equal (plist-get item :type) "function_call")) + (tool-call + (list :id (plist-get item :call_id) + :name (plist-get item :name) + :args (ignore-errors + (gptel--json-read-string + (plist-get item :arguments)))))) + (plist-put info :tool-use + (cons tool-call (plist-get info :tool-use))) + (plist-put info :partial_json nil))) + ;; Reasoning content + ((or "response.reasoning_summary_text.delta" + "response.reasoning.delta") + (when-let* ((delta (plist-get data :delta))) + (plist-put info :reasoning + (concat (plist-get info :reasoning) delta)))) + ((or "response.reasoning_summary_text.done" + "response.reasoning.done") + (plist-put info :reasoning-block t)) + ;; NOTE: backend tools are not supported in gptel yet, this + ;; parsing is for the future + ;; Web search completed (server-side tool) + ("response.web_search_call.completed" + (push "\n[Web search completed]" content-strs)) + ;; Code interpreter output (server-side tool) + ("response.code_interpreter_call.completed" + (when-let* ((item (plist-get data :item)) + (results (plist-get item :results))) + (cl-loop + for result across results + if (equal (plist-get result :type) "logs") + do (push (format "\n```\n%s\n```" (plist-get result :logs)) + content-strs)))) + ;; Response completed + ("response.completed" + (when-let* ((tool-use (plist-get info :tool-use))) + ;; Inject tool calls into prompt data for continuation + ;; TODO(responses-api) Avoid re-encoding these tool calls, + ;; especially :arguments + (gptel--inject-prompt + (plist-get info :backend) (plist-get info :data) + (mapcar (lambda (tc) + (list :type "function_call" + :call_id (plist-get tc :id) + :name (plist-get tc :name) + :arguments + (gptel--json-encode (plist-get tc :args)))) + tool-use))) + (when-let* ((resp (plist-get data :response))) + (plist-put info :stop-reason (plist-get resp :status)) + (plist-put info :tokens (gptel--openai-responses-update-tokens + (plist-get resp :usage) info)))))))) + (error (goto-char (match-beginning 0)))) + (apply #'concat (nreverse content-strs)))) + +(cl-defmethod gptel--parse-response ((_backend gptel-openai-responses) response info) + "Parse an OpenAI Responses API RESPONSE and return response text. +Mutate state INFO with response metadata." + (let ((output-items (plist-get response :output)) + (content-strs) (tool-use) (tool-calls)) + ;; Store usage info + (plist-put info :stop-reason (plist-get response :status)) + (plist-put info :tokens (gptel--openai-responses-update-tokens + (plist-get response :usage) info)) + ;; Process output items + (cl-loop + for item across output-items + for item-type = (plist-get item :type) + do + (pcase item-type + ;; Text message output + ("message" + (when-let* ((content (plist-get item :content))) + (cl-loop + for part across content + for part-type = (plist-get part :type) + if (equal part-type "output_text") + do (push (plist-get part :text) content-strs) + else if (equal part-type "refusal") + do (push (format "[Refused: %s]" (plist-get part :refusal)) + content-strs)))) + ;; Function call from model (user-defined tools) + ("function_call" + (push item tool-calls) + (push (list :id (plist-get item :call_id) + :name (plist-get item :name) + :args (ignore-errors + (gptel--json-read-string + (plist-get item :arguments)))) + tool-use)) + ;; Reasoning summary + ("reasoning" + (cl-loop with summary = (plist-get item :summary) + with content = (plist-get item :content) + for s across + (if (length= content 0) summary content) + collect (plist-get s :text) into reasoning + finally do + (plist-put info :reasoning (apply #'concat reasoning)))) + ;; Web search results (server-side tool) + ("web_search_call" + (when-let* ((status (plist-get item :status)) + ((equal status "completed"))) + ;; Results are inline, just note that search was done + (push "\n[Web search completed]" content-strs))) + ;; Code interpreter output (server-side tool) + ("code_interpreter_call" + (when-let* ((status (plist-get item :status)) + ((equal status "completed")) + (results (plist-get item :results))) + (cl-loop + for result across results + for result-type = (plist-get result :type) + if (equal result-type "logs") + do (push (format "\n```\n%s\n```" (plist-get result :logs)) + content-strs)))) + ;; File search results (server-side tool) + ("file_search_call" + (when-let* ((status (plist-get item :status)) + ((equal status "completed")) + (results (plist-get item :results))) + (push (format "\n[File search: %d results]" (length results)) + content-strs))))) + ;; Store tool calls for user-defined function tools + (when tool-use + (plist-put info :tool-use (nreverse tool-use)) + ;; Inject into prompts for conversation continuity + (gptel--inject-prompt + (plist-get info :backend) (plist-get info :data) + (nreverse tool-calls))) + ;; Return concatenated content + (when content-strs + (apply #'concat (nreverse content-strs))))) + +(cl-defmethod gptel--request-data ((backend gptel-openai-responses) prompts) + "JSON encode PROMPTS for sending to OpenAI Responses API." + (let ((prompts-plist + `( :model ,(gptel--model-name gptel-model) + :input ,(vconcat prompts) + ;; Stateless: don't store responses server-side, don't use + ;; previous_response_id. Each request contains full context. + :store :json-false + :stream ,(or gptel-stream :json-false))) + (o-model-p (memq gptel-model '(o1 o1-preview o1-mini o3-mini o3 o4-mini)))) + ;; System message becomes instructions + (when gptel--system-message + (plist-put prompts-plist :instructions gptel--system-message)) + ;; Temperature + (when (and gptel-temperature (not o-model-p)) + (plist-put prompts-plist :temperature gptel-temperature)) + ;; Max tokens + (when gptel-max-tokens + (plist-put prompts-plist :max_output_tokens gptel-max-tokens)) + (when gptel-use-tools + (let ((tools-array + (vconcat + (when gptel-tools + (gptel--parse-tools backend gptel-tools))))) + (when (> (length tools-array) 0) + (plist-put prompts-plist :tools tools-array)) + (when (eq gptel-use-tools 'force) + (plist-put prompts-plist :tool_choice "required")))) + ;; Structured output via text format + (when gptel--schema + (plist-put prompts-plist :text + (list :format + (list :type "json_schema" + :name (md5 (format "%s" (random))) + :schema (gptel--preprocess-schema + (gptel--dispatch-schema-type gptel--schema)) + :strict t)))) + ;; Merge request params + (gptel--merge-plists + prompts-plist + gptel--request-params + (gptel-backend-request-params gptel-backend) + (gptel--model-request-params gptel-model)))) + +;; Helper functions for Responses API format conversion + +(cl-defmethod gptel--parse-schema ((_backend gptel-openai-responses) schema) + "Parse SCHEMA for Responses API structured output. +In Responses API, the schema format uses text.format instead of response_format." + (list :type "json_schema" + :name (md5 (format "%s" (random))) + :schema (gptel--preprocess-schema + (gptel--dispatch-schema-type schema)) + :strict t)) + +(cl-defmethod gptel--parse-tools ((_backend gptel-openai-responses) tools) + "Parse TOOLS and return a list of prompts. + +TOOLS is a list of `gptel-tool' structs, which see. + +_BACKEND is the LLM backend in use. This is the default +implementation, used by OpenAI-compatible APIs and Ollama." + (vconcat + (mapcar + (lambda (tool) + (nconc + (list + :type "function" + :name (gptel-tool-name tool) + :description (gptel-tool-description tool)) + (if (gptel-tool-args tool) + (list + :parameters + (list :type "object" + ;; gptel's tool args spec is close to the JSON schema, except + ;; that we use (:name "argname" ...) + ;; instead of (:argname (...)), and + ;; (:optional t) for each arg instead of (:required [...]) + ;; for all args at once. Handle this difference by + ;; modifying a copy of the gptel tool arg spec. + :properties + (cl-loop + for arg in (gptel-tool-args tool) + for argspec = (copy-sequence arg) + for name = (plist-get arg :name) ;handled differently + for newname = (or (and (keywordp name) name) + (make-symbol (concat ":" name))) + do ;ARGSPEC is ARG without unrecognized keys + (cl-remf argspec :name) + (cl-remf argspec :optional) + if (equal (plist-get arg :type) "object") + do (unless (plist-member argspec :required) + (plist-put argspec :required [])) + (plist-put argspec :additionalProperties :json-false) + append (list newname argspec)) + :required + (vconcat + (delq nil (mapcar + (lambda (arg) (and (not (plist-get arg :optional)) + (plist-get arg :name))) + (gptel-tool-args tool)))) + :additionalProperties :json-false)) + (list :parameters (list :type "object" :properties nil))))) + (ensure-list tools)))) + +(cl-defmethod gptel--inject-tool-call ((_backend gptel-openai-responses) data tool-call new-call) + "Replace TOOL-CALL in query DATA with NEW-CALL. + +BACKEND is the `gptel-backend'. See the generic function documentation +for details. This implementation handles the OpenAI Responses API." + (if-let* ((input (plist-get data :input)) + (indexed-call + (cl-loop for item across input + for i upfrom 0 + if (and (equal (plist-get item :type) "function_call") + (equal (plist-get item :call_id) (plist-get tool-call :id))) + return (cons i item))) + (index (car indexed-call)) + (call (cdr indexed-call))) + (if (null new-call) ;delete tool call if new-call is nil + (plist-put data :input (vconcat (substring input 0 index) + (substring input (1+ index)))) + (progn + (when-let* ((args (plist-get new-call :args))) + (plist-put call :arguments (gptel--json-encode args))) + (when-let* ((name (plist-get new-call :name))) + (plist-put call :name name)))) + (display-warning + '(gptel tool-call) + (format "Could not inject updated tool-call arguments for tool call %s, %s" + (plist-get tool-call :name) + (truncate-string-to-width (prin1-to-string new-call) 50 nil nil t))))) + +(cl-defmethod gptel--parse-tool-results ((_backend gptel-openai-responses) tool-use) + "Format TOOL-USE results for Responses API. +Returns prompts in Responses API format with function_call_output items." + (mapcar + (lambda (tool-call) + (list + :type "function_call_output" + :call_id (plist-get tool-call :id) + :output (plist-get tool-call :result))) + tool-use)) + +(cl-defmethod gptel--inject-prompt + ((_backend gptel-openai-responses) data new-prompt &optional position) + "Inject NEW-PROMPT into existing prompts in query DATA. + +NEW-PROMPT can be a single message or a list of messages. + +If POSITION is +- nil, append NEW-PROMPT at the end of DATA +- a non-negative integer, insert it at that position in DATA. +- a negative integer, insert it there counting from the end." + (when (keywordp (car-safe new-prompt)) ;Is new-prompt one or many? + (setq new-prompt (list new-prompt))) + (let ((prompts (plist-get data :input))) + (pcase position + ('nil (plist-put data :input (vconcat prompts new-prompt))) + ((pred integerp) + (when (< position 0) (setq position (+ (length prompts) position))) + (plist-put data :input (vconcat (substring prompts 0 position) + new-prompt + (substring prompts position))))))) + +(cl-defmethod gptel--parse-list ((backend gptel-openai-responses) prompt-list) + (if (consp (car prompt-list)) + (let ((full-prompt)) ; Advanced format, list of lists + (dolist (entry prompt-list) + (pcase entry + (`(prompt . ,msg) + (push (list :role "user" :content (or (car-safe msg) msg)) full-prompt)) + (`(response . ,msg) + (push (list :role "assistant" :content (or (car-safe msg) msg)) full-prompt)) + (`(tool . ,call) + (unless (plist-get call :id) + (plist-put call :id (gptel--openai-format-tool-id nil))) + (push + (list :type "function_call" + :call_id (plist-get call :id) + :name (plist-get call :name) + :arguments (decode-coding-string + (gptel--json-encode (plist-get call :args)) + 'utf-8 t)) + full-prompt) + (push (car (gptel--parse-tool-results backend (list (cdr entry)))) full-prompt)))) + (nreverse full-prompt)) + (cl-loop for text in prompt-list ; Simple format, list of strings + for role = t then (not role) + if text collect + (list :role (if role "user" "assistant") :content text)))) + +(cl-defmethod gptel--parse-buffer ((backend gptel-openai-responses) &optional max-entries) + (let ((prompts) (prev-pt (point))) + (if (or gptel-mode gptel-track-response) + (while (and (or (not max-entries) (>= max-entries 0)) + (/= prev-pt (point-min)) + (goto-char (previous-single-property-change + (point) 'gptel nil (point-min)))) + (pcase (get-char-property (point) 'gptel) + ('response + (when-let* ((content (gptel--trim-prefixes + (buffer-substring-no-properties (point) prev-pt)))) + (push (list :role "assistant" :content content) prompts))) + (`(tool . ,id) + (save-excursion + (condition-case nil + (let* ((tool-call (read (current-buffer))) + (name (plist-get tool-call :name)) + (arguments (decode-coding-string + (gptel--json-encode (plist-get tool-call :args)) + 'utf-8 t))) + (setq id (gptel--openai-format-tool-id id)) + (plist-put tool-call :id id) + (plist-put tool-call :result + (string-trim (buffer-substring-no-properties + (point) prev-pt))) + (push (car (gptel--parse-tool-results backend (list tool-call))) + prompts) + (push (list :type "function_call" + :call_id id + :name name + :arguments arguments) + prompts)) + ((end-of-file invalid-read-syntax) + (message (format "Could not parse tool-call %s on line %s" + id (line-number-at-pos (point)))))))) + ('ignore) + ('nil + (and max-entries (cl-decf max-entries)) + (if gptel-track-media + (when-let* ((content (gptel--openai-responses-parse-multipart + (gptel--parse-media-links major-mode + (point) prev-pt)))) + (when (> (length content) 0) + (push (list :role "user" :content content) prompts))) + (when-let* ((content (gptel--trim-prefixes (buffer-substring-no-properties + (point) prev-pt)))) + (push (list :role "user" :content content) prompts))))) + (setq prev-pt (point))) + (let ((content (string-trim (buffer-substring-no-properties + (point-min) (point-max))))) + (push (list :role "user" :content content) prompts))) + prompts)) + +(defun gptel--openai-responses-parse-multipart (parts) + "Convert a multipart prompt PARTS to the OpenAI API format. + +The input is an alist of the form + ((:text \"some text\") + (:media \"/path/to/media.png\" :mime \"image/png\") + (:text \"More text\")). + +The output is a vector of entries in a backend-appropriate +format." + (cl-loop + for part in parts + for n upfrom 1 + with last = (length parts) + for text = (plist-get part :text) + for media = (plist-get part :media) + if text do + (and (or (= n 1) (= n last)) (setq text (gptel--trim-prefixes text))) + and if text + collect `(:type "input_text" :text ,text) into parts-array end + else if media collect + `(:type "input_image" + :image_url ,(concat "data:" (plist-get part :mime) + ";base64," (gptel--base64-encode media))) + into parts-array + else if (plist-get part :textfile) collect + `(:type "input_text" + :text ,(with-temp-buffer + (gptel--insert-file-string (plist-get part :textfile)) + (buffer-string))) + into parts-array end and + if (plist-get part :url) + collect + `(:type "input_image" + :image_url ,(plist-get part :url)) + into parts-array + finally return (vconcat parts-array))) + +(cl-defmethod gptel--inject-media ((_backend gptel-openai-responses) prompts) + "Wrap the first user prompt in PROMPTS with included media files. + +Media files, if present, are placed in `gptel-context'." + (when-let* ((media-list (gptel-context--collect-media))) + (cl-callf (lambda (current) + (vconcat + (gptel--openai-responses-parse-multipart media-list) + (cl-typecase current + (string `((:type "input_text" :text ,current))) + (vector current) + (t current)))) + (plist-get (car prompts) :content)))) + +;;;###autoload +(cl-defun gptel-make-openai-responses + (name &key curl-args (models gptel--openai-models) + stream key request-params + (header + (lambda (_info) (when-let* ((key (gptel--get-api-key))) + `(("Authorization" . ,(concat "Bearer " key)))))) + (host "api.openai.com") + (protocol "https") + (endpoint "/v1/responses")) + "Register an OpenAI Responses API backend for gptel with NAME. + +The Responses API is OpenAI's new API for agentic applications that +provides built-in tools like web search, code interpreter, and file +search. + +Keyword arguments: + +CURL-ARGS (optional) is a list of additional Curl arguments. + +HOST (optional) is the API host, typically \"api.openai.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'. + +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 +\"/v1/responses\". + +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\")) + +KEY (optional) is a variable whose value is the API key, or +function that returns the key. + +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. + +Example: +------- + + (gptel-make-openai-responses + \"OpenAI-Responses\" + :stream t + :models '((gpt-4o + :capabilities (media tool-use json url responses-api) + :mime-types (\"image/jpeg\" \"image/png\" \"image/gif\" \"image/webp\")) + (gpt-4o-mini + :capabilities (media tool-use json url responses-api) + :mime-types (\"image/jpeg\" \"image/png\" \"image/gif\" \"image/webp\"))))" + (declare (indent 1)) + (let ((backend (gptel--make-openai-responses + :curl-args curl-args + :name name + :host host + :header header + :key key + :models (gptel--process-models models) + :protocol protocol + :endpoint endpoint + :stream stream + :request-params request-params + :url (if protocol + (concat protocol "://" host endpoint) + (concat host endpoint))))) + (prog1 backend + (setf (alist-get name gptel--known-backends + nil nil #'equal) + backend)))) + +(provide 'gptel-openai-responses) +;;; gptel-openai-responses.el ends here + +;; Local Variables: +;; byte-compile-warnings: (not docstrings) +;; End: diff --git a/elpa/gptel-20260123.739/gptel-openai.el b/elpa/gptel-20260425.846/gptel-openai.el similarity index 62% rename from elpa/gptel-20260123.739/gptel-openai.el rename to elpa/gptel-20260425.846/gptel-openai.el index 6f96bf19..86151d67 100644 --- a/elpa/gptel-20260123.739/gptel-openai.el +++ b/elpa/gptel-20260425.846/gptel-openai.el @@ -23,148 +23,49 @@ ;;; Code: (require 'cl-generic) -(eval-when-compile - (require 'cl-lib)) +(eval-when-compile (require 'cl-lib)) (require 'map) +(eval-and-compile (require 'gptel-request)) -(defvar gptel-model) -(defvar gptel-stream) -(defvar gptel-use-curl) -(defvar gptel-backend) -(defvar gptel-temperature) -(defvar gptel-max-tokens) -(defvar gptel--system-message) (defvar json-object-type) (defvar gptel-mode) -(defvar gptel-track-response) -(defvar gptel-track-media) -(defvar gptel-use-tools) -(defvar gptel-tools) -(defvar gptel--schema) -(defvar gptel--request-params) (declare-function gptel-context--collect-media "gptel-context") -(declare-function gptel--base64-encode "gptel") -(declare-function gptel--trim-prefixes "gptel") -(declare-function gptel--parse-media-links "gptel") -(declare-function gptel--model-capable-p "gptel") -(declare-function gptel--model-name "gptel") -(declare-function gptel--get-api-key "gptel") -(declare-function gptel--insert-file-string "gptel") -(declare-function prop-match-value "text-property-search") -(declare-function text-property-search-backward "text-property-search") (declare-function json-read "json") -(declare-function gptel-prompt-prefix-string "gptel") -(declare-function gptel-response-prefix-string "gptel") -(declare-function gptel--merge-plists "gptel") -(declare-function gptel--model-request-params "gptel") (declare-function gptel-context--wrap "gptel-context") -(declare-function gptel--inject-prompt "gptel") -(declare-function gptel--parse-tools "gptel") -(declare-function gptel--parse-schema "gptel") -(declare-function gptel--preprocess-schema "gptel") -(declare-function gptel--dispatch-schema-type "gptel") - -;; JSON conversion semantics used by gptel -;; empty object "{}" => empty list '() == nil -;; null => :null -;; false => :json-false - -;; TODO(tool) Except when reading JSON from a string, where null => nil - -(defmacro gptel--json-read () - (if (fboundp 'json-parse-buffer) - `(json-parse-buffer - :object-type 'plist - :null-object :null - :false-object :json-false) - (require 'json) - (defvar json-object-type) - (defvar json-null) - (declare-function json-read "json" ()) - `(let ((json-object-type 'plist) - (json-null :null)) - (json-read)))) - -(defmacro gptel--json-read-string (str) - (if (fboundp 'json-parse-string) - `(json-parse-string ,str - :object-type 'plist - :null-object nil - :false-object :json-false) - (require 'json) - (defvar json-object-type) - (declare-function json-read-from-string "json" ()) - `(let ((json-object-type 'plist)) - (json-read-from-string ,str)))) - -(defmacro gptel--json-encode (object) - (if (fboundp 'json-serialize) - `(json-serialize ,object - :null-object :null - :false-object :json-false) - (require 'json) - (defvar json-false) - (defvar json-null) - (declare-function json-encode "json" (object)) - `(let ((json-false :json-false) - (json-null :null)) - (json-encode ,object)))) - -(defun gptel--process-models (models) - "Convert items in MODELS to symbols with appropriate properties." - (let ((models-processed)) - (dolist (model models) - (cl-etypecase model - (string (push (intern model) models-processed)) - (symbol (push model models-processed)) - (cons - (cl-destructuring-bind (name . props) model - (setf (symbol-plist name) - ;; MAYBE: Merging existing symbol plists is safer, but makes it - ;; difficult to reset a symbol plist, since removing keys from - ;; it (as opposed to setting them to nil) is more work. - ;; - ;; (map-merge 'plist (symbol-plist name) props) - props) - (push name models-processed))))) - (nreverse models-processed))) - -;;; Common backend struct for LLM support -(defvar gptel--known-backends nil - "Alist of LLM backends known to gptel. - -This is an alist mapping user-provided names to backend structs, -see `gptel-backend'. - -You can have more than one backend pointing to the same resource -with differing settings.") - -(defun gptel-get-backend (name) - "Return gptel backend with NAME. - -Throw an error if there is no match." - (or (alist-get name gptel--known-backends nil nil #'equal) - (user-error "Backend %s is not known to be defined" - name))) - -(gv-define-setter gptel-get-backend (val name) - `(setf (alist-get ,name gptel--known-backends - nil t #'equal) - ,val)) - -(cl-defstruct - (gptel-backend (:constructor gptel--make-backend) - (:copier gptel--copy-backend)) - name host header protocol stream - endpoint key models url request-params - curl-args - (coding-system nil :documentation "Can be set to `binary' if the backend expects non UTF-8 output.")) - -;;; OpenAI (ChatGPT) + +;; OpenAI Completions Backend (cl-defstruct (gptel-openai (:constructor gptel--make-openai) (:copier nil) (:include gptel-backend))) +;; OpenAI Responses Backend +(cl-defstruct (gptel-openai-responses (:constructor gptel--make-openai-responses) + (:copier nil) + (:include gptel-backend))) + + +(defun gptel--openai-update-tokens (usage info) + "Update token usage information from USAGE. +USAGE is part of the response, INFO is the request plist. + +This function accumulates token counts across multiple turns in a +multi-turn request (e.g., during tool use where results are fed +back to the LLM)." + (when usage + (let* ((tokens (plist-get info :tokens)) + (input (+ (or (plist-get usage :prompt_tokens) 0) + (or (plist-get tokens :input) 0))) + (output (+ (or (plist-get usage :completion_tokens) 0) + (or (plist-get tokens :output) 0))) + (cached (+ (or (map-nested-elt + usage '(:prompt_tokens_details :cached_tokens)) + 0) + (or (plist-get tokens :cached) 0))) + (cache (+ (or (plist-get usage :prompt_cache_hit_tokens) 0) + (or (plist-get tokens :cache) 0)))) + (list :input input :output output + :cache cache :cached cached)))) + ;; How the following function works: ;; ;; The OpenAI API returns a stream of data chunks. Each data chunk has a @@ -227,6 +128,14 @@ information if the stream contains it." (plist-get spec :arguments)))) into call-specs finally (plist-put info :tool-use call-specs))) + ;; Update token usage if present + (when-let* ((last-resp (save-excursion + (forward-line -1) + (and (re-search-backward "^data:" nil t) + (goto-char (match-end 0)) + (ignore-errors (gptel--json-read))))) + (usage (plist-get last-resp :usage))) + (plist-put info :tokens (gptel--openai-update-tokens usage info))) (when (plist-member info :reasoning-chunks) (plist-put info :reasoning-chunks nil))) (when-let* ((response (gptel--json-read)) (delta (map-nested-elt response '(:choices 0 :delta)))) @@ -281,8 +190,8 @@ Mutate state INFO with response metadata." (content (plist-get message :content))) (plist-put info :stop-reason (plist-get choice0 :finish_reason)) - (plist-put info :output-tokens - (map-nested-elt response '(:usage :completion_tokens))) + (plist-put info :tokens (gptel--openai-update-tokens + (map-nested-elt response '(:usage)) info)) ;; OpenAI returns either non-blank text content or a tool call, not both. ;; However OpenAI-compatible APIs like llama.cpp can include both (#819), so ;; we check for both tool calls and responses independently. @@ -314,13 +223,12 @@ Mutate state INFO with response metadata." :content gptel--system-message) prompts)) (let ((prompts-plist - `(:model ,(gptel--model-name gptel-model) - :messages [,@prompts] - :stream ,(or gptel-stream :json-false))) - (reasoning-model-p ; TODO: Embed this capability in the model's properties - (memq gptel-model '(o1 o1-preview o1-mini o3-mini o3 o4-mini - gpt-5 gpt-5-mini gpt-5-nano gpt-5.1 gpt-5.2)))) - (when (and gptel-temperature (not reasoning-model-p)) + (list :model (gptel--model-name gptel-model) + :messages (vconcat prompts) + :stream (or gptel-stream :json-false)))) + (when gptel-stream + (plist-put prompts-plist :stream_options '(:include_usage t))) + (when gptel-temperature (plist-put prompts-plist :temperature gptel-temperature)) (when gptel-use-tools (when (eq gptel-use-tools 'force) @@ -328,14 +236,11 @@ Mutate state INFO with response metadata." (when gptel-tools (plist-put prompts-plist :tools (gptel--parse-tools backend gptel-tools)) - (unless reasoning-model-p - (plist-put prompts-plist :parallel_tool_calls t)))) + (plist-put prompts-plist :parallel_tool_calls t))) (when gptel-max-tokens ;; HACK: The OpenAI API has deprecated max_tokens, but we still need it ;; for OpenAI-compatible APIs like GPT4All (#485) - (plist-put prompts-plist - (if reasoning-model-p :max_completion_tokens :max_tokens) - gptel-max-tokens)) + (plist-put prompts-plist :max_tokens gptel-max-tokens)) (when gptel--schema (plist-put prompts-plist :response_format (gptel--parse-schema backend gptel--schema))) @@ -357,6 +262,43 @@ Mutate state INFO with response metadata." ;; NOTE: No `gptel--parse-tools' method required for gptel-openai, since this is ;; handled by its defgeneric implementation +(cl-defmethod gptel--inject-tool-call ((_backend gptel-openai) data tool-call new-call) + "Replace TOOL-CALL in query DATA with NEW-CALL. + +BACKEND is the `gptel-backend'. See the generic function documentation +for details. This implementation handles the OpenAI-compatible +Completions API." + ;; FIXME: We currently assume that the tool call being modified is in the last + ;; position in the messages array. + (if-let* ((messages (plist-get data :messages)) + (entry (aref messages (1- (length messages)))) + (calls (plist-get entry :tool_calls)) + (id (plist-get tool-call :id)) + (indexed-call + (cl-loop for chunk across calls + for i upfrom 0 + if (equal (plist-get chunk :id) id) + return (cons i (plist-get chunk :function)) + finally return nil)) + (index (car indexed-call)) + (call (cdr indexed-call))) + (if (null new-call) ;delete tool call if new-call is nil + (if (= (length calls) 1) ;only tool call, delete message entirely + (plist-put data :messages (substring messages nil -1)) + ;; delete new-call selectively from parallel tool calls + (plist-put entry :tool_calls + (vconcat (substring calls 0 index) + (substring calls (1+ index))))) + (when-let* ((args (plist-get new-call :args))) + (plist-put call :arguments (gptel--json-encode args))) + (when-let* ((name (plist-get new-call :name))) + (plist-put call :name name))) + (display-warning + '(gptel tool-call) + (format "Could not inject updated tool-call arguments for tool call %s, %s" + (plist-get tool-call :name) + (truncate-string-to-width (prin1-to-string new-call) 50 nil nil t))))) + (cl-defmethod gptel--parse-tool-results ((_backend gptel-openai) tool-use) "Return a prompt containing tool call results in TOOL-USE." ;; (declare (side-effect-free t)) @@ -383,6 +325,7 @@ If the ID has the format used by a different backend, use as-is." (format "call_%s" tool-id))) (defun gptel--openai-unformat-tool-id (tool-id) + "Return the raw tool ID for TOOL-ID." (or (and (string-match "call_\\(.+\\)" tool-id) (match-string 1 tool-id)) tool-id)) @@ -529,15 +472,250 @@ Media files, if present, are placed in `gptel-context'." (t current)))) (plist-get (car prompts) :content)))) +(defconst gptel--openai-models + '((gpt-5.4-mini + :description "Faster, more cost-efficient version of GPT-5.4" + :capabilities (media tool-use json url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 400 + :input-cost 0.75 + :output-cost 4.50 + :cutoff-date "2025-08") + (gpt-5.4-nano + :description "Fastest, cheapest version of GPT-5.4" + :capabilities (media tool-use json url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 400 + :input-cost 0.20 + :output-cost 1.25 + :cutoff-date "2025-08") + (gpt-5.4 + :description "The best model for coding and agentic tasks" + :capabilities (media tool-use json url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 1050 + :input-cost 2.50 + :output-cost 15 + :cutoff-date "2025-08") + (gpt-5.4-pro + :description "Maximum performance model for reasoning tasks" + :capabilities (media tool-use json url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 1050 + :input-cost 30 + :output-cost 180 + :cutoff-date "2025-08") + (gpt-5.5 + :description "The best model for coding and agentic tasks" + :capabilities (media tool-use json url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 1050 + :input-cost 5 + :output-cost 30 + :cutoff-date "2025-12") + (gpt-5.5-pro + :description "Maximum performance model for reasoning tasks" + :capabilities (media tool-use json url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 1050 + :input-cost 30 + :output-cost 180 + :cutoff-date "2025-12") + (gpt-5.3-chat-latest + :description "Answers right away" + :capabilities (media tool-use json url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 400 + :input-cost 1.75 + :output-cost 14 + :cutoff-date "2025-08") + (gpt-5.2 + :description "The best model for coding and agentic tasks" + :capabilities (media tool-use json url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 400 + :input-cost 1.75 + :output-cost 14 + :cutoff-date "2025-08") + (gpt-5.1 + :description "The best model for coding and agentic tasks" + :capabilities (media tool-use json url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 400 + :input-cost 1.25 + :output-cost 10 + :cutoff-date "2024-09") + (gpt-5-mini + :description "Faster, more cost-efficient version of GPT-5" + :capabilities (media tool-use json url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 400 + :input-cost 0.25 + :output-cost 2.0 + :cutoff-date "2024-09") + (gpt-5-nano + :description "Fastest, cheapest version of GPT-5" + :capabilities (media tool-use json url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 400 + :input-cost 0.05 + :output-cost 0.40 + :cutoff-date "2024-09") + (gpt-5 + :description "Flagship model for coding, reasoning, and agentic tasks across domains" + :capabilities (media tool-use json url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 400 + :input-cost 1.25 + :output-cost 10 + :cutoff-date "2024-09") + (gpt-4.1-mini + :description "Balance between intelligence, speed and cost" + :capabilities (media tool-use json url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 1024 + :input-cost 0.4 + :output-cost 1.6) + (gpt-4.1-nano + :description "Fastest, most cost-effective GPT-4.1 model" + :capabilities (media tool-use json url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 1024 + :input-cost 0.10 + :output-cost 0.40 + :cutoff-date "2024-05") + (gpt-4.1 + :description "Flagship model for complex tasks" + :capabilities (media tool-use json url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 1024 + :input-cost 2.0 + :output-cost 8.0 + :cutoff-date "2024-05") + (gpt-4o-mini + :description "Cheap model for fast tasks; cheaper & more capable than GPT-3.5 Turbo" + :capabilities (media tool-use json url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 128 + :input-cost 0.15 + :output-cost 0.60 + :cutoff-date "2023-10") + (gpt-4o + :description "Advanced model for complex tasks; cheaper & faster than GPT-Turbo" + :capabilities (media tool-use json url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 128 + :input-cost 2.50 + :output-cost 10 + :cutoff-date "2023-10") + (gpt-4.5-preview + :description "DEPRECATED: Use gpt-4.1 instead" + :capabilities (media tool-use url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 128 + :input-cost 75 + :output-cost 150 + :cutoff-date "2023-10") + (gpt-4-turbo + :description "Previous high-intelligence model" + :capabilities (media tool-use url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 128 + :input-cost 10 + :output-cost 30 + :cutoff-date "2023-11") + (gpt-4 + :description "GPT-4 snapshot from June 2023 with improved function calling support" + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :capabilities (media url responses-api) + :context-window 8.192 + :input-cost 30 + :output-cost 60 + :cutoff-date "2023-11") + (o4-mini + :description "Fast, effective reasoning with efficient performance in coding and visual tasks" + :capabilities (reasoning media tool-use json url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 200 + :input-cost 1.10 + :output-cost 4.40 + :cutoff-date "2024-05") + (o3-mini + :description "High intelligence at the same cost and latency targets of o1-mini" + :context-window 200 + :input-cost 1.10 + :output-cost 4.40 + :cutoff-date "2023-10" + :capabilities (reasoning tool-use json responses-api)) + (o3 + :description "Well-rounded and powerful model across domains" + :capabilities (reasoning media tool-use json url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 200 + :input-cost 2 + :output-cost 8 + :cutoff-date "2024-05") + (o3-pro + :description "Maximum performance model for reasoning tasks" + :capabilities (reasoning media tool-use json url responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 200 + :input-cost 20 + :output-cost 80 + :cutoff-date "2024-05") + (o1-mini + :description "Faster and cheaper reasoning model good at coding, math, and science" + :context-window 128 + :input-cost 1.10 + :output-cost 4.40 + :cutoff-date "2023-10" + :capabilities (nosystem reasoning responses-api)) + (o1 + :description "Reasoning model designed to solve hard problems across domains" + :capabilities (media reasoning responses-api) + :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") + :context-window 200 + :input-cost 15 + :output-cost 60 + :cutoff-date "2023-10")) + "List of available OpenAI models and associated properties. + +Each model symbol is associated with the following keys, all optional: + +- `:description': a brief description of the model. + +- `:capabilities': a list of capabilities supporte responses-apid by the model. + +- `:mime-types': a list of supported MIME types for media files. + +- `:context-window': the context window size, in thousands of tokens. + +- `:input-cost': the input cost, in US dollars per million tokens. + +- `:output-cost': the output cost, in US dollars per million tokens. + +- `:cutoff-date': the knowledge cutoff date. + +- `:request-params': a plist of additional request parameters to + include when using this model. + +Information about the OpenAI models was obtained from the following +sources: + +- +- ") + ;;;###autoload (cl-defun gptel-make-openai - (name &key curl-args models stream key request-params + (name &key curl-args (models gptel--openai-models) + stream key request-params (header - (lambda () (when-let* ((key (gptel--get-api-key))) - `(("Authorization" . ,(concat "Bearer " key)))))) + (lambda (_info) + (when-let* ((key (gptel--get-api-key))) + `(("Authorization" . ,(concat "Bearer " key)))))) (host "api.openai.com") (protocol "https") - (endpoint "/v1/chat/completions")) + endpoint) "Register an OpenAI API-compatible backend for gptel with NAME. Keyword arguments: @@ -588,20 +766,31 @@ 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-openai - :curl-args curl-args - :name name - :host host - :header header - :key key - :models (gptel--process-models models) - :protocol protocol - :endpoint endpoint - :stream stream - :request-params request-params - :url (if protocol - (concat protocol "://" host endpoint) - (concat host endpoint))))) + (let* ((responses-api (string-match-p "api\\.openai\\.com" host)) + ;; Use the OpenAI Responses API if required + ;; TODO: Find a more reliable way to dispatch. Checking the host isn't + ;; reliable. For example, it won't work when using the Responses API + ;; via a proxy. + (constructor (if (not responses-api) + #'gptel--make-openai + (require 'gptel-openai-responses) + #'gptel--make-openai-responses)) + (endpoint (or endpoint + (if responses-api "/v1/responses" "/v1/chat/completions"))) + (backend (funcall constructor + :curl-args curl-args + :name name + :host host + :header header + :key key + :models (gptel--process-models models) + :protocol protocol + :endpoint endpoint + :stream stream + :request-params request-params + :url (if protocol + (concat protocol "://" host endpoint) + (concat host endpoint))))) (prog1 backend (setf (alist-get name gptel--known-backends nil nil #'equal) @@ -612,7 +801,7 @@ for." (cl-defun gptel-make-azure (name &key curl-args host (protocol "https") - (header (lambda () `(("api-key" . ,(gptel--get-api-key))))) + (header (lambda (_info) `(("api-key" . ,(gptel--get-api-key))))) (key 'gptel-api-key) models stream endpoint request-params) "Register an Azure backend for gptel with NAME. diff --git a/elpa/gptel-20260123.739/gptel-org.el b/elpa/gptel-20260425.846/gptel-org.el similarity index 84% rename from elpa/gptel-20260123.739/gptel-org.el rename to elpa/gptel-20260425.846/gptel-org.el index e46f81f2..32bafd09 100644 --- a/elpa/gptel-20260123.739/gptel-org.el +++ b/elpa/gptel-20260425.846/gptel-org.el @@ -23,11 +23,11 @@ ;; ;;; Code: -(eval-when-compile (require 'gptel)) (require 'cl-lib) (require 'org-element) (require 'outline) (require 'mailcap) ;FIXME Avoid this somehow +(eval-when-compile (require 'gptel-request)) ;; Functions used for saving/restoring gptel state in Org buffers (defvar gptel--num-messages-to-send) @@ -39,23 +39,24 @@ (defvar gptel-temperature) (defvar gptel-max-tokens) (defvar gptel--link-type-cache) +(defvar gptel--preset) (defvar org-link-angle-re) (defvar org-link-bracket-re) (declare-function mailcap-file-name-to-mime-type "mailcap") -(declare-function gptel--model-capable-p "gptel") -(declare-function gptel--model-mime-capable-p "gptel") -(declare-function gptel--model-name "gptel") -(declare-function gptel--to-string "gptel") -(declare-function gptel--to-number "gptel") -(declare-function gptel--intern "gptel") +(declare-function gptel--model-capable-p "gptel-request") +(declare-function gptel--model-mime-capable-p "gptel-request") +(declare-function gptel--model-name "gptel-request") +(declare-function gptel--to-string "gptel-request") +(declare-function gptel--to-number "gptel-request") +(declare-function gptel--intern "gptel-request") +(declare-function gptel-backend-name "gptel-request") +(declare-function gptel--parse-buffer "gptel-request") +(declare-function gptel--parse-directive "gptel-request") +(declare-function gptel--with-buffer-copy "gptel-request") +(declare-function gptel--file-binary-p "gptel-request") (declare-function gptel--get-buffer-bounds "gptel") -(declare-function gptel-backend-name "gptel") -(declare-function gptel--parse-buffer "gptel") -(declare-function gptel--parse-directive "gptel") (declare-function gptel--restore-props "gptel") -(declare-function gptel--with-buffer-copy "gptel") -(declare-function gptel--file-binary-p "gptel") (declare-function org-entry-get "org") (declare-function org-entry-put "org") (declare-function org-with-wide-buffer "org-macs") @@ -397,15 +398,15 @@ first nil value in REST is guaranteed to be correct." (readablep (or (eq resource-type 'url) (file-remote-p path) (file-readable-p path))) (mime-valid - (if (or (eq resource-type 'url) - (cdr (with-memoization - (alist-get (expand-file-name path) - gptel--link-type-cache - nil nil #'string=) - (cons t (gptel--file-binary-p path))))) - (gptel--model-mime-capable-p - (setq mime (mailcap-file-name-to-mime-type path))) - t))) + (or (eq resource-type 'url) + (and (with-memoization + (alist-get (expand-file-name path) + gptel--link-type-cache + nil nil #'string=) + (if (gptel--file-binary-p path) t)) + (setq mime (mailcap-file-name-to-mime-type path)) + (gptel--model-mime-capable-p mime)) + t))) (list t link-type path resource-type user-check readablep mime-valid mime) (list nil link-type path resource-type user-check readablep mime-valid mime)))) @@ -499,13 +500,13 @@ parameters. ARGS are the original function call arguments." (if (derived-mode-p 'org-mode) - (pcase-let ((`(,gptel--system-message ,gptel-backend ,gptel-model - ,gptel-temperature ,gptel-max-tokens - ,gptel--num-messages-to-send ,gptel-tools) + (pcase-let ((`( ,gptel--preset ,gptel--system-message ,gptel-backend + ,gptel-model ,gptel-temperature ,gptel-max-tokens + ,gptel--num-messages-to-send ,gptel-tools) (seq-mapn (lambda (a b) (or a b)) (gptel-org--entry-properties) - (list gptel--system-message gptel-backend gptel-model - gptel-temperature gptel-max-tokens + (list gptel--preset gptel--system-message gptel-backend + gptel-model gptel-temperature gptel-max-tokens gptel--num-messages-to-send gptel-tools)))) (apply send-fun args)) (apply send-fun args))) @@ -523,12 +524,13 @@ ARGS are the original function call arguments." (defun gptel-org--entry-properties (&optional pt) "Find gptel configuration properties stored at PT." (pcase-let - ((`(,system ,backend ,model ,temperature ,tokens ,num ,tools) + ((`(,preset ,system ,backend ,model ,temperature ,tokens ,num ,tools) (mapcar (lambda (prop) (org-entry-get (or pt (point)) prop 'selective)) - '("GPTEL_SYSTEM" "GPTEL_BACKEND" "GPTEL_MODEL" - "GPTEL_TEMPERATURE" "GPTEL_MAX_TOKENS" + '("GPTEL_PRESET" "GPTEL_SYSTEM" "GPTEL_BACKEND" + "GPTEL_MODEL" "GPTEL_TEMPERATURE" "GPTEL_MAX_TOKENS" "GPTEL_NUM_MESSAGES_TO_SEND" "GPTEL_TOOLS")))) + (when preset (setq preset (gptel--intern preset))) (when system (setq system (string-replace "\\n" "\n" system))) (when backend @@ -548,70 +550,101 @@ ARGS are the original function call arguments." (display-warning '(gptel org tools) (format "Tool %s not found, ignoring" tname))))) - (list system backend model temperature tokens num tools))) + (list preset system backend model temperature tokens num tools))) (defun gptel-org--restore-state () "Restore gptel state for Org buffers when turning on `gptel-mode'." (save-restriction - (let ((modified (buffer-modified-p))) - (widen) - (condition-case status - (progn - (when-let* ((bounds (org-entry-get (point-min) "GPTEL_BOUNDS"))) - (gptel--restore-props (read bounds))) - (pcase-let ((`(,system ,backend ,model ,temperature ,tokens ,num ,tools) - (gptel-org--entry-properties (point-min)))) - (when system (setq-local gptel--system-message system)) - (if backend (setq-local gptel-backend backend) - (message - (substitute-command-keys - (concat - "Could not activate gptel backend \"%s\"! " - "Switch backends with \\[universal-argument] \\[gptel-send]" - " before using gptel.")) - backend)) - (when model (setq-local gptel-model model)) - (when temperature (setq-local gptel-temperature temperature)) - (when tokens (setq-local gptel-max-tokens tokens)) - (when num (setq-local gptel--num-messages-to-send num)) - (when tools (setq-local gptel-tools tools)))) - (:success (message "gptel chat restored.")) - (error (message "Could not restore gptel state, sorry! Error: %s" status))) - (set-buffer-modified-p modified)))) + (widen) + (condition-case status + (progn + (when-let* ((bounds (org-entry-get (point-min) "GPTEL_BOUNDS"))) + (gptel--restore-props (read bounds))) + (pcase-let ((`(,preset ,system ,backend ,model ,temperature ,tokens ,num ,tools) + (gptel-org--entry-properties (point-min)))) + (when preset + (if (gptel-get-preset preset) + (progn (gptel--apply-preset + preset (lambda (sym val) (set (make-local-variable sym) val))) + (setq gptel--preset preset)) + (display-warning + '(gptel presets) + (format "Could not activate gptel preset `%s' in buffer \"%s\"" + preset (buffer-name))))) + (when system (setq-local gptel--system-message system)) + (if backend (setq-local gptel-backend backend) + (message + (substitute-command-keys + (concat + "Could not activate gptel backend \"%s\"! " + "Switch backends with \\[universal-argument] \\[gptel-send]" + " before using gptel.")) + backend)) + (when model (setq-local gptel-model model)) + (when temperature (setq-local gptel-temperature temperature)) + (when tokens (setq-local gptel-max-tokens tokens)) + (when num (setq-local gptel--num-messages-to-send num)) + (when tools (setq-local gptel-tools tools)))) + (:success (message "gptel chat restored.")) + (error (message "Could not restore gptel state, sorry! Error: %s" status))))) (defun gptel-org-set-properties (pt &optional msg) "Store the active gptel configuration under the current heading. -The active gptel configuration includes the current system -message, language model and provider (backend), and additional -settings when applicable. +PT is the cursor position by default. If MSG is non-nil (default), +display a message afterwards. -PT is the cursor position by default. If MSG is -non-nil (default), display a message afterwards." +If a gptel preset has been applied in this buffer, a reference to it is +saved. + +Additional metadata is stored only if no preset was applied or if it +differs from the preset specification. This is limited to the active +gptel model and backend names, the system message, active tools, the +response temperature, max tokens and number of conversation turns to +send in queries. (See `gptel--num-messages-to-send' for the last one.)" (interactive (list (point) t)) - (org-entry-put pt "GPTEL_MODEL" (gptel--model-name gptel-model)) - (org-entry-put pt "GPTEL_BACKEND" (gptel-backend-name gptel-backend)) - (org-entry-put pt "GPTEL_SYSTEM" ;TODO: Handle nil case correctly - (and-let* ((msg (car-safe - (gptel--parse-directive - gptel--system-message)))) - (string-replace "\n" "\\n" msg))) - (if gptel-tools - (org-entry-put - pt "GPTEL_TOOLS" (mapconcat #'gptel-tool-name gptel-tools " ")) - (org-entry-delete pt "GPTEL_TOOLS")) - (if (equal (default-value 'gptel-temperature) gptel-temperature) - (org-entry-delete pt "GPTEL_TEMPERATURE") - (org-entry-put pt "GPTEL_TEMPERATURE" - (number-to-string gptel-temperature))) - (if (natnump gptel--num-messages-to-send) - (org-entry-put pt "GPTEL_NUM_MESSAGES_TO_SEND" - (number-to-string gptel--num-messages-to-send)) - (org-entry-delete pt "GPTEL_NUM_MESSAGES_TO_SEND")) - (if gptel-max-tokens - (org-entry-put - pt "GPTEL_MAX_TOKENS" (number-to-string gptel-max-tokens)) - (org-entry-delete pt "GPTEL_MAX_TOKENS")) + (let ((preset-spec (and gptel--preset (gptel-get-preset gptel--preset)))) + (if preset-spec + (org-entry-put pt "GPTEL_PRESET" (gptel--to-string gptel--preset)) + (org-entry-delete pt "GPTEL_PRESET")) + + ;; FIXME: nil can mean "no value was explicitly set by the user" as well as + ;; "this setting has been set to nil". We are not yet distinguishing + ;; between the two when saving Org properties. This is particularly + ;; relevant for the system message, whose explicit nil value will not be + ;; captured when saving Org buffers. + + ;; Model and backend + (if (gptel--preset-mismatch-value preset-spec :model gptel-model) + (org-entry-put pt "GPTEL_MODEL" (gptel--model-name gptel-model))) + (if (gptel--preset-mismatch-value preset-spec :backend gptel-backend) + (org-entry-put pt "GPTEL_BACKEND" (gptel-backend-name gptel-backend))) + ;; System message + (let ((parsed (car-safe (gptel--parse-directive gptel--system-message)))) + (if (gptel--preset-mismatch-value preset-spec :system parsed) + (when parsed + (org-entry-put pt "GPTEL_SYSTEM" (string-replace "\n" "\\n" parsed))) + (org-entry-delete pt "GPTEL_SYSTEM"))) + ;; Tools + (let ((tool-names (mapcar #'gptel-tool-name gptel-tools))) + (if (gptel--preset-mismatch-value preset-spec :tools tool-names) + (org-entry-put pt "GPTEL_TOOLS" (string-join tool-names " ")) + (org-entry-delete pt "GPTEL_TOOLS"))) + ;; Temperature, max tokens and cutoff + (if (and (gptel--preset-mismatch-value preset-spec :temperature gptel-temperature) + (not (equal (default-value 'gptel-temperature) gptel-temperature))) + (org-entry-put pt "GPTEL_TEMPERATURE" (number-to-string gptel-temperature)) + (org-entry-delete pt "GPTEL_TEMPERATURE")) + (if (and (gptel--preset-mismatch-value preset-spec :max-tokens gptel-max-tokens) + gptel-max-tokens) + (org-entry-put pt "GPTEL_MAX_TOKENS" (number-to-string gptel-max-tokens)) + (org-entry-delete pt "GPTEL_MAX_TOKENS")) + (if (and (gptel--preset-mismatch-value + preset-spec :num-messages-to-send gptel--num-messages-to-send) + (natnump gptel--num-messages-to-send)) + (org-entry-put pt "GPTEL_NUM_MESSAGES_TO_SEND" + (number-to-string gptel--num-messages-to-send)) + (org-entry-delete pt "GPTEL_NUM_MESSAGES_TO_SEND"))) (when msg (message "Added gptel configuration to current headline."))) diff --git a/elpa/gptel-20260123.739/gptel-pkg.el b/elpa/gptel-20260425.846/gptel-pkg.el similarity index 77% rename from elpa/gptel-20260123.739/gptel-pkg.el rename to elpa/gptel-20260425.846/gptel-pkg.el index af2614c2..61c61cc3 100644 --- a/elpa/gptel-20260123.739/gptel-pkg.el +++ b/elpa/gptel-20260425.846/gptel-pkg.el @@ -1,12 +1,12 @@ ;; -*- no-byte-compile: t; lexical-binding: nil -*- -(define-package "gptel" "20260123.739" +(define-package "gptel" "20260425.846" "Interact with ChatGPT or other LLMs." '((emacs "27.1") (transient "0.7.4") (compat "30.1.0.0")) :url "https://github.com/karthink/gptel" - :commit "96199593c107767ffe0b59ab2a1f996c112e4fdf" - :revdesc "96199593c107" + :commit "101904a1a8568fe42df49f0f1acd4ddab39ae1f8" + :revdesc "101904a1a856" :keywords '("convenience" "tools") :authors '(("Karthik Chikmagalur" . "karthik.chikmagalur@gmail.com")) :maintainers '(("Karthik Chikmagalur" . "karthik.chikmagalur@gmail.com"))) diff --git a/elpa/gptel-20260123.739/gptel-request.el b/elpa/gptel-20260425.846/gptel-request.el similarity index 78% rename from elpa/gptel-20260123.739/gptel-request.el rename to elpa/gptel-20260425.846/gptel-request.el index 041e03ae..f50173a0 100644 --- a/elpa/gptel-20260123.739/gptel-request.el +++ b/elpa/gptel-20260425.846/gptel-request.el @@ -42,9 +42,7 @@ ;;; Code: -(require 'gptel-openai) -(eval-when-compile - (require 'subr-x)) +(eval-when-compile (require 'subr-x)) (require 'compat nil t) (require 'cl-lib) (require 'url) @@ -63,6 +61,7 @@ (declare-function gptel--transform-apply-preset "gptel") (declare-function gptel--insert-response "gptel") (declare-function gptel-curl--stream-insert-response "gptel") +(declare-function gptel-make-openai "gptel-openai") ;;; User options @@ -323,236 +322,199 @@ the same as t." (repeat symbol)) :group 'gptel) -(defvar gptel--known-backends) - -(defconst gptel--openai-models - '((gpt-4o - :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.50 - :output-cost 10 - :cutoff-date "2023-10") - (gpt-4o-mini - :description "Cheap model for fast tasks; cheaper & more capable than GPT-3.5 Turbo" - :capabilities (media tool-use json url) - :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") - :context-window 128 - :input-cost 0.15 - :output-cost 0.60 - :cutoff-date "2023-10") - (gpt-4.1 - :description "Flagship model for complex tasks" - :capabilities (media tool-use json url) - :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") - :context-window 1024 - :input-cost 2.0 - :output-cost 8.0 - :cutoff-date "2024-05") - (gpt-4.5-preview - :description "DEPRECATED: Use gpt-4.1 instead" - :capabilities (media tool-use url) - :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") - :context-window 128 - :input-cost 75 - :output-cost 150 - :cutoff-date "2023-10") - (gpt-4.1-mini - :description "Balance between intelligence, speed and cost" - :capabilities (media tool-use json url) - :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") - :context-window 1024 - :input-cost 0.4 - :output-cost 1.6) - (gpt-4.1-nano - :description "Fastest, most cost-effective GPT-4.1 model" - :capabilities (media tool-use json url) - :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") - :context-window 1024 - :input-cost 0.10 - :output-cost 0.40 - :cutoff-date "2024-05") - (gpt-4-turbo - :description "Previous high-intelligence model" - :capabilities (media tool-use url) - :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") - :context-window 128 - :input-cost 10 - :output-cost 30 - :cutoff-date "2023-11") - (gpt-4 - :description "GPT-4 snapshot from June 2023 with improved function calling support" - :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") - :capabilities (media url) - :context-window 8.192 - :input-cost 30 - :output-cost 60 - :cutoff-date "2023-11") - (gpt-5 - :description "Flagship model for coding, reasoning, and agentic tasks across domains" - :capabilities (media tool-use json url) - :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") - :context-window 400 - :input-cost 1.25 - :output-cost 10 - :cutoff-date "2024-09") - (gpt-5-mini - :description "Faster, more cost-efficient version of GPT-5" - :capabilities (media tool-use json url) - :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") - :context-window 400 - :input-cost 0.25 - :output-cost 2.0 - :cutoff-date "2024-09") - (gpt-5-nano - :description "Fastest, cheapest version of GPT-5" - :capabilities (media tool-use json url) - :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") - :context-window 400 - :input-cost 0.05 - :output-cost 0.40 - :cutoff-date "2024-09") - (gpt-5.1 - :description "The best model for coding and agentic tasks" - :capabilities (media tool-use json url) - :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") - :context-window 400 - :input-cost 1.25 - :output-cost 10 - :cutoff-date "2024-09") - (gpt-5.2 - :description "The best model for coding and agentic tasks" - :capabilities (media tool-use json url) - :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") - :context-window 400 - :input-cost 1.75 - :output-cost 14 - :cutoff-date "2025-08") - (o1 - :description "Reasoning model designed to solve hard problems across domains" - :capabilities (media reasoning) - :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") - :context-window 200 - :input-cost 15 - :output-cost 60 - :cutoff-date "2023-10") - (o1-mini - :description "Faster and cheaper reasoning model good at coding, math, and science" - :context-window 128 - :input-cost 3 - :output-cost 12 - :cutoff-date "2023-10" - :capabilities (nosystem reasoning)) - (o3 - :description "Well-rounded and powerful model across domains" - :capabilities (reasoning media tool-use json url) - :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") - :context-window 200 - :input-cost 2 - :output-cost 8 - :cutoff-date "2024-05") - (o3-mini - :description "High intelligence at the same cost and latency targets of o1-mini" - :context-window 200 - :input-cost 1.10 - :output-cost 4.40 - :cutoff-date "2023-10" - :capabilities (reasoning tool-use json)) - (o4-mini - :description "Fast, effective reasoning with efficient performance in coding and visual tasks" - :capabilities (reasoning media tool-use json url) - :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") - :context-window 200 - :input-cost 1.10 - :output-cost 4.40 - :cutoff-date "2024-05") - (gpt-3.5-turbo - :description "More expensive & less capable than GPT-4o-mini; use that instead" - :capabilities (tool-use) - :context-window 16.358 - :input-cost 0.50 - :output-cost 1.50 - :cutoff-date "2021-09") - (gpt-3.5-turbo-16k - :description "More expensive & less capable than GPT-4o-mini; use that instead" - :capabilities (tool-use) - :context-window 16.385 - :input-cost 3 - :output-cost 4 - :cutoff-date "2021-09")) - "List of available OpenAI models and associated properties. -Keys: - -- `:description': a brief description of the model. - -- `:capabilities': a list of capabilities supported by the model. - -- `:mime-types': a list of supported MIME types for media files. - -- `:context-window': the context window size, in thousands of tokens. - -- `:input-cost': the input cost, in US dollars per million tokens. - -- `:output-cost': the output cost, in US dollars per million tokens. - -- `:cutoff-date': the knowledge cutoff date. - -- `:request-params': a plist of additional request parameters to - include when using this model. - -Information about the OpenAI models was obtained from the following -sources: - -- -- ") - -(defcustom gptel-model 'gpt-4o-mini - (concat - "Model for chat. +(defvar gptel--known-backends nil + "Alist of LLM backends known to gptel. -The name of the model, as a symbol. This is the name as expected -by the LLM provider's API. +This is an alist mapping user-provided names to backend structs, +see `gptel-backend'. -To set the model for a chat session interactively call -`gptel-send' with a prefix argument.") - :safe #'always - :type `(choice - (symbol :tag "Specify model name") - ,@(mapcar (lambda (model) - (list 'const :tag (symbol-name (car model)) - (car model))) - gptel--openai-models))) - -(defvar gptel--openai - (gptel-make-openai - "ChatGPT" - :key 'gptel-api-key - :stream t - :models gptel--openai-models)) - -(defcustom gptel-backend gptel--openai +You can have more than one backend pointing to the same resource +with differing settings.") + +(defvar gptel--openai nil) +(make-obsolete-variable 'gptel--openai "No longer used" "v0.9.9.5") + +(defcustom gptel-backend nil "LLM backend to use. -This is the default \"backend\", an object of type -`gptel-backend' containing connection, authentication and model -information. +This is the default \"backend\" used by gptel, an object specifying +connection, authentication and model information required to send LLM +requests. + +There are two ways to set `gptel-backend': + +1. with `setopt' or the Customize interface, +2. and via constructors (such as `gptel-make-openai') in Elisp code. + +When using `setopt' or the customize interface, a backend may be +specified in a list such as + + (BACKEND-TYPE NAME . PLIST) + +where: + +BACKEND-TYPE is one of `gptel-openai' (all OpenAI-compatible +services), `gptel-anthropic', `gptel-gemini', `gptel-ollama', +`gptel-kagi', `gptel--gh' (GitHub Copilot), `gptel-bedrock' (AWS +Bedrock), `gptel-perplexity', `gptel-deepseek' or `gptel-privategpt'. + +NAME is a string, any backend name of your choosing. + +PLIST is an optional plist specifying connection and authentication +information, with keys + +:protocol - \"http\" or \"https\" +:host - Host, such as \"api.openai.com\" or \"localhost:1616\" +:endpoint - connection endpoint, such as \"/v1/chat/completions\" +:header - HTTP header to inclue with the request, a string + or function +:key - API key, if required for authentication + String, symbol or function, see `gptel-api-key' +:models - List of supported models (symbols, see + `gptel--openai-models' for an example) +:stream - Whether to stream responses, boolean +:request-params - Additional parameters to send with backend queries, as + a plist. This plist is converted to JSON when sending. + This is meant for request parameters that gptel does not + provide user options for. +:curl-args - list of strings representing additional Curl arguments (if + `gptel-use-curl' is set) + +When using the OpenAI, Anthropic, Gemini, Kagi, Github Copilot, +Perplexity or Deepseek backends, all plist keys are optional. For other +services, specifying some fields may be required. Examples: + + (setopt gptel-backend \\='(gptel-openai \"OpenAI\" + :key gptel-api-key-from-auth-source + :stream t)) + + (setopt gptel-backend \\='(gptel-anthropic \"Claude\" :key \"sk-...\")) + + (setopt gptel-backend \\='(gptel-ollama \"Ollama\" + :host \"localhost:11434\" + :models (qwen3:4b llama3.1:8b) + :stream t)) + +This list of keys is non-exhaustive. Some backends (such as +`gptel-bedrock') recognize or require additional keys. To see what +other keys are available, check the corresponding constructor (such as +`gptel-make-bedrock'). + +When not using `setopt', backends for LLM providers (local or remote) +may be constructed and registered using one of the available backend +constructor functions: -A backend for ChatGPT is pre-defined by gptel. Backends for -other LLM providers (local or remote) may be constructed using -one of the available backend creation functions: - `gptel-make-openai' -- `gptel-make-azure' +- `gptel-make-anthropic' +- `gptel-make-gemini' - `gptel-make-ollama' +- `gptel-make-azure' - `gptel-make-gpt4all' -- `gptel-make-gemini' -See their documentation for more information and the package -README for examples." +- `gptel-make-kagi' +- `gptel-make-privategpt' +- `gptel-make-perplexity' +- `gptel-make-deepseek' +- `gptel-make-xai' +- `gptel-make-gh-copilot' +- `gptel-make-bedrock' + +In addition, `gptel-backend' can be assigned to them. Examples: + + (setq gptel-backend (gptel-make-openai \"llamacpp\" + :host \"localhost:8080\" + :protocol \"http\" + :models \\='(gpt-oss-120b glm-4.7-flash))) + + (setq gptel-backend (gptel-make-gemini \"Gemini\" + :key gptel-api-key :stream t)) + + (setq gptel-backend (gptel-make-anthropic \"Claude-think\" + :key gptel-api-key + :request-params + \\='(:thinking (:type \"enabled\" :budget_tokens 1024) + :max_tokens 2048))) + +See their documentation for more information and the package README for +examples. Once registered, backends may be retrieved using +`gptel-get-backend' or switched to interactively from gptel's menu (see +`gptel-menu')." + :safe #'always + :type + (let ((types '( choice :tag "Type" + (const :tag "OpenAI compatible" gptel-openai) + (const gptel-anthropic) + (const gptel-gemini) + (const gptel-ollama) + (const gptel-kagi) + (const :tag "GitHub Copilot" gptel-gh) + (const gptel-bedrock) + (const gptel-perplexity) + (const gptel-deepseek) + (const gptel-privategpt)))) + `(choice + (restricted-sexp :match-alternatives (gptel-backend-p 'nil) + :tag "No backend") + (cons :tag "(BACKEND-TYPE NAME . PLIST)" ;accommodate (gptel-openai "chatgpt" . plist) + ,types (cons string + (plist :value-type (choice string symbol function + (repeat symbol))))) + (cons :tag "(BACKEND-TYPE . PLIST)" ;accommodate (gptel-openai :name "chatgpt" . plist) + ,types (plist :value-type (choice string symbol function + (repeat symbol)))))) + :get + (lambda (sym) + (when-let* ((backend (default-toplevel-value sym)) + (type (type-of backend)) + (plist (list :protocol (gptel-backend-protocol backend) + :host (gptel-backend-host backend) + :endpoint (gptel-backend-endpoint backend) + :header (gptel-backend-header backend) + :key (gptel-backend-key backend) + :models (gptel-backend-models backend) + :stream (gptel-backend-stream backend) + :curl-args (gptel-backend-curl-args backend) + :request-params (gptel-backend-request-params backend)))) + (apply #'list type (gptel-backend-name backend) + (cl-loop for (k v) on plist by #'cddr + if (and (readablep v) (not (null v))) + collect k and collect v)))) + :set + (lambda (sym val) + (cond + ((null val) (set-default-toplevel-value sym val)) + ((listp val) + (let* ((name (if (stringp (cadr val)) ;explicit and implicit :name specification + (cadr val) (plist-get (cdr val) :name))) + (args (if name (cddr val) (cdr val))) + type) + (cl-remf args :name) + (if (memq (car val) '(gptel-gh gptel--gh)) + (setq type 'gptel-gh-copilot) + (setq type (car val))) + (set-default-toplevel-value + sym (apply (intern (concat "gptel-make-" + (substring (symbol-name type) 6))) + name args)))) + ((gptel-backend-p val) (set-default-toplevel-value sym val))))) + +(defcustom gptel-model nil + (concat + "Model for gptel queries. + +The name of the model, as a symbol. This is the name as expected +by the LLM provider's API. + +To set the model for a chat session interactively call +`gptel-send' with a prefix argument.") :safe #'always :type `(choice - (const :tag "ChatGPT" ,gptel--openai) - (restricted-sexp :match-alternatives (gptel-backend-p 'nil) - :tag "Other backend"))) + (symbol :tag "Specify model name") + ,@(cl-loop + for (_name . backend) in gptel--known-backends + append (mapcar + (lambda (model) (list 'const :tag (symbol-name model) + model)) + (gptel-backend-models backend))))) (defvar gptel-expert-commands nil "Whether experimental gptel options should be enabled. @@ -560,7 +522,7 @@ README for examples." This opens up advanced options in `gptel-menu'.") (defvar gptel--num-messages-to-send nil) -(put 'gptel--num-messages-to-send 'safe-local-variable #'always) +(put 'gptel--num-messages-to-send 'safe-local-variable #'integer-or-null-p) (defcustom gptel-log-level nil "Logging level for gptel. @@ -598,19 +560,18 @@ always handled separately." :type 'boolean) (defcustom gptel-track-media nil - "Whether supported media in chat buffers should be sent. + "Whether links to supported media types should be followed. When this is non-nil, gptel will send text, images or other media from -links in chat buffers to the LLM. +links in Org and Markdown buffers to the LLM. Sending images or other binary media from links requires the active `gptel-model' to support it. See `gptel-make-openai', `gptel-make-anthropic', `gptel-make-ollama' or `gptel-make-gemini' for details on how to specify media support for models. -This option has no effect in non-chat buffers. To include -media (including images) more generally, use `gptel-add' or -`gptel-add-file'." +To include media (including binary formats like images) more generally, +you can also use `gptel-add' or `gptel-add-file' instead." :type 'boolean) (defcustom gptel-use-context 'system @@ -636,7 +597,7 @@ Currently supported options are: (const :tag "With system message" system) (const :tag "With user prompt" user))) -(defcustom gptel-include-reasoning t +(defcustom gptel-include-reasoning 'ignore "How to handle LLM reasoning or \"thinking\" text blocks. Some LLMs include in their response a \"thinking\" section. This @@ -645,10 +606,10 @@ be interesting to you by itself. Supported options are the symbols - t - Include with the response, the default + ignore - Include in the response but ignore on subsequent + conversation turns (default) + t - Include in the response nil - Do not include - ignore - Include with the response but ignore on subsequent - conversation turns It can also be a string naming a buffer, in which case the reasoning text will be inserted at the end of that buffer." @@ -763,11 +724,16 @@ See `gptel-backend'." :type '(repeat string)) (defconst gptel-curl--common-args - (if (memq system-type '(windows-nt ms-dos)) - '("--disable" "--location" "--silent" "-XPOST" - "-y7200" "-Y1" "-D-") + (cond + ((memq system-type '(windows-nt ms-dos)) + '("--disable" "--location" "--silent" "-XPOST" + "-y7200" "-Y1" "-N" "-D-")) + ((eq system-type 'darwin) + '("--disable" "--location" "--silent" "--compressed" + "-XPOST" "-y7200" "-Y1" "-N" "-D-")) + (t '("--disable" "--location" "--silent" "--compressed" - "-XPOST" "-y7200" "-Y1" "-D-")) + "-XPOST" "-y7200" "-Y1" "-N" "-D-"))) "Arguments always passed to Curl for gptel queries.") (defvar gptel--link-type-cache nil @@ -787,9 +753,137 @@ binary-encoded.") "\\(?:\\(?1:!\\)?\\(?2:\\[\\)\\(?3:\\^?\\(?:\\\\\\]\\|[^]]\\)*\\|\\)\\(?4:\\]\\)\\(?5:(\\)\\s-*\\(?6:[^)]*?\\)\\(?:\\s-+\\(?7:\"[^\"]*\"\\)\\)?\\s-*\\(?8:)\\)\\|\\(<\\)\\([a-z][a-z0-9.+-]\\{1,31\\}:[^] \n <>,;()]+\\)\\(>\\)\\)" "Link regex for `gptel-mode' in Markdown mode.") +(defvar gptel--mode-description-alist + '((js2-mode . "Javascript") + (sh-mode . "Shell") + (enh-ruby-mode . "Ruby") + (yaml-mode . "Yaml") + (yaml-ts-mode . "Yaml") + (rustic-mode . "Rust") + (tuareg-mode . "OCaml")) + "Mapping from unconventionally named major modes to languages. + +This is used when generating system prompts for rewriting and +when including context from these major modes.") + ;;; Utility functions +;;;; JSON parsing helpers +;; JSON conversion semantics used by gptel +;; empty object "{}" => empty list '() == nil +;; null => :null +;; false => :json-false + +;; TODO(tool) Except when reading JSON from a string, where null => nil + +(defmacro gptel--json-read () + "Parse JSON at point in buffer." + (if (fboundp 'json-parse-buffer) + `(json-parse-buffer + :object-type 'plist + :null-object :null + :false-object :json-false) + (require 'json) + (defvar json-object-type) + (defvar json-null) + (declare-function json-read "json" ()) + `(let ((json-object-type 'plist) + (json-null :null)) + (json-read)))) + +(defmacro gptel--json-read-string (str) + "Pasre JSON string STR." + (if (fboundp 'json-parse-string) + `(json-parse-string ,str + :object-type 'plist + :null-object nil + :false-object :json-false) + (require 'json) + (defvar json-object-type) + (declare-function json-read-from-string "json" ()) + `(let ((json-object-type 'plist)) + (json-read-from-string ,str)))) + +(defmacro gptel--json-encode (object) + "Serialize OBJECT as JSON." + (if (fboundp 'json-serialize) + `(json-serialize ,object + :null-object :null + :false-object :json-false) + (require 'json) + (defvar json-false) + (defvar json-null) + (declare-function json-encode "json" (object)) + `(let ((json-false :json-false) + (json-null :null)) + (json-encode ,object)))) + +(defmacro gptel--maybe-funcall (func-or-sym &rest args) + "If FUNC-OR-SYM is a function, call it with ARGS. + +Otherwise, evaluate it as a variable." + `(if (functionp ,func-or-sym) + ;; TODO(v1.0) Remove this condition-case. This arity check is for + ;; benefit of users who have personal customizations touching gptel's + ;; internal API re: backend header and url functions. + (condition-case nil + (apply ,func-or-sym (list ,@args)) + (wrong-number-of-arguments + (message "Displaying warning") + (display-warning + 'gptel (format "%s calling convention has changed: \ +Called with %d arguments but accept %d. \ +Please update them, and see NEWS (0.9.9.5) for details." + (if (symbolp ,func-or-sym) (format "Function %s" ,func-or-sym) + "gptel-backend-header/gptel-backend-url function") + (length ',args) (car (func-arity ,func-or-sym)))) + (funcall ,func-or-sym))) + ,func-or-sym)) + +(defun gptel--process-models (models) + "Convert items in MODELS to symbols with appropriate properties." + (let ((models-processed)) + (dolist (model models) + (cl-etypecase model + (string (push (intern model) models-processed)) + (symbol (push model models-processed)) + (cons + (cl-destructuring-bind (name . props) model + (setf (symbol-plist name) + ;; MAYBE: Merging existing symbol plists is safer, but makes it + ;; difficult to reset a symbol plist, since removing keys from + ;; it (as opposed to setting them to nil) is more work. + ;; + ;; (map-merge 'plist (symbol-plist name) props) + props) + (push name models-processed))))) + (nreverse models-processed))) + +;;;; Backend interface +(defun gptel-get-backend (name) + "Return gptel backend with NAME. + +Throw an error if there is no match." + (or (alist-get name gptel--known-backends nil nil #'equal) + (user-error "Backend %s is not known to be defined" + name))) + +(gv-define-setter gptel-get-backend (val name) + `(setf (alist-get ,name gptel--known-backends + nil t #'equal) + ,val)) + +(cl-defstruct + (gptel-backend (:constructor gptel--make-backend) + (:copier gptel--copy-backend)) + name host header protocol stream + endpoint key models url request-params + curl-args + (coding-system + nil :documentation "Can be set to `binary' if the backend expects non UTF-8 output.")) + +;;;; Misc utilities (defun gptel-api-key-from-auth-source (&optional host user) "Lookup api key in the auth source. By default, the LLM host for the active backend is used as HOST, @@ -922,6 +1016,7 @@ MODE-SYM is typically a major-mode symbol." (skip-syntax-forward "w.") ,(macroexp-progn body))) +;; NOTE: Remove after we drop Emacs 27.1 (#724) (defmacro gptel--temp-buffer (buf) "Generate a temp buffer BUF. @@ -1045,7 +1140,10 @@ FILE is assumed to exist and be a regular file." (or (alist-get 'default gptel-directives) "You are a large language model living in Emacs and a helpful assistant. Respond concisely.") "The system message used by gptel.") -(put 'gptel--system-message 'safe-local-variable #'always) +(put 'gptel--system-message 'safe-local-variable + #'(lambda (v) (or (string-or-null-p v) + (and (listp v) + (cl-every #'string-or-null-p v))))) (defun gptel--describe-directive (directive width &optional replacement) "Find description for DIRECTIVE, truncated to WIDTH. @@ -1324,7 +1422,7 @@ feed the LLM the results. You can add tools via (async nil :type boolean :documentation "Whether the function runs asynchronously") (category nil :type string :documentation "Use to group tools by purpose") (confirm nil :type boolean :documentation "Seek confirmation before running tool?") - (include nil :type boolean :documentation "Include tool results in buffer?")) + (include t :type boolean :documentation "Include tool results in buffer?")) (defun gptel--preprocess-tool-args (spec) "Convert symbol :type values in tool SPEC to strings destructively." @@ -1547,22 +1645,54 @@ send to the LLM.") ;; FIXME(fsm) unify this with `gptel--inject-media', which is a mess (cl-defgeneric gptel--inject-prompt - (_backend data new-prompt &optional _position) - "Append NEW-PROMPT to existing prompts in query DATA. + (_backend data new-prompt &optional position) + "Inject NEW-PROMPT into existing prompts in query DATA. NEW-PROMPT can be a single message or a list of messages. -Not implemented: if POSITION is -- a non-negative number, insert it at that position in PROMPTS. -- a negative number, insert it there counting from the end. +If POSITION is +- nil, append NEW-PROMPT at the end of DATA +- a non-negative integer, insert it at that position in DATA. +- a negative integer, insert it there counting from the end. + +- Not implemented: a list of accessors, inject it at that position. This generic implementation handles the Anthropic, OpenAI-compatible and Ollama message formats." - ;; ;TODO(fsm): implement _POSITION (when (keywordp (car-safe new-prompt)) ;Is new-prompt one or many? (setq new-prompt (list new-prompt))) (let ((prompts (plist-get data :messages))) - (plist-put data :messages (vconcat prompts new-prompt)))) + (pcase position + ('nil (plist-put data :messages (vconcat prompts new-prompt))) + ((pred integerp) + (when (< position 0) (setq position (+ (length prompts) position))) + (plist-put data :messages (vconcat (substring prompts 0 position) + new-prompt + (substring prompts position))))))) + +(cl-defgeneric gptel--inject-tool-call (backend _data _tool-call new-call) + "Replace TOOL-CALL in query DATA with NEW-CALL. + +DATA is the request payload containing the array of user and assistant +messages, typically available as (plist-get INFO :data). TOOL-CALL is +the call plist as recorded by gptel's response parser(s). NEW-CALL is +the replacement plist containing the new tool name and arguments, in the +form + + (:name \"newName\" :args (:arg1 \"newArg\" :arg2 ...)) + +:name and :args are both optional. + +If NEW-CALL is nil, the tool call is removed from DATA and thus the turn +history. + +BACKEND is the `gptel-backend'." + (display-warning + '(gptel tool-call) + (format "Editing tool call arguments is not implemented for %s.\ + Ignoring new arguments %s" + (type-of backend) + (truncate-string-to-width (prin1-to-string new-call) 50 nil nil t)))) ;;; State machine for driving requests @@ -1572,7 +1702,8 @@ OpenAI-compatible and Ollama message formats." (TYPE . ((,#'gptel--error-p . ERRS) (,#'gptel--tool-use-p . TOOL) (t . DONE))) - (TOOL . ((,#'gptel--error-p . ERRS) + (TOOL . ((t . TRET))) + (TRET . ((,#'gptel--error-p . ERRS) (,#'gptel--tool-result-p . WAIT) (t . DONE)))) "Alist specifying gptel's default state transition table for requests. @@ -1588,6 +1719,7 @@ considered a success and acts as a default.") (defvar gptel-request--handlers `((WAIT ,#'gptel--handle-wait) (TOOL ,#'gptel--handle-tool-use) + (TRET ,#'gptel--handle-tool-result) (DONE ,#'gptel--handle-post) (ERRS ,#'gptel--handle-post) (ABRT ,#'gptel--handle-post)) @@ -1668,9 +1800,9 @@ MACHINE is an instance of `gptel-fsm'" "Fire the request contained in state machine FSM's info." ;; Reset some flags in info. This is necessary when reusing fsm's context for ;; a second network request: gptel tests for the presence of these flags to - ;; handle state transitions. (NOTE: Don't add :token to this.) + ;; handle state transitions. (NOTE: Don't add :uuid to this.) (let ((info (gptel-fsm-info fsm))) - (dolist (key '(:tool-success :tool-use :error :http-status :reasoning)) + (dolist (key '(:tool-result :tool-use :error :http-status :reasoning)) (when (plist-get info key) (plist-put info key nil)))) (funcall @@ -1680,41 +1812,49 @@ MACHINE is an instance of `gptel-fsm'" fsm) (run-hooks 'gptel-post-request-hook)) +(defun gptel--process-tool-call (fsm tool-spec tool-call result) + "Add tool RESULT to a TOOL-CALL and transition FSM if required. + +TOOL-CALL is a plist with the tool :name, :args and other metadata. +TOOL-SPEC is the `gptel-tool' object, and FSM is the request state. + +If all pending tool calls in the current request have finished, it +injects the results into the prompt data and transitions the FSM." + (let* ((info (gptel-fsm-info fsm)) + (tool-result-alist (plist-get info :tool-result)) + ;; MAYBE(tool-hooks): Use plist-member for valid nil :result? + (remaining (cl-loop for call in (plist-get info :tool-use) + count (not (plist-get call :result))))) + (let ((result (gptel--to-string result))) + ;; FIXME(tool-hooks): If a hook has changed the tool that was called + ;; tool-spec needs to be updated. + (push (list tool-spec (plist-get tool-call :args) result) + tool-result-alist) + (plist-put info :tool-result tool-result-alist) ;for the callback + ;; NOTE: tool-call is a member of (plist-get info :tool-use), so :tool-use + ;; is modified by side effect. + ;; FIXME: Make the implicit addition to :tool-use explicit + (plist-put tool-call :result result)) ;for the LLM + ;; All tools have run + (when (<= (cl-decf remaining) 0) (gptel--fsm-transition fsm)))) + (defun gptel--handle-tool-use (fsm) "Run tool calls captured in FSM, and advance the state machine with the results." (when-let* ((info (gptel-fsm-info fsm)) (backend (plist-get info :backend)) ;; This function might run many times, so only act on the remaining tool calls. (tool-use (cl-remove-if (lambda (tc) (plist-get tc :result)) - (plist-get info :tool-use))) - (ntools (length tool-use)) - (tool-idx 0)) + (plist-get info :tool-use)))) (with-current-buffer (plist-get info :buffer) - (let ((result-alist) (pending-calls)) + (let ((pending-calls)) (mapc ; Construct function calls (lambda (tool-call) (letrec ((args (plist-get tool-call :args)) (name (plist-get tool-call :name)) - (arg-values nil) - (tool-spec - (cl-find-if - (lambda (ts) (equal (gptel-tool-name ts) name)) - (plist-get info :tools))) - (process-tool-result - (lambda (result) - (plist-put info :tool-success t) - (let ((result (gptel--to-string result))) - (plist-put tool-call :result result) - (push (list tool-spec args result) result-alist)) - (cl-incf tool-idx) - (when (>= tool-idx ntools) ; All tools have run - (gptel--inject-prompt - backend (plist-get info :data) - (gptel--parse-tool-results - backend (plist-get info :tool-use))) - (funcall (plist-get info :callback) - (cons 'tool-result result-alist) info) - (gptel--fsm-transition fsm))))) + (tool-spec (cl-find-if (lambda (ts) (equal (gptel-tool-name ts) name)) + (plist-get info :tools))) + (process-tool-result (apply-partially #'gptel--process-tool-call + fsm tool-spec tool-call))) (if (null tool-spec) (if (equal name gptel--ersatz-json-tool) ;Could be a JSON response ;; Handle structured JSON output supplied as tool call @@ -1722,34 +1862,57 @@ MACHINE is an instance of `gptel-fsm'" (gptel--json-encode (plist-get tool-call :args)) info) (message "Unknown tool called by model: %s" name)) - (setq arg-values - (mapcar - (lambda (arg) - (let ((key (intern (concat ":" (plist-get arg :name))))) - (plist-get args key))) - (gptel-tool-args tool-spec))) - ;; Check if tool requires confirmation - (if (and gptel-confirm-tool-calls + (let ((confirm)) ;Check if tool requires confirmation + (cond ;:confirm in tool-call (from hooks) takes precedence + ((and-let* ((call-confirm (plist-member tool-call :confirm))) + (setq confirm (cadr call-confirm)))) + ((and gptel-confirm-tool-calls ;global and tool-specific setting (or (eq gptel-confirm-tool-calls t) ;always confirm, or (and-let* ((confirm (gptel-tool-confirm tool-spec))) - (or (not (functionp confirm)) (apply confirm arg-values))))) - (push (list tool-spec arg-values process-tool-result) - pending-calls) - ;; If not, run the tool - (if (gptel-tool-async tool-spec) - (apply (gptel-tool-function tool-spec) - process-tool-result arg-values) - (let ((result - (condition-case errdata - (apply (gptel-tool-function tool-spec) arg-values) - (error (mapconcat #'gptel--to-string errdata " "))))) - (funcall process-tool-result result))))))) + (or (not (functionp confirm)) + (apply confirm (gptel--map-tool-args tool-spec args)))))) + (setq confirm t))) + (if confirm ;To send to callback for confirmation + (push (list tool-spec args process-tool-result) pending-calls) + (let ((arg-values (gptel--map-tool-args tool-spec args))) + (if (gptel-tool-async tool-spec) ;If not, run the tool + (apply (gptel-tool-function tool-spec) + process-tool-result arg-values) + (let ((result (condition-case errdata + (apply (gptel-tool-function tool-spec) arg-values) + (error (mapconcat #'gptel--to-string errdata " "))))) + (funcall process-tool-result result))))))))) tool-use) (when pending-calls (plist-put info :tool-pending t) (funcall (plist-get info :callback) (cons 'tool-call pending-calls) info)))))) +(defun gptel--map-tool-args (tool-spec args) + "Create a tool call argument list from TOOL-SPEC and ARGS. + +TOOL-SPEC is a `gptel-tool' and ARGS is a plist of arguments for a tool +call. The argument list is suitable for supplying to the tool function." + (mapcar + (lambda (arg) + (let ((key (intern (concat ":" (plist-get arg :name))))) + (plist-get args key))) + (gptel-tool-args tool-spec))) + +(defun gptel--handle-tool-result (fsm) + "Handle the results of tool execution in FSM. + +Inject tool results into into the prompt data (for the LLM), run the +callback (for the user), and transition the request state." + (let ((info (gptel-fsm-info fsm))) + (gptel--inject-prompt + (plist-get info :backend) (plist-get info :data) + (gptel--parse-tool-results (plist-get info :backend) + (plist-get info :tool-use))) + (funcall (plist-get info :callback) + (cons 'tool-result (plist-get info :tool-result)) info)) + (gptel--fsm-transition fsm)) + (defun gptel--handle-post (fsm) "Run cleanup for `gptel-request' with FSM." (when-let* ((info (gptel-fsm-info fsm)) @@ -1764,7 +1927,7 @@ MACHINE is an instance of `gptel-fsm'" (defun gptel--tool-use-p (info) (plist-get info :tool-use)) -(defun gptel--tool-result-p (info) (plist-get info :tool-success)) +(defun gptel--tool-result-p (info) (plist-get info :tool-result)) ;;; Send gptel requests @@ -1837,8 +2000,8 @@ cases. The INFO plist has (at least) the following keys: :data - The request data included with the query -:position - marker at the point the request was sent, unless - POSITION is specified. +:position - marker where the response will (nominally) be inserted. + Of course, the insertion is left to the CALLBACK. :buffer - The buffer current when the request was sent, unless BUFFER is specified. :status - Short string describing the result of the request, @@ -1965,17 +2128,19 @@ be used to rerun or continue the request at a later time." (gptel--schema schema) (prompt-buffer (cond ;prompt from buffer or explicitly supplied - ((null prompt) - (gptel--create-prompt-buffer (point))) + ((null prompt) ;Send text up to end of word (for evil-mode users) + (gptel--create-prompt-buffer (gptel--at-word-end (point)))) ((stringp prompt) (gptel--with-buffer-copy buffer nil nil (insert prompt) + (setq major-mode 'fundamental-mode) ;Avoid mode-specific behavior (current-buffer))) ((consp prompt) ;; (gptel--parse-list gptel-backend prompt) (gptel--with-buffer-copy buffer nil nil ;; TEMP Decide on the annoated prompt-list format (gptel--parse-list-and-insert prompt) + (setq major-mode 'fundamental-mode) ;Avoid mode-specific behavior (current-buffer))))) (system-list (gptel--parse-directive system 'raw)) ;eval function-valued system prompts (info (list :data prompt-buffer @@ -2253,15 +2418,15 @@ first nil value in REST is guaranteed to be correct." (file-remote-p path) (file-readable-p path))) (mime-valid - (if (or (eq resource-type 'url) - (cdr (with-memoization - (alist-get (expand-file-name path) - gptel--link-type-cache - nil nil #'string=) - (cons t (gptel--file-binary-p path))))) - (gptel--model-mime-capable-p - (setq mime (mailcap-file-name-to-mime-type path))) - t))) + (or (eq resource-type 'url) + (and (with-memoization + (alist-get (expand-file-name path) + gptel--link-type-cache + nil nil #'string=) + (if (gptel--file-binary-p path) t)) + (setq mime (mailcap-file-name-to-mime-type path)) + (gptel--model-mime-capable-p mime)) + t))) (list t link-type path resource-type user-check readablep mime-valid mime) (list nil link-type path resource-type user-check readablep mime-valid mime)))) @@ -2334,6 +2499,13 @@ PROMPTS is the plist of previous user queries and LLM responses.") "Check if MODEL is available in BACKEND, adjust accordingly. If SHOOSH is true, don't issue a warning." + (unless backend + (unless shoosh + (display-warning + 'gptel "No gptel-backend defined: defaulting to ChatGPT")) + (setq gptel-backend + (gptel-make-openai "ChatGPT" :key 'gptel-api-key :stream t) + backend gptel-backend)) (let ((available (gptel-backend-models backend))) (when (stringp model) (unless shoosh @@ -2380,8 +2552,7 @@ the response is inserted into the current buffer after point." (url-request-extra-headers (append '(("Content-Type" . "application/json")) (when-let* ((header (gptel-backend-header gptel-backend))) - (if (functionp header) - (funcall header) header)))) + (gptel--maybe-funcall header info)))) (callback (or (plist-get info :callback) ;if not the first run #'gptel--insert-response)) ;default callback ;; NOTE: We don't need the decode-coding-string dance here since we @@ -2402,8 +2573,7 @@ the response is inserted into the current buffer after point." (gptel--log url-request-data "request body")) (let ((proc-buf (url-retrieve (let ((backend-url (gptel-backend-url gptel-backend))) - (if (functionp backend-url) - (funcall backend-url) backend-url)) + (gptel--maybe-funcall backend-url info)) (lambda (_) (set-buffer-multibyte t) (set-buffer-file-coding-system 'utf-8-unix) @@ -2497,10 +2667,12 @@ See `gptel-curl--get-response' for its contents.") ;;; Curl request response handling -(defun gptel-curl--get-args (info token) +(defun gptel-curl--get-args (info uuid include-headers) "Produce list of arguments for calling Curl. -INFO contains the request data, TOKEN is a unique identifier." +INFO contains the request data, UUID is a unique identifier. + +If INCLUDE-HEADERS is non-nil, include headers with the -H option." (let* ((data (plist-get info :data)) ;; We have to let-bind the following three since their dynamic ;; values are used for key lookup and url resolution @@ -2508,27 +2680,23 @@ INFO contains the request data, TOKEN is a unique identifier." (gptel-model (plist-get info :model)) (gptel-stream (plist-get info :stream)) (url (let ((backend-url (gptel-backend-url gptel-backend))) - (if (functionp backend-url) - (funcall backend-url) backend-url))) - (data-json (decode-coding-string (gptel--json-encode data) 'utf-8 t)) - (headers - (append '(("Content-Type" . "application/json")) - (when-let* ((header (gptel-backend-header gptel-backend))) - (if (functionp header) - (funcall header) header))))) - (when gptel-log-level - (when (eq gptel-log-level 'debug) - (gptel--log (gptel--json-encode - (mapcar (lambda (pair) (cons (intern (car pair)) (cdr pair))) - headers)) - "request headers")) - (gptel--log data-json "request body")) + (gptel--maybe-funcall backend-url info))) + (data-json (decode-coding-string (gptel--json-encode data) 'utf-8 t))) + (when gptel-log-level (gptel--log data-json "request body")) (append gptel-curl--common-args gptel-curl-extra-args + (if include-headers + (cl-loop + for (key . val) in + (append '(("Content-Type" . "application/json")) + (when-let* ((header (gptel-backend-header gptel-backend))) + (gptel--maybe-funcall header info))) + collect (format "-H%s: %s" key val)) + (list "-H@-")) (and-let* ((curl-args (gptel-backend-curl-args gptel-backend))) - (if (functionp curl-args) (funcall curl-args) curl-args)) - (list (format "-w(%s . %%{size_header})" token)) + (gptel--maybe-funcall curl-args)) + (list (format "-w(%s . %%{size_header})" uuid)) (if (< (string-bytes data-json) gptel-curl-file-size-threshold) (list (format "-d%s" data-json)) (let* ((write-region-inhibit-fsync t) @@ -2543,8 +2711,6 @@ INFO contains the request data, TOKEN is a unique identifier." (list "--proxy" gptel-proxy "--proxy-negotiate" "--proxy-user" ":")) - (cl-loop for (key . val) in headers - collect (format "-H%s: %s" key val)) (list url)))) ;;;###autoload @@ -2563,18 +2729,18 @@ plist with the following keys, among others: Call CALLBACK with the response and INFO afterwards. If omitted the response is inserted into the current buffer after point." - (let* ((token (md5 (format "%s%s%s%s" - (random) (emacs-pid) (user-full-name) - (recent-keys)))) + (let* ((uuid (md5 (format "%s%s%s%s" + (random) (emacs-pid) (user-full-name) + (recent-keys)))) (info (gptel-fsm-info fsm)) (backend (plist-get info :backend)) - (args (gptel-curl--get-args info token)) + (args (gptel-curl--get-args info uuid nil)) (stream (plist-get info :stream)) - (process (apply #'start-process "gptel-curl" - (gptel--temp-buffer " *gptel-curl*") (gptel--curl-path) args))) - (when (eq gptel-log-level 'debug) - (gptel--log (mapconcat #'shell-quote-argument (cons (gptel--curl-path) args) " \\\n") - "request Curl command" 'no-json)) + (process (make-process + :name "gptel-curl" + :buffer (gptel--temp-buffer " *gptel-curl*") + :command (cons (gptel--curl-path) args) + :connection-type 'pipe))) (with-current-buffer (process-buffer process) (cond ((eq (gptel-backend-coding-system backend) 'binary) @@ -2587,10 +2753,27 @@ the response is inserted into the current buffer after point." ;; for cases when buffer coding system is not set to utf-8. (set-process-coding-system process 'utf-8-unix 'utf-8-unix))) (set-process-query-on-exit-flag process nil) - (if (plist-get info :token) ;not the first run, set only the token - (plist-put info :token token) + (let* ((gptel-backend backend) ;Required for header function's environment + (gptel-model (plist-get info :model)) + (headers + (append '(("Content-Type" . "application/json")) + (when-let* ((header (gptel-backend-header backend))) + (gptel--maybe-funcall header info))))) + (when (eq gptel-log-level 'debug) + (gptel--log (gptel--json-encode + (mapcar (lambda (pair) (cons (intern (car pair)) (cdr pair))) + headers)) + "request headers") + (gptel--log (mapconcat #'shell-quote-argument + (cons (gptel--curl-path) args) " \\\n") + "request Curl command" 'no-json)) + (dolist (header headers) + (process-send-string process (concat (car header) ": " (cdr header) "\n")))) + (process-send-eof process) + (if (plist-get info :uuid) ;not the first run, set only the uuid + (plist-put info :uuid uuid) (setf (gptel-fsm-info fsm) ;fist run, set all process parameters - (nconc (list :token token + (nconc (list :uuid uuid :transformer (when (with-current-buffer (plist-get info :buffer) (and (derived-mode-p 'org-mode) @@ -2647,7 +2830,7 @@ PROC-INFO is the plist containing process metadata." (point-min) (1- (point)))) "response headers")) (let ((p (point))) - (when (search-forward (plist-get proc-info :token) nil t) + (when (search-forward (plist-get proc-info :uuid) nil t) (goto-char (1- (match-beginning 0))) (gptel--log (buffer-substring-no-properties p (point)) "response body"))))))) @@ -2657,35 +2840,51 @@ PROC-INFO is the plist containing process metadata." "Process sentinel for gptel curl requests. PROCESS and _STATUS are process parameters." - (let ((proc-buf (process-buffer process))) + (let ((proc-buf (process-buffer process)) + (exit-status (process-exit-status process))) (let* ((fsm (car (alist-get process gptel--request-alist))) (info (gptel-fsm-info fsm)) (http-status (plist-get info :http-status))) (when gptel-log-level (gptel-curl--log-response proc-buf info)) ;logging - (if (member http-status '("200" "100")) ;Finish handling response - ;; Run the callback one last time to signal that the process has ended - (with-demoted-errors "gptel callback error: %S" - (funcall (plist-get info :callback) t info)) - (with-current-buffer proc-buf ; Or Capture error message - (goto-char (point-max)) - (search-backward (plist-get info :token)) - (backward-char) - (pcase-let* ((`(,_ . ,header-size) (read (current-buffer))) - (response (progn (goto-char header-size) - (condition-case nil (gptel--json-read) - (error 'json-read-error)))) - (error-data - (cond ((plistp response) (plist-get response :error)) - ((arrayp response) - (cl-some (lambda (el) (plist-get el :error)) response))))) - (cond - (error-data - (plist-put info :error error-data)) - ((eq response 'json-read-error) - (plist-put info :error "Malformed JSON in response.")) - (t (plist-put info :error "Could not parse HTTP response."))))) + (cond + ;; Curl exited with a non-zero status: connection-level failure + ((not (zerop exit-status)) + ;; MAYBE: This transition should happen in the process filter, but it's + ;; not clear how to reliably detect Curl failure there. + (gptel--fsm-transition fsm) ;Curl failed, WAIT -> TYPE + (plist-put info :error + (format "Curl failed with exit code %d. See Curl manpage for details." + exit-status)) + (plist-put info :status "Curl failure") (with-demoted-errors "gptel callback error: %S" (funcall (plist-get info :callback) nil info))) + ;; Finish handling a successful streaming response + ((member http-status '("200" "100")) + (with-demoted-errors "gptel callback error: %S" + (funcall (plist-get info :callback) t info))) + ;; Capture error message from HTTP error response + (t + (with-current-buffer proc-buf + (goto-char (point-max)) + (if (not (search-backward (plist-get info :uuid) nil t)) + (plist-put info :error "Could not parse Curl response") + (backward-char) + (pcase-let* ((`(,_ . ,header-size) (read (current-buffer))) + (response (progn (goto-char header-size) + (condition-case nil (gptel--json-read) + (error 'json-read-error)))) + (error-data + (cond ((plistp response) (plist-get response :error)) + ((arrayp response) + (cl-some (lambda (el) (plist-get el :error)) response))))) + (cond + (error-data + (plist-put info :error error-data)) + ((eq response 'json-read-error) + (plist-put info :error "Malformed JSON in response.")) + (t (plist-put info :error "Could not parse HTTP response.")))))) + (with-demoted-errors "gptel callback error: %S" + (funcall (plist-get info :callback) nil info)))) (gptel--fsm-transition fsm)) ; Move to next state (setf (alist-get process gptel--request-alist nil 'remove) nil) (kill-buffer proc-buf))) @@ -2701,7 +2900,7 @@ PROCESS and _STATUS are process parameters." (goto-char (process-mark process)) (insert output) (set-marker (process-mark process) (point))) - + ;; Find HTTP status (unless (plist-get proc-info :http-status) (save-excursion @@ -2715,8 +2914,8 @@ PROCESS and _STATUS are process parameters." (match-string 1 http-msg))))) (plist-put proc-info :http-status http-status) (plist-put proc-info :status (string-trim http-msg)) - (gptel--fsm-transition fsm)))) - + (gptel--fsm-transition fsm)))) ;Response started, WAIT -> TYPE + (when-let* ((http-msg (plist-get proc-info :status)) (http-status (plist-get proc-info :http-status))) ;; Find data chunk(s) and run callback @@ -2801,29 +3000,39 @@ PROCESS and _STATUS are process parameters." (proc-info (gptel-fsm-info fsm)) (proc-callback (plist-get proc-info :callback))) (when gptel-log-level (gptel-curl--log-response proc-buf proc-info)) ;logging - (pcase-let ((`(,response ,http-status ,http-msg ,error) - (with-current-buffer proc-buf - (gptel-curl--parse-response proc-info)))) - (plist-put proc-info :http-status http-status) - (plist-put proc-info :status http-msg) - (gptel--fsm-transition fsm) ;WAIT -> TYPE - (when error (plist-put proc-info :error error)) - ;; Look for a reasoning block - (if (and (stringp response) (string-match-p "^\\s-*" response)) - (when-let* ((idx (string-search "" response))) - (with-demoted-errors "gptel callback error: %S" - (funcall proc-callback - (cons 'reasoning (substring response nil (+ idx 8))) - proc-info)) - (setq response - (string-trim-left (substring response (+ idx 8))))) - (when-let* ((reasoning (plist-get proc-info :reasoning)) - ((stringp reasoning))) - (funcall proc-callback (cons 'reasoning reasoning) proc-info))) - ;; Call callback with response text - (when (or response (not (member http-status '("200" "100")))) + (let ((exit-status (process-exit-status process))) + (if (zerop exit-status) + (pcase-let ((`(,response ,http-status ,http-msg ,error) + (with-current-buffer proc-buf + (gptel-curl--parse-response proc-info)))) + (plist-put proc-info :http-status http-status) + (plist-put proc-info :status http-msg) + (gptel--fsm-transition fsm) ;WAIT -> TYPE + (when error (plist-put proc-info :error error)) + ;; Look for a reasoning block + (if (and (stringp response) (string-match-p "^\\s-*" response)) + (when-let* ((idx (string-search "" response))) + (with-demoted-errors "gptel callback error: %S" + (funcall proc-callback + (cons 'reasoning (substring response nil (+ idx 8))) + proc-info)) + (setq response + (string-trim-left (substring response (+ idx 8))))) + (when-let* ((reasoning (plist-get proc-info :reasoning)) + ((stringp reasoning))) + (funcall proc-callback (cons 'reasoning reasoning) proc-info))) + ;; Call callback with response text + (when (or response (not (member http-status '("200" "100")))) + (with-demoted-errors "gptel callback error: %S" + (funcall proc-callback response proc-info)))) + ;; Curl exited with a non-zero status: connection-level failure + (plist-put proc-info :error + (format "Curl failed with exit code %d. See Curl manpage for details." + exit-status)) + (plist-put proc-info :status "Curl failure") + (gptel--fsm-transition fsm) ;WAIT -> TYPE (with-demoted-errors "gptel callback error: %S" - (funcall proc-callback response proc-info)))) + (funcall proc-callback nil proc-info)))) (gptel--fsm-transition fsm)) ;TYPE -> next (setf (alist-get process gptel--request-alist nil 'remove) nil) (kill-buffer proc-buf))) @@ -2832,44 +3041,44 @@ PROCESS and _STATUS are process parameters." "Parse the buffer BUF with curl's response. PROC-INFO is a plist with contextual information." - (let ((token (plist-get proc-info :token))) + (let ((uuid (plist-get proc-info :uuid))) (goto-char (point-max)) - (search-backward token) - (backward-char) - (pcase-let* ((`(,_ . ,header-size) (read (current-buffer)))) - (goto-char (point-min)) - - (if-let* ((http-msg (string-trim - (buffer-substring (line-beginning-position) - (line-end-position)))) - (http-status - (save-match-data - (and (string-match "HTTP/[.0-9]+ +\\([0-9]+\\)" http-msg) - (match-string 1 http-msg)))) - (response (progn (goto-char header-size) - (condition-case nil - (gptel--json-read) - (error 'json-read-error))))) - (cond - ;; FIXME Handle the case where HTTP 100 is followed by HTTP (not 200) BUG #194 - ((member http-status '("200" "100")) - (list (and-let* ((resp (gptel--parse-response - (plist-get proc-info :backend) response proc-info)) - ((not (string-blank-p resp)))) - (string-trim resp)) - http-status http-msg)) - ((and-let* ((error-data - (cond ((plistp response) (plist-get response :error)) - ((arrayp response) - (cl-some (lambda (el) (plist-get el :error)) response))))) - (list nil http-status http-msg error-data))) - ((eq response 'json-read-error) - (list nil http-status (concat "(" http-msg ") Malformed JSON in response.") - "Malformed JSON in response")) - (t (list nil http-status (concat "(" http-msg ") Could not parse HTTP response.") - "Could not parse HTTP response."))) - (list nil http-status (concat "(" http-msg ") Could not parse HTTP response.") - "Could not parse HTTP response."))))) + (if (not (search-backward uuid nil t)) + (list nil nil nil "Could not parse curl response.") + (backward-char) + (pcase-let* ((`(,_ . ,header-size) (read (current-buffer)))) + (goto-char (point-min)) + (if-let* ((http-msg (string-trim + (buffer-substring (line-beginning-position) + (line-end-position)))) + (http-status + (save-match-data + (and (string-match "HTTP/[.0-9]+ +\\([0-9]+\\)" http-msg) + (match-string 1 http-msg)))) + (response (progn (goto-char header-size) + (condition-case nil + (gptel--json-read) + (error 'json-read-error))))) + (cond + ;; FIXME Handle the case where HTTP 100 is followed by HTTP (not 200) BUG #194 + ((member http-status '("200" "100")) + (list (and-let* ((resp (gptel--parse-response + (plist-get proc-info :backend) response proc-info)) + ((not (string-blank-p resp)))) + (string-trim resp)) + http-status http-msg)) + ((and-let* ((error-data + (cond ((plistp response) (plist-get response :error)) + ((arrayp response) + (cl-some (lambda (el) (plist-get el :error)) response))))) + (list nil http-status http-msg error-data))) + ((eq response 'json-read-error) + (list nil http-status (concat "(" http-msg ") Malformed JSON in response.") + "Malformed JSON in response")) + (t (list nil http-status (concat "(" http-msg ") Could not parse HTTP response.") + "Could not parse HTTP response."))) + (list nil http-status (concat "(" http-msg ") Could not parse HTTP response.") + "Could not parse HTTP response.")))))) (provide 'gptel-request) ;;; gptel-request.el ends here diff --git a/elpa/gptel-20260123.739/gptel-rewrite.el b/elpa/gptel-20260425.846/gptel-rewrite.el similarity index 86% rename from elpa/gptel-20260123.739/gptel-rewrite.el rename to elpa/gptel-20260425.846/gptel-rewrite.el index acd08b24..4b388e42 100644 --- a/elpa/gptel-20260123.739/gptel-rewrite.el +++ b/elpa/gptel-20260425.846/gptel-rewrite.el @@ -116,7 +116,7 @@ overlay." "List of active rewrite overlays in the buffer.") (defvar-local gptel--rewrite-message nil - "Request-specific instructions for a gptel-rewrite action.") + "Request-specific instructions for a `gptel-rewrite' action.") ;; Add the rewrite directive to `gptel-directives' (unless (alist-get 'rewrite gptel-directives) @@ -174,13 +174,62 @@ which see." " Generate ONLY the replacement text," " without any explanation or markdown code fences."))))) +;; MAYBE: Save FSM to `gptel--fsm-last' on request end? (defvar gptel--rewrite-handlers - `((WAIT ,#'gptel--handle-wait) - (TOOL ,#'gptel--update-tool-call ,#'gptel--handle-tool-use)) + `((WAIT ,#'gptel--handle-wait ,#'gptel--rewrite-update-wait) + (TPRE ,#'gptel--handle-pre-tool ,#'gptel--fsm-transition) + (TOOL ,#'gptel--rewrite-update-tool-call ,#'gptel--handle-tool-use) + (TRET ,#'gptel--handle-post-tool ,#'gptel--rewrite-update-tool-call + ,#'gptel--handle-tool-result)) "Alist specifying FSM handlers for `gptel-rewrite' state transitions.") ;; * Helper functions +;; ** UI Indicators +(defun gptel--rewrite-update-tool-call (fsm) + "Update the rewrite overlay to indicate tool call progress for FSM." + (when-let* ((info (gptel-fsm-info fsm)) + (ov (car-safe (plist-get info :context))) + (names (cl-loop for call in (plist-get info :tool-use) + collect (plist-get call :name)))) + (with-current-buffer (plist-get info :buffer) + (setq gptel--fsm-last fsm) + (gptel--rewrite-update-status + ov (concat + (propertize + (if (length> names 1) " Calling tools (" " Calling tool (") + 'face '(mode-line-emphasis default)) + (mapconcat (lambda (name) (propertize name 'face '(font-lock-keyword-face default))) + names (propertize ", " 'face '(mode-line-emphasis default))) + (propertize ")" 'face '(mode-line-emphasis default))))))) + +(defun gptel--rewrite-update-wait (fsm) + "Update the rewrite overlay status for FSM to indicate a waiting state." + (when-let* ((info (gptel-fsm-info fsm)) + (ov (car-safe (plist-get info :context))) + (model (gptel--model-name + (or (plist-get info :model) gptel-model))) + (hint-str (concat "[" model "]\n"))) + (overlay-put + ov 'status + (list (propertize "REWRITE" 'face '(warning default)) ;status element 0 + (propertize " Waiting..." 'face '(warning default)) ;status element 1 + (propertize ;status element 2 + " " 'display + (if (fboundp 'string-pixel-width) + `(space :align-to (- right (,(string-pixel-width hint-str)))) + `(space :align-to (- right ,(+ 1 (string-width hint-str)))))) + (propertize hint-str 'face '(warning default)))) ;status element 3 + (overlay-put ov 'before-string (apply #'concat (overlay-get ov 'status))))) + +(defun gptel--rewrite-update-status (ov msg &optional face) + "Update overlay OV's status with MSG and refresh its before-string. +If FACE is non-nil, apply that face to MSG when storing the status." + (setq msg (or msg "")) + (setf (cadr (overlay-get ov 'status)) + (if face (propertize msg 'face face) msg)) + (overlay-put ov 'before-string (apply #'concat (overlay-get ov 'status)))) + (defun gptel--rewrite-key-help (callback) "Eldoc documentation function for gptel rewrite actions. @@ -192,6 +241,8 @@ CALLBACK is supplied by Eldoc, see (format (substitute-command-keys "%s rewrite available: accept \\[gptel--rewrite-accept], iterate \\[gptel--rewrite-iterate], clear \\[gptel--rewrite-reject], merge \\[gptel--rewrite-merge], diff \\[gptel--rewrite-diff] or ediff \\[gptel--rewrite-ediff]") (propertize (gptel--model-name gptel-model) 'face 'mode-line-emphasis))))) +;; ** Navigation across rewrite regions + (defun gptel--rewrite-move (search-func) "Move directionally to a gptel rewrite location using SEARCH-FUNC." (let* ((ov (cdr (get-char-property-and-overlay (point) 'gptel-rewrite))) @@ -217,6 +268,8 @@ CALLBACK is supplied by Eldoc, see (interactive) (gptel--rewrite-move #'previous-single-char-property-change)) +;; ** Rewrite actions helpers + (defun gptel--rewrite-overlay-at (&optional pt) "Check for a gptel rewrite overlay at PT and return it. @@ -224,8 +277,8 @@ If no suitable overlay is found, raise an error." (pcase-let ((`(,response . ,ov) (get-char-property-and-overlay (or pt (point)) 'gptel-rewrite)) (diff-entire-buffers nil)) - (unless ov (user-error "Could not find region being rewritten.")) - (unless response (user-error "No LLM output available for this rewrite.")) + (unless ov (user-error "Could not find region being rewritten")) + (unless response (user-error "No LLM output available for this rewrite")) ov)) (defun gptel--rewrite-prepare-buffer (ovs &optional buf) @@ -234,7 +287,7 @@ If no suitable overlay is found, raise an error." This is used for (e)diff purposes. RESPONSE is the LLM response. OVS are the overlays specifying -the changed regions. BUF is the (current) buffer." +the changed regions. BUF is the (current) buffer." (setq buf (or buf (overlay-buffer (or (car-safe ovs) ovs)))) (with-current-buffer buf (let ((pmin (point-min)) @@ -260,8 +313,10 @@ the changed regions. BUF is the (current) buffer." newbuf))) (defun gptel--rewrite-read-message (prompt &optional _ history) - "Read a rewrite message from the minibuffer with custom keybindings for -cycling, editing, and submitting the gptel rewrite. + "Read a rewrite message from the minibuffer. + +Provide custom keybindings for cycling, editing, and submitting the +`gptel-rewrite' action directly from this prompt. PROMPT is the prompt string to display. HISTORY, if provided, is the input history list." @@ -281,12 +336,12 @@ input history list." (delete-dups (cons message transient--history)))))) (start-rewrite-maybe (lambda () (interactive) + (when (minibufferp) (funcall set-rewrite-message)) (if transient--prefix ;Called from transient? Don't start rewrite (run-at-time 0 nil #'transient-setup 'gptel-rewrite) - (run-at-time 0 nil #'gptel--suffix-rewrite gptel--rewrite-message)) - (when (minibufferp) - (funcall set-rewrite-message) - (exit-minibuffer)))) + (with-current-buffer cb + (gptel--suffix-rewrite gptel--rewrite-message))) + (when (minibufferp) (exit-minibuffer)))) (start-transient (lambda () (interactive) (run-at-time 0 nil #'transient-setup 'gptel-rewrite) @@ -302,9 +357,9 @@ input history list." :callback (lambda (msg) (when msg - (run-at-time 0 nil #'gptel--suffix-rewrite) (push (buffer-local-value 'gptel--rewrite-message cb) - (alist-get 'gptel--infix-rewrite-extra transient-history))) + (alist-get 'gptel--infix-rewrite-extra transient-history)) + (with-current-buffer cb (gptel--suffix-rewrite))) (when (minibufferp) (exit-minibuffer))))))) (minibuffer-local-map (make-composed-keymap (define-keymap @@ -343,11 +398,10 @@ BUF is the buffer to modify, defaults to the overlay buffer." for ov-beg = (overlay-start ov) for ov-end = (overlay-end ov) for response = (overlay-get ov 'gptel-rewrite) - do (overlay-put ov 'before-string nil) - (with-current-buffer buf - (goto-char ov-beg) - (delete-region ov-beg ov-end) - (insert response)))) + do (with-current-buffer buf + (goto-char ov-beg) + (delete-region ov-beg ov-end) + (insert response)))) (message "Replaced region(s) with LLM output in buffer: %s." (buffer-name ov-buf)))) @@ -355,7 +409,9 @@ BUF is the buffer to modify, defaults to the overlay buffer." "Iterate on pending LLM response at point.") (defun gptel--rewrite-diff (&optional ovs switches) - "Diff pending LLM responses in OVS or at point." + "Diff pending LLM responses in OVS or at point. + +SWITCHES are diff arguments." (interactive (list (gptel--rewrite-overlay-at))) (when-let* ((ov-buf (overlay-buffer (or (car-safe ovs) ovs))) ((buffer-live-p ov-buf))) @@ -389,7 +445,7 @@ BUF is the buffer to modify, defaults to the overlay buffer." (funcall hideshow 'restore) (remove-hook 'ediff-quit-hook gptel--ediff-restore)))) (funcall hideshow) - (add-hook 'ediff-quit-hook gptel--ediff-restore) + (add-hook 'ediff-quit-hook gptel--ediff-restore 50) (let ((ediff-window-setup-function #'ediff-setup-windows-plain) (ediff-split-window-function #'split-window-horizontally)) (ediff-buffers ov-buf newbuf))))) @@ -452,23 +508,18 @@ NEW-STR is the new string intended to replace the region." OV is the rewrite overlay, CI is true for interactive calls." (interactive (list (gptel--rewrite-overlay-at) t)) - (let ((choice)) + (let ((choice) + (orig-status (copy-sequence (overlay-get ov 'status)))) (unwind-protect (pcase-let ((choices '((?a "accept") (?k "reject") (?r "iterate") - (?m "merge") (?d "diff") (?e "ediff"))) - (hint-str (concat "[" (gptel--model-name gptel-model) "]\n"))) - (overlay-put - ov 'before-string - (concat - (unless (eq (char-before (overlay-start ov)) ?\n) "\n") - (propertize "REWRITE READY: " 'face 'success) - (when (fboundp #'rmc--add-key-description) ; introduced in Emacs 29 - (mapconcat (lambda (e) (cdr e)) (mapcar #'rmc--add-key-description choices) ", ")) - (propertize - " " 'display `(space :align-to (- right ,(1+ (length hint-str))))) - (propertize hint-str 'face 'success))) + (?m "merge") (?d "diff") (?e "ediff")))) + (gptel--rewrite-update-status + ov (when (fboundp #'rmc--add-key-description) ; introduced in Emacs 29 + (concat " " (mapconcat (lambda (e) (cdr e)) + (mapcar #'rmc--add-key-description choices) ", ")))) (setq choice (read-multiple-choice "Action: " choices))) - (overlay-put ov 'before-string nil)) + (overlay-put ov 'status orig-status) + (overlay-put ov 'before-string (apply #'concat orig-status))) (if ci (call-interactively (intern (concat "gptel--rewrite-" (cadr choice)))) (funcall (intern (concat "gptel--rewrite-" (cadr choice))) ov)))) @@ -495,6 +546,7 @@ INFO is the async communication channel for the rewrite request." (when (= (buffer-size) 0) (buffer-disable-undo) (overlay-put ov 'gptel-rewrite nil) + (gptel--rewrite-update-status ov " Typing..." '(success default)) (insert-buffer-substring buf (overlay-start ov) (overlay-end ov)) (when (eq (char-before (point-max)) ?\n) (plist-put info :newline t)) @@ -504,7 +556,7 @@ INFO is the async communication channel for the rewrite request." (insert response) (unless (eobp) (ignore-errors (delete-char (length response)))) (font-lock-ensure) - (overlay-put ov 'display (buffer-string)))) + (overlay-put ov 'display (propertize (buffer-string) 'face 'default)))) (unless (plist-get info :stream) (gptel--rewrite-callback t info))) ((eq response 'abort) ;request aborted @@ -513,8 +565,10 @@ INFO is the async communication channel for the rewrite request." (delete-overlay ov)) ((eq (car-safe response) 'tool-call) ;tool call confirmation - (gptel--display-tool-calls ;use minibuffer if buffer is read-only - (cdr response) info (buffer-local-value 'buffer-read-only buf))) + (gptel--rewrite-update-status ov " Run tools?" '(mode-line-emphasis default)) + (gptel--display-tool-calls ;use minibuffer + (cdr response) info ;; (buffer-local-value 'buffer-read-only buf) + t)) ((null response) ;finished with error (message (concat "LLM response error: %s. Rewrite in buffer %s canceled.") @@ -552,6 +606,11 @@ INFO is the async communication channel for the rewrite request." (overlay-put ov 'priority 2000) (overlay-put ov 'keymap gptel-rewrite-actions-map) (overlay-put ov 'mouse-face 'highlight) + (let ((status (overlay-get ov 'status))) + (dolist (idx '(0 1 3)) + (setf (nth idx status) + (propertize (nth idx status) 'face '(success default)))) + (gptel--rewrite-update-status ov " Ready" '(success default))) (overlay-put ov 'help-echo (format (concat "%s rewrite available: %s or \\[gptel--rewrite-dispatch] for options") @@ -596,7 +655,8 @@ By default, gptel uses the directive associated with the `rewrite' (gptel--describe-directive gptel--rewrite-directive (max (- (window-width) 14) 20) " ")) ["" - (gptel--preset + (gptel-preset + :transient t :if (lambda () (or (get-char-property (point) 'gptel-rewrite) (use-region-p))) :key "@" :format "%d" @@ -666,7 +726,7 @@ By default, gptel uses the directive associated with the `rewrite' (gptel--rewrite-overlays ;Rewrite actions pending, show options (transient-setup 'gptel-rewrite)) (t (user-error - "`gptel-rewrite' requires an active region or rewrite in progress.")))) + "`gptel-rewrite' requires an active region or rewrite in progress")))) ;; * Transient infixes for rewriting @@ -733,7 +793,6 @@ generated from functions." :context (let ((ov (or (cdr-safe (get-char-property-and-overlay (point) 'gptel-rewrite)) (make-overlay (region-beginning) (region-end) nil t)))) - (overlay-put ov 'category 'gptel) (overlay-put ov 'evaporate t) ;; NOTE: Switch to `generate-new-buffer' after we drop Emacs 27.1 (#724) (cons ov (gptel--temp-buffer " *gptel-rewrite*"))) @@ -742,8 +801,8 @@ generated from functions." :callback #'gptel--rewrite-callback) ;; Move back so that the cursor is on the overlay when done. (unless (get-char-property (point) 'gptel-rewrite) - (when (= (point) (region-end)) (backward-char 1))) - (deactivate-mark)))) + (when (= (point) (region-end)) (run-at-time 0 nil #'backward-char 1))) + (setq deactivate-mark t)))) ;; Allow this to be called non-interactively for dry runs (put 'gptel--suffix-rewrite 'interactive-only nil) @@ -765,7 +824,7 @@ generated from functions." (gptel--rewrite-ediff gptel--rewrite-overlays)) (transient-define-suffix gptel--suffix-rewrite-merge () - "Insert LLM output as merge conflicts" + "Insert LLM output as merge conflicts." :if (lambda () gptel--rewrite-overlays) :key "M" :description "Merge with conflicts" diff --git a/elpa/gptel-20260123.739/gptel-transient.el b/elpa/gptel-20260425.846/gptel-transient.el similarity index 91% rename from elpa/gptel-20260123.739/gptel-transient.el rename to elpa/gptel-20260425.846/gptel-transient.el index 881d3478..66ba3a1d 100644 --- a/elpa/gptel-20260123.739/gptel-transient.el +++ b/elpa/gptel-20260425.846/gptel-transient.el @@ -32,6 +32,7 @@ (declare-function ediff-regions-internal "ediff") (declare-function ediff-make-cloned-buffer "ediff-utils") (declare-function org-escape-code-in-string "org-src") +(declare-function gptel--vterm-delete "gptel-integrations") ;; * Helper functions and vars @@ -58,28 +59,27 @@ If SCOPE is 1, reset it after the next gptel request. (oneshot) Otherwise, clear any buffer-local value and set its default global value." (pcase scope - (1 (put sym 'gptel-history (symbol-value sym)) - (set sym value) - (letrec ((restore-value - (lambda () - (remove-hook 'gptel-post-request-hook restore-value) - (run-at-time ; Required to work around let bindings - 0 nil (lambda (s) ; otherwise this change is overwritten! - (set s (get s 'gptel-history)) - (put s 'gptel-history nil)) - sym)))) - (add-hook 'gptel-post-request-hook restore-value))) + (1 (unless (get sym 'gptel-history) + (put sym 'gptel-history (symbol-value sym)) + (letrec ((restore-value + (lambda () + (remove-hook 'gptel-post-request-hook restore-value) + (run-at-time ; Required to work around let bindings + 0 nil (lambda (s) ; otherwise this change is overwritten! + (set s (get s 'gptel-history)) + (put s 'gptel-history nil)) + sym)))) + (add-hook 'gptel-post-request-hook restore-value))) + (set sym value)) ('t (set (make-local-variable sym) value)) (_ (kill-local-variable sym) (set sym value)))) -(defvar gptel--preset nil - "Name of last applied gptel preset. - -For internal use only.") - (defun gptel--preset-mismatch-p (name) - "Check if gptel preset with NAME is in effect." + "Check if gptel preset with NAME is in effect. + +This is intended to be fast but imperfect. See +`gptel--preset-mismatch-value' for more granular checking." (let ((elm (or (gptel-get-preset name) (gptel-get-preset (intern-soft name)))) key val) @@ -294,28 +294,28 @@ PROMPT, _INITIAL-INPUT and HISTORY are as in the transient reader documention. Return nil if user does not provide a number, for default." ;; Workaround for buggy transient behaviour when dealing with ;; non-string values. See: https://github.com/magit/transient/issues/172 - (when-let* ((val (symbol-value history))) - (when (not (stringp (car val))) + (when-let* ((history-symbol (or (car-safe history) history)) + (val (and (symbolp history-symbol) (symbol-value history-symbol)))) + (unless (stringp (car val)) (setcar val (number-to-string (car val))))) (let* ((minibuffer-default-prompt-format "") (num (read-number prompt -1 history))) (if (= num -1) nil num))) -(defun gptel-system-prompt--format (&optional message) - "Format the system MESSAGE for display in gptel's transient menus. +(defun gptel-system-prompt--format () + "Format the system prompt for display in gptel's transient menus. -Handle formatting for system messages when the active -`gptel-model' does not support system messages." - (setq message (or message gptel--system-message)) +Handle formatting for system messages when the active `gptel-model' does +not support system messages." (if (gptel--model-capable-p 'nosystem) (concat (propertize "[No system message support for model " 'face 'transient-heading) (propertize (gptel--model-name gptel-model) 'face 'warning) (propertize "]" 'face 'transient-heading)) - (if message + (if gptel--system-message (gptel--describe-directive - message (max (- (window-width) 12) 14) "⮐ ") + gptel--system-message (max (- (window-width) 12) 14) "⮐ ") "[No system message set]"))) (defun gptel--tools-init-value (obj) @@ -328,7 +328,7 @@ OBJ is a tool-infix of type `gptel--switch'." (oset obj value (list (oref obj category) name)))) (defvar gptel--crowdsourced-prompts-url - "https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv" + "https://raw.githubusercontent.com/f/prompts.chat/main/prompts.csv" "URL for crowdsourced LLM system prompts.") (defvar gptel--crowdsourced-prompts @@ -336,20 +336,44 @@ OBJ is a tool-infix of type `gptel--switch'." "Crowdsourced LLM system prompts.") (defun gptel--read-csv-column () - "Read next CSV column in the current buffer. - -Supports both quoted and non-quoted columns (RFC 4180)." - (let ((start (point))) - (unless (eolp) - (let ((column - (if (eq (char-after) ?\") - (when (re-search-forward "\",\\|\"$" nil t) - (let ((end (match-beginning 0))) - (buffer-substring-no-properties (+ start 1) (if (eolp) (- end 1) end)))) - (when (search-forward "," (line-end-position) t) - (let ((end (match-beginning 0))) - (buffer-substring-no-properties start end)))))) - (string-replace "\"\"" "\"" column))))) + "Read the next CSV column in the current buffer. + +Supports RFC 4180 quoted and unquoted fields, including embedded +newlines and escaped quotes in quoted fields." + (cond + ((eobp) nil) + ((eq (char-after) ?,) + (forward-char 1) + "") + ((eq (char-after) ?\") + (forward-char 1) + (let ((parts nil) + (start (point)) + done) + (while (not done) + (if (search-forward "\"" nil t) + (if (eq (char-after) ?\") + (progn + (push (buffer-substring-no-properties start (1- (point))) parts) + (push "\"" parts) + (forward-char 1) + (setq start (point))) + (push (buffer-substring-no-properties start (1- (point))) parts) + (setq done t)) + (push (buffer-substring-no-properties start (point-max)) parts) + (goto-char (point-max)) + (setq done t))) + (when (eq (char-after) ?,) + (forward-char 1)) + (apply #'concat (nreverse parts)))) + (t + (let ((start (point))) + (while (and (not (eobp)) + (not (memq (char-after) '(?, ?\n ?\r)))) + (forward-char 1)) + (prog1 (buffer-substring-no-properties start (point)) + (when (eq (char-after) ?,) + (forward-char 1))))))) (defun gptel--crowdsourced-prompts () "Acquire and read crowdsourced LLM system prompts. @@ -369,7 +393,7 @@ which see." (when (y-or-n-p (concat "Fetch crowdsourced system prompts from " - (propertize "https://github.com/f/awesome-chatgpt-prompts" 'face 'link) + (propertize gptel--crowdsourced-prompts-url 'face 'link) "?")) ;; Fetch file (message "Fetching prompts...") @@ -518,36 +542,6 @@ which see." (propertize "@" 'face 'transient-key) (propertize "preset" 'face 'transient-inactive-value)))) -;; TODO(preset): Unify this with `gptel--apply-preset'? -(defun gptel--read-apply-preset (name) - "Read gptel preset NAME and apply it." - (interactive - (list - (let ((completion-extra-properties - `(:annotation-function - ,(lambda (comp) - (and-let* ((desc - (plist-get (gptel-get-preset (intern-soft comp)) - :description))) - (concat (propertize " " 'display '(space :align-to 32)) - (if (string-match "\\(\n\\)" desc) - (substring desc 0 (match-beginning 1)) - desc))))))) - (intern - (completing-read (format "Apply preset (%s): " - (pcase gptel--set-buffer-locally - (1 "for next request only") - ('t "buffer-locally") - (_ "globally"))) - gptel--known-presets nil t))))) - (gptel--set-with-scope 'gptel--preset name gptel--set-buffer-locally) - (gptel--apply-preset - name (lambda (sym val) - (gptel--set-with-scope sym val gptel--set-buffer-locally))) - (message "Applied gptel preset %s" - (propertize (symbol-name name) 'face 'transient-value)) - (when transient--stack (run-at-time 0 nil #'transient-setup))) - ;; * Transient classes and methods for gptel @@ -814,7 +808,8 @@ Also format the value of OBJ in the transient menu." (lambda () (interactive) (gptel--handle-tool-use gptel--fsm-last)) :if (lambda () (and gptel--fsm-last (eq (gptel-fsm-state gptel--fsm-last) 'TOOL))))]] - [[(gptel--preset + [[(gptel-preset + :transient t :key "@" :format "%d" :description (lambda () @@ -1004,63 +999,65 @@ Customize `gptel-directives' for task-specific prompts." :pad-keys t]) ;; ** Prefix for saving and applying presets +;;;###autoload +(defun gptel-preset (preset &optional setter) + "Load gptel PRESET with SETTER. -(transient-define-prefix gptel--preset () - "Apply a gptel preset, or save the current configuration as a preset. - -A \"preset\" is a collection of gptel settings, such as the model, -backend, system message and enabled tools, that are applied and used -together. See `gptel-make-preset' for details." - :transient-suffix #'transient--do-return - [:description "Save or apply a preset collection of gptel options" - [:pad-keys t - ("@" "Select via completing-read" gptel--read-apply-preset) - ("C-s" "Save current settings as new preset" gptel--save-preset)]] - [:if (lambda () gptel--known-presets) - :class transient-column - :setup-children - (lambda (_) - (transient-parse-suffixes - 'gptel--preset - (cl-loop - for (name-sym . preset) in gptel--known-presets - for name = (format "%s" name-sym) - with unused-keys = (nconc (number-sequence ?a ?z) - (number-sequence ?0 ?9)) - for description = (plist-get preset :description) - for key = (seq-find (lambda (k) (member k unused-keys)) - name (seq-first unused-keys)) - do (setq unused-keys (delq key unused-keys)) - collect - (list - (key-description (list key)) - (concat name - (propertize " " 'display '(space :align-to 20)) - (and description - (propertize (concat - "(" (gptel--describe-directive - description (- (window-width) 30)) - ")") - 'face 'shadow))) - `(lambda () (interactive) - (gptel--set-with-scope 'gptel--preset ',name-sym - gptel--set-buffer-locally) - (gptel--apply-preset ',preset - (lambda (sym val) (gptel--set-with-scope - sym val gptel--set-buffer-locally))) - (message "Applied gptel preset %s" - (propertize ,name 'face 'transient-value)) - (when transient--stack - (run-at-time 0 nil #'transient-setup)))) - into generated - finally return - (nconc (list '(gptel--infix-variable-scope - :format "%d %k %v" - :description - (lambda () (format "%s %s" - (propertize "Apply preset" 'face 'transient-heading) - (propertize "Scope" 'face 'transient-active-prefix))))) - generated))))]) +Interactively, query for PRESET, allow the preset scope to be set +dynamically, and offer to save the current gptel settings as a new or +existing preset, as well." + (interactive + (let ((hint (concat (propertize "C-s" 'face 'transient-value) + (propertize " Save as preset" + 'face 'transient-inactive-value) + ", " (propertize "=" 'face 'transient-value) + (propertize " Scope " 'face 'transient-inactive-argument))) + (scope-obj (gptel--scope :variable 'gptel--set-buffer-locally)) + (completion-extra-properties + (list :annotation-function + (lambda (cand) + (and-let* ((str (plist-get + (cdr (assq (intern cand) gptel--known-presets)) + :description))) + (concat (propertize " " 'display '(space :align-to 25)) + (truncate-string-to-width str (- (frame-width) 40) + nil nil t))))))) + (cl-flet* ((key-hint-ov () + (or (cdr-safe (get-char-property-and-overlay (point-min) 'gptel)) + (let ((ov (make-overlay (point-min) (minibuffer-prompt-end)))) + (overlay-put ov 'gptel 'prefix) + (overlay-put ov 'before-string + (concat hint (transient-format-value scope-obj) + "\n")) + ov))) + (save-preset () + (interactive) + (when (minibufferp) + (run-at-time 0 nil (lambda (menu) (call-interactively #'gptel--save-preset) + (when menu (transient-setup 'gptel-menu))) + transient--prefix) + (minibuffer-quit-recursive-edit))) + (update-scope () + (interactive) + (transient-infix-set + scope-obj + (pcase gptel--set-buffer-locally (1 nil) ('nil t) ('t 1))) + (overlay-put (key-hint-ov) 'before-string + (concat hint + (transient-format-value scope-obj) + "\n")))) + (minibuffer-with-setup-hook + (lambda () (use-local-map (make-composed-keymap + (define-keymap "C-s" #'save-preset "=" #'update-scope) + (current-local-map))) + (key-hint-ov)) + (list (intern (completing-read (format "Load preset: ") + gptel--known-presets nil t))))))) + (gptel--apply-preset preset + (or setter (lambda (sym val) (gptel--set-with-scope + sym val gptel--set-buffer-locally)))) + (when transient--prefix + (transient-setup 'gptel-menu))) ;; ** Prefix for selecting tools @@ -1598,7 +1595,7 @@ This sets the variable `gptel-include-tool-results', which see." ;; ** Suffix to send prompt (transient-define-suffix gptel--suffix-send (args) - "Send ARGS." + "Call `gptel-send' with ARGS." :key "RET" :description #'gptel--describe-suffix-send (interactive (list (transient-args @@ -1626,7 +1623,7 @@ This sets the variable `gptel-include-tool-results', which see." ;; Output redirection: Send response elsewhere? (cond - ((member "e" args) + ((member "e" args) ;Send to echo-area (setq redirect-output t) (setq stream nil) (setq callback @@ -1637,7 +1634,7 @@ This sets the variable `gptel-include-tool-results', which see." (_ (when (and (null resp) (plist-get info :error)) (message "%s response error: %s" backend-name (plist-get info :status)))))))) - ((member "k" args) + ((member "k" args) ;Send to kill-ring (setq redirect-output t) (setq stream nil) (setq callback @@ -1656,7 +1653,7 @@ This sets the variable `gptel-include-tool-results', which see." (concat "%s response error: %s." (and accum " Partial response copied to kill-ring.")) backend-name (plist-get info :status))))))))) - ((setq gptel-buffer-name + ((setq gptel-buffer-name ;Send to gptel buffer (cl-some (lambda (s) (and (stringp s) (string-prefix-p "g" s) (substring s 1))) args)) @@ -1714,7 +1711,7 @@ This sets the variable `gptel-include-tool-results', which see." (get-char-property (point) 'read-only)) (unless (bolp) (insert "\n")) (insert reduced-prompt)) - (setq position (point)) + (setq position (point-marker)) (when (and gptel-mode (not dry-run)) (gptel--update-status " Waiting..." 'warning)))) ;; Insert into new gptel session @@ -1735,21 +1732,20 @@ This sets the variable `gptel-include-tool-results', which see." (setq gptel-model model) (unless dry-run (gptel--update-status " Waiting..." 'warning)) - (setq position (point))))))) - ((setq gptel-buffer-name + (setq position (point-marker))))))) + ((setq gptel-buffer-name ;Send to specified buffer (cl-some (lambda (s) (and (stringp s) (string-prefix-p "b" s) (substring s 1))) args)) (setq redirect-output t) (setq buffer (get-buffer-create gptel-buffer-name)) - (with-current-buffer buffer (setq position (point))))) + (with-current-buffer buffer (setq position (point-marker))))) ;; MAYBE: This is no a good way to handle two-part (region + instruction) prompts ;; If the prompt is a cons (region-text . instructions), collapse it (when (consp prompt) (setq prompt (concat (car prompt) "\n\n" (cdr prompt)))) (prog1 (gptel-request prompt - :buffer (or buffer (current-buffer)) :position position :in-place in-place :stream stream @@ -1770,7 +1766,11 @@ This sets the variable `gptel-include-tool-results', which see." ;; text is killed below. (when in-place (if (or buffer-read-only (get-char-property (point) 'read-only)) - (message "Not replacing prompt: region is read-only") + (cond + ((derived-mode-p 'vterm-mode) + (require 'gptel-integrations) + (gptel--vterm-delete)) + (t (message "Not replacing prompt: region is read-only"))) (let ((beg (if (use-region-p) (region-beginning) (max (previous-single-property-change @@ -1853,15 +1853,19 @@ This uses the prompts in the variable (lambda (str pred action) (if (eq action 'metadata) `(metadata - (affixation-function . - (lambda (cands) - (mapcar - (lambda (c) - (list c "" - (concat (propertize " " 'display '(space :align-to 22)) - " " (propertize (gethash c gptel--crowdsourced-prompts) - 'face 'completions-annotations)))) - cands)))) + ( affixation-function . + (lambda (cands) + (mapcar + (lambda (c) + ( list c "" + (concat + (propertize " " 'display '(space :align-to 22)) + " " (propertize + (gptel--describe-directive + (gethash c gptel--crowdsourced-prompts) + 54 " ") + 'face 'completions-annotations)))) + cands)))) (complete-with-action action gptel--crowdsourced-prompts str pred))) nil t))) (when-let* ((prompt (gethash choice gptel--crowdsourced-prompts))) diff --git a/elpa/gptel-20260123.739/gptel.el b/elpa/gptel-20260425.846/gptel.el similarity index 71% rename from elpa/gptel-20260123.739/gptel.el rename to elpa/gptel-20260425.846/gptel.el index ff2e55e2..32bd1cbc 100644 --- a/elpa/gptel-20260123.739/gptel.el +++ b/elpa/gptel-20260425.846/gptel.el @@ -3,8 +3,8 @@ ;; Copyright (C) 2023-2025 Karthik Chikmagalur ;; Author: Karthik Chikmagalur -;; Package-Version: 20260123.739 -;; Package-Revision: 96199593c107 +;; Package-Version: 20260425.846 +;; Package-Revision: 101904a1a856 ;; Package-Requires: ((emacs "27.1") (transient "0.7.4") (compat "30.1.0.0")) ;; Keywords: convenience, tools ;; URL: https://github.com/karthink/gptel @@ -69,22 +69,20 @@ ;; ;; ChatGPT is configured out of the box. For the other sources: ;; -;; - For Azure: define a gptel-backend with `gptel-make-azure', which see. -;; - For Gemini: define a gptel-backend with `gptel-make-gemini', which see. -;; - For Anthropic (Claude): define a gptel-backend with `gptel-make-anthropic', -;; which see. -;; - For AI/ML API, Together.ai, Anyscale, Groq, OpenRouter, DeepSeek, Cerebras or -;; Github Models: define a gptel-backend with `gptel-make-openai', which see. -;; - For PrivateGPT: define a backend with `gptel-make-privategpt', which see. -;; - For Perplexity: define a backend with `gptel-make-perplexity', which see. -;; - For Deepseek: define a backend with `gptel-make-deepseek', which see. -;; - For Kagi: define a gptel-backend with `gptel-make-kagi', which see. +;; - For Azure: define a gptel-backend with `gptel-make-azure'. +;; - For Gemini: define a gptel-backend with `gptel-make-gemini'. +;; - For Anthropic (Claude): define a gptel-backend with `gptel-make-anthropic'. +;; - For AI/ML API, Together.ai, Anyscale, Groq, OpenRouter, DeepSeek, Cerebras +;; or Github Models: define a gptel-backend with `gptel-make-openai'. +;; - For PrivateGPT: define a backend with `gptel-make-privategpt'. +;; - For Perplexity: define a backend with `gptel-make-perplexity'. +;; - For Deepseek: define a backend with `gptel-make-deepseek'. +;; - For Kagi: define a gptel-backend with `gptel-make-kagi'. ;; ;; For local models using Ollama, Llama.cpp or GPT4All: ;; ;; - The model has to be running on an accessible address (or localhost) -;; - Define a gptel-backend with `gptel-make-ollama' or `gptel-make-gpt4all', -;; which see. +;; - Define a gptel-backend with `gptel-make-ollama' or `gptel-make-gpt4all'. ;; - Llama.cpp or Llamafiles: Define a gptel-backend with `gptel-make-openai'. ;; ;; Consult the package README for examples and more help with configuring @@ -191,12 +189,13 @@ ;; usage. ;;; Code: -(defconst gptel-version "0.9.9.3") +(defconst gptel-version "0.9.9.4") (declare-function markdown-mode "markdown-mode") (declare-function gptel-menu "gptel-transient") (declare-function gptel-system-prompt "gptel-transient") (declare-function gptel-tools "gptel-transient") +(declare-function gptel--vterm-pre-insert "gptel-integrations") (declare-function pulse-momentary-highlight-region "pulse") (declare-function ediff-make-cloned-buffer "ediff-util") @@ -221,7 +220,7 @@ (require 'map) (require 'text-property-search) (require 'cl-generic) -(require 'gptel-request) +(eval-and-compile (require 'gptel-request)) ;;; User options @@ -265,6 +264,86 @@ to the LLM, and after a text insertion." :type 'hook :group 'gptel) +(defcustom gptel-pre-tool-call-functions nil + "Abnormal hook called before each tool call. + +Each hook function is called a plist with the following keys: + +:name - the name of the tool being called, a string +:args - a plist of the tool call arguments, as specified in the tool + definition. For a hypothetical edit_file tool that takes three + arguments, a FILENAME, an ORIGINAL and REPLACEMENT strings, this + plist is structured as + + (:filename \"/path/to/file.md\" + :original \"...\" + :replacement \"...\") + +:buffer - The name of the buffer from which the request was sent. +:backend - The name of the gptel backend used for the request. +:model - The name of the gptel model used for the request. + +The function can work by side effects and return nil, or return a plist +with one or more of the following keys. + +:stop - If non-nil, stop the request entirely. +:stop-reason - If :stop is non-nil, the reason for stopping. Intended + for the user, not the LLM. + +:block - If non-nil, continue the request but block this tool call and + mark it as having erred. Can be a string to send as the + result instead, typically an explanation for why the tool was + not run. Intended for the LLM, not the user. + +:confirm - Whether the tool call should seek confirmation from the user. + t and nil are both meaningful, signifying that the tool call + should and should not seek user confirmation, respectively. + When present, this key overrides all other confirmation + options (such as `gptel-confirm-tool-calls' and the tool's + CONFIRM slot). +:args - The updated argument plist for the tool call. +:result - The result of this tool call, used instead of the tool call + output. Not marked as an error." + :type 'hook + :group 'gptel) + +(defcustom gptel-post-tool-call-functions nil + "Abnormal hook called after each tool call. + +Each hook function is called a plist with the following keys: + +:name - the name of the tool being called, a string +:args - a plist of the tool call arguments, as specified in the tool + definition. For a hypothetical edit_file tool that takes three + arguments, a FILENAME, an ORIGINAL and REPLACEMENT strings, this + plist is structured as + + (:filename \"/path/to/file.md\" + :original \"...\" + :replacement \"...\") + +:result - The tool call result, serialized to a string. +:buffer - The name of the buffer from which the request was sent. +:backend - The name of the gptel backend used for the request. +:model - The name of the gptel model used for the request. + +The function can work by side effects and return nil, or return a plist +with one or more of the following keys. + +:stop - If non-nil, stop the request entirely. +:stop-reason - If :stop is non-nil, the reason for stopping. Intended + for the user, not the LLM. + +:block - If non-nil, continue the request but block this tool call and + mark it as having erred. Can be a string to send as the + result instead, typically an explanation for why the tool was + not run. Intended for the LLM, not the user. + +:result - The updated result of this tool call, used instead of the + tool call output. Not marked as an error." + :type 'hook + :group 'gptel) + (defcustom gptel-save-state-hook nil "Hook run before gptel saves model parameters to a file. @@ -329,21 +408,27 @@ This hook runs in gptel chat buffers after making a change to gptel's configuration that might require a UI update.") (defvar-local gptel--bounds nil) -(put 'gptel--bounds 'safe-local-variable #'always) +(put 'gptel--bounds 'safe-local-variable #'listp) + +(defvar gptel--preset nil + "Name of last applied gptel preset. + +For internal use only.") +(put 'gptel--preset 'safe-local-variable #'symbolp) (defvar-local gptel--tool-names nil "Store to persist tool names to file across Emacs sessions. Note: Changing this variable does not affect gptel\\='s behavior in any way.") -(put 'gptel--tool-names 'safe-local-variable #'always) +(put 'gptel--tool-names 'safe-local-variable #'listp) (defvar-local gptel--backend-name nil "Store to persist backend name across Emacs sessions. Note: Changing this variable does not affect gptel\\='s behavior in any way.") -(put 'gptel--backend-name 'safe-local-variable #'always) +(put 'gptel--backend-name 'safe-local-variable #'stringp) (defvar-local gptel--old-header-line nil) @@ -567,19 +652,6 @@ Link failed to validate, see `gptel-markdown-validate-link' or `gptel-org-valida "Check if gptel response at position PT has variants." (get-char-property (or pt (point)) 'gptel-history)) -(defvar gptel--mode-description-alist - '((js2-mode . "Javascript") - (sh-mode . "Shell") - (enh-ruby-mode . "Ruby") - (yaml-mode . "Yaml") - (yaml-ts-mode . "Yaml") - (rustic-mode . "Rust") - (tuareg-mode . "OCaml")) - "Mapping from unconventionally named major modes to languages. - -This is used when generating system prompts for rewriting and -when including context from these major modes.") - ;;; Saving and restoring state @@ -594,7 +666,9 @@ the gptel property is set to just PROP. The legacy structure, a list of (BEG . END) is also supported and will be applied before being re-persisted in the new structure." - (let ((modified (buffer-modified-p))) + ;; Run silently to avoid `gptel--inherit-stickiness' and other hooks that + ;; might modify the gptel text property. + (with-silent-modifications (if (symbolp (caar bounds-alist)) (mapc (lambda (bounds) @@ -615,8 +689,7 @@ applied before being re-persisted in the new structure." (mapc (lambda (bound) (add-text-properties (car bound) (cdr bound) '(gptel response front-sticky (gptel)))) - bounds-alist)) - (set-buffer-modified-p modified))) + bounds-alist)))) (defun gptel--restore-state () "Restore gptel state when turning on `gptel-mode'." @@ -628,6 +701,14 @@ applied before being re-persisted in the new structure." (when gptel--bounds (gptel--restore-props gptel--bounds) (message "gptel chat restored.")) + (when gptel--preset + (if (gptel-get-preset gptel--preset) + (gptel--apply-preset + gptel--preset (lambda (sym val) (set (make-local-variable sym) val))) + (display-warning + '(gptel presets) + (format "Could not activate gptel preset `%s' in buffer \"%s\"" + gptel--preset (buffer-name))))) (when gptel--backend-name (if-let* ((backend (alist-get gptel--backend-name gptel--known-backends @@ -656,34 +737,59 @@ applied before being re-persisted in the new structure." This saves chat metadata when writing the buffer to disk. To restore a chat session, turn on `gptel-mode' after opening the -file." +file. + +If a gptel preset has been applied in this buffer, a reference to it is +saved. + +Additional metadata is stored only if no preset was applied or if it +differs from the preset specification. This is limited to the active +gptel model and backend names, the system message, active tools, the +response temperature, max tokens and number of conversation turns to +send in queries. (See `gptel--num-messages-to-send' for the last one.)" (run-hooks 'gptel-save-state-hook) (if (derived-mode-p 'org-mode) (progn (require 'gptel-org) (gptel-org--save-state)) - (let ((print-escape-newlines t)) + (let ((print-escape-newlines t) + (preset-spec (and gptel--preset + (gptel-get-preset gptel--preset)))) (save-excursion (save-restriction - (add-file-local-variable 'gptel-model gptel-model) - (add-file-local-variable 'gptel--backend-name - (gptel-backend-name gptel-backend)) - (unless (equal (default-value 'gptel--system-message) - gptel--system-message) - (add-file-local-variable - 'gptel--system-message ;TODO: Handle nil case correctly - (car-safe (gptel--parse-directive gptel--system-message)))) - (if gptel-tools - (add-file-local-variable - 'gptel--tool-names (mapcar #'gptel-tool-name gptel-tools)) - (delete-file-local-variable 'gptel--tool-names)) - (if (equal (default-value 'gptel-temperature) gptel-temperature) - (delete-file-local-variable 'gptel-temperature) - (add-file-local-variable 'gptel-temperature gptel-temperature)) - (if gptel-max-tokens + + (if preset-spec + (add-file-local-variable 'gptel--preset gptel--preset) + (delete-file-local-variable 'gptel--preset)) + + ;; Model and backend + (if (gptel--preset-mismatch-value preset-spec :model gptel-model) + (add-file-local-variable 'gptel-model gptel-model)) + (if (gptel--preset-mismatch-value preset-spec :backend gptel-backend) + (add-file-local-variable 'gptel--backend-name + (gptel-backend-name gptel-backend))) + ;; System message + (let ((parsed (car-safe (gptel--parse-directive gptel--system-message)))) + (if (gptel--preset-mismatch-value preset-spec :system parsed) + (add-file-local-variable 'gptel--system-message parsed) + (delete-file-local-variable 'gptel--system-message))) + ;; Tools + (let ((tool-names (mapcar #'gptel-tool-name gptel-tools))) + (if (gptel--preset-mismatch-value preset-spec :tools tool-names) + (add-file-local-variable 'gptel--tool-names tool-names) + (delete-file-local-variable 'gptel--tool-names))) + ;; Temperature, max tokens and cutoff + (if (and (gptel--preset-mismatch-value preset-spec :temperature gptel-temperature) + (not (equal (default-value 'gptel-temperature) gptel-temperature))) + (add-file-local-variable 'gptel-temperature gptel-temperature) + (delete-file-local-variable 'gptel-temperature)) + (if (and (gptel--preset-mismatch-value preset-spec :max-tokens gptel-max-tokens) + gptel-max-tokens) (add-file-local-variable 'gptel-max-tokens gptel-max-tokens) (delete-file-local-variable 'gptel-max-tokens)) - (if (natnump gptel--num-messages-to-send) + (if (and (gptel--preset-mismatch-value + preset-spec :num-messages-to-send gptel--num-messages-to-send) + (natnump gptel--num-messages-to-send)) (add-file-local-variable 'gptel--num-messages-to-send gptel--num-messages-to-send) (delete-file-local-variable 'gptel--num-messages-to-send)) @@ -803,19 +909,22 @@ Search between BEG and END." toggle-tools) 'mouse-face 'highlight 'help-echo "Select tools")))) - (concat - (propertize - " " 'display - `(space :align-to (- right - ,(+ 5 (length model) (length system) - (length track-media) (length context) (length tools))))) - tools (and track-media " ") track-media (and context " ") context " " system " " - (propertize - (buttonize (concat "[" model "]") - (lambda (&rest _) (gptel-menu))) - 'mouse-face 'highlight - 'help-echo "Model in use")))) - "Information segment for the header-line in gptel-mode.") + (let ((rhs (concat + tools (and track-media " ") track-media + (and context " ") context " " system " " + (propertize + (buttonize (concat "[" model "]") + (lambda (&rest _) (gptel-menu))) + 'mouse-face 'highlight + 'help-echo "Model in use")))) + (concat + (propertize + " " 'display + (if (fboundp 'string-pixel-width) + `(space :align-to (- right (,(string-pixel-width rhs)))) + `(space :align-to (- right ,(+ 5 (string-width rhs)))))) + rhs)))) + "Information segment for the header-line in `gptel-mode'.") (defun gptel-use-header-line () "Set up the header-line for a gptel buffer. @@ -861,9 +970,7 @@ context, tools, system prompt, model and more." (if gptel-use-header-line (progn (setq gptel--old-header-line header-line-format) (gptel-use-header-line)) - (setq mode-line-process - '(:eval (concat " " (buttonize (gptel--model-name gptel-model) - (lambda (&rest _) (gptel-menu)))))))) + (gptel--update-status " Ready" 'success))) (remove-hook 'before-save-hook #'gptel--save-state t) (remove-hook 'after-change-functions 'gptel--inherit-stickiness t) (cond @@ -881,23 +988,21 @@ context, tools, system prompt, model and more." ;; ;TODO(request-lib): Declaration no longer needed (defvar gptel--fsm-last) ;Defined further below -(defun gptel--update-status (&optional msg face) - "Update status MSG in FACE." +(defun gptel--update-status (msg &optional face) + "Update status MSG with FACE." (when gptel-mode - (if gptel-use-header-line - (and (consp header-line-format) - (setf (nth 1 header-line-format) - (thread-first - msg - (buttonize (lambda (_) (gptel--inspect-fsm))) - (propertize 'face face 'mouse-face 'highlight)))) - (if (member msg '(" Typing..." " Waiting...")) - (setq mode-line-process (propertize msg 'face face)) - (setq mode-line-process - '(:eval (concat " " - (buttonize (gptel--model-name gptel-model) - (lambda (&rest _) (gptel-menu)))))) - (message (propertize msg 'face face)))) + (let* ((inspect (lambda (&rest _) (gptel--inspect-fsm))) + (button (propertize (buttonize msg inspect) + 'mouse-face 'highlight))) + (when face (setq button (propertize button 'face face))) + (if gptel-use-header-line + (and (consp header-line-format) (setf (nth 1 header-line-format) button)) + (if (equal msg " Ready") + (setq mode-line-process + `(:eval (concat " " (buttonize (gptel--model-name gptel-model) + ,inspect)))) + (setq mode-line-process button) + (message msg)))) (force-mode-line-update))) @@ -928,7 +1033,10 @@ To enable this face for responses, `gptel-highlight-methods' must be set." :group 'gptel) (defface gptel-response-fringe-highlight - '((t :inherit outline-1 :height reset)) + ;; NOTE: Remove conditional after we drop Emacs 28.1 (#1254) + (if (< emacs-major-version 29) + '((t :inherit outline-1 :height 1.0)) + '((t :inherit outline-1 :height reset))) "LLM response fringe/margin face when using `gptel-highlight-mode'. To enable response highlights in the fringe, `gptel-highlight-methods' @@ -1049,11 +1157,30 @@ buffers." ;;; State machine additions for `gptel-send'. +(defvar gptel-send--transitions + `((INIT . ((t . WAIT))) + (WAIT . ((t . TYPE))) + (TYPE . ((,#'gptel--error-p . ERRS) + (,#'gptel--tool-use-p . TPRE) + (t . DONE))) + (TPRE . ((,#'gptel--error-p . ERRS) + (t . TOOL))) + (TOOL . ((t . TRET))) + (TRET . ((,#'gptel--error-p . ERRS) + (,#'gptel--tool-result-p . WAIT) + (t . DONE)))) + "Alist specifying state transitions for `gptel-send'. + +See `gptel-request--transitions' for details.") + (defvar gptel-send--handlers `((WAIT ,#'gptel--handle-wait ,#'gptel--update-wait) (TYPE ,#'gptel--handle-pre-insert) (ERRS ,#'gptel--handle-error ,#'gptel--fsm-last) - (TOOL ,#'gptel--update-tool-call ,#'gptel--handle-tool-use ,#'gptel--update-tool-ask) + (TPRE ,#'gptel--handle-pre-tool ,#'gptel--fsm-transition) + (TOOL ,#'gptel--update-tool-call ,#'gptel--handle-tool-use + ,#'gptel--update-tool-ask) + (TRET ,#'gptel--handle-post-tool ,#'gptel--handle-tool-result) (DONE ,#'gptel--handle-post-insert ,#'gptel--fsm-last) (ABRT ,#'gptel--handle-abort)) "Alist specifying handlers for `gptel-send' state transitions. @@ -1081,15 +1208,15 @@ See `gptel-request--handlers' for details.") FSM defaults to the state of the last request in the current buffer." (unless fsm - (setq fsm (or (cdr-safe (cl-find-if - (lambda (proc-list) - (eq (thread-first (cadr proc-list) - (gptel-fsm-info) - (plist-get :buffer)) - (current-buffer))) - gptel--request-alist)) - gptel--fsm-last))) - (unless (cl-typep gptel--fsm-last 'gptel-fsm) + (setq fsm (or gptel--fsm-last + (cadr (cl-find-if + (lambda (proc-list) + (eq (thread-first (cadr proc-list) + (gptel-fsm-info) + (plist-get :buffer)) + (current-buffer))) + gptel--request-alist))))) + (unless (cl-typep fsm 'gptel-fsm) (user-error "No gptel request log in this buffer yet!")) (require 'tabulated-list) (with-current-buffer (get-buffer-create "*gptel-diagnostic*") @@ -1153,22 +1280,25 @@ Handle read-only buffers and run pre-response hooks (but only if the request succeeded)." (let* ((info (gptel-fsm-info fsm)) (start-marker (plist-get info :position))) - (when (and - (memq (plist-get info :callback) - '(gptel--insert-response gptel-curl--stream-insert-response)) - (with-current-buffer (plist-get info :buffer) - (or buffer-read-only - (get-char-property start-marker 'read-only)))) - (message "Buffer is read only, displaying reply in buffer \"*LLM response*\"") - (display-buffer - (with-current-buffer (get-buffer-create "*LLM response*") - (visual-line-mode 1) - (goto-char (point-max)) - (move-marker start-marker (point) (current-buffer)) - (current-buffer)) - '((display-buffer-reuse-window - display-buffer-pop-up-window) - (reusable-frames . visible)))) + (when (memq (plist-get info :callback) + '(gptel--insert-response gptel-curl--stream-insert-response)) + (with-current-buffer (marker-buffer start-marker) + (when (or buffer-read-only (get-char-property start-marker 'read-only)) + (cond + ((derived-mode-p 'vterm-mode) + (require 'gptel-integrations) + (gptel--vterm-pre-insert info)) + (t + (message "Buffer is read only, displaying reply in buffer \"*LLM response*\"") + (display-buffer + (with-current-buffer (get-buffer-create "*LLM response*") + (visual-line-mode 1) + (goto-char (point-max)) + (move-marker start-marker (point) (current-buffer)) + (current-buffer)) + '((display-buffer-reuse-window + display-buffer-pop-up-window) + (reusable-frames . visible)))))))) (with-current-buffer (marker-buffer start-marker) (when (plist-get info :stream) (gptel--update-status " Typing..." 'success)) @@ -1218,38 +1348,38 @@ No state transition here since that's handled by the process sentinels." Perform UI updates and run post-response hooks." (when-let* ((info (gptel-fsm-info fsm)) - (error-data (plist-get info :error)) - (http-msg (plist-get info :status)) - (gptel-buffer (plist-get info :buffer)) - (start-marker (plist-get info :position)) - (tracking-marker (or (plist-get info :tracking-marker) - start-marker)) - (backend-name - (gptel-backend-name - (buffer-local-value 'gptel-backend gptel-buffer)))) - (if (stringp error-data) - (message "%s error: (%s) %s" backend-name http-msg (string-trim error-data)) - (when-let* ((error-type (plist-get error-data :type))) - (setq http-msg (concat "(" http-msg ") " + (error-data (plist-get info :error))) + (let* ((status (plist-get info :status)) + (gptel-buffer (plist-get info :buffer)) + (start-marker (plist-get info :position)) + (tracking-marker (or (plist-get info :tracking-marker) + start-marker)) + (backend-name + (gptel-backend-name + (buffer-local-value 'gptel-backend gptel-buffer)))) + (if (stringp error-data) + (message "%s error: (%s) %s" backend-name status (string-trim error-data)) + (when-let* ((error-type (plist-get error-data :type))) + (setq status (concat "(" status ") " (string-trim (gptel--to-string error-type))))) - (when-let* ((error-msg (plist-get error-data :message))) - (message "%s error: (%s) %s" backend-name http-msg - (string-trim (gptel--to-string error-msg))))) - (if-let* ((gptel-window (get-buffer-window gptel-buffer 'visible))) - (with-selected-window gptel-window + (when-let* ((error-msg (plist-get error-data :message))) + (message "%s error: (%s) %s" backend-name status + (string-trim (gptel--to-string error-msg))))) + (if-let* ((gptel-window (get-buffer-window gptel-buffer 'visible))) + (with-selected-window gptel-window + (mapc (lambda (f) (funcall f info)) (plist-get info :post)) + (run-hook-with-args + 'gptel-post-response-functions + (marker-position start-marker) (marker-position tracking-marker))) + (with-current-buffer gptel-buffer (mapc (lambda (f) (funcall f info)) (plist-get info :post)) (run-hook-with-args 'gptel-post-response-functions - (marker-position start-marker) (marker-position tracking-marker))) + (marker-position start-marker) (marker-position tracking-marker)))) (with-current-buffer gptel-buffer - (mapc (lambda (f) (funcall f info)) (plist-get info :post)) - (run-hook-with-args - 'gptel-post-response-functions - (marker-position start-marker) (marker-position tracking-marker)))) - (with-current-buffer gptel-buffer - (when gptel-mode - (gptel--update-status - (format " Error: %s" http-msg) 'error))))) + (when gptel-mode + (gptel--update-status + (format " Error: %s" status) 'error)))))) (defun gptel--handle-abort (fsm) "Perform UI update on `gptel-abort' for FSM." @@ -1272,21 +1402,149 @@ Perform UI updates and run post-response hooks." (with-current-buffer gptel-buffer (when gptel-mode (gptel--update-status " Abort" 'error))))) +(defun gptel--handle-pre-tool (fsm) + "Run `gptel-pre-tool-call-functions' for FSM." + (let* ((info (gptel-fsm-info fsm)) + (buffer (plist-get info :buffer))) + (when (buffer-local-value 'gptel-pre-tool-call-functions buffer) + ;; This function might run many times, so only act on the remaining tool calls. + (let ((tool-use (cl-remove-if (lambda (tc) (plist-get tc :result)) + (plist-get info :tool-use))) + (hook-func-args (list :buffer (buffer-name buffer) + :backend (plist-get info :backend) + :model (plist-get info :model)))) + (with-current-buffer buffer + (run-hook-wrapped ; Run pre tool call functions + 'gptel-pre-tool-call-functions + (lambda (hook-func) + (prog1 nil + (dolist (tool-call tool-use) + (let* ((name (plist-get tool-call :name)) + (args (plist-get tool-call :args)) + (hook-func-result + (with-demoted-errors "gptel-pre-tool-call hook error: %S" + (funcall hook-func (nconc (list :name name :args args) + hook-func-args))))) + (if (plist-get hook-func-result :stop) ; Stop the request immediately + (let ((reason (or (plist-get hook-func-result :stop-reason) + (concat "Request stopped by pre-tool-call hook " + (and (symbolp hook-func) + (symbol-name hook-func)) + " (tool \"" name "\")")))) + (plist-put info :stop-reason reason) + (plist-put info :status "Stopped by hook") + (plist-put info :error reason)) + ;; if hook-func returns :confirm, add the check + (when (plist-get hook-func-result :confirm) + (plist-put tool-call :confirm t)) + ;; if hook-func returns :args or :name, replace in the call + (when (or (plist-get hook-func-result :args) + (plist-get hook-func-result :name)) + ;; Merge with args in the messages array sent to the LLM + (gptel--inject-tool-call + (plist-get info :backend) (plist-get info :data) + tool-call hook-func-result) + ;; Merge with the tool-call in tool-use that actually runs + (gptel--merge-plists tool-call hook-func-result)) + ;; TODO(tool-hooks): :block behavior not final! + (let ((blockp (plist-get hook-func-result :block)) + (result (plist-get hook-func-result :result))) + (when blockp + (plist-put tool-call :error t) + (setq result + (concat "\n" + (if (stringp blockp) blockp + (format "Tool %s blocked by user" name)) + "\n"))) + (when result + (gptel--process-tool-call + fsm (cl-find-if (lambda (ts) (equal (gptel-tool-name ts) name)) + (plist-get info :tools)) + tool-call result)))))))))))))) + +(defun gptel--handle-post-tool (fsm) + "Run `gptel-post-tool-call-functions for FSM." + (let* ((info (gptel-fsm-info fsm)) + (buffer (plist-get info :buffer))) + (when (buffer-local-value 'gptel-post-tool-call-functions buffer) + (let ((hook-func-args (list :buffer (buffer-name buffer) + :backend (plist-get info :backend) + :model (plist-get info :model)))) + (with-current-buffer buffer + (run-hook-wrapped ; Run pre tool call functions + 'gptel-post-tool-call-functions + (lambda (hook-func) + (prog1 nil + (dolist (tool-call (plist-get info :tool-use)) + (let* ((name (plist-get tool-call :name)) + (args (plist-get tool-call :args)) + (hook-func-result + (with-demoted-errors "gptel-post-tool-call hook error: %S" + (funcall hook-func + (nconc (list :name name :args args + :result (plist-get tool-call :result)) + hook-func-args))))) + (if (plist-get hook-func-result :stop) + (let ((reason (or (plist-get hook-func-result :stop-reason) + (concat "Request stopped by post-tool-call hook " + (and (symbolp hook-func) + (symbol-name hook-func)) + " (tool \"" name "\")")))) + (plist-put info :stop-reason reason) + (plist-put info :status "Stopped by hook") + (plist-put info :error reason)) + ;; TODO(tool-hooks): :block behavior not final! + (let ((blockp (plist-get hook-func-result :block)) + (result (plist-get hook-func-result :result))) + (when blockp + (plist-put tool-call :error t) + (setq result + (concat "\n" + (if (stringp blockp) blockp + (format "Tool %s blocked by user" name)) + "\n"))) + (when result + (cl-loop ; Update results sent to callback + for call in (plist-get info :tool-result) + for (spec stored-args _) = call + when (and (equal (gptel-tool-name spec) name) + (null (cl-set-difference + stored-args args :test #'equal))) + do (setf (caddr call) result) and return nil + finally + (display-warning + '(gptel tools) + (format "Tool %s: Could not replace tool results" name))) + ;; Update results sent to LLM + (plist-put tool-call :result result)))))))))))))) + (defun gptel--update-wait (fsm) - "Update gptel's status after sending a request." + "Update gptel's status in FSM after sending a request." (with-current-buffer (plist-get (gptel-fsm-info fsm) :buffer) (when gptel-mode (gptel--update-status " Waiting..." 'warning)))) (defun gptel--update-tool-call (fsm) - "Update gptel's status when calling a tool." + "Update gptel's status in FSM when calling a tool." (with-current-buffer (plist-get (gptel-fsm-info fsm) :buffer) (setq gptel--fsm-last fsm) (when gptel-mode - (gptel--update-status " Calling tool..." 'mode-line-emphasis)))) + (if-let* ((info (gptel-fsm-info fsm)) + (names (cl-loop for call in (plist-get info :tool-use) + collect (plist-get call :name)))) + (gptel--update-status + (concat + (propertize + (if (length> names 1) " Calling tools (" " Calling tool (") + 'face 'mode-line-emphasis) + (mapconcat (lambda (name) (propertize name 'face 'font-lock-keyword-face)) + names (propertize ", " 'face 'mode-line-emphasis)) + (propertize ")" 'face 'mode-line-emphasis))) + ;; FIXME: Is this branch reachable? + (gptel--update-status " Calling tool..." 'mode-line-emphasis))))) (defun gptel--update-tool-ask (fsm) - "Update gptel's status when there are pending tool calls." + "Update gptel's status in FSM when there are pending tool-calls." (when (plist-get (gptel-fsm-info fsm) :tool-pending) (plist-put (gptel-fsm-info fsm) :tool-pending nil) (when gptel-mode @@ -1313,7 +1571,8 @@ waiting for the response." (if (and arg (require 'gptel-transient nil t)) (call-interactively #'gptel-menu) (gptel--sanitize-model) - (let ((fsm (gptel-make-fsm :handlers gptel-send--handlers))) + (let ((fsm (gptel-make-fsm :table gptel-send--transitions + :handlers gptel-send--handlers))) (gptel-request nil :stream gptel-stream :transforms gptel-prompt-transform-functions @@ -1404,7 +1663,7 @@ kill ring instead." (plist-put (gptel-fsm-info gptel--fsm-last) :data data) (if copy ;Copy Curl command instead of sending request (let ((args (gptel-curl--get-args (gptel-fsm-info gptel--fsm-last) - (md5 (format "%s" (random)))))) + (md5 (format "%s" (random))) t))) (kill-new (mapconcat #'shell-quote-argument (cons (gptel--curl-path) args) " \\\n")) @@ -1548,27 +1807,30 @@ buffer created or switched to. INTERACTIVEP is t when gptel is called interactively." (interactive - (let* ((backend (default-value 'gptel-backend)) - (backend-name - (format "*%s*" (gptel-backend-name backend)))) - (list (read-buffer - "Create or choose gptel buffer: " - backend-name nil ; DEFAULT and REQUIRE-MATCH - (lambda (b) ; PREDICATE - ;; NOTE: buffer check is required (#450) - (and-let* ((buf (get-buffer (or (car-safe b) b)))) - (buffer-local-value 'gptel-mode buf)))) - (condition-case nil - (gptel--get-api-key - (gptel-backend-key backend)) - ((error user-error) - (setq gptel-api-key - (read-passwd - (format "%s API key: " backend-name))))) - (and (use-region-p) - (buffer-substring (region-beginning) - (region-end))) - t))) + (progn + (gptel--sanitize-model :backend (default-value 'gptel-backend) + :shoosh t) + (let* ((backend (default-value 'gptel-backend)) + (backend-name + (format "*%s*" (gptel-backend-name backend)))) + (list (read-buffer + "Create or choose gptel buffer: " + backend-name nil ; DEFAULT and REQUIRE-MATCH + (lambda (b) ; PREDICATE + ;; NOTE: buffer check is required (#450) + (and-let* ((buf (get-buffer (or (car-safe b) b)))) + (buffer-local-value 'gptel-mode buf)))) + (condition-case nil + (gptel--get-api-key + (gptel-backend-key backend)) + ((error user-error) + (setq gptel-api-key + (read-passwd + (format "%s API key: " backend-name))))) + (and (use-region-p) + (buffer-substring (region-beginning) + (region-end))) + t)))) (with-current-buffer (get-buffer-create name) (cond ;Set major mode ((eq major-mode gptel-default-mode)) @@ -1658,7 +1920,7 @@ for streaming responses only." Each key is a tool name (string) and value is a list of one or two functions, for preview-setup and (optional) preview-teardown. -The preview-setup function is called with two arguments: a list of the +The preview-setup function is called with two arguments: a plist of the corresponding tool call arguments and the request INFO plist. It must set up the preview for the tool call and return a handle to the preview, which can be any object, but typically an overlay or a buffer. @@ -1697,44 +1959,39 @@ USE-MINIBUFFER is non-nil)." (let* ((start-marker (plist-get info :position)) (tracking-marker (plist-get info :tracking-marker))) ;; pending tool calls look like ((tool callback args) ...) - (with-current-buffer (plist-get info :buffer) - (if (or use-minibuffer ;prompt for confirmation from the minibuffer + (with-current-buffer (marker-buffer start-marker) + (if (or use-minibuffer ;prompt for confirmation from the minibuffer buffer-read-only ;TEMP(tool-preview) Handle read-only buffers better (get-char-property (max (point-min) (1- (or tracking-marker start-marker))) 'read-only)) (let* ((minibuffer-allow-text-properties t) (backend-name (gptel-backend-name (plist-get info :backend))) - (prompt (format "%s wants to run " backend-name))) - (map-y-or-n-p - (lambda (tool-call-spec) - (concat prompt (propertize (gptel-tool-name (car tool-call-spec)) - 'face 'font-lock-keyword-face) - ": ")) - (lambda (tcs) (gptel--accept-tool-calls (list tcs) nil)) - tool-calls '("tool call" "tool calls" "run") - `((?i ,(lambda (_) (save-window-excursion - (with-selected-window - (gptel--inspect-fsm gptel--fsm-last) - (goto-char (point-min)) - (when (search-forward-regexp "^:tool-use" nil t) - (forward-line 0) (hl-line-highlight)) - (use-local-map - (make-composed-keymap - (define-keymap "q" (lambda () (interactive) - (quit-window) - (exit-recursive-edit))) - (current-local-map))) - (recursive-edit) nil))) - "inspect call(s)")))) - ;; Prompt for confirmation from the chat buffer + (tool-call-names + (mapconcat (lambda (c) (propertize (gptel-tool-name (car c)) + 'face 'font-lock-keyword-face)) + tool-calls ", ")) + (len (length tool-calls)) + (prompt (format "%s wants to run %s tool %s (%s). " + backend-name len (if (> len 1) "calls" "call") + tool-call-names)) + (choices '((?y "Run tools") (?n "Cancel (resumable)") + (?i "Inspect or edit"))) + (choice (read-multiple-choice prompt choices))) + (pcase (car choice) + (?y (gptel--accept-tool-calls tool-calls)) + (?n (gptel--reject-tool-calls)) + (?i (gptel--inspect-tool-calls + ;; TODO Verify that the query buffer is the correct source to send + tool-calls (plist-get info :buffer))))) + ;; Prompt for confirmation from the response buffer (let* ((backend-name (gptel-backend-name (plist-get info :backend))) (actions-string (concat (propertize "Run tools: " 'face 'font-lock-string-face) (propertize "C-c C-c" 'face 'help-key-binding) (propertize ", Cancel request: " 'face 'font-lock-string-face) (propertize "C-c C-k" 'face 'help-key-binding) - (propertize ", Inspect: " 'face 'font-lock-string-face) + (propertize ", Inspect or Edit: " 'face 'font-lock-string-face) (propertize "C-c C-i" 'face 'help-key-binding))) (confirm-strings) ;; FIXME(tool) use a wrapper instead of a manual text-property search, @@ -1748,14 +2005,16 @@ USE-MINIBUFFER is non-nil)." start-marker 'gptel-tool)) (make-overlay ov-start (or tracking-marker start-marker) nil nil nil))) + (arg-values) (prompt-ov)) ;; If the cursor is at the overlay-end, it ends up outside, so move it back (unless tracking-marker (when (= (point) start-marker) (ignore-errors (backward-char)))) (save-excursion (goto-char (overlay-end ov)) - (pcase-dolist (`(,tool-spec ,arg-values _) tool-calls) + (pcase-dolist (`(,tool-spec ,arg-plist _) tool-calls) ;; Call tool-specific confirmation prompt + (setq arg-values (gptel--map-tool-args tool-spec arg-plist)) (if-let* ((funcs (cdr (assoc (gptel-tool-name tool-spec) gptel--tool-preview-alist))) ((functionp (car-safe funcs)))) @@ -1770,24 +2029,28 @@ USE-MINIBUFFER is non-nil)." (setq prompt-ov (make-overlay (overlay-end ov) (point) nil t)) (overlay-put prompt-ov 'before-string - (concat "\n" - (propertize " " 'display `(space :align-to (- right ,(length actions-string) 2)) - 'face '(:inherit font-lock-string-face :underline t :extend t)) - actions-string - (format (propertize "\n%s wants to run:\n\n" - 'face 'font-lock-string-face) - backend-name))) + (concat + "\n" + (propertize " " 'display `(space :align-to (- right ,(length actions-string) 2)) + 'face '(:inherit font-lock-string-face :underline t :extend t)) + actions-string + (format (propertize "\n%s wants to run:\n\n" + 'face 'font-lock-string-face) + backend-name))) (overlay-put prompt-ov 'after-string (concat (propertize "\n" 'face '(:inherit font-lock-string-face :underline t :extend t)))) (overlay-put prompt-ov 'evaporate t) - (overlay-put ov 'prompt prompt-ov) + (overlay-put ov 'prompt (cons prompt-ov (overlay-get ov 'prompt))) (move-overlay ov ov-start (point))) ;; Add confirmation prompt to the overlay - (when preview-handlers (overlay-put ov 'previews preview-handlers)) + (when preview-handlers + (overlay-put ov 'previews + (nconc (overlay-get ov 'previews) preview-handlers))) (overlay-put ov 'mouse-face 'highlight) - (overlay-put ov 'gptel-tool tool-calls) + (overlay-put ov 'gptel-tool + (nconc (overlay-get ov 'gptel-tool) tool-calls)) (overlay-put ov 'help-echo (concat "Tool call(s) requested: " actions-string)) (overlay-put ov 'keymap gptel-tool-call-actions-map) @@ -1822,9 +2085,10 @@ for tool call results. INFO contains the state of the request." (and gptel-mode (not (string-suffix-p "\n" (gptel-response-prefix-string))) - "\n")) ;start of response - ((not (and tool-marker ;not consecutive tool result blocks - (= tracking-marker tool-marker))) + "\n")) ;start of response + ((not tool-marker) gptel-response-separator) + ((and (not (= tracking-marker tool-marker)) + (not (eq (char-before tracking-marker) ?\n))) gptel-response-separator))) (tool-use ;; TODO(tool) also check args since there may be more than @@ -1899,22 +2163,43 @@ NAME and ARG-VALUES are the name and arguments for the call." arg-values " ") 'font-lock-face 'font-lock-constant-face))) -(defun gptel--inspect-tool-calls () - (interactive) - (with-selected-window - (gptel--inspect-fsm gptel--fsm-last) - (goto-char (point-min)) - (when (search-forward-regexp "^:tool-use" nil t) - (forward-line 0) - (hl-line-highlight)))) +(defun gptel--accept-tool-calls (&optional tool-calls ov) + "Run pending tool-calls. -(defun gptel--accept-tool-calls (&optional response ov) +TOOL-CALLS is the edited tool call list, OV is the tool call dispatch +overlay in the query buffer." (interactive (pcase-let ((`(,resp . ,o) (get-char-property-and-overlay (point) 'gptel-tool))) (list resp o))) - (gptel--update-status " Calling tool..." 'mode-line-emphasis) + (when (overlayp ov) ;Update UI indicator + (with-current-buffer (overlay-buffer ov) + (when gptel-mode + (let ((names (cl-loop for call in tool-calls + collect (gptel-tool-name (car call))))) + (gptel--update-status + (concat + (propertize + (if (length> names 1) " Calling tools (" " Calling tool (") + 'face 'mode-line-emphasis) + (mapconcat (lambda (name) (propertize name 'face 'font-lock-keyword-face)) + names (propertize ", " 'face 'mode-line-emphasis)) + (propertize ")" 'face 'mode-line-emphasis))))))) + ;; Clear the overlays first, because we need the buffer to be cleaned up + ;; before inserting synchronous tool results. + (when (and (overlayp ov) (overlay-buffer ov)) + (with-current-buffer (overlay-buffer ov) + (when-let* ((preview-handles (overlay-get ov 'previews))) + (dolist (func-to-handle preview-handles) + (when (car func-to-handle) (apply func-to-handle)))) + (dolist (prompt-ov (overlay-get ov 'prompt)) + (when-let* (((overlay-buffer prompt-ov)) + (inhibit-read-only t)) + (delete-region (overlay-start prompt-ov) + (overlay-end prompt-ov))))) + (delete-overlay ov)) (message "Continuing query...") - (cl-loop for (tool-spec arg-values process-tool-result) in response + (cl-loop for (tool-spec arg-plist process-tool-result) in tool-calls + for arg-values = (gptel--map-tool-args tool-spec arg-plist) do (if (gptel-tool-async tool-spec) (apply (gptel-tool-function tool-spec) @@ -1923,20 +2208,12 @@ NAME and ARG-VALUES are the name and arguments for the call." (condition-case errdata (apply (gptel-tool-function tool-spec) arg-values) (error (mapconcat #'gptel--to-string errdata " "))))) - (funcall process-tool-result result)))) - (when (and (overlayp ov) (overlay-buffer ov)) - (with-current-buffer (overlay-buffer ov) - (when-let* ((preview-handles (overlay-get ov 'previews))) - (dolist (func-to-handle preview-handles) - (when (car func-to-handle) (apply func-to-handle)))) - (when-let* ((prompt-ov (overlay-get ov 'prompt)) - ((overlay-buffer prompt-ov)) - (inhibit-read-only t)) - (delete-region (overlay-start prompt-ov) - (overlay-end prompt-ov)))) - (delete-overlay ov))) + (funcall process-tool-result result))))) -(defun gptel--reject-tool-calls (&optional _response ov) +(defun gptel--reject-tool-calls (&optional _tool-calls ov) + "Cancel pending tool-calls. + +OV is the tool call dispatch overlay." (interactive (pcase-let ((`(,resp . ,o) (get-char-property-and-overlay (point) 'gptel-tool))) (list resp o))) @@ -1948,14 +2225,15 @@ NAME and ARG-VALUES are the name and arguments for the call." (when-let* ((preview-handles (overlay-get ov 'previews))) (dolist (func-to-handle preview-handles) (when (car func-to-handle) (apply func-to-handle)))) - (when-let* ((prompt-ov (overlay-get ov 'prompt)) - ((overlay-buffer prompt-ov)) - (inhibit-read-only t)) - (delete-region (overlay-start prompt-ov) - (overlay-end prompt-ov)))) + (dolist (prompt-ov (overlay-get ov 'prompt)) + (when-let* (((overlay-buffer prompt-ov)) + (inhibit-read-only t)) + (delete-region (overlay-start prompt-ov) + (overlay-end prompt-ov))))) (delete-overlay ov))) (defun gptel--dispatch-tool-calls (choice) + "Dispatch on tool-calls with CHOICE." (interactive (list (let ((choices '((?y "yes") (?n "do nothing") @@ -1964,12 +2242,192 @@ NAME and ARG-VALUES are the name and arguments for the call." (pcase (car choice) (?y (call-interactively #'gptel--accept-tool-calls)) (?k (call-interactively #'gptel--reject-tool-calls)) - (?i (gptel--inspect-fsm gptel--fsm-last)))) + (?i (call-interactively #'gptel--inspect-tool-calls)))) + +;;;; Tool call inspection UI +(defvar-keymap gptel-tool-call-inspection-map + :doc "Actions in the gptel tool inspection buffer." + "C-c C-c" #'gptel--inspect-accept-tool-calls + "C-c C-k" #'gptel--inspect-reject-tool-calls + "C-c C-i" #'gptel--inspect-quit-tool-calls) + +(defun gptel--inspect-accept-tool-calls (&optional _) + "Run possibly edited tool-calls read from the tool call inspection buffer." + (interactive) + (let ((call) (index) + (read-error + (lambda () (user-error + "Cannot read modified arguments, please check modifications"))) + (apply-error + (lambda () (message "Cannot apply argument modifications. \ +This is a bug, please report it!")))) + (unless (gptel-fsm-p gptel--fsm-last) (funcall apply-error)) + (cond + ((buffer-modified-p) + (let* ((info (gptel-fsm-info gptel--fsm-last)) + (backend (plist-get info :backend)) + (tool-use (plist-get info :tool-use)) + (tool-spec-args-cb) (name) + (tool-calls)) + (dolist (o (cl-remove-if-not (lambda (ov) (overlay-get ov 'gptel-overlay)) + (overlays-in (point-min) (point-max)))) + (goto-char (overlay-start o)) + (condition-case nil + (save-restriction + (narrow-to-region (point) (overlay-end o)) + (skip-chars-forward "\n\r\ ") + (unless (eobp) + (setq call (read (current-buffer)) + name (plist-get call :name)))) + (error (funcall read-error))) + (unless (integerp (setq index (overlay-get o 'gptel-overlay))) + (funcall read-error)) + ;; Merge with or remove from messages array... + (gptel--inject-tool-call + backend (plist-get info :data) (nth index tool-use) call) + (if (not call) + ;; Remove from tool-use + (plist-put info :tool-use + (append (cl-subseq tool-use 0 index) + (cl-subseq tool-use (1+ index)))) + ;; ...before modifying the tool use block + (gptel--merge-plists (nth index tool-use) call) + (setq tool-spec-args-cb (overlay-get o 'gptel-tool)) + ;; and modifying the arguments sent to the callback + (setf (nth 0 tool-spec-args-cb) + (or (cl-find name (plist-get info :tools) + :key #'gptel-tool-name :test #'string=) + (gptel-get-tool name))) + (setf (nth 1 tool-spec-args-cb) (plist-get call :args)) + (push tool-spec-args-cb tool-calls))) + (gptel--accept-tool-calls ;include overlay to clean up if there is one + tool-calls (cadr (plist-get info :tool-display))))) + (t (let* ((tool-display (plist-get (gptel-fsm-info gptel--fsm-last) + :tool-display))) + (apply #'gptel--accept-tool-calls tool-display))))) + (quit-window t)) + +(defun gptel--inspect-reject-tool-calls (&optional _) + "Cancel tool-calls and return to query buffer." + (interactive) + (quit-window t) + (apply #'gptel--reject-tool-calls + (thread-first (gptel-fsm-info gptel--fsm-last) + (plist-get :tool-display)))) + +(defun gptel--inspect-quit-tool-calls (&optional _) + "Quit inspection window and return to query buffer." + (interactive) + (quit-window t)) + +(defalias 'gptel--inspect-tool-post-command + (let ((highlight-ov)) + (lambda () + (unless (memq highlight-ov (overlays-at (point))) + (let ((context-ov + (cl-loop for ov in (overlays-at (point)) + thereis (and (overlay-get ov 'gptel-overlay) ov))) + (line (propertize "\n" 'font-lock-face + '(:inherit separator-line :extend t)))) + (when highlight-ov + (overlay-put highlight-ov 'face nil) + (overlay-put highlight-ov 'before-string nil) + (overlay-put highlight-ov 'after-string nil)) + (when context-ov + (overlay-put context-ov 'face 'gptel-response-highlight) + (overlay-put context-ov 'before-string line) + (overlay-put context-ov 'after-string (concat "\n" line))) + (setq highlight-ov context-ov))))) + "Highlight tool call under cursor in gptel tool call inspection buffers.") + +(defun gptel--inspect-tool-calls (&optional tool-calls loc) + "Set up and switch to a buffer to inspect pending tool-calls. + +TOOL-CALLS is the alist of tool calls. LOC is the source of the query; +either the query buffer or the tool call dispatch overlay in the query +buffer. If it is an overlay, it will be cleaned up when dispatching on +TOOL-CALLS." + (interactive (pcase-let ((`(,resp . ,o) (get-char-property-and-overlay + (point) 'gptel-tool))) + (list resp o))) + (with-current-buffer (get-buffer-create "*gptel-tool-calls*") + (let ((inhibit-read-only t) tool-use + (tool-overlay (and (overlayp loc) loc))) + (remove-overlays) + (erase-buffer) + (unless (derived-mode-p 'lisp-data-mode) + (lisp-data-mode) + (add-hook 'post-command-hook #'gptel--inspect-tool-post-command nil t)) + ;; NOTE: This needs to be called after setting the major mode, as + ;; buffer-local variables are wiped out. + (setq gptel--fsm-last + (buffer-local-value 'gptel--fsm-last + (if tool-overlay (overlay-buffer loc) loc)) + tool-use (plist-get (gptel-fsm-info gptel--fsm-last) :tool-use)) + ;; For access from the dispatch functions, add tool-calls and the location + ;; (if it's an overlay) to INFO. Overlays will be cleaned up. + (plist-put (gptel-fsm-info gptel--fsm-last) + :tool-display (list tool-calls tool-overlay)) + (insert ";; Inspect or edit tool calls. +;; Adding or deleting tool calls is not supported.\n\n") + (cl-loop for tool-spec-args-cb in tool-calls + for (tool-spec arg-plist _process-tool-result) = tool-spec-args-cb + with o + for name = (gptel-tool-name tool-spec) + for pt = (point) + for index = + (cl-position-if + (lambda (call) (and (not (plist-get call :result)) + (string= (plist-get call :name) name) + (null (cl-set-difference + (plist-get call :args) arg-plist + :test #'equal)))) + tool-use) + do (prin1 (list :name name :args arg-plist) + (current-buffer) '((length . nil) (level . nil))) + (insert "\n\n") ;Avoid extending the overlay + (setq o (make-overlay pt (- (point) 2) nil nil t)) + (overlay-put o 'gptel-tool tool-spec-args-cb) + (overlay-put o 'gptel-overlay index)) + (goto-char (point-min)) (forward-line 3)) + (use-local-map + (make-composed-keymap + gptel-tool-call-inspection-map (current-local-map))) + (unless header-line-format + (setq header-line-format + (substitute-command-keys + (concat + (propertize "Tool calls" 'face 'font-lock-string-face) ": " + (buttonize "Confirm" #'gptel--inspect-accept-tool-calls) + " \\[gptel--inspect-accept-tool-calls], " + (buttonize "Cancel" #'gptel--inspect-reject-tool-calls) + " \\[gptel--inspect-reject-tool-calls], " + (buttonize "Return" #'gptel--inspect-quit-tool-calls) + " \\[gptel--inspect-quit-tool-calls], " + (buttonize "Edit" (lambda (_) (read-only-mode 'toggle))) + " \\[read-only-mode]")))) + (set-buffer-modified-p nil) + (read-only-mode) + (gptel--inspect-tool-post-command) + (display-buffer (current-buffer) gptel-display-buffer-action))) ;;; Presets ;;;; Presets implementation -(defvar gptel--known-presets nil +(defvar gptel--known-presets + '((gptel-default + :description "Use gptel's default configuration." + :context nil :use-context system + :tools nil :use-tools t + :temperature nil :max-tokens nil + :num-messages-to-send nil + :request-params nil + :org-convert-response t + :track-media nil + :track-response t + :system nil + :stream t + :cache nil)) "Alist of presets for gptel. Each entry maps a preset name (a symbol) to a plist of @@ -2129,15 +2587,22 @@ SETTER is the function used to set the gptel options. It must accept two arguments, the symbol being set and the value to set it to. It defaults to `set', and can be set to a different function to (for example) apply the preset buffer-locally." - (when (memq (type-of preset) '(string symbol)) - (let ((spec (or (gptel-get-preset preset) - (user-error "gptel preset \"%s\": Cannot find preset" - preset)))) - (setq preset spec))) (unless setter (setq setter #'set)) - (when-let* ((func (plist-get preset :pre))) (funcall func)) - (when-let* ((parents (plist-get preset :parents))) - (mapc (lambda (parent) (gptel--apply-preset parent setter)) (ensure-list parents))) + (cl-flet ((preset-spec (preset) + (if (memq (type-of preset) '(symbol string)) + (or (gptel-get-preset preset) + (user-error "gptel preset \"%s\": Cannot find preset" + preset)) + preset))) + ;; Record preset name for persistence and UI display + (when (memq (type-of preset) '(string symbol)) + (funcall setter 'gptel--preset preset)) + ;; Ensure that preset is a plist spec + (setq preset (preset-spec preset)) + (when-let* ((func (plist-get preset :pre))) (funcall func)) + (when-let* ((parents (plist-get preset :parents))) + (mapc (lambda (parent) (gptel--apply-preset (preset-spec parent) setter)) + (ensure-list parents)))) (map-do (lambda (key val) (pcase key @@ -2211,9 +2676,8 @@ PRESET is the name of a preset, or a spec (plist) of the form (pcase key ((or :description :pre :post)) (:parents - (mapc (lambda (parent-preset) - (nconc syms (gptel--preset-syms parent-preset))) - (ensure-list val))) + (setq syms + (nconc syms (mapcan #'gptel--preset-syms (ensure-list val))))) (:system (push 'gptel--system-message syms)) (_ (if-let* ((var (or (intern-soft (concat "gptel-" (substring (symbol-name key) 1))) @@ -2240,12 +2704,65 @@ NAME is the name of a preset, or a spec (plist) of the form (let ((syms (make-symbol "syms")) (binds (make-symbol "binds")) (bodyfun (make-symbol "body"))) - `(let* ((,syms (gptel--preset-syms ,name)) + ;; Let-bind symbols that we want to modify with the presets. Also include + ;; `gptel--preset' in this list as we don't want to change its value outside + ;; of this macro's scope. + `(let* ((,syms (cons 'gptel--preset (gptel--preset-syms ,name))) (,bodyfun (lambda () (gptel--apply-preset ,name) ,@body)) (,binds nil)) (while ,syms (push (list (car ,syms) (pop ,syms)) ,binds)) (eval (list 'let (nreverse ,binds) (list 'funcall (list 'quote ,bodyfun))))))) +(defun gptel--preset-mismatch-value (preset-spec key val) + "Determine if the value of KEY in PRESET-SPEC matches VAL. + +This is an imperfect check for whether the value corresponding to KEY (a +keyword) in PRESET-SPEC (a plist) matches VAL. This is required +primarily to identify which gptel variable values have changed since +PRESET-SPEC was applied, which is relevant when writing gptel metadata +to a chat file. + +See also `gptel--preset-mismatch-p'." + ;; In all cases, assume a mismatch if the preset's value for KEY is a + ;; modify-list spec, such as (:append ...) + ;; Mismatches may not even be well-defined/determinable in these cases. + (or (not preset-spec) + (pcase key + ;; special cases + ((or :system :system-message) + (let ((system (plist-get preset-spec :system))) + (or (and (stringp system) (not (equal system val))) + (functionp system) + (and (consp system) (keywordp (car system))) + (and (consp system) + (not (equal (car-safe (gptel--parse-directive system)) + val)))))) + (:backend + (let ((backend (plist-get preset-spec :backend))) + (or (and (consp backend) (keywordp (car-safe backend))) + (not (equal (or (and (gptel-backend-p val) (gptel-backend-name val)) + val) + (or (and (gptel-backend-p backend) (gptel-backend-name backend)) + backend)))))) + ;; FIXME: We're assuming that val is a list of tool names, not tools + (:tools + (and-let* ((preset-tools (plist-get preset-spec :tools))) + (or (keywordp (car-safe preset-tools)) + (cl-loop + for tool in preset-tools + for tool-name = + (or (and (stringp tool) tool) + (ignore-errors (gptel-tool-name tool))) + if (not (member tool-name uniq-tool-names)) + collect tool-name into uniq-tool-names + finally return + (not (equal (sort uniq-tool-names #'string-lessp) + (sort (copy-sequence (ensure-list val)) #'string-lessp))))))) + ;; Generic case + (_ (let ((field-val (plist-get preset-spec key))) + (or (and (consp field-val) (keywordp (car field-val))) + (not (equal field-val val)))))))) + ;;;; Presets in-buffer UI (defun gptel--transform-apply-preset (_fsm) "Apply a gptel preset to the buffer depending on the prompt. -- 2.47.3