Skip to content

Add support for AWS Bedrock LLM integration #9

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
836 changes: 835 additions & 1 deletion Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[workspace]
resolver = "2"

members = ["llm", "llm-anthropic", "llm-grok", "llm-openai", "llm-openrouter"]
members = ["llm", "llm-anthropic", "llm-grok", "llm-openai", "llm-openrouter", "llm-bedrock"]

[profile.release]
debug = false
Expand Down
39 changes: 39 additions & 0 deletions Makefile.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,23 @@ install_crate = { crate_name = "cargo-component", version = "0.20.0" }
command = "cargo-component"
args = ["build", "-p", "golem-llm-openrouter", "--no-default-features"]

[tasks.build-bedrock]
install_crate = { crate_name = "cargo-component", version = "0.20.0" }
command = "cargo-component"
args = ["build", "-p", "golem-llm-bedrock"]

[tasks.build-bedrock-portable]
install_crate = { crate_name = "cargo-component", version = "0.20.0" }
command = "cargo-component"
args = ["build", "-p", "golem-llm-bedrock", "--no-default-features"]

[tasks.build]
dependencies = [
"build-anthropic",
"build-grok",
"build-openai",
"build-openrouter",
"build-bedrock",
]

[tasks.build-portable]
Expand All @@ -64,6 +75,7 @@ dependencies = [
"build-grok-portable",
"build-openai-portable",
"build-openrouter-portable",
"build-bedrock-portable",
]

[tasks.build-all]
Expand All @@ -78,6 +90,7 @@ cp target/wasm32-wasip1/debug/golem_llm_anthropic.wasm components/debug/golem_ll
cp target/wasm32-wasip1/debug/golem_llm_grok.wasm components/debug/golem_llm_grok.wasm
cp target/wasm32-wasip1/debug/golem_llm_openai.wasm components/debug/golem_llm_openai.wasm
cp target/wasm32-wasip1/debug/golem_llm_openrouter.wasm components/debug/golem_llm_openrouter.wasm
cp target/wasm32-wasip1/debug/golem_llm_bedrock.wasm components/debug/golem_llm_bedrock.wasm

cm_run_task clean
cm_run_task build-portable
Expand All @@ -86,6 +99,7 @@ cp target/wasm32-wasip1/debug/golem_llm_anthropic.wasm components/debug/golem_ll
cp target/wasm32-wasip1/debug/golem_llm_grok.wasm components/debug/golem_llm_grok-portable.wasm
cp target/wasm32-wasip1/debug/golem_llm_openai.wasm components/debug/golem_llm_openai-portable.wasm
cp target/wasm32-wasip1/debug/golem_llm_openrouter.wasm components/debug/golem_llm_openrouter-portable.wasm
cp target/wasm32-wasip1/debug/golem_llm_bedrock.wasm components/debug/golem_llm_bedrock-portable.wasm
'''

[tasks.release-build-anthropic]
Expand Down Expand Up @@ -140,12 +154,29 @@ args = [
"--no-default-features",
]

[tasks.release-build-bedrock]
install_crate = { crate_name = "cargo-component", version = "0.20.0" }
command = "cargo-component"
args = ["build", "-p", "golem-llm-bedrock", "--release"]

[tasks.release-build-bedrock-portable]
install_crate = { crate_name = "cargo-component", version = "0.20.0" }
command = "cargo-component"
args = [
"build",
"-p",
"golem-llm-bedrock",
"--release",
"--no-default-features",
]

[tasks.release-build]
dependencies = [
"release-build-anthropic",
"release-build-grok",
"release-build-openai",
"release-build-openrouter",
"release-build-bedrock",
]

[tasks.release-build-portable]
Expand All @@ -154,6 +185,7 @@ dependencies = [
"release-build-grok-portable",
"release-build-openai-portable",
"release-build-openrouter-portable",
"release-build-bedrock-portable",
]

[tasks.release-build-all]
Expand All @@ -170,6 +202,7 @@ cp target/wasm32-wasip1/release/golem_llm_anthropic.wasm components/release/gole
cp target/wasm32-wasip1/release/golem_llm_grok.wasm components/release/golem_llm_grok.wasm
cp target/wasm32-wasip1/release/golem_llm_openai.wasm components/release/golem_llm_openai.wasm
cp target/wasm32-wasip1/release/golem_llm_openrouter.wasm components/release/golem_llm_openrouter.wasm
cp target/wasm32-wasip1/release/golem_llm_bedrock.wasm components/release/golem_llm_bedrock.wasm

cm_run_task clean
cm_run_task release-build-portable
Expand All @@ -178,6 +211,7 @@ cp target/wasm32-wasip1/release/golem_llm_anthropic.wasm components/release/gole
cp target/wasm32-wasip1/release/golem_llm_grok.wasm components/release/golem_llm_grok-portable.wasm
cp target/wasm32-wasip1/release/golem_llm_openai.wasm components/release/golem_llm_openai-portable.wasm
cp target/wasm32-wasip1/release/golem_llm_openrouter.wasm components/release/golem_llm_openrouter-portable.wasm
cp target/wasm32-wasip1/release/golem_llm_bedrock.wasm components/release/golem_llm_bedrock-portable.wasm
'''

