Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@
For logistical reasons, the =gptel-request= library will continue to
be shipped with =gptel=.

- New user option ~gptel-context~: This variable can be used to specify
additional context sources for gptel queries, usually files or
buffers. It serves the longstanding requests of enabling buffer-local
context specification, as well as context specification in gptel
presets and programmatic gptel use. Each entry is a file path or a
buffer object, but other kinds of specification are possible. See its
documentation for details.

- ~gptel-mcp-connect~ can now start MCP servers synchronously. This
is useful for scripting purposes, when MCP tools need to be
available before performing other actions. One common use is
Expand All @@ -58,6 +66,11 @@
context. This setting can be controlled via the user option
~gptel-context-restrict-to-project-files~.

- ~gptel-make-bedrock~ now checks for the ~AWS_BEARER_TOKEN_BEDROCK~ environment
variable parameter and uses it for Bedrock API key based authentication if
present. See
https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html.

* 0.9.9 2025-08-02

** Breaking changes
Expand Down
15 changes: 11 additions & 4 deletions README.org
Original file line number Diff line number Diff line change
Expand Up @@ -1113,10 +1113,15 @@ Register a backend with
:model-region 'apac)
#+end_src

The Bedrock backend gets your AWS credentials from the environment variables. It expects to find either
~AWS_ACCESS_KEY_ID~, ~AWS_SECRET_ACCESS_KEY~, ~AWS_SESSION_TOKEN~ (optional), or if present, can use ~AWS_PROFILE~ to get these directly from the ~aws~ cli.

NOTE: The Bedrock backend needs curl >= 8.5 in order for the sigv4 signing to work properly,
AWS has numerous credential provisions; we follow this order precedence,
- (argument) ~:aws-bearer-token~
- (env. variable) ~AWS_BEARER_TOKEN_BEDROCK~
- (argument) ~:aws-profile~
If this option is specified, the Bedrock-backend uses the shared AWS config and credentials files to obtain credentials based on the AWS Profile selected. If ~:aws-profile~ is set to the keyword ~:static~, the IAM credentials are imported without a profile argument.
- (env. varible) ~AWS_PROFILE~
- (env. varible) ~AWS_ACCESS_KEY_ID~, ~AWS_SECRET_ACCESS_KEY~ and ~AWS_SESSION_TOKEN~

NOTE: Unless ~AWS_BEARER_TOKEN_BEDROCK~ token is used, the Bedrock backend needs curl >= 8.9 in order for the sigv4 signing to work properly,
https://github.com/curl/curl/issues/11794

An error will be signalled if ~gptel-curl~ is ~NIL~.
Expand All @@ -1134,6 +1139,8 @@ The above code makes the backend available to select. If you want it to be the
(gptel-make-bedrock "AWS"
;; optionally enable streaming
:stream t
;; optionally specify the aws profile
;; :profile
:region "ap-northeast-1"
;; subset of gptel--bedrock-models
:models '(claude-sonnet-4-20250514)
Expand Down
41 changes: 14 additions & 27 deletions gptel-anthropic.el
Original file line number Diff line number Diff line change
Expand Up @@ -480,34 +480,21 @@ format."
into parts-array
finally return (vconcat parts-array)))

(cl-defmethod gptel--wrap-user-prompt ((_backend gptel-anthropic) prompts
&optional inject-media)
"Wrap the last user prompt in PROMPTS with the context string.

If INJECT-MEDIA is non-nil wrap it with base64-encoded media
files in the context."
(if inject-media
;; Wrap the first user prompt with included media files/contexts
(when-let* ((media-list (gptel-context--collect-media)))
(cl-callf (lambda (current)
(vconcat
(gptel--anthropic-parse-multipart media-list)
(cl-typecase current
(string `((:type "text" :text ,current)))
(vector current)
(t current))))
(plist-get (car prompts) :content)))
;; Wrap the last user prompt with included text contexts
(cl-defmethod gptel--inject-media ((_backend gptel-anthropic) 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)
(cl-etypecase current
(string (gptel-context--wrap current))
(vector (if-let* ((wrapped (gptel-context--wrap nil)))
(vconcat `((:type "text" :text ,wrapped))
current)
current))))
(plist-get (car (last prompts)) :content))))

