use std::sync::Arc;
use aws_config::BehaviorVersion;
use futures::StreamExt as _;
use modelplease::{
BedrockMantleAuth, BedrockMantleProvider, BedrockMantleProviderConfig,
BedrockMantleProviderDeps, GenerateRequest, LanguageModelConfig, LanguageModelProvider,
Message, ModelId, ReasoningConfig, ReasoningEffort, RetryConfig,
};
async fn make_provider() -> Result<BedrockMantleProvider, &'static str> {
let default_region = std::env::var("BEDROCK_MANTLE_DEFAULT_REGION")
.unwrap_or_else(|_| BedrockMantleProviderConfig::DEFAULT_REGION.to_owned());
let openai_gpt5_region = std::env::var("BEDROCK_MANTLE_OPENAI_GPT5_REGION")
.unwrap_or_else(|_| BedrockMantleProviderConfig::DEFAULT_OPENAI_GPT5_REGION.to_owned());
let anthropic_region = std::env::var("BEDROCK_MANTLE_ANTHROPIC_REGION")
.unwrap_or_else(|_| BedrockMantleProviderConfig::DEFAULT_ANTHROPIC_REGION.to_owned());
let sdk = aws_config::defaults(BehaviorVersion::latest())
.region(aws_config::Region::new(default_region.clone()))
.load()
.await;
let credentials_provider = sdk
.credentials_provider()
.ok_or("AWS credential chain produced no provider — run via aws-vault")?;
Ok(BedrockMantleProvider::new(
BedrockMantleProviderDeps {
client: Arc::new(reqwest::Client::new()),
auth: BedrockMantleAuth::Sigv4 {
credentials_provider,
},
},
BedrockMantleProviderConfig {
default_region,
openai_gpt5_region,
anthropic_region,
retry_config: RetryConfig::default(),
},
))
}
fn capital_question() -> Vec<Message> {
vec![
Message::system("You are a helpful assistant. Be concise."),
Message::user("What is the capital of France? Answer in one word."),
]
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "hits real AWS Bedrock Mantle; run via ... --run-ignored all"]
async fn bedrock_mantle_live_list_models_merges_per_region_catalogs() {
let provider = make_provider().await.expect("build mantle provider");
let models = provider.list_models().await.expect("list_models");
assert!(
models.len() >= 40,
"expected ≥40 merged models from Mantle catalog, got {}",
models.len()
);
let ids: std::collections::HashSet<String> =
models.iter().map(|m| m.id.as_str().to_owned()).collect();
assert!(
ids.contains("openai.gpt-oss-120b"),
"Chat Completions exemplar missing"
);
assert!(
ids.contains("anthropic.claude-haiku-4-5"),
"Messages exemplar missing"
);
assert!(ids.contains("openai.gpt-5.5"), "Responses exemplar missing");
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "hits real AWS Bedrock Mantle; run via ... --run-ignored all"]
async fn bedrock_mantle_live_chat_completions_returns_text() {
let provider = make_provider().await.expect("build mantle provider");
let model = ModelId::new("mistral.ministral-3-8b-instruct");
let config = LanguageModelConfig {
temperature: Some(0.0),
max_tokens: Some(32),
..Default::default()
};
let messages = capital_question();
let response = provider
.generate(GenerateRequest {
model: &model,
messages: &messages,
config: &config,
})
.await
.expect("generate");
assert!(
!response.content.is_empty(),
"empty response from Chat Completions"
);
assert!(
response.content.to_lowercase().contains("paris"),
"unexpected reply: {}",
response.content
);
let usage = response.usage.expect("usage missing");
assert!(usage.input_tokens > 0);
assert!(usage.output_tokens > 0);
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "hits real AWS Bedrock Mantle; run via ... --run-ignored all"]
async fn bedrock_mantle_live_chat_completions_streams_text() {
let provider = make_provider().await.expect("build mantle provider");
let model = ModelId::new("deepseek.v3.2");
let config = LanguageModelConfig {
temperature: Some(0.0),
max_tokens: Some(32),
..Default::default()
};
let messages = capital_question();
let mut stream = provider
.generate_stream(GenerateRequest {
model: &model,
messages: &messages,
config: &config,
})
.await
.expect("generate_stream");
let mut accumulated = String::new();
let mut final_usage = None;
while let Some(delta) = stream.next().await {
let delta = delta.expect("stream delta");
accumulated.push_str(&delta.content);
if delta.usage.is_some() {
final_usage = delta.usage;
}
if delta.is_final {
break;
}
}
assert!(
accumulated.to_lowercase().contains("paris"),
"unexpected streamed text: {accumulated}"
);
let usage = final_usage.expect("final usage chunk missing — Mantle didn't emit include_usage");
assert!(usage.input_tokens > 0);
assert!(usage.output_tokens > 0);
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "hits real AWS Bedrock Mantle; run via ... --run-ignored all"]
async fn bedrock_mantle_live_anthropic_messages_returns_text() {
let provider = make_provider().await.expect("build mantle provider");
let model = ModelId::new("anthropic.claude-haiku-4-5");
let config = LanguageModelConfig {
temperature: Some(0.0),
max_tokens: Some(32),
..Default::default()
};
let messages = capital_question();
let response = provider
.generate(GenerateRequest {
model: &model,
messages: &messages,
config: &config,
})
.await
.expect("generate");
assert!(
response.content.to_lowercase().contains("paris"),
"unexpected Messages reply: {}",
response.content
);
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "hits real AWS Bedrock Mantle; run via ... --run-ignored all"]
async fn bedrock_mantle_live_chat_completions_cache_attribution_probe() {
let provider = make_provider().await.expect("build mantle provider");
let model = ModelId::new(
std::env::var("BEDROCK_MANTLE_PROBE_MODEL").unwrap_or_else(|_| "zai.glm-4.7-flash".into()),
);
let config = LanguageModelConfig {
temperature: Some(0.0),
max_tokens: Some(16),
..Default::default()
};
let mut system_body =
String::from("You are a precise field extractor. Follow the rules below strictly.\n");
for i in 0..400 {
use std::fmt::Write as _;
let _ = writeln!(
system_body,
"Rule {i}: extract each requested value exactly and respond using the documented \
field markers, never inventing data that is not present in the input.",
);
}
let messages = vec![
Message::system(system_body),
Message::user("Reply with the single word: ok."),
];
let first = provider
.generate(GenerateRequest {
model: &model,
messages: &messages,
config: &config,
})
.await
.expect("generate call 1");
let u1 = first.usage.expect("call 1 usage");
let second = provider
.generate(GenerateRequest {
model: &model,
messages: &messages,
config: &config,
})
.await
.expect("generate call 2");
let u2 = second.usage.expect("call 2 usage");
for (label, u) in [("call 1", &u1), ("call 2", &u2)] {
if u.cache_read_input_tokens > 0 {
assert!(
u.input_tokens < u.cache_read_input_tokens,
"{label}: cache_read_input_tokens populated but input_tokens not normalized — \
wire mapping is dropping the subtraction. {u:?}",
);
}
}
#[allow(
clippy::print_stderr,
reason = "manual live probe reports upstream usage"
)]
{
eprintln!("bedrock_mantle chat-completions cache probe: call_1 = {u1:?}, call_2 = {u2:?}");
}
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "hits real AWS Bedrock Mantle; run via ... --run-ignored all"]
async fn bedrock_mantle_live_responses_reasoning_returns_text() {
let provider = make_provider().await.expect("build mantle provider");
let model = ModelId::new("openai.gpt-5.5");
let config = LanguageModelConfig {
max_tokens: Some(128),
reasoning: Some(ReasoningConfig::Adaptive {
effort: ReasoningEffort::Low,
}),
..Default::default()
};
let messages = capital_question();
let response = provider
.generate(GenerateRequest {
model: &model,
messages: &messages,
config: &config,
})
.await
.expect("generate");
assert!(
response.content.to_lowercase().contains("paris"),
"unexpected Responses reply: {}",
response.content
);
}