summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSergey Kostyaev <s-kostyaev@users.noreply.github.com>2026-05-01 16:46:20 +0200
committerGitHub <noreply@github.com>2026-05-01 10:46:20 -0400
commit4ef511ad1ce11539e3848edf8a2f0ef439506c01 (patch)
treefc3ff18907dbddc713f4defbe7fd5d17947652ca
parent4f69909fcc81d9d0f54ccb0d44ef3fa6e95ee09c (diff)
Redact provider keys in printed structs (#276)externals/llm
## Summary - wrap string provider keys in zero-argument functions so provider structs do not print raw secrets in backtraces - update key-bearing provider constructors to apply the wrapping at construction time - add a regression test for OpenAI-compatible providers Original issue: https://github.com/s-kostyaev/ellama/issues/318 ## Testing - emacs -Q --batch -L . -L /Users/sergeykostyaev/.emacs.d/elpa/plz-0.9.1 -L /Users/sergeykostyaev/.emacs.d/elpa/plz-event-source-0.1.3 -L /Users/sergeykostyaev/.emacs.d/elpa/plz-media-type-0.2.4 -L /Users/sergeykostyaev/.emacs.d/elpa-old/compat-30.0.2.0 --eval "(setq load-prefer-newer t)" -l llm-test.el -l llm-provider-utils-test.el -l llm-prompt-test.el -l llm-models-test.el --eval "(ert-run-tests-batch-and-exit)" - byte-compilation of touched files with the same load path
-rw-r--r--llm-azure.el13
-rw-r--r--llm-claude.el11
-rw-r--r--llm-deepseek.el17
-rw-r--r--llm-gemini.el12
-rw-r--r--llm-github.el15
-rw-r--r--llm-gpt4all.el15
-rw-r--r--llm-llamacpp.el16
-rw-r--r--llm-ollama.el15
-rw-r--r--llm-openai.el44
-rw-r--r--llm-provider-utils.el13
-rw-r--r--llm-test.el17
11 files changed, 171 insertions, 17 deletions
diff --git a/llm-azure.el b/llm-azure.el
index c717000..1d72de9 100644
--- a/llm-azure.el
+++ b/llm-azure.el
@@ -29,7 +29,18 @@
(require 'llm-openai)
(require 'cl-lib)
-(cl-defstruct (llm-azure (:include llm-openai-compatible)))
+(cl-defstruct (llm-azure
+ (:include llm-openai-compatible)
+ (:constructor make-llm-azure
+ (&key default-chat-temperature
+ default-chat-max-tokens
+ default-chat-non-standard-params
+ ((:key raw-key))
+ (chat-model "unset")
+ (embedding-model "unset")
+ url
+ &aux
+ (key (llm-provider-utils--wrap-key raw-key))))))
(cl-defmethod llm-nonfree-message-info ((_ llm-azure))
"Return Azure's nonfree terms of service."
diff --git a/llm-claude.el b/llm-claude.el
index c349c11..0e5542c 100644
--- a/llm-claude.el
+++ b/llm-claude.el
@@ -32,7 +32,16 @@
(require 'rx)
;; Models defined at https://docs.anthropic.com/claude/docs/models-overview
-(cl-defstruct (llm-claude (:include llm-standard-chat-provider))
+(cl-defstruct (llm-claude
+ (:include llm-standard-chat-provider)
+ (:constructor make-llm-claude
+ (&key default-chat-temperature
+ default-chat-max-tokens
+ default-chat-non-standard-params
+ ((:key raw-key))
+ (chat-model "claude-sonnet-4-6")
+ &aux
+ (key (llm-provider-utils--wrap-key raw-key)))))
(key nil :read-only t)
(chat-model "claude-sonnet-4-6" :read-only t))
diff --git a/llm-deepseek.el b/llm-deepseek.el
index 799c627..8855166 100644
--- a/llm-deepseek.el
+++ b/llm-deepseek.el
@@ -29,9 +29,20 @@
(require 'llm-models)
(require 'cl-lib)
-(cl-defstruct (llm-deepseek (:include llm-openai-compatible
- (url "https://api.deepseek.com")
- (chat-model "deepseek-chat"))))
+(cl-defstruct (llm-deepseek
+ (:include llm-openai-compatible
+ (url "https://api.deepseek.com")
+ (chat-model "deepseek-chat"))
+ (:constructor make-llm-deepseek
+ (&key default-chat-temperature
+ default-chat-max-tokens
+ default-chat-non-standard-params
+ ((:key raw-key))
+ (chat-model "deepseek-chat")
+ (embedding-model "unset")
+ (url "https://api.deepseek.com")
+ &aux
+ (key (llm-provider-utils--wrap-key raw-key))))))
(cl-defmethod llm-nonfree-message-info ((_ llm-deepseek))
"Location for the terms of service and privacy policy."
diff --git a/llm-gemini.el b/llm-gemini.el
index 4f9e06d..c7eb222 100644
--- a/llm-gemini.el
+++ b/llm-gemini.el
@@ -32,7 +32,17 @@
(require 'llm-provider-utils)
(require 'json)
-(cl-defstruct (llm-gemini (:include llm-google))
+(cl-defstruct (llm-gemini
+ (:include llm-google)
+ (:constructor make-llm-gemini
+ (&key default-chat-temperature
+ default-chat-max-tokens
+ default-chat-non-standard-params
+ ((:key raw-key))
+ (embedding-model "embedding-001")
+ (chat-model "gemini-3.1-pro-preview")
+ &aux
+ (key (llm-provider-utils--wrap-key raw-key)))))
"A struct representing a Gemini client.
KEY is the API key for the client.
diff --git a/llm-github.el b/llm-github.el
index 4bb1448..063030f 100644
--- a/llm-github.el
+++ b/llm-github.el
@@ -28,8 +28,19 @@
(require 'llm)
(require 'llm-azure)
-(cl-defstruct (llm-github (:include llm-azure
- (url "https://models.inference.ai.azure.com"))))
+(cl-defstruct (llm-github
+ (:include llm-azure
+ (url "https://models.inference.ai.azure.com"))
+ (:constructor make-llm-github
+ (&key default-chat-temperature
+ default-chat-max-tokens
+ default-chat-non-standard-params
+ ((:key raw-key))
+ (chat-model "unset")
+ (embedding-model "unset")
+ (url "https://models.inference.ai.azure.com")
+ &aux
+ (key (llm-provider-utils--wrap-key raw-key))))))
(cl-defmethod llm-provider-chat-url ((provider llm-github))
(format "%s/chat/completions" (llm-azure-url provider)))
diff --git a/llm-gpt4all.el b/llm-gpt4all.el
index 39e4e0d..aa2e445 100644
--- a/llm-gpt4all.el
+++ b/llm-gpt4all.el
@@ -34,7 +34,20 @@
(require 'llm-openai)
(require 'llm-provider-utils)
-(cl-defstruct (llm-gpt4all (:include llm-openai-compatible))
+(cl-defstruct (llm-gpt4all
+ (:include llm-openai-compatible)
+ (:constructor make-llm-gpt4all
+ (&key default-chat-temperature
+ default-chat-max-tokens
+ default-chat-non-standard-params
+ ((:key raw-key))
+ (chat-model "unset")
+ (embedding-model "unset")
+ url
+ host
+ port
+ &aux
+ (key (llm-provider-utils--wrap-key raw-key)))))
"A structure for holding information needed by GPT4All.
CHAT-MODEL is the model to use for chat queries. It must be set.
diff --git a/llm-llamacpp.el b/llm-llamacpp.el
index 348c8fc..e882f9e 100644
--- a/llm-llamacpp.el
+++ b/llm-llamacpp.el
@@ -46,7 +46,21 @@ This is needed because there is no API support for previous chat conversation."
:type 'string)
;; Obsolete, llm-openai-compatible can be used directly instead.
-(cl-defstruct (llm-llamacpp (:include llm-openai-compatible))
+(cl-defstruct (llm-llamacpp
+ (:include llm-openai-compatible)
+ (:constructor make-llm-llamacpp
+ (&key default-chat-temperature
+ default-chat-max-tokens
+ default-chat-non-standard-params
+ ((:key raw-key))
+ (chat-model "unset")
+ (embedding-model "unset")
+ url
+ (scheme "http")
+ (host "localhost")
+ (port 8080)
+ &aux
+ (key (llm-provider-utils--wrap-key raw-key)))))
"A struct representing a llama.cpp instance."
(scheme "http") (host "localhost") (port 8080))
diff --git a/llm-ollama.el b/llm-ollama.el
index 9048070..bf272c6 100644
--- a/llm-ollama.el
+++ b/llm-ollama.el
@@ -63,7 +63,20 @@ CHAT-MODEL is the model to use for chat queries. It is required.
EMBEDDING-MODEL is the model to use for embeddings. It is required."
(scheme "http") (host "localhost") (port 11434) chat-model embedding-model)
-(cl-defstruct (llm-ollama-authed (:include llm-ollama))
+(cl-defstruct (llm-ollama-authed
+ (:include llm-ollama)
+ (:constructor make-llm-ollama-authed
+ (&key default-chat-temperature
+ default-chat-max-tokens
+ default-chat-non-standard-params
+ (scheme "http")
+ (host "localhost")
+ (port 11434)
+ chat-model
+ embedding-model
+ ((:key raw-key))
+ &aux
+ (key (llm-provider-utils--wrap-key raw-key)))))
"Similar to llm-ollama, but also with a key."
key)
diff --git a/llm-openai.el b/llm-openai.el
index f27807c..453d901 100644
--- a/llm-openai.el
+++ b/llm-openai.el
@@ -42,7 +42,17 @@
:type 'string
:group 'llm-openai)
-(cl-defstruct (llm-openai (:include llm-standard-full-provider))
+(cl-defstruct (llm-openai
+ (:include llm-standard-full-provider)
+ (:constructor make-llm-openai
+ (&key default-chat-temperature
+ default-chat-max-tokens
+ default-chat-non-standard-params
+ ((:key raw-key))
+ (chat-model "gpt-5.4-mini")
+ (embedding-model "text-embedding-3-small")
+ &aux
+ (key (llm-provider-utils--wrap-key raw-key)))))
"A structure for holding information needed by Open AI's API.
KEY is the API key for Open AI, which is required.
@@ -54,9 +64,20 @@ EMBEDDING-MODEL is the model to use for embeddings. If unset, it
will use a reasonable default."
key (chat-model "gpt-5.4-mini") (embedding-model "text-embedding-3-small"))
-(cl-defstruct (llm-openai-compatible (:include llm-openai
- (chat-model "unset")
- (embedding-model "unset")))
+(cl-defstruct (llm-openai-compatible
+ (:include llm-openai
+ (chat-model "unset")
+ (embedding-model "unset"))
+ (:constructor make-llm-openai-compatible
+ (&key default-chat-temperature
+ default-chat-max-tokens
+ default-chat-non-standard-params
+ ((:key raw-key))
+ (chat-model "unset")
+ (embedding-model "unset")
+ url
+ &aux
+ (key (llm-provider-utils--wrap-key raw-key)))))
"A structure for other APIs that use the Open AI's API.
URL is the URL to use for the API, up to the command. So, for
@@ -65,8 +86,19 @@ https://api.example.com/v1/chat, then URL should be
\"https://api.example.com/v1/\"."
url)
-(cl-defstruct (llm-openrouter (:include llm-openai-compatible
- (url "https://openrouter.ai/api/v1/")))
+(cl-defstruct (llm-openrouter
+ (:include llm-openai-compatible
+ (url "https://openrouter.ai/api/v1/"))
+ (:constructor make-llm-openrouter
+ (&key default-chat-temperature
+ default-chat-max-tokens
+ default-chat-non-standard-params
+ ((:key raw-key))
+ (chat-model "unset")
+ (embedding-model "unset")
+ (url "https://openrouter.ai/api/v1/")
+ &aux
+ (key (llm-provider-utils--wrap-key raw-key)))))
"A structure for Open Router.
This is mostly compatible with Open AI's API but has some minor API
diff --git a/llm-provider-utils.el b/llm-provider-utils.el
index fbfb245..088fdbd 100644
--- a/llm-provider-utils.el
+++ b/llm-provider-utils.el
@@ -73,6 +73,19 @@ NAME is the tool name.
ARG is an alist of arguments to their values."
id name args)
+(defun llm-provider-utils--wrap-key (key)
+ "Return KEY wrapped so provider structs do not print secret strings.
+Function values are already suitable because providers resolve them
+at request time."
+ (if (or (null key) (functionp key))
+ key
+ (let ((token (make-symbol "llm-key")))
+ (put token 'secret key)
+ (fset token
+ (lambda ()
+ (get token 'secret)))
+ token)))
+
;; Methods necessary for both embedding and chat requests.
(cl-defgeneric llm-provider-request-prelude (provider)
diff --git a/llm-test.el b/llm-test.el
index 4f0d2d6..8e2747a 100644
--- a/llm-test.el
+++ b/llm-test.el
@@ -96,6 +96,23 @@
(mapcar #'llm-test-normalize json-obj))))
(t json-obj)))
+(ert-deftest llm-test-provider-key-not-printed ()
+ (let* ((secret "llm-test-secret")
+ (provider (make-llm-openai-compatible
+ :url "http://127.0.0.1:8000/v1"
+ :chat-model "model"
+ :key secret))
+ (printed (prin1-to-string provider))
+ (key (llm-openai-compatible-key provider)))
+ (should (functionp key))
+ (should (symbolp key))
+ (should (equal secret (funcall key)))
+ (should-not (string-match-p (regexp-quote secret) printed))
+ (should-not (string-match-p (regexp-quote secret)
+ (prin1-to-string (symbol-function key))))
+ (should (equal `(("Authorization" . ,(format "Bearer %s" secret)))
+ (llm-provider-headers provider)))))
+
(defconst llm-test-chat-requests-to-responses
`((:name "Simple request"
:prompt (lambda () (llm-make-chat-prompt "Hello world"))