branch: elpa/gptel
commit bcdf2038271a2744b52ec009873a7ea314c15b27
Author: akṣaya śrīnivāsan <aks...@vakra.xyz>
Commit: Karthik Chikmagalur <karthikchikmaga...@gmail.com>

    gptel-bedrock: Get secret/token from AWS profile
    
    * gptel-bedrock.el (gptel-bedrock)
    (gptel-bedrock--aws-profile-cache)
    (gptel-bedrock--fetch-aws-profile-credentials)
    (gptel-bedrock--get-credentials, gptel-bedrock--get-model-id)
    (gptel-make-bedrock):
    - Add caching of AWS profile creds
    - Handle AWS model-region for provisioned models
    - Use token-expiration for caching
---
 gptel-bedrock.el | 68 +++++++++++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 57 insertions(+), 11 deletions(-)

diff --git a/gptel-bedrock.el b/gptel-bedrock.el
index 3fab6d3ead..570ac2a0c4 100644
--- a/gptel-bedrock.el
+++ b/gptel-bedrock.el
@@ -33,7 +33,8 @@
 
 (cl-defstruct (gptel-bedrock (:constructor gptel--make-bedrock)
                              (:copier nil)
-                             (:include gptel-backend)))
+                             (:include gptel-backend))
+             model-region)
 
 (defconst gptel-bedrock--prompt-type
   ;; For documentation purposes only -- this describes the type of prompt 
objects that get passed
@@ -481,6 +482,41 @@ conversation."
                       :content [(:text ,(plist-get tool-call :result))])))
      tool-use-requests))))
 
+(defvar gptel-bedrock--aws-profile-cache nil
+  "Cache for AWS profile credentials in the form of (PROFILE . CREDS).")
+
+(defmacro gptel-bedrock--alist-get! (key alist &optional default force-updatep 
testfn)
+  (let ((key-sym (gensym "key")))
+    (gv-letplace (getter setter) alist
+      `(let ((,key-sym ,key))
+        (or (and (not ,force-updatep) (cdr (assoc ,key-sym ,getter ,@(if 
testfn `(,testfn)))))
+            ,@(when default
+                `((let ((new-val ,default))
+                    ,(funcall setter `(cons (cons ,key-sym new-val) ,getter))
+                    new-val))))))))
+
+(defun gptel-bedrock--fetch-aws-profile-credentials (profile &optional 
clear-cachep)
+  "Fetch & cache AWS credentials for PROFILE using aws-cli."
+  (let* ((creds-json
+          (gptel-bedrock--alist-get! profile gptel-bedrock--aws-profile-cache
+             (with-temp-buffer
+                 (unless (zerop (call-process "aws" nil t nil "configure" 
"export-credentials"
+                                              (format "--profile=%s" profile)))
+                   (user-error "Failed to get AWS credentials from profile"))
+               (json-parse-string (buffer-string)))
+             clear-cachep #'string=))
+        (expiration (if-let (exp (gethash "Expiration" creds-json))
+                            (date-to-time exp))))
+    (cond
+      ((time-less-p (current-time) expiration)
+       (let ((access-key (gethash "AccessKeyId" creds-json))
+            (secret-key (gethash "SecretAccessKey" creds-json))
+            (session-token (gethash "SessionToken" creds-json)))
+        (cl-values access-key secret-key session-token)))
+      ((not clear-cachep)
+       (gptel-bedrock--fetch-aws-profile-credentials profile t))
+      (t (user-error "AWS credentials expired for profile: %s" profile)))))
+
 (defun gptel-bedrock--get-credentials ()
   "Return the AWS credentials to use for the request.
 
@@ -491,12 +527,12 @@ AWS_SESSION_TOKEN).
 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")))
+        (token (getenv "AWS_SESSION_TOKEN"))
+       (profile (getenv "AWS_PROFILE")))
     (cond
-     ((and key-id secret-key token) (cl-values key-id secret-key token))
-     ((and key-id secret-key) (cl-values key-id secret-key))
-     ;; TODO: Add support for more credential sources
-     (t (user-error "Missing AWS credentials; currently only environment 
variables are supported")))))
+      ((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")))))
 
 (defvar gptel-bedrock--model-ids
   ;; https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html
@@ -531,10 +567,17 @@ IDs can be added or replaced by calling
     (cl-remove-if-not (lambda (model) (memq (car model) known-ids)) 
gptel--anthropic-models))
   "List of available AWS Bedrock models and associated properties.")
 
-(defun gptel-bedrock--get-model-id (model)
-  "Return the Bedrock model ID for MODEL."
-  (or (alist-get model gptel-bedrock--model-ids nil nil #'eq)
-      (error "Unknown Bedrock model: %s" model)))
+(defun gptel-bedrock--get-model-id (model &optional region)
+  "Return the Bedrock model ID for MODEL.
+
+REGION is one of apac, eu or us."
+  (concat
+   (when region
+     (or (member region '(apac eu us))
+        (error "Unknown Bedrock region %s" region))
+     (concat (symbol-name region) "."))
+   (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."
@@ -553,6 +596,7 @@ IDs can be added or replaced by calling
     (name &key
           region
           (models gptel--bedrock-models)
+         (model-region nil)
           (stream nil)
          curl-args
           (protocol "https"))
@@ -562,6 +606,7 @@ 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
 STREAM - Whether to use streaming responses or not."
   (declare (indent 1))
   (let ((host (format "bedrock-runtime.%s.amazonaws.com" region)))
@@ -571,6 +616,7 @@ STREAM - Whether to use streaming responses or not."
            :host host
            :header nil           ; x-amz-security-token is set in curl-args if 
needed
            :models (gptel--process-models models)
+          :model-region model-region
            :protocol protocol
            :endpoint "" ; Url is dynamically constructed based on other args
            :stream stream
@@ -579,7 +625,7 @@ STREAM - Whether to use streaming responses or not."
            :url
            (lambda ()
              (concat protocol "://" host
-                     "/model/" (gptel-bedrock--get-model-id gptel-model)
+                     "/model/" (gptel-bedrock--get-model-id gptel-model 
model-region)
                      "/" (if stream "converse-stream" "converse")))
            ))))
 

Reply via email to