[tasks.wit-update]
Expand All @@ -197,6 +231,7 @@ dependencies = ["wit-update"]
# "llm-grok/wit/deps/golem-llm/golem-llm.wit",
# "llm-openai/wit/deps/golem-llm/golem-llm.wit",
# "llm-openrouter/wit/deps/golem-llm/golem-llm.wit",
# "llm-bedrock/wit/deps/golem-llm/golem-llm.wit",
#] } }

script_runner = "@duckscript"
Expand All @@ -221,6 +256,10 @@ rm -r llm-openrouter/wit/deps
mkdir llm-openrouter/wit/deps/golem-llm
cp wit/golem-llm.wit llm-openrouter/wit/deps/golem-llm/golem-llm.wit
cp wit/deps/wasi:io llm-openrouter/wit/deps
rm -r llm-bedrock/wit/deps
mkdir llm-bedrock/wit/deps/golem-llm
cp wit/golem-llm.wit llm-bedrock/wit/deps/golem-llm/golem-llm.wit
cp wit/deps/wasi:io llm-bedrock/wit/deps

rm -r test/wit
mkdir test/wit/deps/golem-llm
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ There are 8 published WASM files for each release:
| `golem-llm-grok.wasm` | LLM implementation for xAI (Grok), using custom Golem specific durability features |
| `golem-llm-openai.wasm` | LLM implementation for OpenAI, using custom Golem specific durability features |
| `golem-llm-openrouter.wasm` | LLM implementation for OpenRouter, using custom Golem specific durability features |
| `golem-llm-bedrock.wasm` | LLM implementation for AWS Bedrock, using custom Golem specific durability features |
| `golem-llm-anthropic-portable.wasm` | LLM implementation for Anthropic AI, with no Golem specific dependencies. |
| `golem-llm-grok-portable.wasm` | LLM implementation for xAI (Grok), with no Golem specific dependencies. |
| `golem-llm-openai-portable.wasm` | LLM implementation for OpenAI, with no Golem specific dependencies. |
| `golem-llm-openrouter-portable.wasm` | LLM implementation for OpenRouter, with no Golem specific dependencies. |
| `golem-llm-bedrock-portable.wasm` | LLM implementation for AWS Bedrock, with no Golem specific dependencies. |

Every component **exports** the same `golem:llm` interface, [defined here](wit/golem-llm.wit).

Expand All @@ -34,6 +36,7 @@ Each provider has to be configured with an API key passed as an environment vari
| Grok | `XAI_API_KEY` |
| OpenAI | `OPENAI_API_KEY` |
| OpenRouter | `OPENROUTER_API_KEY` |
| Bedrock | AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN (optional), AWS_REGION (or AWS_DEFAULT_REGION). Relies on standard AWS SDK credential chain. The region can also be set via provider-options in the Config with key AWS_REGION. |

Additionally, setting the `GOLEM_LLM_LOG=trace` environment variable enables trace logging for all the communication
with the underlying LLM provider.
Expand Down
55 changes: 55 additions & 0 deletions llm-bedrock/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
[package]
name = "golem-llm-bedrock"
version = "0.0.0"
edition = "2021"
license = "Apache-2.0"
homepage = "https://golem.cloud"
repository = "https://github.com/golemcloud/golem-llm"
description = "WebAssembly component for working with AWS Bedrock APIs, with special support for Golem Cloud"

[lib]
path = "src/lib.rs"
crate-type = ["cdylib"]

[features]
default = ["durability"]
durability = ["golem-rust/durability", "golem-llm/durability"]

[dependencies]
golem-llm = { path = "../llm", version = "0.0.0", default-features = false }
golem-rust = { workspace = true }
log = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
wit-bindgen-rt = { workspace = true }

