use async_trait::async_trait; use aws_sdk_bedrockruntime::Client as AwsBedrockClient; use galaxy_agent_core::{ AgentError, AgentErrorKind, AgentEventStream, AgentRuntime, RuntimeCapabilities, RuntimeDescriptor, RuntimeKind, TurnControl, TurnRequest, }; use rig_bedrock::client::Client as RigBedrockClient; use rig_bedrock::completion::CompletionModel; use rig_core::client::CompletionClient; use rig_core::completion::CompletionRequest; use crate::request::build_completion_request; use crate::stream::start_model_turn; const INFERENCE_PROFILE_PREFIXES: &[&str] = &["us.", "eu.", "apac.", "jp.", "au.", "global."]; #[derive(Clone, Debug, PartialEq, Eq)] pub struct BedrockRigConfig { pub model: String, pub region: String, pub cross_region_inference: bool, pub prompt_caching: bool, pub max_output_tokens: Option, } /// A Rig Bedrock client built from Galaxy's already-resolved AWS SDK client. /// /// Credential/profile/SSO resolution remains in Galaxy's explicit Bedrock /// configuration boundary. Rig receives the resulting SDK client and owns the /// Converse request/stream conversion from that point onward. #[derive(Clone)] pub struct BedrockRuntime { client: RigBedrockClient, config: BedrockRigConfig, resolved_model: String, descriptor: RuntimeDescriptor, } impl BedrockRuntime { pub fn from_aws_client( client: AwsBedrockClient, config: BedrockRigConfig, ) -> Result { let resolved_model = resolve_bedrock_model_id(&config.model, &config.region, config.cross_region_inference)?; let descriptor = RuntimeDescriptor { id: format!("rig-bedrock:{resolved_model}"), display_name: format!("Rig / Bedrock / {resolved_model}"), kind: RuntimeKind::Provider, capabilities: RuntimeCapabilities::provider(), }; Ok(Self { client: RigBedrockClient::from(client), config, resolved_model, descriptor, }) } pub fn resolved_model(&self) -> &str { &self.resolved_model } pub fn completion_model(&self) -> CompletionModel { let model = self.client.completion_model(&self.resolved_model); if self.config.prompt_caching { model.with_prompt_caching() } else { model } } } #[async_trait] impl AgentRuntime for BedrockRuntime { fn descriptor(&self) -> &RuntimeDescriptor { &self.descriptor } async fn start_turn( &self, request: TurnRequest, control: TurnControl, ) -> Result { let max_output_tokens = request.max_output_tokens.or(self.config.max_output_tokens); let mut completion_request = build_bedrock_completion_request(request, self.config.max_output_tokens)?; // Context markers and inference-profile expansion are Galaxy model // configuration, not identifiers that Rig should send unchanged. completion_request.model = Some(self.resolved_model.clone()); start_model_turn( self.completion_model(), completion_request, control, max_output_tokens, ) .await } } pub fn build_bedrock_completion_request( request: TurnRequest, configured_max_output_tokens: Option, ) -> Result { build_completion_request(request, configured_max_output_tokens, true, true, None) } pub fn resolve_bedrock_model_id( configured_model: &str, region: &str, cross_region_inference: bool, ) -> Result { let model = strip_context_marker(configured_model.trim()); if model.is_empty() { return Err(AgentError::new( AgentErrorKind::Configuration, "Bedrock model ID is empty", )); } if !cross_region_inference || model.starts_with("arn:") || INFERENCE_PROFILE_PREFIXES .iter() .any(|prefix| model.starts_with(prefix)) { return Ok(model.to_string()); } let prefix = inference_profile_prefix(region); Ok(prefix .map(|prefix| format!("{prefix}.{model}")) .unwrap_or_else(|| model.to_string())) } fn strip_context_marker(model: &str) -> &str { model .get(..model.len().saturating_sub(4)) .filter(|_| model.ends_with("[1m]") || model.ends_with("[1M]")) .unwrap_or(model) } fn inference_profile_prefix(region: &str) -> Option<&'static str> { match region { region if region.starts_with("us-") || region.starts_with("ca-") => Some("us"), region if region.starts_with("eu-") || region == "il-central-1" => Some("eu"), "ap-northeast-1" | "ap-northeast-3" => Some("jp"), "ap-southeast-2" | "ap-southeast-4" | "ap-southeast-6" => Some("au"), region if region.starts_with("ap-") => Some("apac"), _ => None, } } #[cfg(test)] #[path = "bedrock_tests.rs"] mod tests;