open-agent-sdk 0.11.3

Production-ready Rust SDK for building AI agents over two wire protocols: OpenAI chat completions (LMStudio, Ollama, llama.cpp, vLLM, OpenRouter) and Anthropic messages. Features streaming, tools, hooks, retry logic, and comprehensive examples.
Documentation
#[test]
fn image_urls_preserve_supported_sources_and_default_detail() {
    for url in [
        "http://example.com/image.jpg",
        "https://example.com/image.jpg",
        "data:image/png;base64,AAAA",
        "data:image/png;charset=utf-8;base64,iVBORw0KGgoAAAA=",
    ] {
        let image = ImageBlock::from_url(url).unwrap();
        assert_eq!(image.url(), url);
        assert_eq!(image.detail(), ImageDetail::Auto);
    }
}

#[test]
fn image_urls_reject_empty_unsupported_and_malformed_sources() {
    for (url, expected_error) in [
        ("", "empty"),
        ("ftp://example.com/image.jpg", "http"),
        ("/images/photo.jpg", "http"),
        ("javascript:alert('XSS')", "http"),
        ("file:///etc/passwd", "http"),
        ("data:notanimage", "format"),
    ] {
        let error = ImageBlock::from_url(url).unwrap_err();
        assert!(matches!(error, crate::Error::InvalidInput(_)));
        assert!(
            error.to_string().contains(expected_error),
            "{url:?}: {error}"
        );
    }
}

#[test]
fn image_urls_reject_control_characters() {
    for control in ['\n', '\t', '\0', '\r', '\x1b'] {
        let error =
            ImageBlock::from_url(format!("https://example.com{control}/image.jpg")).unwrap_err();
        assert!(error.to_string().contains("control"), "{error}");
    }
}

#[test]
fn long_image_urls_remain_accepted_without_truncation() {
    for path_length in [1900, 2980] {
        let url = format!("https://example.com/{}", "a".repeat(path_length));
        assert_eq!(ImageBlock::from_url(&url).unwrap().url(), url);
    }
}

#[test]
fn base64_images_preserve_data_and_every_supported_mime_type() {
    for mime in [
        "image/jpeg",
        "image/png",
        "image/gif",
        "image/webp",
        "image/avif",
        "image/bmp",
        "image/tiff",
    ] {
        let image = ImageBlock::from_base64("iVBORw0KGgoAAAA=", mime).unwrap();
        assert_eq!(image.url(), format!("data:{mime};base64,iVBORw0KGgoAAAA="));
        assert_eq!(image.detail(), ImageDetail::Auto);
    }
}

#[test]
fn base64_images_reject_empty_nonimage_and_injected_mime_types() {
    for mime in [
        "",
        "text/plain",
        "image/png;charset=utf-8",
        "image/png\n",
        "image/png\r",
        "image/png\r\n",
        "image/png,extra",
        "image/png\r\nX-Custom: bad",
    ] {
        let error = ImageBlock::from_base64("AAAA", mime).unwrap_err();
        assert!(matches!(error, crate::Error::InvalidInput(_)));
        assert!(error.to_string().contains("MIME"), "{mime:?}: {error}");
    }
}

#[test]
fn both_image_constructors_reject_invalid_base64() {
    for data in [
        "",
        "hello world",
        "test@data",
        "test#data",
        "test$data",
        "test%data",
        "abc\ndef",
        "@@@",
        "A",
        "AB",
        "ABC",
        "ABCD===",
        "==abc",
        "ab==cd",
        "ab==cdef",
        "A===",
        "éé",
    ] {
        for result in [
            ImageBlock::from_base64(data, "image/png"),
            ImageBlock::from_url(format!("data:image/png;base64,{data}")),
        ] {
            assert!(
                matches!(result.unwrap_err(), crate::Error::InvalidInput(_)),
                "{data:?}"
            );
        }
    }
}

#[test]
fn large_base64_image_remains_accepted_without_truncation() {
    let data = "A".repeat(15_000_000);
    let image = ImageBlock::from_base64(&data, "image/png").unwrap();
    assert_eq!(
        image.url().strip_prefix("data:image/png;base64,"),
        Some(data.as_str())
    );
}

#[test]
fn file_images_encode_bytes_and_validate_extensions() {
    use base64::{Engine as _, engine::general_purpose};

    let directory = tempfile::tempdir().unwrap();
    let png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==";
    let bytes = general_purpose::STANDARD.decode(png).unwrap();
    let image_path = directory.path().join("image.png");
    std::fs::write(&image_path, &bytes).unwrap();
    let image = ImageBlock::from_file_path(&image_path).unwrap();
    assert_eq!(
        image.url().strip_prefix("data:image/png;base64,"),
        Some(png)
    );
    assert_eq!(image.detail(), ImageDetail::Auto);

    for (filename, expected_error) in [("image", "extension"), ("image.txt", "Unsupported")] {
        let path = directory.path().join(filename);
        std::fs::write(&path, &bytes).unwrap();
        let error = ImageBlock::from_file_path(path).unwrap_err();
        assert!(error.to_string().contains(expected_error), "{error}");
    }
}

#[test]
fn image_detail_builder_display_and_serialization_agree() {
    for (detail, expected) in [
        (ImageDetail::Low, "low"),
        (ImageDetail::High, "high"),
        (ImageDetail::Auto, "auto"),
    ] {
        let image = ImageBlock::from_url("https://example.com/image.jpg")
            .unwrap()
            .with_detail(detail);
        assert_eq!(image.detail(), detail);
        assert_eq!(detail.to_string(), expected);
        assert_eq!(serde_json::to_value(detail).unwrap(), expected);
    }
}

#[test]
fn openai_text_content_roundtrips_as_a_string() {
    for text in ["", "Hello world"] {
        let content = OpenAIContent::Text(text.to_string());
        let value = serde_json::to_value(content).unwrap();
        assert_eq!(value, text);
        let decoded: OpenAIContent = serde_json::from_value(value).unwrap();
        assert!(matches!(decoded, OpenAIContent::Text(actual) if actual == text));
    }
}

#[test]
fn openai_parts_serialize_validated_images_and_text_as_distinct_variants() {
    let image = ImageBlock::from_base64("AAAA", "image/png")
        .unwrap()
        .with_detail(ImageDetail::High);
    let content = OpenAIContent::Parts(vec![
        OpenAIContentPart::text("Inspect:"),
        OpenAIContentPart::from_image(&image),
    ]);
    assert_eq!(
        serde_json::to_value(content).unwrap(),
        serde_json::json!([
            {"type": "text", "text": "Inspect:"},
            {"type": "image_url", "image_url": {"url": "data:image/png;base64,AAAA", "detail": "high"}}
        ])
    );
}

#[test]
#[allow(deprecated)]
fn deprecated_image_part_constructor_keeps_its_wire_format() {
    let part = OpenAIContentPart::image_url("https://example.com/image.jpg", ImageDetail::Low);
    assert_eq!(
        serde_json::to_value(part).unwrap(),
        serde_json::json!({
            "type": "image_url", "image_url": {"url": "https://example.com/image.jpg", "detail": "low"}
        })
    );
}