diff --git a/Cargo.lock b/Cargo.lock index afe45ce7..643183aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -753,7 +753,7 @@ checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] name = "dspy-rs" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "arrow", @@ -783,7 +783,7 @@ dependencies = [ [[package]] name = "dsrs_macros" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "dspy-rs", diff --git a/README.md b/README.md index e193d9e3..d0ad05d0 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,10 @@ Add DSRs to your `Cargo.toml`: ```toml [dependencies] # Option 1: Use the shorter alias (recommended) -dsrs = { package = "dspy-rs", version = "0.7.0" } +dsrs = { package = "dspy-rs", version = "0.7.1" } # Option 2: Use the full name -dspy-rs = "0.7.0" +dspy-rs = "0.7.1" ``` Or use cargo: @@ -64,17 +64,15 @@ struct SentimentAnalyzer { #[tokio::main] async fn main() -> Result<()> { - let lm = LM::builder() - .api_key(std::env::var("OPENAI_API_KEY")?.into()) - .config( - LMConfig::builder() - .model("gpt-4.1-nano".to_string()) - .temperature(0.5) - .build(), - ) - .build(); - - configure(lm, ChatAdapter); + // API key automatically read from OPENAI_API_KEY env var + configure( + LM::builder() + .model("gpt-4o-mini".to_string()) + .temperature(0.5) + .build() + .await?, + ChatAdapter, + ); // Create a predictor let predictor = Predict::new(SentimentAnalyzer::new()); @@ -154,13 +152,20 @@ let predict = Predict::new(MySignature::new()); #### 4. **Language Models** - Configurable LM Backends ```rust -// Configure with OpenAI +// Configure with OpenAI (API key read from OPENAI_API_KEY env var) let lm = LM::builder() - .api_key(secret_key) - .model("gpt-4") + .model("gpt-4o-mini".to_string()) .temperature(0.7) .max_tokens(1000) - .build(); + .build() + .await?; + +// For local models (e.g., vLLM, Ollama) +let lm = LM::builder() + .base_url("http://localhost:11434".to_string()) + .model("llama3".to_string()) + .build() + .await?; ``` #### 5. **Evaluation** - Evaluating your Modules diff --git a/crates/dspy-rs/Cargo.toml b/crates/dspy-rs/Cargo.toml index 687ae44c..e03ce733 100644 --- a/crates/dspy-rs/Cargo.toml +++ b/crates/dspy-rs/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "dspy-rs" authors = ["Herumb Shandilya "] -version = "0.7.0" +version = "0.7.1" edition = "2024" description = "A DSPy rewrite(not port) to Rust." readme = "../../README.md" @@ -25,7 +25,7 @@ tokio = { version = "1.46.1", features = ["full"] } async-trait = "0.1.83" anyhow = "1.0.99" bon = "3.7.0" -dsrs_macros = { version = "0.7.0", path = "../dsrs-macros" } +dsrs_macros = { version = "0.7.1", path = "../dsrs-macros" } csv = { version = "1.3.1" } hf-hub = { version = "0.4.3", features = ["tokio"] } parquet = { version = "56.1.0" } diff --git a/crates/dspy-rs/examples/01-simple.rs b/crates/dspy-rs/examples/01-simple.rs index d37710c7..5de26c1f 100644 --- a/crates/dspy-rs/examples/01-simple.rs +++ b/crates/dspy-rs/examples/01-simple.rs @@ -10,8 +10,8 @@ cargo run --example 01-simple use anyhow::Result; use bon::Builder; use dspy_rs::{ - ChatAdapter, Example, LM, LMConfig, Module, Predict, Prediction, Predictor, Signature, - configure, example, prediction, + ChatAdapter, Example, LM, Module, Predict, Prediction, Predictor, Signature, configure, + example, prediction, }; #[Signature(cot)] @@ -70,11 +70,11 @@ impl Module for QARater { #[tokio::main] async fn main() -> Result<()> { configure( - LM::new(LMConfig { - model: "openai:gpt-4o-mini".to_string(), - ..LMConfig::default() - }) - .await, + LM::builder() + .model("openai:gpt-4o-mini".to_string()) + .build() + .await + .unwrap(), ChatAdapter, ); diff --git a/crates/dspy-rs/examples/03-evaluate-hotpotqa.rs b/crates/dspy-rs/examples/03-evaluate-hotpotqa.rs index 89c3de4b..5798195d 100644 --- a/crates/dspy-rs/examples/03-evaluate-hotpotqa.rs +++ b/crates/dspy-rs/examples/03-evaluate-hotpotqa.rs @@ -62,7 +62,14 @@ impl Evaluator for QARater { #[tokio::main] async fn main() -> anyhow::Result<()> { - configure(LM::default(), ChatAdapter {}); + configure( + LM::builder() + .model("openai:gpt-4o-mini".to_string()) + .build() + .await + .unwrap(), + ChatAdapter {}, + ); let examples = DataLoader::load_hf( "hotpotqa/hotpot_qa", diff --git a/crates/dspy-rs/examples/04-optimize-hotpotqa.rs b/crates/dspy-rs/examples/04-optimize-hotpotqa.rs index 913ccbcd..be3f56b3 100644 --- a/crates/dspy-rs/examples/04-optimize-hotpotqa.rs +++ b/crates/dspy-rs/examples/04-optimize-hotpotqa.rs @@ -58,7 +58,14 @@ impl Evaluator for QARater { #[tokio::main] async fn main() -> anyhow::Result<()> { - configure(LM::default(), ChatAdapter {}); + configure( + LM::builder() + .model("openai:gpt-4o-mini".to_string()) + .build() + .await + .unwrap(), + ChatAdapter {}, + ); let examples = DataLoader::load_hf( "hotpotqa/hotpot_qa", diff --git a/crates/dspy-rs/examples/05-heterogenous-examples.rs b/crates/dspy-rs/examples/05-heterogenous-examples.rs index 43799ee5..a013f704 100644 --- a/crates/dspy-rs/examples/05-heterogenous-examples.rs +++ b/crates/dspy-rs/examples/05-heterogenous-examples.rs @@ -11,7 +11,14 @@ use dspy_rs::{ChatAdapter, LM, Predict, Predictor, configure, example, sign}; #[tokio::main] async fn main() -> anyhow::Result<()> { - configure(LM::default(), ChatAdapter {}); + configure( + LM::builder() + .model("openai:gpt-4o-mini".to_string()) + .build() + .await + .unwrap(), + ChatAdapter {}, + ); let exp = example! { "number": "input" => 10, diff --git a/crates/dspy-rs/examples/06-other-providers-batch.rs b/crates/dspy-rs/examples/06-other-providers-batch.rs index e70cca2c..45e25fec 100644 --- a/crates/dspy-rs/examples/06-other-providers-batch.rs +++ b/crates/dspy-rs/examples/06-other-providers-batch.rs @@ -10,8 +10,8 @@ cargo run --example 01-simple use anyhow::Result; use bon::Builder; use dspy_rs::{ - ChatAdapter, Example, LM, LMConfig, Module, Predict, Prediction, Predictor, Signature, - configure, example, hashmap, prediction, + ChatAdapter, Example, LM, Module, Predict, Prediction, Predictor, Signature, configure, + example, hashmap, prediction, }; #[Signature(cot)] @@ -77,11 +77,11 @@ impl Module for QARater { async fn main() { // Anthropic configure( - LM::new(LMConfig { - model: "anthropic:claude-sonnet-4-5-20250929".to_string(), - ..LMConfig::default() - }) - .await, + LM::builder() + .model("anthropic:claude-sonnet-4-5-20250929".to_string()) + .build() + .await + .unwrap(), ChatAdapter, ); @@ -103,11 +103,11 @@ async fn main() { // Gemini configure( - LM::new(LMConfig { - model: "gemini:gemini-2.0-flash".to_string(), - ..LMConfig::default() - }) - .await, + LM::builder() + .model("gemini:gemini-2.0-flash".to_string()) + .build() + .await + .unwrap(), ChatAdapter, ); diff --git a/crates/dspy-rs/examples/07-inspect-history.rs b/crates/dspy-rs/examples/07-inspect-history.rs index c9f01ad6..62cf1a9e 100644 --- a/crates/dspy-rs/examples/07-inspect-history.rs +++ b/crates/dspy-rs/examples/07-inspect-history.rs @@ -28,7 +28,11 @@ impl Module for QARater { #[tokio::main] async fn main() { - let lm = LM::default(); + let lm = LM::builder() + .model("openai:gpt-4o-mini".to_string()) + .build() + .await + .unwrap(); configure(lm, ChatAdapter); let example = example! { diff --git a/crates/dspy-rs/examples/09-gepa-sentiment.rs b/crates/dspy-rs/examples/09-gepa-sentiment.rs index 04916e13..a92a7926 100644 --- a/crates/dspy-rs/examples/09-gepa-sentiment.rs +++ b/crates/dspy-rs/examples/09-gepa-sentiment.rs @@ -117,11 +117,7 @@ async fn main() -> Result<()> { println!("GEPA Sentiment Analysis Optimization Example\n"); // Setup LM - let lm = LM::new(LMConfig { - temperature: 0.7, - ..LMConfig::default() - }) - .await; + let lm = LM::builder().temperature(0.7).build().await.unwrap(); configure(lm.clone(), ChatAdapter); diff --git a/crates/dspy-rs/examples/10-gepa-llm-judge.rs b/crates/dspy-rs/examples/10-gepa-llm-judge.rs index cb8ae440..fda4c924 100644 --- a/crates/dspy-rs/examples/10-gepa-llm-judge.rs +++ b/crates/dspy-rs/examples/10-gepa-llm-judge.rs @@ -223,18 +223,10 @@ async fn main() -> Result<()> { // Setup: Configure the LLM // Main LM for the task - let task_lm = LM::new(LMConfig { - temperature: 0.7, - ..LMConfig::default() - }) - .await; + let task_lm = LM::builder().temperature(0.7).build().await.unwrap(); // Judge LM (could use a different/cheaper model) - let judge_lm = LM::new(LMConfig { - temperature: 0.3, - ..LMConfig::default() - }) - .await; + let judge_lm = LM::builder().temperature(0.3).build().await.unwrap(); configure(task_lm, ChatAdapter); diff --git a/crates/dspy-rs/src/adapter/chat.rs b/crates/dspy-rs/src/adapter/chat.rs index 13187632..2a46a646 100644 --- a/crates/dspy-rs/src/adapter/chat.rs +++ b/crates/dspy-rs/src/adapter/chat.rs @@ -283,7 +283,7 @@ impl Adapter for ChatAdapter { inputs: Example, ) -> Result { // Check cache first (release lock immediately after checking) - if lm.config.cache + if lm.cache && let Some(cache) = lm.cache_handler.as_ref() { let cache_key = inputs.clone(); @@ -304,7 +304,7 @@ impl Adapter for ChatAdapter { }; // Store in cache if enabled - if lm.config.cache + if lm.cache && let Some(cache) = lm.cache_handler.as_ref() { let (tx, rx) = tokio::sync::mpsc::channel(1); diff --git a/crates/dspy-rs/src/core/lm/client_registry.rs b/crates/dspy-rs/src/core/lm/client_registry.rs index 640a7ce4..533017c2 100644 --- a/crates/dspy-rs/src/core/lm/client_registry.rs +++ b/crates/dspy-rs/src/core/lm/client_registry.rs @@ -3,8 +3,9 @@ use enum_dispatch::enum_dispatch; use reqwest; use rig::{ completion::{CompletionError, CompletionRequest, CompletionResponse}, - providers::{anthropic, cohere, gemini, groq, openai, perplexity}, + providers::{anthropic, gemini, groq, ollama, openai, openrouter}, }; +use std::borrow::Cow; #[enum_dispatch] #[allow(async_fn_in_trait)] @@ -19,11 +20,11 @@ pub trait CompletionProvider { #[derive(Clone)] pub enum LMClient { OpenAI(openai::completion::CompletionModel), - Anthropic(anthropic::completion::CompletionModel), - Cohere(cohere::completion::CompletionModel), Gemini(gemini::completion::CompletionModel), + Anthropic(anthropic::completion::CompletionModel), Groq(groq::CompletionModel), - Perplexity(perplexity::CompletionModel), + OpenRouter(openrouter::completion::CompletionModel), + Ollama(ollama::CompletionModel), } // Implement the trait for each concrete provider type using the CompletionModel trait from rig @@ -56,7 +57,7 @@ impl CompletionProvider for anthropic::completion::CompletionModel { } } -impl CompletionProvider for cohere::completion::CompletionModel { +impl CompletionProvider for gemini::completion::CompletionModel { async fn completion( &self, request: CompletionRequest, @@ -70,7 +71,7 @@ impl CompletionProvider for cohere::completion::CompletionModel { } } -impl CompletionProvider for gemini::completion::CompletionModel { +impl CompletionProvider for groq::CompletionModel { async fn completion( &self, request: CompletionRequest, @@ -84,7 +85,7 @@ impl CompletionProvider for gemini::completion::CompletionModel { } } -impl CompletionProvider for groq::CompletionModel { +impl CompletionProvider for openrouter::completion::CompletionModel { async fn completion( &self, request: CompletionRequest, @@ -98,7 +99,7 @@ impl CompletionProvider for groq::CompletionModel { } } -impl CompletionProvider for perplexity::CompletionModel { +impl CompletionProvider for ollama::CompletionModel { async fn completion( &self, request: CompletionRequest, @@ -113,66 +114,83 @@ impl CompletionProvider for perplexity::CompletionModel { } impl LMClient { - pub fn from_model_string(model_str: &str) -> Result { - let parts: Vec<&str> = model_str.split(':').collect(); - if parts.len() != 2 { - anyhow::bail!("Model string must be in format 'provider:model_name'"); + fn get_api_key<'a>(provided: Option<&'a str>, env_var: &str) -> Result> { + match provided { + Some(k) => Ok(Cow::Borrowed(k)), + None => Ok(Cow::Owned(std::env::var(env_var).map_err(|_| { + anyhow::anyhow!("{} environment variable not set", env_var) + })?)), } + } + + /// Build case 1: OpenAI-compatible API from base_url + api_key + pub fn from_openai_compatible(base_url: &str, api_key: &str, model: &str) -> Result { + let client = openai::ClientBuilder::new(api_key) + .base_url(base_url) + .build(); + Ok(LMClient::OpenAI(openai::completion::CompletionModel::new( + client, model, + ))) + } - let provider = parts[0]; - let model_id = parts[1]; + /// Build case 2: Local OpenAI-compatible model from base_url (vLLM, etc.) + /// Uses a dummy API key since local servers don't require authentication + pub fn from_local(base_url: &str, model: &str) -> Result { + let client = openai::ClientBuilder::new("dummy-key-for-local-server") + .base_url(base_url) + .build(); + Ok(LMClient::OpenAI(openai::completion::CompletionModel::new( + client, model, + ))) + } + + /// Build case 3: From provider via model name (provider:model format) + pub fn from_model_string(model_str: &str, api_key: Option<&str>) -> Result { + let (provider, model_id) = model_str.split_once(':').ok_or(anyhow::anyhow!( + "Model string must be in format 'provider:model_name'" + ))?; match provider { "openai" => { - let api_key = std::env::var("OPENAI_API_KEY") - .map_err(|_| anyhow::anyhow!("OPENAI_API_KEY environment variable not set"))?; - let client = openai::ClientBuilder::new(&api_key).build(); + let key = Self::get_api_key(api_key, "OPENAI_API_KEY")?; + let client = openai::ClientBuilder::new(&key).build(); Ok(LMClient::OpenAI(openai::completion::CompletionModel::new( client, model_id, ))) } "anthropic" => { - let api_key = std::env::var("ANTHROPIC_API_KEY").map_err(|_| { - anyhow::anyhow!("ANTHROPIC_API_KEY environment variable not set") - })?; - let client = anthropic::ClientBuilder::new(&api_key).build()?; + let key = Self::get_api_key(api_key, "ANTHROPIC_API_KEY")?; + let client = anthropic::ClientBuilder::new(&key).build()?; Ok(LMClient::Anthropic( anthropic::completion::CompletionModel::new(client, model_id), )) } - "cohere" => { - let api_key = std::env::var("COHERE_API_KEY") - .map_err(|_| anyhow::anyhow!("COHERE_API_KEY environment variable not set"))?; - let client = cohere::client::ClientBuilder::new(&api_key).build(); - Ok(LMClient::Cohere(cohere::completion::CompletionModel::new( - client, model_id, - ))) - } "gemini" => { - let api_key = std::env::var("GEMINI_API_KEY") - .map_err(|_| anyhow::anyhow!("GEMINI_API_KEY environment variable not set"))?; - let client = - gemini::client::ClientBuilder::::new(&api_key).build()?; + let key = Self::get_api_key(api_key, "GEMINI_API_KEY")?; + let client = gemini::client::ClientBuilder::::new(&key).build()?; Ok(LMClient::Gemini(gemini::completion::CompletionModel::new( client, model_id, ))) } - "groq" => { - let api_key = std::env::var("GROQ_API_KEY") - .map_err(|_| anyhow::anyhow!("GROQ_API_KEY environment variable not set"))?; - let client = groq::ClientBuilder::new(&api_key).build(); - Ok(LMClient::Groq(groq::CompletionModel::new(client, model_id))) - } - "perplexity" => { - let api_key = std::env::var("PERPLEXITY_API_KEY").map_err(|_| { - anyhow::anyhow!("PERPLEXITY_API_KEY environment variable not set") - })?; - let client = perplexity::ClientBuilder::new(&api_key).build(); - Ok(LMClient::Perplexity(perplexity::CompletionModel::new( + "ollama" => { + let client = ollama::ClientBuilder::new().build(); + Ok(LMClient::Ollama(ollama::CompletionModel::new( client, model_id, ))) } - _ => anyhow::bail!("Unsupported provider: {}", provider), + "openrouter" => { + let key = Self::get_api_key(api_key, "OPENROUTER_API_KEY")?; + let client = openrouter::ClientBuilder::new(&key).build(); + Ok(LMClient::OpenRouter( + openrouter::completion::CompletionModel::new(client, model_id), + )) + } + _ => { + anyhow::bail!( + "Unsupported provider: {}. Supported providers are: openai, anthropic, gemini, groq, openrouter, ollama", + provider + ); + } } } } diff --git a/crates/dspy-rs/src/core/lm/config.rs b/crates/dspy-rs/src/core/lm/config.rs deleted file mode 100644 index 16819ee4..00000000 --- a/crates/dspy-rs/src/core/lm/config.rs +++ /dev/null @@ -1,21 +0,0 @@ -use bon::Builder; - -/// Tunable inference parameters applied to each [`LM::call`]. -#[derive(Clone, Debug, Builder)] -pub struct LMConfig { - #[builder(default = "openai:gpt-4o-mini".to_string())] - pub model: String, - /// Sampling temperature. Higher values increase randomness. - #[builder(default = 0.7)] - pub temperature: f32, - #[builder(default = 512)] - pub max_tokens: u32, - #[builder(default = true)] - pub cache: bool, -} - -impl Default for LMConfig { - fn default() -> Self { - LMConfig::builder().build() - } -} diff --git a/crates/dspy-rs/src/core/lm/mod.rs b/crates/dspy-rs/src/core/lm/mod.rs index d2c59a34..e7b4adc1 100644 --- a/crates/dspy-rs/src/core/lm/mod.rs +++ b/crates/dspy-rs/src/core/lm/mod.rs @@ -1,11 +1,9 @@ pub mod chat; pub mod client_registry; -pub mod config; pub mod usage; pub use chat::*; pub use client_registry::*; -pub use config::*; pub use usage::*; use anyhow::Result; @@ -27,55 +25,111 @@ pub struct LMResponse { pub chat: Chat, } +#[derive(Builder)] +#[builder(finish_fn(vis = "", name = __internal_build))] pub struct LM { - pub config: LMConfig, - client: Arc, + pub base_url: Option, + pub api_key: Option, + #[builder(default = "openai:gpt-4o-mini".to_string())] + pub model: String, + #[builder(default = 0.7)] + pub temperature: f32, + #[builder(default = 512)] + pub max_tokens: u32, + #[builder(default = true)] + pub cache: bool, pub cache_handler: Option>>, + #[builder(skip)] + client: Option>, } impl Default for LM { fn default() -> Self { - // Use a blocking tokio runtime to call the async new function - tokio::runtime::Runtime::new() - .expect("Failed to create tokio runtime") - .block_on(Self::new(LMConfig::default())) + tokio::runtime::Handle::current().block_on(async { Self::builder().build().await.unwrap() }) } } impl Clone for LM { fn clone(&self) -> Self { Self { - config: self.config.clone(), - client: self.client.clone(), + base_url: self.base_url.clone(), + api_key: self.api_key.clone(), + model: self.model.clone(), + temperature: self.temperature, + max_tokens: self.max_tokens, + cache: self.cache, cache_handler: self.cache_handler.clone(), + client: self.client.clone(), } } } impl LM { - /// Creates a new LM with the given configuration. - /// Uses enum dispatch for optimal runtime performance. + /// Finalizes construction of an [`LM`], initializing the HTTP client and + /// optional response cache based on provided parameters. /// - /// This is an async function because it initializes the cache handler when - /// `config.cache` is `true`. For synchronous contexts where cache initialization - /// is not needed, use `new_sync` instead. - pub async fn new(config: LMConfig) -> Self { - let client = LMClient::from_model_string(&config.model) - .expect("Failed to create client from model string"); - - let cache_handler = if config.cache { - Some(Arc::new(Mutex::new(ResponseCache::new().await))) - } else { - None + /// Supports 3 build cases: + /// 1. OpenAI-compatible with auth: `base_url` + `api_key` provided + /// → Uses OpenAI client with custom base URL + /// 2. Local OpenAI-compatible: `base_url` only (no `api_key`) + /// → Uses OpenAI client for vLLM/local servers (dummy key) + /// 3. Provider via model string: no `base_url`, model in "provider:model" format + /// → Uses provider-specific client (openai, anthropic, gemini, etc.) + async fn initialize_client(mut self) -> Result { + // Determine which build case based on what's provided + let client = match (&self.base_url, &self.api_key, &self.model) { + // Case 1: OpenAI-compatible with authentication (base_url + api_key) + // For custom OpenAI-compatible APIs that require API keys + (Some(base_url), Some(api_key), _) => Arc::new(LMClient::from_openai_compatible( + base_url, + api_key, + &self.model, + )?), + // Case 2: Local OpenAI-compatible server (base_url only, no api_key) + // For vLLM, text-generation-inference, and other local OpenAI-compatible servers + (Some(base_url), None, _) => Arc::new(LMClient::from_local(base_url, &self.model)?), + // Case 3: Provider via model string (no base_url, model in "provider:model" format) + // Uses provider-specific clients + (None, api_key, model) if model.contains(':') => { + Arc::new(LMClient::from_model_string(model, api_key.as_deref())?) + } + // Default case: assume OpenAI provider if no colon in model name + (None, api_key, model) => { + let model_str = if model.contains(':') { + model.to_string() + } else { + format!("openai:{}", model) + }; + Arc::new(LMClient::from_model_string(&model_str, api_key.as_deref())?) + } }; - Self { - config, - client: Arc::new(client), - cache_handler, + self.client = Some(client); + + // Initialize cache if enabled + if self.cache && self.cache_handler.is_none() { + self.cache_handler = Some(Arc::new(Mutex::new(ResponseCache::new().await))); } + + Ok(self) } +} +// Implement build() for all builder states since optional fields don't require setting +impl LMBuilder { + /// Builds the LM instance with proper client initialization + /// + /// Supports 3 build cases: + /// 1. OpenAI-compatible with auth: `base_url` + `api_key` provided + /// 2. Local OpenAI-compatible: `base_url` only (for vLLM, etc.) + /// 3. Provider via model string: model in "provider:model" format + pub async fn build(self) -> Result { + let lm = self.__internal_build(); + lm.initialize_client().await + } +} + +impl LM { /// Executes a chat completion against the configured provider. /// /// `messages` must already be formatted as OpenAI-compatible chat turns. @@ -100,14 +154,21 @@ impl LM { }, documents: Vec::new(), tools: Vec::new(), - temperature: Some(self.config.temperature as f64), - max_tokens: Some(self.config.max_tokens as u64), + temperature: Some(self.temperature as f64), + max_tokens: Some(self.max_tokens as u64), tool_choice: None, additional_params: None, }; // Execute the completion using enum dispatch (zero-cost abstraction) - let response = self.client.completion(request).await?; + let response = self + .client + .as_ref() + .ok_or_else(|| { + anyhow::anyhow!("LM client not initialized. Call build() on LMBuilder.") + })? + .completion(request) + .await?; let first_choice = match response.choice.first() { AssistantContent::Text(text) => Message::assistant(&text.text), @@ -152,9 +213,12 @@ pub struct DummyLM { pub api_key: String, #[builder(default = "https://api.openai.com/v1".to_string())] pub base_url: String, - /// Static configuration applied to stubbed responses. - #[builder(default = LMConfig::default())] - pub config: LMConfig, + #[builder(default = 0.7)] + pub temperature: f32, + #[builder(default = 512)] + pub max_tokens: u32, + #[builder(default = true)] + pub cache: bool, /// Cache backing storage shared with the real implementation. pub cache_handler: Option>>, } @@ -166,7 +230,9 @@ impl DummyLM { Self { api_key: "".into(), base_url: "https://api.openai.com/v1".to_string(), - config: LMConfig::default(), + temperature: 0.7, + max_tokens: 512, + cache: true, cache_handler: Some(cache_handler), } } @@ -186,7 +252,7 @@ impl DummyLM { content: prediction.clone(), }); - if self.config.cache + if self.cache && let Some(cache) = self.cache_handler.as_ref() { let (tx, rx) = tokio::sync::mpsc::channel(1); diff --git a/crates/dspy-rs/src/optimizer/copro.rs b/crates/dspy-rs/src/optimizer/copro.rs index c57c46df..1cc3c233 100644 --- a/crates/dspy-rs/src/optimizer/copro.rs +++ b/crates/dspy-rs/src/optimizer/copro.rs @@ -126,7 +126,7 @@ impl Optimizer for COPRO { for _ in 0..self.breadth - 1 { let inst = basic_instruction.clone(); if let Some(mut prompt_model) = self.prompt_model.clone() { - prompt_model.config.temperature = self.init_temperature; + prompt_model.temperature = self.init_temperature; futures.push(Box::pin(async move { BASIC_GENERATOR .forward_with_config( @@ -348,7 +348,7 @@ impl Optimizer for COPRO { // Generate new candidates let results = if let Some(mut prompt_model) = self.prompt_model.clone() { - prompt_model.config.temperature = self.init_temperature; + prompt_model.temperature = self.init_temperature; let attempts = attempts_str.clone(); REFINEMENT_GENERATOR diff --git a/crates/dspy-rs/src/optimizer/gepa.rs b/crates/dspy-rs/src/optimizer/gepa.rs index 857e9fbc..42307ff3 100644 --- a/crates/dspy-rs/src/optimizer/gepa.rs +++ b/crates/dspy-rs/src/optimizer/gepa.rs @@ -325,7 +325,7 @@ impl GEPA { }; let reflection_output = if let Some(mut prompt_model) = self.prompt_model.clone() { - prompt_model.config.temperature = self.temperature; + prompt_model.temperature = self.temperature; reflect_predictor .forward_with_config(reflection_input, Arc::new(prompt_model)) .await? @@ -348,7 +348,7 @@ impl GEPA { }; let proposal_output = if let Some(mut prompt_model) = self.prompt_model.clone() { - prompt_model.config.temperature = self.temperature; + prompt_model.temperature = self.temperature; propose_predictor .forward_with_config(proposal_input, Arc::new(prompt_model)) .await? diff --git a/crates/dspy-rs/src/optimizer/mipro.rs b/crates/dspy-rs/src/optimizer/mipro.rs index dd06bc83..a5e84357 100644 --- a/crates/dspy-rs/src/optimizer/mipro.rs +++ b/crates/dspy-rs/src/optimizer/mipro.rs @@ -301,7 +301,7 @@ impl MIPROv2 { }; let prediction = if let Some(mut pm) = self.prompt_model.clone() { - pm.config.temperature = 0.7; + pm.temperature = 0.7; description_generator .forward_with_config(input, Arc::new(pm)) .await? @@ -352,7 +352,7 @@ impl MIPROv2 { }; let result = if let Some(mut pm) = self.prompt_model.clone() { - pm.config.temperature = self.temperature; + pm.temperature = self.temperature; instruction_generator .forward_with_config(input, Arc::new(pm)) .await diff --git a/crates/dspy-rs/tests/test_adapters.rs b/crates/dspy-rs/tests/test_adapters.rs index a6634be2..d5b0d42f 100644 --- a/crates/dspy-rs/tests/test_adapters.rs +++ b/crates/dspy-rs/tests/test_adapters.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use tokio::sync::Mutex; use dspy_rs::{ - Cache, Chat, ChatAdapter, DummyLM, Example, LMConfig, Message, MetaSignature, Signature, + Cache, Chat, ChatAdapter, DummyLM, Example, Message, MetaSignature, Signature, adapter::Adapter, example, hashmap, sign, }; @@ -448,18 +448,7 @@ async fn test_chat_adapter_demo_format_multiple_fields() { #[tokio::test] #[cfg_attr(miri, ignore)] async fn test_chat_adapter_with_cache_hit() { - // Create DummyLM with cache enabled - let config = LMConfig { - cache: true, - ..Default::default() - }; - - let dummy_lm = DummyLM { - api_key: "test_key".to_string(), - base_url: "https://api.openai.com/v1".to_string(), - config, - cache_handler: Some(Arc::new(Mutex::new(Cache::new().await))), - }; + let dummy_lm = DummyLM::default(); // Create test input example let input = example! { @@ -504,18 +493,12 @@ async fn test_chat_adapter_with_cache_hit() { #[cfg_attr(miri, ignore)] async fn test_chat_adapter_cache_miss_different_inputs() { // Create DummyLM with cache enabled - let config = LMConfig { - cache: true, - ..Default::default() - }; let cache_handler = Arc::new(Mutex::new(Cache::new().await)); - let dummy_lm = DummyLM { - api_key: "test_key".to_string(), - base_url: "https://api.openai.com/v1".to_string(), - config, - cache_handler: Some(cache_handler.clone()), - }; + let dummy_lm = DummyLM::builder() + .cache_handler(cache_handler) + .api_key("test_key".to_string()) + .build(); // First input let input1 = example! { @@ -568,17 +551,7 @@ async fn test_chat_adapter_cache_miss_different_inputs() { #[cfg_attr(miri, ignore)] async fn test_chat_adapter_cache_disabled() { // Create DummyLM with cache disabled - let config = LMConfig { - cache: false, - ..Default::default() - }; - - let dummy_lm = DummyLM { - api_key: "test_key".to_string(), - base_url: "https://api.openai.com/v1".to_string(), - config, - cache_handler: None, // No cache handler when cache is disabled - }; + let dummy_lm = DummyLM::default(); // Create test input let input = example! { diff --git a/crates/dspy-rs/tests/test_lm.rs b/crates/dspy-rs/tests/test_lm.rs index 1c6967dd..842eca2b 100644 --- a/crates/dspy-rs/tests/test_lm.rs +++ b/crates/dspy-rs/tests/test_lm.rs @@ -1,4 +1,4 @@ -use dspy_rs::{Cache, Chat, DummyLM, Example, LM, LMConfig, LmUsage, Message, hashmap}; +use dspy_rs::{Cache, Chat, DummyLM, Example, LM, LmUsage, Message, hashmap}; use rstest::*; #[cfg_attr(miri, ignore)] // Miri doesn't support tokio's I/O driver @@ -44,7 +44,7 @@ async fn test_dummy_lm() { tokio::time::sleep(std::time::Duration::from_secs(5)).await; // Check cache functionality if caching is enabled - if dummy_lm.config.cache { + if dummy_lm.cache { let history = dummy_lm.inspect_history(1).await; assert_eq!(history.len(), 1); assert_eq!(history[0].prompt, chat.to_json().to_string()); @@ -87,12 +87,12 @@ async fn test_lm_with_cache_enabled() { std::env::set_var("OPENAI_API_KEY", "test"); } // Create LM with cache enabled - let config = LMConfig { - cache: true, - ..Default::default() - }; - - let lm = LM::new(config).await; + let lm = LM::builder() + .model("openai:gpt-4o-mini".to_string()) + .cache(true) + .build() + .await + .unwrap(); // Verify cache handler is initialized assert!(lm.cache_handler.is_some()); @@ -105,12 +105,12 @@ async fn test_lm_with_cache_disabled() { std::env::set_var("OPENAI_API_KEY", "test"); } // Create LM with cache explicitly disabled - let config = LMConfig { - cache: false, - ..Default::default() - }; - - let lm = LM::new(config).await; + let lm = LM::builder() + .model("openai:gpt-4o-mini".to_string()) + .cache(false) + .build() + .await + .unwrap(); // Verify cache handler is NOT initialized when cache is disabled assert!(lm.cache_handler.is_none()); @@ -123,12 +123,12 @@ async fn test_lm_cache_initialization_on_first_call() { std::env::set_var("OPENAI_API_KEY", "test"); } // Create LM with cache enabled - let config = LMConfig { - cache: true, - ..Default::default() - }; - - let lm = LM::new(config).await; + let lm = LM::builder() + .model("openai:gpt-4o-mini".to_string()) + .cache(true) + .build() + .await + .unwrap(); // After build, cache_handler should be initialized assert!(lm.cache_handler.is_some()); @@ -144,12 +144,12 @@ async fn test_lm_cache_direct_operations() { use std::collections::HashMap; // Create LM with cache enabled - let config = LMConfig { - cache: true, - ..Default::default() - }; - - let lm = LM::new(config).await; + let lm = LM::builder() + .model("openai:gpt-4o-mini".to_string()) + .cache(true) + .build() + .await + .unwrap(); // Get cache handler let cache = lm @@ -213,13 +213,12 @@ async fn test_lm_cache_with_different_models() { let models = vec!["openai:gpt-3.5-turbo", "anthropic:claude-3-haiku-20240307"]; for model in models { - let config = LMConfig { - cache: true, - model: model.to_string(), - ..Default::default() - }; - - let lm = LM::new(config).await; + let lm = LM::builder() + .model(model.to_string()) + .cache(true) + .build() + .await + .unwrap(); // Cache should be initialized regardless of model assert!( @@ -240,12 +239,12 @@ async fn test_cache_with_complex_inputs() { use std::collections::HashMap; // Create LM with cache enabled - let config = LMConfig { - cache: true, - ..Default::default() - }; - - let lm = LM::new(config).await; + let lm = LM::builder() + .model("openai:gpt-4o-mini".to_string()) + .cache(true) + .build() + .await + .unwrap(); let cache = lm .cache_handler diff --git a/crates/dspy-rs/tests/test_settings.rs b/crates/dspy-rs/tests/test_settings.rs index e44b3485..2b2bea2d 100644 --- a/crates/dspy-rs/tests/test_settings.rs +++ b/crates/dspy-rs/tests/test_settings.rs @@ -1,4 +1,4 @@ -use dspy_rs::{ChatAdapter, LM, LMConfig, configure, get_lm}; +use dspy_rs::{ChatAdapter, LM, configure, get_lm}; #[tokio::test] #[cfg_attr(miri, ignore)] @@ -7,27 +7,27 @@ async fn test_settings() { std::env::set_var("OPENAI_API_KEY", "test"); } configure( - LM::new(LMConfig { - model: "openai:gpt-4o-mini".to_string(), - ..LMConfig::default() - }) - .await, + LM::builder() + .model("openai:gpt-4o-mini".to_string()) + .build() + .await + .unwrap(), ChatAdapter {}, ); let lm = get_lm(); - assert_eq!(lm.config.model, "openai:gpt-4o-mini"); + assert_eq!(lm.model, "openai:gpt-4o-mini"); configure( - LM::new(LMConfig { - model: "openai:gpt-4o".to_string(), - ..LMConfig::default() - }) - .await, + LM::builder() + .model("openai:gpt-4o".to_string()) + .build() + .await + .unwrap(), ChatAdapter {}, ); let lm = get_lm(); - assert_eq!(lm.config.model, "openai:gpt-4o"); + assert_eq!(lm.model, "openai:gpt-4o"); } diff --git a/crates/dsrs-macros/Cargo.toml b/crates/dsrs-macros/Cargo.toml index 962deef7..dfcec323 100644 --- a/crates/dsrs-macros/Cargo.toml +++ b/crates/dsrs-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dsrs_macros" -version = "0.7.0" +version = "0.7.1" edition = "2024" authors = ["Herumb Shandilya "] description = "Derive macros for DSRs (DSPy Rust)" diff --git a/docs/docs/building-blocks/lm.mdx b/docs/docs/building-blocks/lm.mdx index 45defd2f..f1c3c297 100644 --- a/docs/docs/building-blocks/lm.mdx +++ b/docs/docs/building-blocks/lm.mdx @@ -24,11 +24,14 @@ It handles three core responsibilities: ### Structure `LM` is built using the builder pattern and holds: - - `api_key` - Provider API credentials (stored as `SecretString`) - - `base_url` - API endpoint URL - - `config` - An `LMConfig` with model settings - - `client` - Internal HTTP client (OpenAI-compatible) - - `cache_handler` - Optional response cache (enabled by default) + - `model` - Model identifier (e.g., "gpt-4o-mini" or "openai:gpt-4o-mini") + - `api_key` - Provider API credentials (optional for local servers) + - `base_url` - API endpoint URL (optional, inferred from model provider) + - `temperature` - Sampling temperature (default: 0.7) + - `max_tokens` - Maximum completion tokens (default: 512) + - `cache` - Enable response caching (default: true) + - `client` - Internal HTTP client (initialized during build) + - `cache_handler` - Optional response cache (initialized during build if enabled) Cloning an `LM` is cheap - clones share the same HTTP client and cache via `Arc`, making them ideal for concurrent use. @@ -40,28 +43,59 @@ You rarely call `LM` directly—It's the lowest-level DSRs primitive. Instead, a ## Construction and configuration +The `LM::builder()` must be awaited with `.build().await` because client initialization is async. + +### Basic usage + ```rust -use dsrs::{LM, LMConfig}; +use dspy_rs::LM; #[tokio::main] async fn main() -> anyhow::Result<()> { + // OpenAI - API key automatically read from OPENAI_API_KEY env var + let lm = LM::builder() + .model("gpt-4o-mini".to_string()) + .temperature(0.7) + .max_tokens(512) + .build() + .await?; + + // Or explicitly provide API key let lm = LM::builder() - .api_key(std::env::var("OPENAI_API_KEY")?.into()) - .config( - LMConfig::builder() - .model("gpt-4o-mini".to_string()) - .temperature(0.7) - .max_tokens(512) - .build() - ) + .model("gpt-4o-mini".to_string()) + .api_key("your-api-key".into()) .build() - .await; + .await?; - // use lm here Ok(()) } ``` +### Local server usage + +For local OpenAI-compatible servers (vLLM, Ollama, etc.), provide `base_url` without an `api_key`: + +```rust +let lm = LM::builder() + .base_url("http://localhost:11434".to_string()) + .model("llama3".to_string()) + .build() + .await?; +``` + +### Custom OpenAI-compatible endpoints + +For custom endpoints requiring authentication, provide both `base_url` and `api_key`: + +```rust +let lm = LM::builder() + .base_url("https://my-custom-api.com/v1".to_string()) + .api_key(my_api_key.into()) + .model("custom-model".to_string()) + .build() + .await?; +``` + - **Clone semantics:** `LM` implements `Clone`; clones share the underlying client and cache via `Arc`, so they see the same history while carrying their own config copy. ## API Reference @@ -70,12 +104,12 @@ You can browse the full `LM` module reference on [docs.rs](https://docs.rs/dspy- ## Global vs explicit usage -- **Global:** `configure(lm.clone(), ChatAdapter::default())` sets the process-wide default used by predictors. +- **Global:** `configure(lm, ChatAdapter)` sets the process-wide default used by predictors. - **Explicit:** Wrap the model in an `Arc` when you want to override the global instance: `let shared = Arc::new(lm); predictor.forward_with_config(inputs, Arc::clone(&shared)).await`. ## Async execution and sync entry -- **Async:** Calls are `async`; prefer using an async runtime (Tokio). +- **Async:** LM building and calls are `async`; prefer using an async runtime (Tokio). - **Sync-style:** If you need a plain `fn main`, create a runtime and `block_on` the async work. @@ -84,7 +118,10 @@ You can browse the full `LM` module reference on [docs.rs](https://docs.rs/dspy- ```rust #[tokio::main] async fn main() -> anyhow::Result<()> { - // build + use LM here + let lm = LM::builder() + .model("gpt-4o-mini".to_string()) + .build() + .await?; Ok(()) } ``` @@ -96,7 +133,10 @@ async fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> { let rt = tokio::runtime::Runtime::new()?; rt.block_on(async move { - // build + use LM here + let lm = LM::builder() + .model("gpt-4o-mini".to_string()) + .build() + .await?; Ok(()) }) } @@ -117,73 +157,86 @@ for entry in history { > `inspect_history` requires caching to be enabled on the LM; otherwise no history is recorded. -## LMConfig reference - -`LMConfig` centralizes inference settings. All fields have sensible defaults, so you can override only what you need. The builder exposes setters for each field. +## Configuration options -| Field | Type | Default | Notes | -|-------------------------|----------------------------------|-----------------|---------------------------------------------------------------------------------| -| `model` | `String` | `"gpt-4o-mini"` | Supports bare IDs or `provider/model` prefixed IDs; affects base URL inference. | -| `temperature` | `f32` | `0.7` | Higher values increase randomness. | -| `top_p` | `f32` | `0.0` | Nucleus sampling; set one of `temperature` or `top_p`. | -| `max_tokens` | `u32` | `512` | Upper bound on completion tokens. | -| `max_completion_tokens` | `u32` | `512` | Not currently sent to providers; Reserved for future use. | -| `presence_penalty` | `f32` | `0.0` | Penalizes repeated tokens globally. | -| `frequency_penalty` | `f32` | `0.0` | Penalizes repeated tokens proportionally; ignored for Gemini models. | -| `seed` | `i64` | `42` | Optional seed for deterministic sampling; ignored for Gemini models. | -| `logit_bias` | `Option>` | `None` | Token-level logit adjustments; ignored for Gemini models. | -| `cache` | `bool` | `true` | Enables the shared `ResponseCache` and `inspect_history` support. | +All `LM` builder parameters have sensible defaults, so you only need to override what you need. +| Parameter | Type | Default | Notes | +|--------------|-----------------|----------------------|---------------------------------------------------------------------------------| +| `model` | `String` | `"openai:gpt-4o-mini"` | Supports "provider:model" format or bare model name (defaults to OpenAI) | +| `api_key` | `Option`| `None` | Provider API key; omit for local servers | +| `base_url` | `Option`| `None` | Custom endpoint URL; auto-detected from model provider if not provided | +| `temperature`| `f32` | `0.7` | Higher values increase randomness | +| `max_tokens` | `u32` | `512` | Upper bound on completion tokens | +| `cache` | `bool` | `true` | Enables response caching and `inspect_history` support | +### Example with custom settings ```rust -let config = LMConfig::builder() - .model("anthropic/claude-4.5-sonnet".into()) +// API key automatically read from ANTHROPIC_API_KEY env var +let lm = LM::builder() + .model("anthropic:claude-3-5-sonnet-20241022".to_string()) .temperature(0.3) .max_tokens(1_024) .cache(true) - .build(); + .build() + .await?; ``` -> Tip: stick to either `temperature` or `top_p`; providers often ignore one when both are set. +### Provider Support -### Provider Support & Base URLs +DSRs supports multiple LLM providers through [Rig](https://github.com/0xPlaygrounds/rig). Use the `provider:model` format to specify which provider to use. Bare model names default to OpenAI. -DSRs supports multiple LLM providers through automatic endpoint detection. When you pass a `provider/model` string, the builder splits it, rewrites `config.model` to just the model id, and swaps `base_url` to the matching OpenAI-compatible endpoint. Bare model names keep the default OpenAI base URL, and any unknown prefix falls back to OpenRouter. +**Supported providers:** +- `openai` - OpenAI models (requires `OPENAI_API_KEY`) +- `anthropic` - Anthropic models (requires `ANTHROPIC_API_KEY`) +- `gemini` - Google Gemini models (requires `GEMINI_API_KEY`) +- `groq` - Groq models (requires `GROQ_API_KEY`) +- `openrouter` - OpenRouter (requires `OPENROUTER_API_KEY`) +- `ollama` - Local Ollama models (no API key required) -| Prefix | Base URL | -|--------------|-----------------------------------------------------------| -| `openai` | `https://api.openai.com/v1` | -| `anthropic` | `https://api.anthropic.com/v1` | -| `google` | `https://generativelanguage.googleapis.com/v1beta/openai` | -| `cohere` | `https://api.cohere.ai/compatibility/v1` | -| `groq` | `https://api.groq.com/openai/v1` | -| `openrouter` | `https://openrouter.ai/api/v1` | -| `qwen` | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` | -| `together` | `https://api.together.xyz/v1` | -| `xai` | `https://api.x.ai/v1` | +**API keys are automatically read from environment variables.** You only need to provide `.api_key()` if you want to override the default environment variable. -You can still override the base URL manually via the builder if you need a self-hosted proxy. +You can also use `base_url` to connect to any OpenAI-compatible server (vLLM, LiteLLM, etc.). -Use the `provider/model` format to target specific hosts: +### Usage examples ```rust -// Anthropic -.model("anthropic/claude-4-5-sonnet".to_string()) - -// Google -.model("google/gemini-2.0-flash".to_string()) - -// Groq -.model("groq/mixtral-8x7b-32768".to_string()) - -// OpenAI (or just use model name directly) -.model("openai/gpt-4.1".to_string()) -.model("gpt-4.1-mini".to_string()) // defaults to OpenAI +// Anthropic - reads from ANTHROPIC_API_KEY env var +let lm = LM::builder() + .model("anthropic:claude-3-5-sonnet-20241022".to_string()) + .build() + .await?; + +// Google Gemini - reads from GEMINI_API_KEY env var +let lm = LM::builder() + .model("gemini:gemini-2.0-flash-exp".to_string()) + .build() + .await?; + +// Groq - reads from GROQ_API_KEY env var +let lm = LM::builder() + .model("groq:mixtral-8x7b-32768".to_string()) + .build() + .await?; + +// OpenAI (or just use model name directly) - reads from OPENAI_API_KEY env var +let lm = LM::builder() + .model("gpt-4o".to_string()) // defaults to OpenAI + .build() + .await?; + +// Ollama (local, no API key needed) +let lm = LM::builder() + .model("ollama:llama3".to_string()) + .build() + .await?; + +// OpenRouter - reads from OPENROUTER_API_KEY env var +let lm = LM::builder() + .model("openrouter:anthropic/claude-3-opus".to_string()) + .build() + .await?; ``` -Supported prefixes: `openai`, `anthropic`, `google`, `cohere`, `groq`, `openrouter`, `qwen`, `together`, `xai`. Unrecognised prefixes default to OpenRouter. - -### Gemini compatibility - -Gemini models (names starting with `gemini-`) reject several OpenAI parameters. DSRs automatically omits `frequency_penalty`, `seed`, and `logit_bias` when the configured model begins with `gemini-`, so you can keep the same `LMConfig` across providers without triggering errors. Other settings such as `temperature`, `top_p`, `max_tokens`, and `presence_penalty` are still forwarded. +All provider integrations are powered by [Rig](https://github.com/0xPlaygrounds/rig), which handles the provider-specific API details. diff --git a/docs/docs/getting-started/quickstart.mdx b/docs/docs/getting-started/quickstart.mdx index 11f56afc..727a96f6 100644 --- a/docs/docs/getting-started/quickstart.mdx +++ b/docs/docs/getting-started/quickstart.mdx @@ -61,46 +61,38 @@ into a prompt that the LM can follow to complete the task. ```rust lines -use dspy_rs::{configure, ChatAdapter, LM, LMConfig}; -use std::env; +use dspy_rs::{configure, ChatAdapter, LM}; #[tokio::main] async fn main() -> Result<(), anyhow::Error> { - // Define a config for the LM - let config = LMConfig::builder() - .model("gpt-4.1-nano".to_string()) - .build(); - // Create the LM instance via the async builder - let lm = LM::builder() - .config(config) - .api_key(env::var("OPENAI_API_KEY")?.into()) - .build() - .await; - // Configure the global LM and adapter - configure(lm, ChatAdapter::default()); + // API key automatically read from OPENAI_API_KEY env var + configure( + LM::builder() + .model("gpt-4o-mini".to_string()) + .build() + .await?, + ChatAdapter, + ); Ok(()) } ``` -```rust lines highlight={7, 12} -use dspy_rs::{configure, ChatAdapter, LM, LMConfig}; -use std::env; +```rust lines +use dspy_rs::{configure, ChatAdapter, LM}; -fn main() -> Result<(), anyhow::Error> { - //Define a config for the LM - let config = LMConfig::builder() - .model("gpt-oss:20b".to_string()) - .build(); - // Create the LM instance via the builder - let lm = LM::builder() - .config(config) - .base_url("http://localhost:11434") - .api_key("".into()) // Don't need an API key for Ollama - .build(); - // Configure the global LM and adapter - configure(lm, ChatAdapter::default()); +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + // Create the LM instance for local Ollama server + configure( + LM::builder() + .base_url("http://localhost:11434".to_string()) + .model("llama3".to_string()) + .build() + .await?, + ChatAdapter, + ); Ok(()) } @@ -146,9 +138,8 @@ LM calls in DSRs are asynchronous and return a future, so we need to use the `to ```rust lines use dspy_rs::{ - ChatAdapter, Example, LM, LMConfig, Predict, Predictor, Signature, configure, hashmap, + ChatAdapter, Example, LM, Predict, Predictor, Signature, configure, hashmap, }; -use std::env; #[tokio::main] async fn main() -> Result<(), anyhow::Error> { @@ -158,15 +149,14 @@ async fn main() -> Result<(), anyhow::Error> { (question: String) -> answer: String }; - let config = LMConfig::builder().model("gpt-4.1-nano".to_string()) - .build(); - - let lm = LM::builder() - .config(config) - .api_key(env::var("OPENAI_API_KEY")?.into()) - .build(); - - configure(lm.clone(), ChatAdapter::default()); + // API key automatically read from OPENAI_API_KEY env var + configure( + LM::builder() + .model("gpt-4o-mini".to_string()) + .build() + .await?, + ChatAdapter, + ); // Create a question-answering signature instance let signature = qa::new(); // Create a predictor @@ -235,22 +225,19 @@ instructions to the LM. Let us use the above signature in our pipeline to answer questions like a pirate. -```rust lines highlight={16} +```rust lines highlight={4} #[tokio::main] async fn main() -> Result<(), anyhow::Error> { dotenv().ok(); - let config = LMConfig::builder() - .model("gpt-4.1-nano".to_string()) - .build(); - - let lm = LM::builder() - .config(config) - .api_key(env::var("OPENAI_API_KEY")?.into()) - .build() - .await; - - configure(lm.clone(), ChatAdapter::default()); + // API key automatically read from OPENAI_API_KEY env var + configure( + LM::builder() + .model("gpt-4o-mini".to_string()) + .build() + .await?, + ChatAdapter, + ); // Create a question-answering signature instance let signature = QA::new(); // Create a predictor @@ -293,7 +280,7 @@ more complex modules that define your own logic. Let us examine how to do that. ```rust use dspy_rs::{ - ChatAdapter, Example, LM, LMConfig, Module, Predict, Prediction, + ChatAdapter, Example, LM, Module, Predict, Prediction, Predictor, Signature, configure, hashmap, }; use std::env; @@ -327,22 +314,19 @@ We can define the main function to use this module as follows: 1. Create a new instance of the module (line 16). 2. Call the `forward` method on the module (line 30). -```rust lines highlight={16,30} +```rust lines highlight={4,15} #[tokio::main] async fn main() -> Result<(), anyhow::Error> { dotenv().ok(); - let config = LMConfig::builder() - .model("gpt-4.1-nano".to_string()) - .build(); - - let lm = LM::builder() - .config(config) - .api_key(env::var("OPENAI_API_KEY")?.into()) - .build() - .await; - - configure(lm.clone(), ChatAdapter::default()); + // API key automatically read from OPENAI_API_KEY env var + configure( + LM::builder() + .model("gpt-4o-mini".to_string()) + .build() + .await?, + ChatAdapter, + ); let module = AnswerQuestion::new(); diff --git a/docs/docs/optimizers/copro.mdx b/docs/docs/optimizers/copro.mdx index 301159be..95a76b26 100644 --- a/docs/docs/optimizers/copro.mdx +++ b/docs/docs/optimizers/copro.mdx @@ -56,10 +56,16 @@ impl Evaluator for MyModule { #[tokio::main] async fn main() -> Result<()> { - let lm = LM::builder()...build(); - configure(lm, ChatAdapter); + // API key automatically read from OPENAI_API_KEY env var + configure( + LM::builder() + .model("gpt-4o-mini".to_string()) + .build() + .await?, + ChatAdapter, + ); - let mut module = MyModule::builder()...build(); + let mut module = MyModule::builder().build(); let copro = COPRO::builder() .breadth(10)