;; (if-let* ((context-string (gptel-context--string gptel-context--alist)))
(vconcat
(gptel--anthropic-parse-multipart media-list)
(cl-typecase current
(string `((:type "text" :text ,current)))
(vector current)
(t current))))
(plist-get (car prompts) :content))))

;; (if-let* ((context-string (gptel-context--string gptel-context)))
;; (cl-callf (lambda (previous)
;; (cl-typecase previous
;; (string (concat context-string previous))
Expand Down
94 changes: 48 additions & 46 deletions gptel-bedrock.el
Original file line number Diff line number Diff line change
Expand Up @@ -136,32 +136,14 @@ Assumes this is a conversation with alternating roles."
(list :role (if role "user" "assistant")
:content `[(:text ,text)])))

(cl-defmethod gptel--wrap-user-prompt ((_backend gptel-bedrock) prompts &optional inject-media)
"Inject context into a conversation.

PROMPTS is list of prompt objects. If INJECT-MEDIA is non-nil
inject the media files from context into the beginning of the
conversation; otherwise inject the context into the last prompt."
(if inject-media
(gptel-bedrock--inject-media-context prompts)
(gptel-bedrock--inject-text-context prompts)))

(defun gptel-bedrock--inject-media-context (prompts)
"Inject media files from context into a conversation.
Media files will be added at the beginning of the conversation.
PROMPTS should be a non-empty list of prompt objects."
(cl-defmethod gptel--inject-media ((_backend gptel-bedrock) 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-callf2 vconcat (gptel-bedrock--parse-multipart media-list)
(plist-get (car prompts) :content))))

(defun gptel-bedrock--inject-text-context (prompts)
"Inject text context into the last prompt object from a conversation.
PROMPTS should be a non-empty list of prompt objects."
(cl-assert prompts nil "Expected a non-empty list of prompts")
(when-let* ((wrapped (gptel-context--wrap nil)))
(cl-callf2 vconcat `[(:text ,wrapped)]
(plist-get (car (last prompts)) :content))))

(defvar-local gptel-bedrock--stream-cursor nil
"Marker to indicate last point parsed.")

Expand Down Expand Up @@ -499,15 +481,18 @@ conversation."
(defun gptel-bedrock--fetch-aws-profile-credentials (profile &optional clear-cache)
"Fetch & cache AWS credentials for PROFILE using aws-cli.

If PROFILE is the keyword ':static', then it fetches IAM credentials
from the aws-cli without any profile argument.

Non-nil CLEAR-CACHE will refresh credentials."
(let* ((creds-json
(let ((cell (or (assoc profile gptel-bedrock--aws-profile-cache #'string=)
(car (push (cons profile nil) gptel-bedrock--aws-profile-cache)))))
(or (and (not clear-cache) (cdr cell))
(setf (cdr cell)
(with-temp-buffer
(unless (zerop (call-process "aws" nil t nil "configure" "export-credentials"
(format "--profile=%s" profile)))
(unless (zerop (apply #'call-process "aws" 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)))))))
(expiration (if-let (exp (gethash "Expiration" creds-json))
Expand All @@ -522,9 +507,16 @@ Non-nil CLEAR-CACHE will refresh credentials."
(gptel-bedrock--fetch-aws-profile-credentials profile t))
(t (user-error "AWS credentials expired for profile: %s" profile)))))

(defun gptel-bedrock--get-credentials ()
(defun gptel-bedrock--get-credentials (profile)
"Return the AWS credentials to use for the request.

If credentials are not available based on the AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN environment variables,
aws configure export-credentials is used to obtain credentials.
PROFILE specifies the AWS profile to use for retrieving
credentials. If PROFILE is unset, AWS_PROFILE environment
variable is used.

Returns a list of 2-3 elements, depending on whether a session
token is needed, with this form: (AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY
AWS_SESSION_TOKEN).
Expand All @@ -533,11 +525,11 @@ Convenient to use with `cl-multiple-value-bind'"
(let ((key-id (getenv "AWS_ACCESS_KEY_ID"))
(secret-key (getenv "AWS_SECRET_ACCESS_KEY"))
(token (getenv "AWS_SESSION_TOKEN"))
(profile (getenv "AWS_PROFILE")))
(profile (or profile (getenv "AWS_PROFILE"))))
(cond
((and key-id secret-key) (cl-values key-id secret-key token))
((and profile) (gptel-bedrock--fetch-aws-profile-credentials profile))
(t (user-error "Missing AWS credentials; currently only environment variables are supported")))))
((and key-id secret-key) (cl-values key-id secret-key token))
(t (user-error "Missing AWS credentials; provide them either via environment variables or specify PROFILE when calling gptel-make-bedrock")))))

