llm-universal 0.1.0

One async client for OpenAI-compatible Chat Completions and the Anthropic Messages API, with tool calling and SDK-style errors
Documentation
//! Client-side argument validation before the request is sent.

use llm_universal::{
    AnthropicProvider, ChatRequest, LlmError, LlmProvider, Message, OpenAiProvider, Tool,
};
use serde_json::json;

async fn invalid_args(provider: &dyn LlmProvider, req: ChatRequest) -> String {
    match provider.chat(&req).await.unwrap_err() {
        e @ LlmError::InvalidArgument(_) => e.to_string(),
        other => panic!("expected InvalidArgument, got {other:?}"),
    }
}

#[tokio::test]
async fn empty_anthropic_key() {
    let msg = invalid_args(
        &AnthropicProvider::new(""),
        ChatRequest::new("m").message(Message::user("x")),
    )
    .await;
    assert_eq!(
        msg,
        "invalid args: AnthropicProvider: api_key must not be empty"
    );
}

#[tokio::test]
async fn missing_model_and_messages() {
    let p = OpenAiProvider::new("k");
    let msg = invalid_args(&p, ChatRequest::new("").message(Message::user("x"))).await;
    assert_eq!(msg, "invalid args: model must not be empty");
    let msg = invalid_args(&p, ChatRequest::new("m")).await;
    assert_eq!(msg, "invalid args: messages must not be empty");
}

#[tokio::test]
async fn bad_tool_and_base_url() {
    let req = ChatRequest::new("m")
        .message(Message::user("x"))
        .tool(Tool::new("bad name", "d", json!({"type": "object"})));
    let msg = invalid_args(&OpenAiProvider::new("k"), req).await;
    assert_eq!(
        msg,
        "invalid args: tool name `bad name` must match ^[a-zA-Z0-9_-]{1,64}$"
    );

    let p = OpenAiProvider::new("k").with_base_url("localhost:11434");
    let msg = invalid_args(&p, ChatRequest::new("m").message(Message::user("x"))).await;
    assert!(
        msg.contains("base_url `localhost:11434` must start with"),
        "{msg}"
    );
}