#![expect(
clippy::print_stdout,
clippy::unwrap_used,
clippy::expect_used,
reason = "examples exist to demo the API and print results to the terminal; the workspace \
bans on print_*/unwrap/expect target production code, not example binaries"
)]
use std::sync::Arc;
mod common;
use common::build_http_client;
use futures::StreamExt;
use modelplease::{
AnthropicConfig, AnthropicDeps, AnthropicLanguageModel, ApiKey, ContentPart, GenerateRequest,
HttpsUrl, LanguageModelConfig, LanguageModelProvider, MediaSource, MediaType, Message, ModelId,
ReasoningConfig, ReasoningEffort, RetryConfig, Role,
};
const SAMPLE_IMAGE_URL: &str = "https://www.gstatic.com/webp/gallery/1.jpg";
const SAMPLE_IMAGE_MIME: &str = "image/jpeg";
const SAMPLE_IMAGE_USER_AGENT: &str =
"modelplease-example/0.1 (https://github.com/moderately-ai/modelplease)";
async fn fetch_sample_image(client: &reqwest::Client) -> (MediaType, Vec<u8>) {
let bytes = client
.get(SAMPLE_IMAGE_URL)
.header(reqwest::header::USER_AGENT, SAMPLE_IMAGE_USER_AGENT)
.send()
.await
.expect("fetch sample image")
.error_for_status()
.expect("sample image returned non-2xx")
.bytes()
.await
.expect("read sample image bytes")
.to_vec();
(MediaType::parse(SAMPLE_IMAGE_MIME).unwrap(), bytes)
}
#[tokio::main]
async fn main() {
let api_key = std::env::var("ANTHROPIC_API_KEY").expect("ANTHROPIC_API_KEY must be set");
let client = Arc::new(build_http_client().expect("build reqwest client"));
let lm = AnthropicLanguageModel::new(
AnthropicDeps {
client: Arc::clone(&client),
},
AnthropicConfig {
api_key: ApiKey::parse(api_key).unwrap(),
base_url: AnthropicConfig::DEFAULT_BASE_URL.to_owned(),
retry_config: RetryConfig::default(),
},
);
let model = ModelId::new("claude-sonnet-4-6");
let config = LanguageModelConfig {
temperature: Some(0.0),
max_tokens: Some(256),
..Default::default()
};
let messages = vec![
Message::system("You are a helpful assistant. Be concise."),
Message::user("What is the capital of France? Answer in one word."),
];
println!("=== Non-streaming (text) ===");
let request = GenerateRequest {
model: &model,
messages: &messages,
config: &config,
};
let r = lm
.generate(request)
.await
.expect("non-streaming text generate");
println!("Model: {:?}", r.model);
println!("Content: {}", r.content);
if let Some(u) = &r.usage {
println!(
"Usage: {} input, {} output tokens",
u.input_tokens, u.output_tokens
);
}
assert!(
!r.content.is_empty(),
"non-streaming text produced empty content"
);
println!("\n=== Streaming (text) ===");
let request = GenerateRequest {
model: &model,
messages: &messages,
config: &config,
};
let mut stream = lm
.generate_stream(request)
.await
.expect("streaming text generate");
print!("Content: ");
let mut got_any = false;
while let Some(result) = stream.next().await {
let delta = result.expect("streaming delta error");
print!("{}", delta.content);
if !delta.content.is_empty() {
got_any = true;
}
if delta.is_final {
println!(" [done]");
}
}
assert!(got_any, "streaming produced no content");
println!("\n=== Reasoning (adaptive, effort=high) ===");
let reasoning_model = ModelId::new(
std::env::var("ANTHROPIC_REASONING_MODEL")
.unwrap_or_else(|_| "claude-sonnet-4-6".to_owned()),
);
let reasoning_config = LanguageModelConfig {
max_tokens: Some(8192),
reasoning: Some(ReasoningConfig::Adaptive {
effort: ReasoningEffort::High,
}),
..LanguageModelConfig::default()
};
let reasoning_messages = vec![Message::user(
"If a train leaves Boston at 60 mph and another leaves NYC at 75 mph on the same \
track 200 miles apart, when do they meet? Show your reasoning step by step.",
)];
let request = GenerateRequest {
model: &reasoning_model,
messages: &reasoning_messages,
config: &reasoning_config,
};
let r = lm
.generate(request)
.await
.expect("reasoning adaptive generate");
println!("Model: {:?}", r.model);
if let Some(thinking) = &r.thinking {
println!(
"Thinking ({} chars): {}",
thinking.len(),
&thinking.chars().take(280).collect::<String>()
);
} else {
println!("Thinking: <none surfaced — model omitted or summary returned empty>");
}
println!("Content: {}", r.content);
if let Some(u) = &r.usage {
println!(
"Usage: {} input, {} output tokens",
u.input_tokens, u.output_tokens
);
}
assert!(
!r.content.is_empty(),
"reasoning adaptive call produced no visible content"
);
println!("\n=== Reasoning (manual, budget_tokens=4096) ===");
let manual_config = LanguageModelConfig {
max_tokens: Some(8192),
reasoning: Some(ReasoningConfig::Manual {
budget_tokens: 4096,
}),
..LanguageModelConfig::default()
};
let request = GenerateRequest {
model: &reasoning_model,
messages: &reasoning_messages,
config: &manual_config,
};
let r = lm
.generate(request)
.await
.expect("reasoning manual generate");
if let Some(thinking) = &r.thinking {
println!("Thinking length: {} chars", thinking.len());
}
println!("Content: {}", r.content);
if let Some(u) = &r.usage {
println!(
"Usage: {} input, {} output tokens",
u.input_tokens, u.output_tokens
);
}
assert!(
!r.content.is_empty(),
"reasoning manual call produced no visible content"
);
println!("\n=== Vision (image_url, MediaSource::Url) ===");
let url_messages = vec![
Message::system("You are a helpful assistant. Be concise."),
Message::with_parts(
Role::User,
vec![
ContentPart::text("Describe this image in one sentence."),
ContentPart::image(MediaSource::Url {
url: HttpsUrl::parse(SAMPLE_IMAGE_URL).unwrap(),
}),
],
),
];
let request = GenerateRequest {
model: &model,
messages: &url_messages,
config: &config,
};
let r = lm.generate(request).await.expect("vision url generate");
println!("Content: {}", r.content);
if let Some(u) = &r.usage {
println!(
"Usage: {} input, {} output tokens",
u.input_tokens, u.output_tokens
);
}
assert!(
!r.content.is_empty(),
"vision url content must be non-empty"
);
println!("\n=== Vision (image bytes, MediaSource::InlineBytes) ===");
let (mime, image_bytes) = fetch_sample_image(&client).await;
println!(
"Fetched {} bytes from {} ({})",
image_bytes.len(),
SAMPLE_IMAGE_URL,
mime.as_str()
);
let bytes_messages = vec![
Message::system("You are a helpful assistant. Be concise."),
Message::with_parts(
Role::User,
vec![
ContentPart::text("Describe this image in one sentence."),
ContentPart::image(MediaSource::InlineBytes {
mime,
data: image_bytes,
}),
],
),
];
let request = GenerateRequest {
model: &model,
messages: &bytes_messages,
config: &config,
};
let r = lm.generate(request).await.expect("vision bytes generate");
println!("Content: {}", r.content);
if let Some(u) = &r.usage {
println!(
"Usage: {} input, {} output tokens",
u.input_tokens, u.output_tokens
);
}
assert!(
!r.content.is_empty(),
"vision bytes content must be non-empty"
);
}