adk-model 2.3.0

LLM model integrations for Rust Agent Development Kit (ADK-Rust) (Gemini, OpenAI, Claude, DeepSeek, etc.)
Documentation
use super::*;
use adk_core::ErrorCategory;
use adk_model::openai::OpenAIReasoningEffort;
use proptest::prelude::*;

#[test]
fn requires_explicit_api_for_unknown_models() {
    let error = OpenCodeClient::new(config("future-model")).err().unwrap();
    assert_eq!(error.code, "model.opencode.invalid_config");
    assert!(OpenCodeClient::new(config("future-model").with_api(OpenCodeApi::Messages)).is_ok());
}

#[test]
fn validates_identity_endpoint_and_reasoning_without_requests() {
    for config in [
        OpenCodeConfig::new(OpenCodeService::Go, "secret", "minimax-m3"),
        config("minimax-m3").with_session_id("\ninvalid"),
        config("minimax-m3").with_user_agent(""),
        config("minimax-m3").with_reasoning_effort(OpenAIReasoningEffort::High),
        config("deepseek-v4.1-flash").with_anthropic_effort(adk_model::anthropic::Effort::High),
        config("deepseek-v4.1-flash").with_base_url("http://example.com/v1"),
        config("deepseek-v4.1-flash").with_base_url("https://opencode.ai/zen/go/v1?secret=secret"),
    ] {
        let error = OpenCodeClient::new(config).err().unwrap();
        assert_eq!(error.category, ErrorCategory::InvalidInput);
        assert!(!error.to_string().contains("secret"));
    }
}

#[test]
fn rejects_whitespace_base_urls_and_empty_keys_without_panicking() {
    let cases = [
        (config("minimax-m3").with_base_url("https://opencode.ai/zen/go/v1\n"), "opencode-go"),
        (config("minimax-m3").with_base_url("https://opencode.ai/zen/go/v1 "), "opencode-go"),
        (config("minimax-m3").with_base_url("https://opencode.ai/zen/go/\tv1"), "opencode-go"),
        (
            config_for(OpenCodeService::Zen, "gemini-3.8-flash")
                .with_base_url(" https://opencode.ai/zen/v1"),
            "opencode",
        ),
        (
            OpenCodeConfig::new(OpenCodeService::Go, " ", "minimax-m3")
                .with_user_agent("test-coding-agent/1.0")
                .with_session_id("conversation-42"),
            "opencode-go",
        ),
    ];
    for (config, provider) in cases {
        let error = OpenCodeClient::new(config).err().unwrap();
        assert_eq!(
            (error.category, error.code, error.details.provider.as_deref()),
            (ErrorCategory::InvalidInput, "model.opencode.invalid_config", Some(provider)),
        );
    }
}

proptest! {
    #![proptest_config(ProptestConfig::with_cases(100))]
    #[test]
    fn debug_omits_credentials(key in "[a-zA-Z0-9]{30,60}") {
        let config = OpenCodeConfig::new(OpenCodeService::Go, &key, "minimax-m3");
        let debug = format!("{config:?}");
        prop_assert!(!debug.contains(&key));
    }
}

#[test]
fn separates_service_routes_and_defaults() {
    for model in ["minimax-m3", "qwen3.8-max"] {
        assert_eq!(OpenCodeService::Go.api(model), Some(OpenCodeApi::Messages));
        assert_eq!(OpenCodeService::Zen.api(model), Some(OpenCodeApi::ChatCompletions));
    }
    assert_eq!(OpenCodeService::Go.base_url(), "https://opencode.ai/zen/go/v1");
    assert_eq!(OpenCodeService::Zen.base_url(), "https://opencode.ai/zen/v1");
    assert_eq!(OpenCodeService::Go.api("gemini-3.8-flash"), None);
    assert_eq!(OpenCodeService::Zen.api("gemini-3.8-flash"), Some(OpenCodeApi::GenerateContent));
    assert_eq!(OpenCodeService::Zen.api("jev-1.13"), None);
    assert!(OpenCodeClient::new(config_for(OpenCodeService::Zen, "unknown")).is_err());
    for config in [
        config_for(OpenCodeService::Zen, "gemini-3.8-flash")
            .with_reasoning_effort(OpenAIReasoningEffort::High),
        config("deepseek-v4.1-flash")
            .with_gemini_thinking(adk_model::gemini::ThinkingConfig::default()),
    ] {
        assert!(OpenCodeClient::new(config).is_err());
    }
}