do not edit — generated by btf.
git.druid.rocksindexdruid520radiumradium-gptel.el

radium-gptel.el


;;; radium-gptel.el, any openai-compatible api in a buffer - tools, mcp -*- lexical-binding: t; -*-
;;
;; a *gptel* chat buffer talks to whatever openai-compatible endpoint
;; radium-gptel-provider names (gptel-make-openai rather than a
;; provider-specific backend, since that one plist shape - host,
;; endpoint, models, a key-file - already covers every real provider
;; ive actually used: deepseek, openrouter, a local llama.cpp server,
;; ... - and it means switching providers is a ~/.radium.el edit, not
;; a re-read of this file).
;;
;; radium-gptel-provider is nil here - this file makes no assumption
;; about which provider im on today, only how to wire *some* openai-
;; compatible one up once told. set it (and call
;; radium-gptel-apply-provider) from ~/.radium.el, loaded dead last
;; (see init.el's own comment on why) - a real per-machine setting,
;; same shape item 13's api-key-path precedent already is, not
;; something that belongs tracked in this repo. left unconfigured,
;; gptel-backend stays nil - checked directly against the installed
;; gptel's own source (gptel-request.el's defcustom): nil is gptel's
;; own real default too, not a gap this file leaves open, so C-c ? c
;; with no provider set fails the same way a stock, untouched gptel
;; install would.
;;
;; auth is always a plain api key, read fresh from the provider's own
;; :key-file on every request (that file holds nothing but the raw
;; key) - never in this repo, never in an env var. missing file ->
;; empty key -> a normal 401 from the api, not an elisp error
;; mid-request.
;;
;; tools are on (gptel-use-tools), so the model can actually read/edit/
;; write files and run commands mid-conversation, not just talk. the
;; read/edit/write tools open the real file buffer and (for anything
;; that changes it) pop it into a window, so an edit shows up live in
;; my editor instead of silently landing on disk - and gptel's own
;; :confirm gate means i see and approve the proposed change first.
;; mcp is wired too: set mcp-hub-servers (see mcp.el) to whatever mcp
;; servers i want and their tools merge into gptel-tools automatically
;; once they connect.
 
(defvar radium-gptel-provider nil
  "plist describing which openai-compatible api gptel talks to - :name
:host :endpoint :key-file :models :model, and optionally :stream
\(defaults to t when omitted). nil means \"not configured\": gptel-
backend is left at gptel's own real default, nil, rather than this
file guessing which provider im on today.
 
set this (and call `radium-gptel-apply-provider' so the change
actually takes) from ~/.radium.el - switching providers should only
ever mean editing that file, never this one. e.g.:
 
  (setq radium-gptel-provider
        (list :name \"OpenRouter\" :host \"openrouter.ai\"
              :endpoint \"/api/v1/chat/completions\"
              :key-file \"~/.openrouter\"
              :models \='(anthropic/claude-opus-5.5 openai/gpt-5)
              :model \='anthropic/claude-opus-5.5))
  (radium-gptel-apply-provider)")
 
(defun radium-gptel-key-from-file (file)
  "return a closure reading FILE (just the raw api key, nothing else)
fresh on every call - never cached, never in this repo, never in an
env var. a missing file becomes an empty key so the api call 401s
normally instead of an elisp file-error mid-request."
  (lambda ()
    (condition-case nil
        (with-temp-buffer
          (insert-file-contents (expand-file-name file))
          (string-trim (buffer-string)))
      (file-missing
       (message "err: no %s key file (put the raw api key there)." file)
       ""))))
 
(defun radium-gptel-apply-provider ()
  "(re)build gptel-backend/gptel-model from `radium-gptel-provider' -
a no-op while its still nil (unconfigured), so requiring this file
never silently assumes any one provider. safe to call again anytime
after changing `radium-gptel-provider', e.g. from ~/.radium.el."
  (when radium-gptel-provider
    (let ((p radium-gptel-provider))
      (setq gptel-backend
            (gptel-make-openai (plist-get p :name)
              :host (plist-get p :host)
              :endpoint (plist-get p :endpoint)
              :stream (if (plist-member p :stream) (plist-get p :stream) t)
              :key (radium-gptel-key-from-file (plist-get p :key-file))
              :models (plist-get p :models))
            gptel-model (plist-get p :model)))))
 
(defun radium-gptel--open (path)
  "find-file-noselect on PATH, resolved against default-directory."
  (find-file-noselect (expand-file-name path)))
 
(defun radium-gptel-tool-read-file (path)
  (with-current-buffer (radium-gptel--open path)
    (buffer-substring-no-properties (point-min) (point-max))))
 
(defun radium-gptel-tool-list-directory (path)
  (mapconcat #'identity
             (directory-files (expand-file-name path) nil "\\`[^.]")
             "\n"))
 