# AWS SDK
aws-config = { version = "1.1.7", default-features = false, features = ["behavior-version-latest"] }
aws-sdk-bedrockruntime = { version = "1.16.0", default-features = false, features = ["rt-tokio"] }
aws-smithy-http = { version = "0.60.0" }
aws-smithy-runtime = { version = "1.1.7", features = ["client"] }
aws-types = { version = "1.1.1", default-features = false } # For Region

# Async utilities
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
async-trait = "0.1" # If needed for traits with async methods

# For Bedrock JSON structures and byte manipulation
bytes = "1.5"

[package.metadata.component]
package = "golem:llm-bedrock"

[package.metadata.component.bindings]
generate_unused_types = true

[package.metadata.component.bindings.with]
"golem:llm/llm@1.0.0" = "golem_llm::golem::llm::llm"
"wasi:io/poll@0.2.0" = "golem_rust::wasm_rpc::wasi::io::poll"

[package.metadata.component.target]
path = "wit"

[package.metadata.component.target.dependencies]
"golem:llm" = { path = "wit/deps/golem-llm" }
"wasi:io" = { path = "wit/deps/wasi:io"}
175 changes: 175 additions & 0 deletions llm-bedrock/src/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
use aws_config::meta::region::RegionProviderChain;
use aws_sdk_bedrockruntime::primitives::Blob;
use aws_sdk_bedrockruntime::Client as AwsBedrockClient;
use aws_sdk_bedrockruntime::error::ProvideErrorMetadata;
use aws_types::SdkConfig;
use golem_llm::golem::llm::llm::{Error, ErrorCode};
use log::{debug, error, trace};

pub struct BedrockClient {
client: AwsBedrockClient,
}

// Helper to construct SDK config using SmithyWasmClient
async fn new_bedrock_sdk_config(aws_region_opt: Option<String>) -> Result<SdkConfig, Error> {
let region_provider = RegionProviderChain::first_try(aws_region_opt.map(aws_types::region::Region::new))
.or_default_provider()
.or_else(aws_types::region::Region::new("us-east-1")); // Default fallback region

let sdk_config = aws_config::defaults(aws_config::BehaviorVersion::latest())
.region(region_provider)
.load()
.await;
Ok(sdk_config)
}

