#![expect(
clippy::print_stdout,
clippy::expect_used,
clippy::unwrap_used,
clippy::panic,
reason = "examples exist to demo the API and print results to the terminal; the workspace \
bans on print_*/unwrap/expect/panic target production code, not example binaries \
that use panic! to surface 'forgot to pull this model' setup errors loudly"
)]
use std::sync::Arc;
mod common;
use common::build_http_client;
use futures::StreamExt;
use modelplease::{
ContentPart, GenerateRequest, LanguageModelConfig, LanguageModelProvider, MediaSource,
MediaType, Message, ModelId, OllamaConfig, OllamaDeps, OllamaLanguageModel, ReasoningConfig,
ReasoningEffort, ResponseFormat, Role,
};
const SAMPLE_IMAGE_URL: &str =
"https://upload.wikimedia.org/wikipedia/en/7/7d/Lenna_%28test_image%29.png";
const SAMPLE_IMAGE_MIME: &str = "image/png";
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 client = Arc::new(build_http_client().expect("build reqwest client"));
let lm = OllamaLanguageModel::new(
OllamaDeps {
client: Arc::clone(&client),
},
OllamaConfig::default(),
);
let model = ModelId::new("gemma4:latest");
let vision_model = ModelId::new(
std::env::var("OLLAMA_VISION_MODEL").unwrap_or_else(|_| "llava:latest".to_owned()),
);
let config = LanguageModelConfig {
temperature: Some(0.0),
max_tokens: Some(8192),
reasoning: Some(ReasoningConfig::Off),
..Default::default()
};
println!("=== Non-streaming ===");
let messages = vec![
Message::system("You are a helpful assistant. Be concise."),
Message::user("What is the capital of France? Answer in one word."),
];
let request = GenerateRequest {
model: &model,
messages: &messages,
config: &config,
};
let r = lm.generate(request).await.expect("non-streaming generate");
println!("Content: {}", r.content);
assert!(
!r.content.is_empty(),
"non-streaming content must be non-empty"
);
println!("\n=== Streaming ===");
let request = GenerateRequest {
model: &model,
messages: &messages,
config: &config,
};
let mut stream = lm.generate_stream(request).await.expect("generate_stream");
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=== Structured: json_object ===");
let json_config = LanguageModelConfig {
response_format: ResponseFormat::JsonObject,
..config.clone()
};
let json_messages = vec![Message::user(
"Return a JSON object with fields \"name\" (string) and \"age\" (integer) for a fictional person.",
)];
let request = GenerateRequest {
model: &model,
messages: &json_messages,
config: &json_config,
};
let r = lm.generate(request).await.expect("json_object generate");
println!("Content: {}", r.content);
let parsed: serde_json::Value =
serde_json::from_str(&r.content).expect("json_object response must be valid JSON");
println!("Parsed: {parsed}");
println!("\n=== Structured: json_schema ===");
let schema_config = LanguageModelConfig {
response_format: ResponseFormat::JsonSchema {
name: "person".into(),
schema: serde_json::json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
}),
strict: true,
},
..config.clone()
};
let schema_messages = vec![Message::user("Create a fictional person.")];
let request = GenerateRequest {
model: &model,
messages: &schema_messages,
config: &schema_config,
};
let r = lm.generate(request).await.expect("json_schema generate");
println!("Content: {}", r.content);
let parsed: serde_json::Value =
serde_json::from_str(&r.content).expect("json_schema response must be valid JSON");
println!("Parsed: {parsed}");
println!(
"\n=== Vision (image bytes via {}) ===",
vision_model.as_str()
);
let vision_config = LanguageModelConfig {
temperature: Some(0.0),
max_tokens: Some(256),
..Default::default()
};
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: &vision_model,
messages: &bytes_messages,
config: &vision_config,
};
let r = lm.generate(request).await.unwrap_or_else(|e| {
panic!(
"vision generate failed (is `{}` pulled? `ollama pull {}`): {e}",
vision_model.as_str(),
vision_model.as_str()
)
});
println!("Content: {}", r.content);
assert!(!r.content.is_empty(), "vision content must be non-empty");
let reasoning_model = ModelId::new(
std::env::var("OLLAMA_REASONING_MODEL").unwrap_or_else(|_| "qwen3:8b".to_owned()),
);
println!(
"\n=== Reasoning (adaptive, effort=high) on {} ===",
reasoning_model.as_str()
);
let reasoning_config = LanguageModelConfig {
max_tokens: Some(8192),
reasoning: Some(ReasoningConfig::Adaptive {
effort: ReasoningEffort::High,
}),
..Default::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.unwrap_or_else(|e| {
panic!(
"reasoning generate failed (is `{}` pulled? `ollama pull {}`): {e}",
reasoning_model.as_str(),
reasoning_model.as_str()
)
});
if let Some(thinking) = &r.thinking {
println!("Thinking ({} 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 content must be non-empty");
}