278 lines
11 KiB
EmacsLisp
278 lines
11 KiB
EmacsLisp
;;; init-ai.el --- Configuración de inteligencias artificales -*- lexical-binding: t -*-
|
|
|
|
;; Author: kj <webmaster@outcontrol.net>
|
|
;; URL: https://git.kj2.me/kj/confi-emacs-actual
|
|
|
|
;;; Commentary:
|
|
|
|
;; Configuración para Inteligencia artifical en Emacs.
|
|
|
|
;;; Code:
|
|
|
|
;; Cliente LLM (ollama, chatgpt, gemini, etc.)
|
|
(use-package gptel
|
|
:defer nil
|
|
:config
|
|
(when (getenv "GEMINI_KEY")
|
|
(gptel-make-gemini "Gemini"
|
|
:key (getenv "GEMINI_KEY")
|
|
:stream t))
|
|
|
|
(gptel-make-openai "llama.cpp"
|
|
:stream t
|
|
:protocol "http"
|
|
:host "127.0.0.1:1945"
|
|
:models '("Qwen3.8-27B"
|
|
"gemma-4-12B"
|
|
"gemma-4-E2B"
|
|
"gemma-4-E4B"
|
|
"gptoss-20b"))
|
|
|
|
(gptel-make-ollama "Ollama"
|
|
:host "localhost:11434"
|
|
:stream t
|
|
:request-params '(:think :json-false)
|
|
:models '("gemma4:31b-cloud"
|
|
"glm-5.1:cloud"
|
|
"kimi-k2.6:cloud"
|
|
"gpt-oss:20b-cloud"
|
|
"minimax-m2.5:cloud"
|
|
"qwen3-coder-next:cloud"))
|
|
|
|
;; Backend opencode-go: suscripción OpenCode Go vía endpoint OpenAI-compatible.
|
|
;; Las versiones actuales de opencode guardan el login de consola en su base
|
|
;; de datos SQLite (token OAuth + organización activa), no en auth.json.
|
|
(defconst kj-opencode-db-file
|
|
(expand-file-name "~/.local/share/opencode/opencode.db")
|
|
"Ruta de la base de datos SQLite de opencode.")
|
|
|
|
(defun kj-opencode-console-credentials ()
|
|
"Return (TOKEN . ORG-ID) for opencode's active console account."
|
|
(when (and (fboundp 'sqlite-select)
|
|
(file-readable-p kj-opencode-db-file))
|
|
(ignore-errors
|
|
(let ((db (sqlite-open kj-opencode-db-file t)))
|
|
(unwind-protect
|
|
(when-let* ((row (car (sqlite-select
|
|
db
|
|
"select a.access_token, s.active_org_id \
|
|
from account_state s join account a on a.id = s.active_account_id \
|
|
limit 1"))))
|
|
(cons (nth 0 row) (nth 1 row)))
|
|
(sqlite-close db))))))
|
|
|
|
(defun kj-opencode-go-key ()
|
|
"Return the credential to use for the OpenCode Go backend."
|
|
(or (car (kj-opencode-console-credentials))
|
|
(getenv "OPENCODE_CONSOLE_TOKEN")
|
|
(let ((auth (with-temp-buffer
|
|
(insert-file-contents "~/.local/share/opencode/auth.json")
|
|
(json-parse-buffer :object-type 'alist))))
|
|
(alist-get 'key (alist-get 'opencode-go auth)))))
|
|
|
|
(defun kj-opencode-go-org-id ()
|
|
"Return opencode's active workspace id, when available."
|
|
(or (cdr (kj-opencode-console-credentials))
|
|
(getenv "OPENCODE_ORG_ID")))
|
|
|
|
(defvar-local kj-opencode-session-id nil
|
|
"Stable OpenCode session id for this conversation buffer.")
|
|
|
|
(defun kj-opencode-go-new-session-id ()
|
|
"Generate a new stable-enough random id for x-opencode-session."
|
|
(md5 (format "opencode-go-%s-%s" (random) (float-time))))
|
|
|
|
(defun kj-opencode-go-session-id (info)
|
|
"Return a stable x-opencode-session id for the buffer in INFO."
|
|
(let ((buf (or (plist-get info :buffer) (current-buffer))))
|
|
(if (buffer-live-p buf)
|
|
(with-current-buffer buf
|
|
(or kj-opencode-session-id
|
|
(setq-local kj-opencode-session-id
|
|
(kj-opencode-go-new-session-id))))
|
|
(kj-opencode-go-new-session-id))))
|
|
|
|
(defun kj-opencode-go-header (info)
|
|
"Auth, workspace and per-conversation session headers for OpenCode Go."
|
|
(append
|
|
(when-let* ((key (gptel--get-api-key)))
|
|
`(("Authorization" . ,(concat "Bearer " key))))
|
|
(when-let* ((org (kj-opencode-go-org-id)))
|
|
`(("x-opencode-org-id" . ,org)))
|
|
`(("x-opencode-session" . ,(kj-opencode-go-session-id info)))))
|
|
|
|
(gptel-make-openai "OpenCode Go"
|
|
:host "opencode.ai"
|
|
:endpoint "/zen/go/v1/chat/completions"
|
|
:key #'kj-opencode-go-key
|
|
:header #'kj-opencode-go-header
|
|
:stream t
|
|
:models '(deepseek-v4.1-flash deepseek-v4-pro
|
|
glm-5.1 glm-5.2 glm-5.3 glm-5.3-flash
|
|
grok-4.6 grok-4.7
|
|
mimo-v2.5 mimo-v2.5-pro mimo-v2.6-flash mimo-v2.6-pro
|
|
minimax-m2.5 minimax-m2.7 minimax-m3
|
|
kimi-k2.6 kimi-k2.7-code kimi-k3
|
|
longcat-2.0
|
|
qwen3.6-plus qwen3.7-plus qwen3.7-max qwen3.8-flash qwen3.8-max
|
|
gpt-5.6-luna gpt-6-luna claude-haiku-5-5
|
|
hy3 hy4-preview omen-alpha step-5-preview
|
|
beam-free minimax-m3.1-flash-free longcat-2.5-preview-free
|
|
muse-spark-1.2-contributor muse-spark-1.3-contributor
|
|
gemini-4-argon-contributor))
|
|
|
|
;; Backend y modelo por defecto
|
|
(setq gptel-backend (gptel-get-backend "OpenCode Go")
|
|
gptel-model 'mimo-v2.6-flash)
|
|
|
|
(setq gptel-default-mode 'markdown-mode
|
|
gptel-prompt-prefix-alist
|
|
'((markdown-mode . "# User\n\n")
|
|
(org-mode . "* User\n\n")
|
|
(text-mode . "# User\n\n"))
|
|
gptel-response-prefix-alist
|
|
'((markdown-mode . "# AI\n\n")
|
|
(org-mode . "* AI\n\n")
|
|
(text-mode . "# AI\n\n"))
|
|
gptel-directives
|
|
'((default . "You are a large language model living in Emacs and a helpful assistant. Respond concisely in Spanish."))
|
|
)
|
|
|
|
(add-hook 'gptel-post-response-functions 'gptel-end-of-response)
|
|
;; El "thinking" de los modelos se muestra en los buffers gptel.
|
|
;; `ignore' (valor por defecto de gptel) lo muestra pero NO lo reenvía
|
|
;; al modelo en turnos posteriores (recomendado: evita llenar el
|
|
;; contexto, ver NEWS de gptel). Usar `t' si además se quiere
|
|
;; reenviarlo y `nil' para deshabilitarlo (útil para respuestas mas rápidas).
|
|
(setopt gptel-include-reasoning 'ignore)
|
|
|
|
(defun gptel-switch+model ()
|
|
"Switch to gptel backend and model in a single completion prompt."
|
|
(interactive)
|
|
(let (choices)
|
|
(dolist (pair gptel--known-backends)
|
|
(let* ((backend-name (car pair))
|
|
(backend (cdr pair))
|
|
(models
|
|
(and (fboundp 'gptel-backend-models)
|
|
(gptel-backend-models backend))))
|
|
(when models
|
|
(dolist (model models)
|
|
(push (cons ; (format "%s:%s" backend-name model)
|
|
(format "%s → %s"
|
|
(propertize backend-name 'face 'font-lock-keyword-face)
|
|
(propertize (symbol-name model) 'face 'font-lock-function-name-face))
|
|
(cons backend-name model))
|
|
choices)))))
|
|
(let* ((choice
|
|
(completing-read "Model: " (mapcar #'car choices) nil t))
|
|
(sel (cdr (assoc choice choices))))
|
|
(setq gptel-backend (cdr (assoc (car sel) gptel--known-backends))
|
|
gptel-model (cdr sel))
|
|
(message "gptel set to %s:%s" (car sel) (cdr sel)))))
|
|
)
|
|
|
|
;; (use-package copilot
|
|
;; :hook (prog-mode . copilot-mode)
|
|
;; :bind (:map copilot-completion-map
|
|
;; ("C-g" . 'copilot-clear-overlay)
|
|
;; ("C-<return>" . 'copilot-accept-completion)
|
|
;; ("S-<return>" . 'copilot-accept-completion-by-word)))
|
|
|
|
(use-package gptel-magit
|
|
:ensure t
|
|
:hook (magit-mode . gptel-magit-install)
|
|
:config
|
|
;; gptel-magit no sabe manejar el contenido de "reasoning": gptel entrega
|
|
;; el thinking al callback como (reasoning . TEXTO) y gptel-magit lo
|
|
;; inserta como si fuera la respuesta final, fallando con
|
|
;; Wrong type argument: char-or-string-p, (reasoning . "...")
|
|
;; (https://github.com/ragnard/gptel-magit/issues/8). Esto ocurre
|
|
;; aunque `gptel-include-reasoning' sea nil, porque gptel entrega las
|
|
;; celdas (reasoning . ...) al callback de forma incondicional (solo
|
|
;; controla si se insertan en el buffer gptel y si se reenvían).
|
|
;; En vez de deshabilitar el thinking globalmente, sanitizamos la
|
|
;; respuesta en el callback: se descartan las celdas de reasoning y solo
|
|
;; se deja pasar la respuesta final (string). Así el thinking sigue
|
|
;; activo en las sesiones gptel normales.
|
|
(defun kj-gptel-magit--string-response-callback (callback)
|
|
"Return a callback that forwards only string responses to CALLBACK.
|
|
Discards the (reasoning . TEXT) cells through which gptel delivers the
|
|
thinking content to the callback (see `gptel-request')."
|
|
(lambda (response &rest args)
|
|
(when (stringp response)
|
|
(apply callback response args))))
|
|
|
|
(defun kj-gptel-magit--sanitize-callback (orig-fn &rest args)
|
|
"Around advice for `gptel-magit--request'.
|
|
Wraps the :callback argument of ARGS so that responses with reasoning
|
|
content do not break gptel-magit."
|
|
(let ((pos (cl-position :callback args)))
|
|
(when (and pos (functionp (nth (1+ pos) args)))
|
|
(setf (nth (1+ pos) args)
|
|
(kj-gptel-magit--string-response-callback
|
|
(nth (1+ pos) args)))))
|
|
(apply orig-fn args))
|
|
|
|
(advice-add 'gptel-magit--request :around
|
|
#'kj-gptel-magit--sanitize-callback))
|
|
|
|
(use-package gptel-autocomplete
|
|
:defer nil
|
|
:ensure (:host github :repo "JDNdeveloper/gptel-autocomplete")
|
|
:bind (("M-<return>" . gptel-complete)
|
|
:map gptel-autocomplete-completion-map
|
|
("C-<return>" . gptel-accept-completion)))
|
|
|
|
(use-package macher
|
|
:ensure (:host github :repo "kmontag/macher")
|
|
:custom
|
|
(macher-action-buffer-ui 'org))
|
|
|
|
;; agent-shell: interfaz nativa de Emacs para interactuar con agentes ACP
|
|
(use-package shell-maker)
|
|
|
|
(use-package acp)
|
|
|
|
(use-package agent-shell
|
|
:init
|
|
(defalias 'opencode #'agent-shell-opencode-start-agent)
|
|
(defalias 'pi #'agent-shell-pi-start-agent)
|
|
:config
|
|
(setq agent-shell-opencode-authentication
|
|
(agent-shell-opencode-make-authentication :none t))
|
|
(setq agent-shell-opencode-default-model-id "opencode-go/qwen3.8-flash")
|
|
|
|
;; Header compacto
|
|
(setq agent-shell-header-style 'text)
|
|
|
|
;; Restaurar sesiones con texto completo
|
|
(setq agent-shell-session-restore-verbosity 'full)
|
|
|
|
;; Desactivar guardado de transcripciones y prompt al cerrar
|
|
(setq agent-shell-transcript-file-path-function nil)
|
|
(setq shell-maker-prompt-before-killing-buffer nil)
|
|
|
|
;; Centralizar datos de agent-shell (screenshots, transcripts, worktrees)
|
|
;; para que no cree .agent-shell/ dentro de los proyectos
|
|
(setq agent-shell-dot-subdir-function
|
|
(lambda (subdir)
|
|
(expand-file-name subdir
|
|
(expand-file-name "agent-shell/" user-emacs-directory))))
|
|
|
|
;; Quitar cape-dabbrev, cape-history, comint-completion-at-point y el centinela t
|
|
;; de agent-shell (evita autocompletar texto del buffer o historial de prompts previos)
|
|
(add-hook 'agent-shell-mode-hook
|
|
(lambda ()
|
|
(remove-hook 'completion-at-point-functions #'cape-dabbrev t)
|
|
(remove-hook 'completion-at-point-functions #'cape-history t)
|
|
(remove-hook 'completion-at-point-functions #'comint-completion-at-point t)
|
|
(setq-local completion-at-point-functions
|
|
(delq t completion-at-point-functions))
|
|
(setq-local corfu-auto nil))
|
|
t) ;; :append para correr DESPUÉS de que agent-shell configure sus CAPFs
|
|
)
|
|
|
|
(provide 'init-ai)
|
|
;;; init-ai.el ends here
|