branch: elpa/aidermacs commit 33dabece6af5d09d252e4d0a000922b53bb63077 Author: Mingde (Matthew) Zeng <matthew...@posteo.net> Commit: Mingde (Matthew) Zeng <matthew...@posteo.net>
Revamp model selection Signed-off-by: Mingde (Matthew) Zeng <matthew...@posteo.net> --- README.md | 5 +- aidermacs-models.el | 135 +++++++++++++++++++++++++++++++++------------------- aidermacs.el | 28 ++++++----- 3 files changed, 103 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 4ceed1247b..b4eeae2a69 100644 --- a/README.md +++ b/README.md @@ -147,8 +147,9 @@ When Architect mode is enabled, the `aidermacs-default-model` setting is ignored ```emacs-lisp (setq aidermacs-use-architect-mode t) -(setq aidermacs-architect-model "o1-mini") ; default -(setq aidermacs-editor-model "deepseek/deepseek-chat") ;; defaults to aidermacs-default-model +;; Both default to aidermacs-default-model +(setq aidermacs-architect-model "sonnet") +(setq aidermacs-editor-model "deepseek/deepseek-chat") ``` *Note: These configurations will be overwritten by the existence of an `.aider.conf.yml` file (see [details](#Overwrite-Configuration-with-Configuration-File)).* diff --git a/aidermacs-models.el b/aidermacs-models.el index a48157424a..62516bd8ed 100644 --- a/aidermacs-models.el +++ b/aidermacs-models.el @@ -42,19 +42,16 @@ "Default AI model to use for aidermacs sessions when not in Architect mode." :type 'string) -(defcustom aidermacs-architect-model "sonnet" - "Default AI model to use for architectural reasoning in aidermacs sessions." +(defcustom aidermacs-architect-model aidermacs-default-model + "Default reasoning AI model to use for architect mode. +Defaults to `aidermacs-default-model' if not explicitly set." :type 'string) (defcustom aidermacs-editor-model aidermacs-default-model - "Default AI model to use for code editing in aidermacs sessions. -Defaults to `aidermacs-default-model` if not explicitly set." + "Default editing AI model to use for architect mode. +Defaults to `aidermacs-default-model' if not explicitly set." :type 'string) -(defcustom aidermacs-use-architect-mode nil - "If non-nil, use separate Architect/Editor mode." - :type 'boolean) - (defcustom aidermacs-popular-models '("sonnet" "o1-mini" @@ -69,54 +66,94 @@ Also based on aidermacs LLM benchmark: https://aidermacs.chat/docs/leaderboards/ (defvar aidermacs--cached-models aidermacs-popular-models "Cache of available AI models.") -(defun aidermacs--fetch-openai-compatible-models (url) +(defconst aidermacs--api-providers + '(("https://openrouter.ai/api/v1" . ((hostname . "openrouter.ai") + (prefix . "openrouter") + (env-var . "OPENROUTER_API_KEY") + (auth-header . (("Authorization" . "Bearer %s"))) + (models-path . "/models") + (models-key . data))) + ("https://api.openai.com/v1" . ((hostname . "api.openai.com") + (prefix . "openai") + (env-var . "OPENAI_API_KEY") + (auth-header . (("Authorization" . "Bearer %s"))) + (models-path . "/models") + (models-key . data))) + ("https://api.deepseek.com" . ((hostname . "api.deepseek.com") + (prefix . "deepseek") + (env-var . "DEEPSEEK_API_KEY") + (auth-header . (("Authorization" . "Bearer %s"))) + (models-path . "/models") + (models-key . data))) + ("https://api.anthropic.com/v1" . ((hostname . "api.anthropic.com") + (prefix . "anthropic") + (env-var . "ANTHROPIC_API_KEY") + (auth-header . (("x-api-key" . "%s") + ("anthropic-version" . "2023-06-01"))) + (models-path . "/models") + (models-key . data))) + ("https://generativelanguage.googleapis.com/v1beta" . ((hostname . "generativelanguage.googleapis.com") + (prefix . "gemini") + (env-var . "GEMINI_API_KEY") + (auth-header . nil) + (models-path . "/models?key=%s") + (models-key . models) + (model-name-transform . (lambda (name) + (replace-regexp-in-string "^models/" "" name))))) + ("https://api.x.ai/v1" . ((hostname . "api.x.ai") + (prefix . "xai") + (env-var . "XAI_API_KEY") + (auth-header . (("Authorization" . "Bearer %s"))) + (models-path . "/models") + (models-key . data)))) + "Configuration for different API providers. +Each entry maps a base URL to a configuration alist with: +- hostname: The API hostname +- prefix: Prefix to add to model names +- env-var: Environment variable containing the API key +- auth-header: Headers for authentication (nil if not needed) +- models-path: Path to fetch models, with %s for token if needed +- models-key: JSON key containing the models list +- model-name-transform: Optional function to transform model names") + +(defun aidermacs--fetch-openai-compatible-models (url token) "Fetch available models from an OpenAI compatible API endpoint. URL should be the base API endpoint, e.g. https://api.openai.com/v1. +TOKEN is the API token for authentication. Returns a list of model names with appropriate prefixes based on the API provider." - (let* ((url-parsed (url-generic-parse-url url)) - (hostname (url-host url-parsed)) - (prefix (pcase hostname - ("api.openai.com" "openai") - ("openrouter.ai" "openrouter") - ("api.deepseek.com" "deepseek") - ("api.anthropic.com" "anthropic") - ("generativelanguage.googleapis.com" "gemini") - (_ (error "Unknown API host: %s" hostname)))) - (token (pcase hostname - ("api.openai.com" (getenv "OPENAI_API_KEY")) - ("openrouter.ai" (getenv "OPENROUTER_API_KEY")) - ("api.deepseek.com" (getenv "DEEPSEEK_API_KEY")) - ("api.anthropic.com" (getenv "ANTHROPIC_API_KEY")) - ("generativelanguage.googleapis.com" (getenv "GEMINI_API_KEY")) - (_ (error "Unknown API host: %s" hostname))))) + (let* ((provider-config (cdr (assoc url aidermacs--api-providers))) + (prefix (alist-get 'prefix provider-config)) + (auth-headers (alist-get 'auth-header provider-config)) + (models-path (alist-get 'models-path provider-config)) + (models-key (alist-get 'models-key provider-config)) + (transform-fn (alist-get 'model-name-transform provider-config))) + + (unless provider-config + (error "Unknown API URL: %s" url)) + (with-local-quit (with-current-buffer (let ((url-request-extra-headers - (pcase hostname - ("api.anthropic.com" - `(("x-api-key" . ,token) - ("anthropic-version" . "2023-06-01"))) - ("generativelanguage.googleapis.com" - nil) ; No auth headers for Gemini, key is in URL - (_ - `(("Authorization" . ,(concat "Bearer " token))))))) + (when auth-headers + (mapcar (lambda (header) + (cons (car header) + (format (cdr header) token))) + auth-headers)))) (url-retrieve-synchronously - (if (string= hostname "generativelanguage.googleapis.com") - (concat url "/models?key=" token) - (concat url "/models")))) + (concat url (if (string-match-p "%s" models-path) + (format models-path token) + models-path)))) + (goto-char url-http-end-of-headers) (let* ((json-object-type 'alist) (json-data (json-read)) - (models (if (string= hostname "generativelanguage.googleapis.com") - (alist-get 'models json-data) - (alist-get 'data json-data)))) + (models (alist-get models-key json-data))) (mapcar (lambda (model) (concat prefix "/" (cond - ((string= hostname "generativelanguage.googleapis.com") - (replace-regexp-in-string "^models/" "" (alist-get 'name model))) - ((stringp model) model) ; Handle case where model is just a string + (transform-fn (funcall transform-fn (alist-get 'name model))) + ((stringp model) model) (t (or (alist-get 'id model) (alist-get 'name model)))))) models)))))) @@ -146,16 +183,14 @@ This fetches models from various API providers and caches them." (mapcar (lambda (line) (substring line 2)) ; Remove "- " prefix supported-models)) - (dolist (url-token-pair '(("https://api.openai.com/v1" . "OPENAI_API_KEY") - ("https://openrouter.ai/api/v1" . "OPENROUTER_API_KEY") - ("https://api.deepseek.com" . "DEEPSEEK_API_KEY") - ("https://api.anthropic.com/v1" . "ANTHROPIC_API_KEY") - ("https://generativelanguage.googleapis.com/v1beta" . "GEMINI_API_KEY"))) - (let ((url (car url-token-pair)) - (token-value (getenv (cdr url-token-pair)))) + (dolist (provider-entry aidermacs--api-providers) + (let* ((url (car provider-entry)) + (config (cdr provider-entry)) + (env-var (alist-get 'env-var config)) + (token-value (getenv env-var))) (when (and token-value (not (string-empty-p token-value))) (condition-case err - (let* ((fetched-models (aidermacs--fetch-openai-compatible-models url)) + (let* ((fetched-models (aidermacs--fetch-openai-compatible-models url token-value)) (filtered-models (seq-filter (lambda (model) (member model supported-models)) fetched-models))) diff --git a/aidermacs.el b/aidermacs.el index bdad034aa2..dedac4c280 100644 --- a/aidermacs.el +++ b/aidermacs.el @@ -55,6 +55,10 @@ "Buffer-local variable to track the current aidermacs mode. Possible values: `code', `ask', `architect', `help'.") +(defcustom aidermacs-use-architect-mode nil + "If non-nil, use separate Architect/Editor mode." + :type 'boolean) + (defcustom aidermacs-config-file nil "Path to aider configuration file. When set, Aidermacs will pass this to aider via --config flag, @@ -84,12 +88,12 @@ When nil, disable auto-commits requiring manual git commits." When nil, require explicit confirmation before applying changes." :type 'boolean) -(defvar aidermacs--cached-version nil - "Cached aider version to avoid repeated version checks.") - (defvar aidermacs--read-string-history nil "History list for aidermacs read string inputs.") +(defvar aidermacs--cached-version nil + "Cached aider version to avoid repeated version checks.") + (defun aidermacs-aider-version () "Check the installed aider version. Returns a version string like \"0.77.0\" or nil if version can't be determined. @@ -147,7 +151,7 @@ This is the file name without path." :if (lambda () aidermacs-auto-commits)) ("R" "Refresh Repo Map" aidermacs-refresh-repo-map) ("h" "Session History" aidermacs-show-output-history) - ("o" "Change Main Model" aidermacs-change-model) + ("o" "Switch Model" aidermacs-change-model) ("?" "Aider Meta-level Help" aidermacs-help)]] ["File Actions" ["Add Files (C-u: read-only)" @@ -234,7 +238,7 @@ If supplied, SUFFIX is appended to the buffer name within the earmuffs." (t root)))) (format "*aidermacs:%s%s*" (file-truename display-root) - (or suffix ""))))) + (or suffix ""))))) ;;;###autoload (defun aidermacs-run () @@ -270,8 +274,6 @@ This function sets up the appropriate arguments and launches the process." '("--config" "-c")))) ;; Check aider version for auto-accept-architect support (aider-version (aidermacs-aider-version)) - (supports-auto-accept-architect (and aider-version - (version<= "0.77.0" aider-version))) (backend-args (if has-config-arg ;; Only need to add aidermacs-config-file manually @@ -290,7 +292,7 @@ This function sets up the appropriate arguments and launches the process." ;; 1. User has disabled auto-accept (aidermacs-auto-accept-architect is nil) ;; 2. Aider version supports this flag (>= 0.77.0) (when (and (not aidermacs-auto-accept-architect) - supports-auto-accept-architect) + (version<= "0.77.0" aider-version)) '("--no-auto-accept-architect")) (when aidermacs-subtree-only '("--subtree-only"))))) @@ -876,11 +878,11 @@ Otherwise implement TODOs for the entire current file." (let* ((current-line (string-trim (thing-at-point 'line t))) (is-comment (aidermacs--is-comment-line current-line))) (when-let* ((command (aidermacs--form-prompt - "/architect" - (concat "Please implement the TODO items." - (and is-comment - (format " on this comment: `%s`." current-line)) - " Keep existing code structure")))) + "/architect" + (concat "Please implement the TODO items." + (and is-comment + (format " on this comment: `%s`." current-line)) + " Keep existing code structure")))) (aidermacs--ensure-current-file-tracked) (aidermacs--send-command command)))))