impl BedrockClient {
pub async fn new(aws_region_from_config: Option<String>) -> Result<Self, Error> {
debug!(
"Initializing BedrockClient with region from config: {:?}",
aws_region_from_config
);
match new_bedrock_sdk_config(aws_region_from_config).await {
Ok(sdk_config) => {
let client = AwsBedrockClient::new(&sdk_config);
Ok(Self { client })
}
Err(e) => {
error!("Failed to initialize Bedrock SDK config: {}", e.message);
Err(e)
}
}
}

pub async fn invoke_model(
&self,
model_id: String,
body: serde_json::Value,
accept: String,
content_type: String,
) -> Result<serde_json::Value, Error> {
trace!(
"Invoking Bedrock model. Model ID: {}, Content-Type: {}, Accept: {}, Body: {}",
model_id, content_type, accept, body
);

let body_blob = Blob::new(body.to_string());

let response = self
.client
.invoke_model()
.model_id(model_id)
.body(body_blob)
.content_type(content_type)
.accept(accept)
.send()
.await
.map_err(|sdk_err| {
let error_message = format!("Bedrock InvokeModel SDK error: {:?}", sdk_err);
error!("{}", error_message);
let provider_error_json = Some(error_message.clone());
let message = sdk_err.message().unwrap_or("Unknown Bedrock SDK error").to_string();

let code = match sdk_err.as_service_error() {
Some(err) => match err {
aws_sdk_bedrockruntime::operation::invoke_model::InvokeModelError::ValidationException(_) => ErrorCode::InvalidRequest,
aws_sdk_bedrockruntime::operation::invoke_model::InvokeModelError::AccessDeniedException(_) => ErrorCode::AuthenticationFailed,
aws_sdk_bedrockruntime::operation::invoke_model::InvokeModelError::ResourceNotFoundException(_) => ErrorCode::InvalidRequest,
aws_sdk_bedrockruntime::operation::invoke_model::InvokeModelError::ThrottlingException(_) => ErrorCode::RateLimitExceeded,
aws_sdk_bedrockruntime::operation::invoke_model::InvokeModelError::ServiceQuotaExceededException(_) => ErrorCode::RateLimitExceeded,
aws_sdk_bedrockruntime::operation::invoke_model::InvokeModelError::ModelTimeoutException(_) => ErrorCode::InternalError,
aws_sdk_bedrockruntime::operation::invoke_model::InvokeModelError::InternalServerException(_) => ErrorCode::InternalError,
aws_sdk_bedrockruntime::operation::invoke_model::InvokeModelError::ModelNotReadyException(_) => ErrorCode::Unsupported,
aws_sdk_bedrockruntime::operation::invoke_model::InvokeModelError::ModelErrorException(_) => ErrorCode::InternalError,
_ => ErrorCode::InternalError,
},
None => ErrorCode::InternalError,
};
Error {
code,
message,
provider_error_json,
}
})?;

let output_body_bytes = response.body.into_inner();
let output_body_str = std::str::from_utf8(&output_body_bytes).map_err(|e| {
let msg = format!("Failed to parse Bedrock response body as UTF-8: {e}");
error!("{}", msg);
Error {
code: ErrorCode::InternalError,
message: msg,
provider_error_json: None,
}
})?;

trace!("Received Bedrock response body: {}", output_body_str);

serde_json::from_str(output_body_str).map_err(|e| {
let msg = format!("Failed to parse Bedrock response JSON: {e}");
error!("{} Body was: {}", msg, output_body_str);
Error {
code: ErrorCode::InternalError,
message: msg,
provider_error_json: Some(output_body_str.to_string()),
}
})
}

pub async fn invoke_model_with_response_stream(
&self,
model_id: String,
body: serde_json::Value,
accept: String,
content_type: String,
) -> Result<
aws_sdk_bedrockruntime::operation::invoke_model_with_response_stream::InvokeModelWithResponseStreamOutput,
Error
> {
trace!(
"Invoking Bedrock model with response stream. Model ID: {}, Body: {}",
model_id, body
);
let body_blob = Blob::new(body.to_string());

self.client
.invoke_model_with_response_stream()
.model_id(model_id)
.body(body_blob)
.content_type(content_type)
.accept(accept)
.send()
.await
.map_err(|sdk_err| {
let error_message = format!(
"Bedrock InvokeModelWithResponseStream SDK error: {:?}",
sdk_err
);
error!("{}", error_message);
let provider_error_json = Some(error_message.clone());
let message = sdk_err.message().unwrap_or("Unknown Bedrock SDK error for stream").to_string();

let code = match sdk_err.as_service_error() {
Some(err) => match err {
aws_sdk_bedrockruntime::operation::invoke_model_with_response_stream::InvokeModelWithResponseStreamError::ValidationException(_) => ErrorCode::InvalidRequest,
aws_sdk_bedrockruntime::operation::invoke_model_with_response_stream::InvokeModelWithResponseStreamError::AccessDeniedException(_) => ErrorCode::AuthenticationFailed,
aws_sdk_bedrockruntime::operation::invoke_model_with_response_stream::InvokeModelWithResponseStreamError::ResourceNotFoundException(_) => ErrorCode::InvalidRequest,
aws_sdk_bedrockruntime::operation::invoke_model_with_response_stream::InvokeModelWithResponseStreamError::ThrottlingException(_) => ErrorCode::RateLimitExceeded,
aws_sdk_bedrockruntime::operation::invoke_model_with_response_stream::InvokeModelWithResponseStreamError::ServiceQuotaExceededException(_) => ErrorCode::RateLimitExceeded,
aws_sdk_bedrockruntime::operation::invoke_model_with_response_stream::InvokeModelWithResponseStreamError::ModelTimeoutException(_) => ErrorCode::InternalError,
aws_sdk_bedrockruntime::operation::invoke_model_with_response_stream::InvokeModelWithResponseStreamError::InternalServerException(_) => ErrorCode::InternalError,
aws_sdk_bedrockruntime::operation::invoke_model_with_response_stream::InvokeModelWithResponseStreamError::ModelNotReadyException(_) => ErrorCode::Unsupported,
aws_sdk_bedrockruntime::operation::invoke_model_with_response_stream::InvokeModelWithResponseStreamError::ModelErrorException(_) => ErrorCode::InternalError,
aws_sdk_bedrockruntime::operation::invoke_model_with_response_stream::InvokeModelWithResponseStreamError::ModelStreamErrorException(_) => ErrorCode::InternalError,
_ => ErrorCode::InternalError,
},
None => ErrorCode::InternalError,
};
Error {
code,
message,
provider_error_json,
}
})
}
}
Loading