adk-model 0.6.0

LLM model integrations for Rust Agent Development Kit (ADK-Rust) (Gemini, OpenAI, Claude, DeepSeek, etc.)
Documentation
use adk_model::openrouter::{
    OPENROUTER_API_BASE, OpenRouterApiMode, OpenRouterClient, OpenRouterConfig,
};
use anyhow::{Result, anyhow};
use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::path::PathBuf;

const DEFAULT_MODEL: &str = "openai/gpt-4.1-mini";
const DEFAULT_SITE_URL: &str = "https://github.com/zavora-ai/adk-rust";
const DEFAULT_APP_NAME: &str = "ADK-Rust OpenRouter Examples";

#[derive(Debug, Clone)]
pub struct ExampleConfig {
    pub model: String,
}

pub fn build_client(
    default_api_mode: OpenRouterApiMode,
) -> Result<(OpenRouterClient, ExampleConfig)> {
    let env_file_values = load_env_file_values();
    let api_key = required_value("OPENROUTER_API_KEY", &env_file_values)?;
    let config = ExampleConfig {
        model: optional_value("OPENROUTER_MODEL", &env_file_values)
            .unwrap_or_else(|| DEFAULT_MODEL.to_string()),
    };

    let client = OpenRouterClient::new(
        OpenRouterConfig::new(api_key, &config.model)
            .with_base_url(
                optional_value("OPENROUTER_BASE_URL", &env_file_values)
                    .unwrap_or_else(|| OPENROUTER_API_BASE.to_string()),
            )
            .with_http_referer(
                optional_value("OPENROUTER_SITE_URL", &env_file_values)
                    .unwrap_or_else(|| DEFAULT_SITE_URL.to_string()),
            )
            .with_title(
                optional_value("OPENROUTER_APP_NAME", &env_file_values)
                    .unwrap_or_else(|| DEFAULT_APP_NAME.to_string()),
            )
            .with_default_api_mode(default_api_mode),
    )?;

    Ok((client, config))
}

pub fn print_section(title: &str) {
    println!("\n== {title} ==");
}

fn required_value(key: &str, env_file_values: &BTreeMap<String, String>) -> Result<String> {
    optional_value(key, env_file_values)
        .ok_or_else(|| anyhow!("missing {key}; set it in the shell or .env"))
}

fn optional_value(key: &str, env_file_values: &BTreeMap<String, String>) -> Option<String> {
    env::var(key)
        .ok()
        .filter(|value| !value.trim().is_empty())
        .or_else(|| env_file_values.get(key).cloned())
}

fn load_env_file_values() -> BTreeMap<String, String> {
    find_env_file()
        .and_then(|path| fs::read_to_string(path).ok())
        .map(|contents| parse_env_file(&contents))
        .unwrap_or_default()
}

fn find_env_file() -> Option<PathBuf> {
    let mut current = env::current_dir().ok()?;

    loop {
        let candidate = current.join(".env");
        if candidate.exists() {
            return Some(candidate);
        }
        if !current.pop() {
            return None;
        }
    }
}

fn parse_env_file(contents: &str) -> BTreeMap<String, String> {
    let mut values = BTreeMap::new();

    for raw_line in contents.lines() {
        let line = raw_line.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }

        let Some((key, value)) = line.split_once('=') else {
            continue;
        };

        values.insert(key.trim().to_string(), normalize_env_value(value.trim()));
    }

    values
}

fn normalize_env_value(value: &str) -> String {
    if value.len() >= 2 {
        let bytes = value.as_bytes();
        let quoted_with_double = bytes[0] == b'"' && bytes[value.len() - 1] == b'"';
        let quoted_with_single = bytes[0] == b'\'' && bytes[value.len() - 1] == b'\'';

        if quoted_with_double || quoted_with_single {
            return value[1..value.len() - 1].to_string();
        }
    }

    value.to_string()
}