(defun radium-gptel-tool-write-file (path content)
  "overwrite PATH with CONTENT entirely (for a new file, or a full
rewrite) - pops the buffer into view so the write is visible, not
just a silent disk write."
  (let ((buf (radium-gptel--open path)))
    (with-current-buffer buf
      (erase-buffer)
      (insert content)
      (save-buffer))
    (display-buffer buf)
    (format "ok: wrote %s (%d bytes)." (buffer-file-name buf) (length content))))
 
(defun radium-gptel-tool-edit-region (path old-string new-string)
  "replace the one occurrence of OLD-STRING with NEW-STRING in PATH -
same contract as my own edit tool: OLD-STRING must be unique in the
file, or this refuses rather than guess which one i meant."
  (let ((buf (radium-gptel--open path)))
    (with-current-buffer buf
      (goto-char (point-min))
      (let ((count 0))
        (while (search-forward old-string nil t) (setq count (1+ count)))
        (cond
         ((= count 0) (error "err: old_string not found in %s." path))
         ((> count 1) (error "err: old_string not unique in %s (%d matches)." path count))
         (t
          (goto-char (point-min))
          (search-forward old-string)
          (replace-match new-string t t)
          (save-buffer)))))
    (display-buffer buf)
    (format "ok: edited %s." (buffer-file-name buf))))
 
(defun radium-gptel-tool-run-shell-command (command)
  (with-temp-buffer
    (let ((code (call-process-shell-command command nil t)))
      (format "exit %d:\n%s" code (buffer-string)))))
 
(use-package gptel
  ;; :bind alone would implicitly defer loading gptel until one of
  ;; those keys is pressed - which would also defer :config, so
  ;; gptel-backend/gptel-model would silently stay unset until then.
  ;; :demand forces it to load (and configure the backend) at startup
  ;; like every other module here, not on first keypress.
  :demand t
  :config
  ;; gptel-make-openai's own default :header already does
  ;; ("Authorization" . "Bearer <key>") via gptel--get-api-key, which
  ;; is exactly the auth shape every openai-compatible provider ive
  ;; actually used shares - just supply :key, no per-provider custom
  ;; header function needed.
  (radium-gptel-apply-provider)
  (setq gptel-use-tools t)
 
  ;; gptel-make-tool only *registers* a tool (into gptel--known-tools,
  ;; discoverable in gptel-menu) - it does NOT add it to gptel-tools,
  ;; the list actually sent with a request. building gptel-tools
  ;; straight from these calls' return values is what actually turns
  ;; tool use on.
  (setq gptel-tools
        (list
         (gptel-make-tool
          :name "read_file" :category "radium"
          :function #'radium-gptel-tool-read-file
          :description "read a file's full contents, resolved against the project's default-directory"
          :args (list '(:name "path" :type string :description "path to the file, relative or absolute")))
 
         (gptel-make-tool
          :name "list_directory" :category "radium"
          :function #'radium-gptel-tool-list-directory
          :description "list a directory's entries (dotfiles excluded)"
          :args (list '(:name "path" :type string :description "path to the directory")))
 
         (gptel-make-tool
          :name "write_file" :category "radium" :confirm t
          :function #'radium-gptel-tool-write-file
          :description "overwrite a file with entirely new content - for a new file or a full rewrite, not a small edit (use edit_region for that)"
          :args (list '(:name "path" :type string :description "path to the file")
                      '(:name "content" :type string :description "the file's entire new content")))
 
         (gptel-make-tool
          :name "edit_region" :category "radium" :confirm t
          :function #'radium-gptel-tool-edit-region
          :description "replace one exact, unique occurrence of old_string with new_string in a file - refuses if old_string isnt found or isnt unique"
          :args (list '(:name "path" :type string :description "path to the file")
                      '(:name "old_string" :type string :description "the exact text to replace, must be unique in the file")
                      '(:name "new_string" :type string :description "the text to replace it with")))
 
         (gptel-make-tool
          :name "run_shell_command" :category "radium" :confirm t
          :function #'radium-gptel-tool-run-shell-command
          :description "run a shell command and return its exit code + combined output"
          :args (list '(:name "command" :type string :description "the shell command line to run")))))
 
  :bind (("C-c ? c" . gptel)
         ("C-c ? s" . gptel-send)
         ("C-c ? m" . gptel-menu)
         ("C-c ? r" . gptel-rewrite)))
 
;; mcp: set mcp-hub-servers (in this file, or a personal override
;; loaded after radium) to whatever servers i want - see mcp.el's own
;; docs for the (NAME . (:command ... :args ...)) / (NAME . (:url ...))
;; shape. mcp-hub-servers only exists once mcp.el itself has loaded,
;; so the package always loads (its just sitting there otherwise);
;; nothing actually starts unless ive configured a server.
(use-package mcp
  :demand t
  :config
  (require 'mcp-hub)
  (when mcp-hub-servers
    (mcp-hub-start-all-server
     (lambda () (setq gptel-tools (append gptel-tools (mcp-hub-get-all-tool)))))))
 
(provide 'radium-gptel)
powered by btf.