anyllm_proxy 0.9.5

HTTP proxy translating Anthropic Messages API to OpenAI Chat Completions
Documentation
// Integration tests for multi-backend path-prefix routing.

use anyllm_proxy::config::MultiConfig;
use anyllm_proxy::server::routes;
use reqwest::Client;

fn test_multi_config() -> MultiConfig {
    // Two backends: openai (default) and gemini
    let toml = r#"
        default_backend = "openai"

        [backends.openai]
        kind = "openai"
        api_key = "sk-test-openai"

        [backends.gemini]
        kind = "gemini"
        api_key = "test-gemini-key"
    "#;
    MultiConfig::from_toml_str(toml)
}

async fn spawn_multi_server() -> String {
    // Enable open-relay mode for tests (no PROXY_API_KEYS configured).
    std::env::set_var("PROXY_OPEN_RELAY", "true");
    let app = routes::app_multi(test_multi_config());
    let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
    let addr = listener.local_addr().unwrap();
    tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
    format!("http://{addr}")
}

#[tokio::test]
async fn health_works_with_multi_backend() {
    let base = spawn_multi_server().await;
    let client = Client::new();
    let resp = client.get(format!("{base}/health")).send().await.unwrap();
    assert_eq!(resp.status(), 200);
}

#[tokio::test]
async fn default_route_requires_auth() {
    let base = spawn_multi_server().await;
    let client = Client::new();
    // /v1/messages without auth should return 401
    let resp = client
        .post(format!("{base}/v1/messages"))
        .header("content-type", "application/json")
        .body(r#"{"model":"claude-sonnet-4-6","max_tokens":100,"messages":[{"role":"user","content":"hi"}]}"#)
        .send()
        .await
        .unwrap();
    assert_eq!(resp.status(), 401);
}

#[tokio::test]
async fn prefixed_route_requires_auth() {
    let base = spawn_multi_server().await;
    let client = Client::new();
    // /openai/v1/messages without auth should return 401
    let resp = client
        .post(format!("{base}/openai/v1/messages"))
        .header("content-type", "application/json")
        .body(r#"{"model":"claude-sonnet-4-6","max_tokens":100,"messages":[{"role":"user","content":"hi"}]}"#)
        .send()
        .await
        .unwrap();
    assert_eq!(resp.status(), 401);
}

#[tokio::test]
async fn prefixed_models_endpoint_works() {
    let base = spawn_multi_server().await;
    let client = Client::new();
    // /openai/v1/models should work with auth
    let resp = client
        .get(format!("{base}/openai/v1/models"))
        .header("x-api-key", "any-key")
        .send()
        .await
        .unwrap();
    assert_eq!(resp.status(), 200);
    let body: serde_json::Value = resp.json().await.unwrap();
    assert!(body["data"].is_array());
}

#[tokio::test]
async fn gemini_prefixed_models_endpoint_works() {
    let base = spawn_multi_server().await;
    let client = Client::new();
    let resp = client
        .get(format!("{base}/gemini/v1/models"))
        .header("x-api-key", "any-key")
        .send()
        .await
        .unwrap();
    assert_eq!(resp.status(), 200);
}

#[tokio::test]
async fn unknown_prefix_returns_404() {
    let base = spawn_multi_server().await;
    let client = Client::new();
    let resp = client
        .get(format!("{base}/unknown/v1/models"))
        .header("x-api-key", "any-key")
        .send()
        .await
        .unwrap();
    assert_eq!(resp.status(), 404);
}

#[tokio::test]
async fn metrics_shows_per_backend_breakdown() {
    let base = spawn_multi_server().await;
    let client = Client::new();
    let resp = client
        .get(format!("{base}/metrics"))
        .header("x-api-key", "any-key")
        .send()
        .await
        .unwrap();
    assert_eq!(resp.status(), 200);
    let body: serde_json::Value = resp.json().await.unwrap();
    // Should have per-backend metrics
    assert!(body["backends"]["openai"].is_object());
    assert!(body["backends"]["gemini"].is_object());
    // And totals
    assert_eq!(body["total"]["requests_total"], 0);
}

#[tokio::test]
async fn default_route_count_tokens_works() {
    let base = spawn_multi_server().await;
    let client = Client::new();
    let resp = client
        .post(format!("{base}/v1/messages/count_tokens"))
        .header("x-api-key", "any-key")
        .header("content-type", "application/json")
        .body(r#"{"model":"claude-sonnet-4-6","max_tokens":100,"messages":[{"role":"user","content":"hello world"}]}"#)
        .send()
        .await
        .unwrap();
    assert_eq!(resp.status(), 200);
    let body: serde_json::Value = resp.json().await.unwrap();
    assert!(body["input_tokens"].as_u64().unwrap() > 0);
}

#[tokio::test]
async fn prefixed_count_tokens_works() {
    let base = spawn_multi_server().await;
    let client = Client::new();
    let resp = client
        .post(format!("{base}/openai/v1/messages/count_tokens"))
        .header("x-api-key", "any-key")
        .header("content-type", "application/json")
        .body(r#"{"model":"claude-sonnet-4-6","max_tokens":100,"messages":[{"role":"user","content":"hello world"}]}"#)
        .send()
        .await
        .unwrap();
    assert_eq!(resp.status(), 200);
}