(defvar gptel-bedrock--model-ids
;; https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html
Expand Down Expand Up @@ -586,19 +578,25 @@ REGION is one of apac, eu or us."
(or (alist-get model gptel-bedrock--model-ids nil nil #'eq)
(error "Unknown Bedrock model: %s" model))))

(defun gptel-bedrock--curl-args (region)
"Generate the curl arguments to get a bedrock request signed for use in REGION."
;; https://curl.se/docs/manpage.html#--aws-sigv4
(cl-multiple-value-bind (key-id secret token) (gptel-bedrock--get-credentials)
(nconc
(list
"--user" (format "%s:%s" key-id secret)
"--aws-sigv4" (format "aws:amz:%s:bedrock" region))
(unless (memq system-type '(windows-nt ms-dos))
;; Without this curl swallows the output
(list "--output" "/dev/stdout"))
(when token
(list (format "-Hx-amz-security-token: %s" token))))))
(defun gptel-bedrock--curl-args (region profile bearer-token)
"Generate the curl arguments to get a bedrock request signed for use in REGION.

PROFILE specifies the aws profile to use for aws configure
export-credentials. BEARER-TOKEN is the token used for authentication."
(let ((bearer-token (or bearer-token (getenv "AWS_BEARER_TOKEN_BEDROCK")))
(output-args (unless (memq system-type '(windows-nt ms-dos))
'("--output" "/dev/stdout"))))
(if bearer-token
(append
(list "-H" (format "Authorization: Bearer %s" bearer-token))
output-args)
(cl-multiple-value-bind (key-id secret token) (gptel-bedrock--get-credentials profile)
(append
(list "--user" (format "%s:%s" key-id secret)
"--aws-sigv4" (format "aws:amz:%s:bedrock" region))
output-args
(when token (list "-H" (format "x-amz-security-token: %s" token))))))))


(defun gptel-bedrock--curl-version ()
"Check Curl version required for gptel-bedrock."
Expand All @@ -614,6 +612,7 @@ REGION is one of apac, eu or us."
(models gptel--bedrock-models)
(model-region nil)
stream curl-args request-params
aws-profile aws-bearer-token
(protocol "https"))
"Register an AWS Bedrock backend for gptel with NAME.

Expand All @@ -622,27 +621,30 @@ Keyword arguments:
REGION - AWS region name (e.g. \"us-east-1\")
MODELS - The list of models supported by this backend
MODEL-REGION - one of apac, eu, us or nil
AWS-PROFILE - the aws profile to use for aws configure export-credentials
AWS-BEARER-TOKEN - the aws bearer-token for authenticating with AWS
CURL-ARGS - additional curl args
STREAM - Whether to use streaming responses or not.
REQUEST-PARAMS - a plist of additional HTTP request
parameters (as plist keys) and values supported by the API."
(declare (indent 1))
(unless (and gptel-use-curl (version<= "8.9" (gptel-bedrock--curl-version)))
(error "Bedrock-backend requires curl >= 8.9, but gptel-use-curl := %s, curl-version := %s"
gptel-use-curl (gptel-bedrock--curl-version)))
(unless (or aws-bearer-token (getenv "AWS_BEARER_TOKEN_BEDROCK"))
(unless (and gptel-use-curl (version<= "8.9" (gptel-bedrock--curl-version)))
(error "Bedrock-backend requires curl >= 8.9, but gptel-use-curl := %s, curl-version := %s"
gptel-use-curl (gptel-bedrock--curl-version))))
(let ((host (format "bedrock-runtime.%s.amazonaws.com" region)))
(setf (alist-get name gptel--known-backends nil nil #'equal)
(gptel--make-bedrock
:name name
:host host
:header nil ; x-amz-security-token is set in curl-args if needed
:models (gptel--process-models models)
:model-region model-region
:model-region model-region
:protocol protocol
:endpoint "" ; Url is dynamically constructed based on other args
:stream stream
:coding-system (and stream 'binary)
:curl-args (lambda () (append curl-args (gptel-bedrock--curl-args region)))
:curl-args (lambda () (append curl-args (gptel-bedrock--curl-args region aws-profile aws-bearer-token)))
:request-params request-params
:url
(lambda ()
Expand Down
Loading