-
-
Notifications
You must be signed in to change notification settings - Fork 37
Add AWS Bedrock Converse/ConverseStream provider support #255
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
CsBigDataHub
wants to merge
13
commits into
editor-code-assistant:master
Choose a base branch
from
CsBigDataHub:feature/support-aws-bedrock-provider
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 4 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
1b4346a
Add AWS Bedrock Converse/ConverseStream provider support
ChetanAtGNU 08948b1
fix(AWS_BEDROCK_EXAMPLE.md): update api to bedrock
ChetanAtGNU 6708ffb
Merge branch 'master' into feature/support-aws-bedrock-provider
ericdallo b837bf0
Merge branch 'master' into feature/support-aws-bedrock-provider
ericdallo 1264202
fix(aws-bedrock): enhance tool result parsing and streaming support
ChetanAtGNU 7bd6868
aws-bedrock-tests:add test for parsing event stream with tool calls
ChetanAtGNU df97a44
Merge branch 'upstream-master' into feature/support-aws-bedrock-provider
ChetanAtGNU 75d8ee3
feat(aws-bedrock):convert keyword values to strings in parsed events
ChetanAtGNU b213ebb
feat: Complete AWS Bedrock provider integration
ChetanAtGNU f7ad1a4
fix(AWS Bedrock): remove legacy placeholder support
ChetanAtGNU f3c7db4
Enhance AWS Bedrock documentation with comprehensive examples
ChetanAtGNU 5542b0b
docs: Clean up and enhance AWS Bedrock documentation
ChetanAtGNU b367d7e
Merge branch 'upstream-master' into feature/support-aws-bedrock-provider
ChetanAtGNU File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| # AWS Bedrock Provider for ECA | ||
|
|
||
| This document explains how to configure and use the AWS Bedrock provider in ECA. | ||
|
|
||
| ## Configuration | ||
|
|
||
| To use AWS Bedrock with ECA, you need to configure the provider in your ECA configuration file (`.eca/config.json`). | ||
|
|
||
| ### Basic Configuration | ||
|
|
||
| ```json | ||
| { | ||
| "providers": { | ||
| "bedrock": { | ||
| "api": "bedrock", | ||
| "key": "${env:BEDROCK_API_KEY}", | ||
| "url": "https://your-proxy.example.com/model/{modelId}/converse", | ||
| "region": "us-east-1", | ||
| "models": { | ||
| "claude-3-sonnet": { | ||
| "modelName": "anthropic.claude-3-sonnet-20240229-v1:0" | ||
| }, | ||
| "claude-3-opus": { | ||
| "modelName": "anthropic.claude-3-opus-20240229-v1:0" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Environment Variable Setup | ||
|
|
||
| Set your AWS Bedrock API key as an environment variable: | ||
|
|
||
| ```bash | ||
| export BEDROCK_API_KEY="your-api-key-here" | ||
| ``` | ||
|
|
||
| ## Usage | ||
|
|
||
| Once configured, you can use the AWS Bedrock provider like any other provider in ECA: | ||
|
|
||
| ### Basic Chat | ||
|
|
||
| ```clojure | ||
| (provider/request bedrock-config messages {:temperature 0.7}) | ||
| ``` | ||
|
|
||
| ### With Tools | ||
|
|
||
| ```clojure | ||
| (provider/request bedrock-config messages | ||
| {:tools [tool-spec] | ||
| :temperature 0.7 | ||
| :top_k 200}) | ||
| ``` | ||
|
|
||
| ### Streaming Responses | ||
|
|
||
| ```clojure | ||
| (provider/request bedrock-stream-config messages {:temperature 0.7}) | ||
| ``` | ||
|
|
||
| ## Supported Parameters | ||
|
|
||
| The AWS Bedrock provider supports the following parameters: | ||
|
|
||
| - `temperature`: Controls randomness (0.0 to 1.0) | ||
| - `top_k`: Number of top tokens to consider (default: 200) | ||
| - `max_tokens`: Maximum tokens to generate (default: 1024) | ||
| - `stopSequences`: Sequences that stop generation | ||
| - `tools`: Tool specifications for tool use | ||
|
|
||
| ## Authentication | ||
|
|
||
| This implementation uses Bearer token authentication via an external proxy that handles AWS SigV4 signing. The proxy should: | ||
|
|
||
| 1. Accept a Bearer token in the Authorization header | ||
| 2. Handle AWS SigV4 signing for the actual AWS Bedrock API calls | ||
| 3. Forward requests to the AWS Bedrock Converse API | ||
|
|
||
| ## Model Aliasing | ||
|
|
||
| You can use model aliases for convenience: | ||
|
|
||
| ```json | ||
| "models": { | ||
| "claude-3-sonnet": { | ||
| "modelName": "anthropic.claude-3-sonnet-20240229-v1:0" | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Then use `bedrock/claude-3-sonnet` as the model identifier. | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
| ### Common Issues | ||
|
|
||
| 1. **Authentication Errors**: Make sure your proxy is correctly configured and the API key is valid. | ||
| 2. **Model Not Found**: Verify that the model ID is correct and available in your AWS region. | ||
| 3. **Streaming Issues**: Ensure your proxy supports the ConverseStream API endpoint. | ||
|
|
||
| ### Debugging | ||
|
|
||
| Enable debug logging to see detailed request/response information: | ||
|
|
||
| ```bash | ||
| ECA_LOG_LEVEL=debug eca | ||
| ``` | ||
|
|
||
| ## References | ||
|
|
||
| - [AWS Bedrock Documentation](https://docs.aws.amazon.com/bedrock/) | ||
| - [AWS Bedrock Converse API](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html) | ||
| - [AWS Bedrock ConverseStream API](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ConverseStream.html) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,261 @@ | ||
| (ns eca.llm-providers.aws-bedrock | ||
| "AWS Bedrock provider implementation using Converse/ConverseStream APIs. | ||
|
|
||
| AUTHENTICATION: | ||
| This implementation uses Bearer token authentication, which requires | ||
| an external proxy/gateway that handles AWS SigV4 signing. | ||
|
|
||
| Set BEDROCK_API_KEY environment variable or configure :key in config.clj | ||
| with a token provided by your authentication proxy. | ||
|
|
||
| ENDPOINTS: | ||
| - Standard: https://your-proxy.com/model/{modelId}/converse | ||
| - Streaming: https://your-proxy.com/model/{modelId}/converse-stream | ||
|
|
||
| Configure the :url in your provider config to point to your proxy endpoint." | ||
| (:require | ||
| [cheshire.core :as json] | ||
| [clojure.string :as str] | ||
| [eca.logger :as logger] | ||
| [hato.client :as http]) | ||
| (:import (java.io DataInputStream BufferedInputStream ByteArrayInputStream))) | ||
|
|
||
| ;; --- Helper Functions --- | ||
|
|
||
| (defn resolve-model-id | ||
| "Resolve model ID from configuration." | ||
| [model-alias config] | ||
| (let [keyword-alias (keyword model-alias) | ||
| model-config (get-in config [:models keyword-alias])] | ||
| (or (:modelName model-config) | ||
| (name model-alias)))) | ||
|
|
||
| (defn format-tool-spec [tool] | ||
| (let [f (:function tool)] | ||
| {:toolSpec {:name (:name f) | ||
| :description (:description f) | ||
| ;; AWS requires inputSchema wrapped in "json" key | ||
| :inputSchema {:json (:parameters f)}}})) | ||
|
|
||
| (defn format-tool-config [tools] | ||
| (let [tools-seq (if (sequential? tools) tools [tools])] | ||
| (when (seq tools-seq) | ||
| {:tools (mapv format-tool-spec tools-seq)}))) | ||
|
|
||
| (defn parse-tool-result [content tool-call-id is-error?] | ||
| (let [inner-content (try | ||
| (if is-error? | ||
| [{:text (str content)}] | ||
| [{:json (json/parse-string content true)}]) | ||
| (catch Exception _ | ||
| [{:text (str content)}]))] | ||
| {:toolUseId tool-call-id | ||
| :content inner-content | ||
| :status (if is-error? "error" "success")})) | ||
|
|
||
| (defn message->bedrock [msg] | ||
| (case (:role msg) | ||
| "tool" | ||
|
||
| {:role "user" | ||
| :content [(parse-tool-result (:content msg) | ||
| (:tool_call_id msg) | ||
| (:error msg))]} | ||
|
|
||
| "assistant" | ||
| {:role "assistant" | ||
| :content (if (:tool_calls msg) | ||
| (mapv (fn [tc] | ||
| {:toolUse {:toolUseId (:id tc) | ||
| :name (get-in tc [:function :name]) | ||
| :input (json/parse-string | ||
| (get-in tc [:function :arguments]) keyword)}}) | ||
| (:tool_calls msg)) | ||
| [{:text (:content msg)}])} | ||
|
|
||
| ;; Default/User | ||
| {:role "user" | ||
| :content [{:text (:content msg)}]})) | ||
|
|
||
| (defn build-payload [messages options] | ||
| (let [system-prompts (filter #(= (:role %) "system") messages) | ||
| conversation (->> messages | ||
| (remove #(= (:role %) "system")) | ||
| (mapv message->bedrock)) | ||
| system-blocks (mapv (fn [m] {:text (:content m)}) system-prompts) | ||
|
|
||
| ;; Base inference config | ||
| base-config {:maxTokens (or (:max_tokens options) (:maxTokens options) 1024) | ||
| :temperature (or (:temperature options) 0.7) | ||
| :topP (or (:top_p options) (:topP options) 1.0)} | ||
|
|
||
| ;; Additional model-specific fields (e.g., top_k for Claude) | ||
| additional-fields (select-keys options [:top_k :topK])] | ||
|
|
||
| (cond-> {:messages conversation | ||
| :inferenceConfig (merge base-config | ||
| (select-keys options [:stopSequences]))} | ||
| (seq system-blocks) | ||
| (assoc :system system-blocks) | ||
|
|
||
| (:tools options) | ||
| (assoc :toolConfig (format-tool-config (:tools options))) | ||
|
|
||
| ;; Add additionalModelRequestFields if present | ||
| (seq additional-fields) | ||
| (assoc :additionalModelRequestFields | ||
| (into {} (map (fn [[k v]] [(name k) v]) additional-fields)))))) | ||
|
|
||
| (defn parse-bedrock-response [body] | ||
| (let [response (json/parse-string body true) | ||
| output-msg (get-in response [:output :message]) | ||
| stop-reason (:stopReason response) | ||
| content (:content output-msg) | ||
| usage (:usage response)] | ||
|
|
||
| ;; Log token usage if present | ||
| (when usage | ||
| (logger/debug "Token usage" {:input (:inputTokens usage) | ||
| :output (:outputTokens usage) | ||
| :total (:totalTokens usage)})) | ||
|
|
||
| (if (= stop-reason "tool_use") | ||
| (let [tool-blocks (filter :toolUse content) | ||
| tool-calls (mapv (fn [b] | ||
| (let [t (:toolUse b)] | ||
| {:id (:toolUseId t) | ||
| :type "function" | ||
| :function {:name (:name t) | ||
| :arguments (json/generate-string (:input t))}})) | ||
| tool-blocks)] | ||
| {:role "assistant" :content nil :tool_calls tool-calls}) | ||
|
|
||
| (let [text (-> (filter :text content) first :text)] | ||
| {:role "assistant" :content text})))) | ||
|
|
||
| ;; --- Binary Stream Parser --- | ||
|
|
||
| (defn parse-event-stream | ||
| "Parses AWS Event Stream (Binary format) from a raw InputStream. | ||
|
|
||
| AWS Event Stream Protocol: | ||
| - Prelude: Total Length (4) + Headers Length (4) | ||
| - Headers: Variable length | ||
| - Headers CRC: 4 bytes | ||
| - Payload: Variable length | ||
| - Message CRC: 4 bytes" | ||
| [^java.io.InputStream input-stream] | ||
| (let [dis (DataInputStream. (BufferedInputStream. input-stream))] | ||
| (lazy-seq | ||
| (try | ||
| ;; 1. Read Prelude (8 bytes, Big Endian) | ||
| (let [total-len (.readInt dis) | ||
| headers-len (.readInt dis)] | ||
|
|
||
| ;; 2. Read and skip headers | ||
| (when (> headers-len 0) | ||
| (let [header-bytes (byte-array headers-len)] | ||
| (.readFully dis header-bytes))) | ||
|
|
||
| ;; 3. Skip headers CRC (4 bytes) | ||
| (.skipBytes dis 4) | ||
|
|
||
| ;; 4. Calculate and read payload | ||
| ;; total-len = prelude(8) + headers + headers-crc(4) + payload + message-crc(4) | ||
| (let [payload-len (- total-len 8 headers-len 4 4) | ||
| payload-bytes (byte-array payload-len)] | ||
|
|
||
| (when (> payload-len 0) | ||
| (.readFully dis payload-bytes)) | ||
|
|
||
| ;; 5. Skip message CRC (4 bytes) | ||
| (.skipBytes dis 4) | ||
|
|
||
| ;; 6. Parse JSON payload if present | ||
| (if (> payload-len 0) | ||
| (let [payload-str (String. payload-bytes "UTF-8") | ||
| event (json/parse-string payload-str true)] | ||
| (cons event (parse-event-stream dis))) | ||
| ;; Empty payload (heartbeat), continue to next event | ||
| (parse-event-stream dis)))) | ||
|
|
||
| (catch java.io.EOFException _ nil) | ||
| (catch Exception e | ||
| (logger/debug "Stream parsing error" e) | ||
| nil))))) | ||
|
|
||
| (defn extract-text-deltas | ||
| "Takes the sequence of parsed JSON events and extracts text content. | ||
| Handles empty events (heartbeats) gracefully." | ||
| [events] | ||
| (vec (keep (fn [event] | ||
| (when-let [delta (get-in event [:contentBlockDelta :delta])] | ||
| (:text delta))) | ||
| events))) | ||
|
|
||
| ;; --- Endpoint Construction --- | ||
|
|
||
| (defn- build-endpoint | ||
| "Constructs the API endpoint URL with model ID interpolation." | ||
| [config model-id stream?] | ||
| (let [raw-url (:url config) | ||
| region (or (:region config) "us-east-1") | ||
| suffix (if stream? "converse-stream" "converse")] | ||
| (if raw-url | ||
| ;; Interpolate {modelId} in custom proxy URLs | ||
| (str/replace raw-url "{modelId}" model-id) | ||
| ;; Construct standard AWS URL | ||
| (format "https://bedrock-runtime.%s.amazonaws.com/model/%s/%s" | ||
| region model-id suffix)))) | ||
|
|
||
| ;; --- Public API Functions --- | ||
|
|
||
| (defn chat! [config callbacks] | ||
| (let [token (or (:key config) (System/getenv "BEDROCK_API_KEY")) | ||
| model-id (resolve-model-id (:model config) config) | ||
| endpoint (build-endpoint config model-id false) | ||
| timeout (or (:timeout config) 30000) | ||
| headers {"Authorization" (str "Bearer " token) | ||
| "Content-Type" "application/json"} | ||
| payload (build-payload (:user-messages config) (:extra-payload config)) | ||
|
|
||
| {:keys [status body error]} (http/post endpoint | ||
| {:headers headers | ||
| :body (json/generate-string payload) | ||
| :timeout timeout})] | ||
| (if (and (not error) (= 200 status)) | ||
| (let [response (parse-bedrock-response body) | ||
| {:keys [on-message-received on-error on-prepare-tool-call on-tools-called on-usage-updated]} callbacks] | ||
| (if-let [tool-calls (:tool_calls response)] | ||
| (do | ||
| (on-prepare-tool-call tool-calls) | ||
| {:tools-to-call tool-calls}) | ||
| (do | ||
| (on-message-received {:type :text :text (:content response)}) | ||
| {:output-text (:content response)}))) | ||
| (do | ||
| (logger/error "Bedrock API error" {:status status :error error :body body}) | ||
| (throw (ex-info "Bedrock API error" {:status status :body body})))))) | ||
|
|
||
| (defn stream-chat! [config callbacks] | ||
| (let [token (or (:key config) (System/getenv "BEDROCK_API_KEY")) | ||
| model-id (resolve-model-id (:model config) config) | ||
| endpoint (build-endpoint config model-id true) | ||
| timeout (or (:timeout config) 30000) | ||
| headers {"Authorization" (str "Bearer " token) | ||
| "Content-Type" "application/json"} | ||
| payload (build-payload (:user-messages config) (:extra-payload config)) | ||
|
|
||
| {:keys [status body error]} (http/post endpoint | ||
| {:headers headers | ||
| :body (json/generate-string payload) | ||
| :timeout timeout})] | ||
| (if (and (not error) (= 200 status)) | ||
| (let [{:keys [on-message-received on-error]} callbacks | ||
| events (parse-event-stream body) | ||
| texts (extract-text-deltas events)] | ||
| (doseq [text texts] | ||
| (on-message-received {:type :text :text text})) | ||
| {:output-text (str/join "" texts)}) | ||
| (do | ||
| (logger/error "Bedrock Stream API error" {:status status :error error}) | ||
| (throw (ex-info "Bedrock Stream API error" {:status status})))))) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this "external proxy" thing?
Won't the implementation work with standard Bedrock APIs, such as
https://bedrock-runtime.eu-west-2.amazonaws.com/model/<model_id>/converse?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will work, but you will need to get the token and configure it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I assumed that the user will create a Bedrock API token first.
But what's the difference when using proxy? Is that for cases when the user has configured their aws credentials on their machine but has no explicit Bedrock API token?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nothing really, except for the URL, endpoints will be the same.