litellm/litellm-rust/crates/testkit/tests/live.rs
devin-ai-integration[bot] 9c10e0f985
feat(testkit): agent clients for Claude Code, Codex and opencode (#43181)
* feat(testkit): install and configure Claude Code, Codex and opencode against a gateway

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* refactor(testkit): derive targets from target-lexicon and split agents behind a trait

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* refactor(testkit): group sources into agent and install folders

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(testkit): split agents into install, configure and drive with semver-aware launch

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* style(testkit): drop a needless borrow

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Yujong Lee <yujong@berri.ai>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 17:27:30 +00:00

133 lines
4.9 KiB
Rust

//! Drives the real agents through a real gateway. Run with `cargo test -p litellm-testkit --test live -- --ignored`
//! after exporting `TESTKIT_GATEWAY_URL`, `TESTKIT_GATEWAY_KEY`, one `TESTKIT_MODEL_<WIRE>` per wire
//! (`MESSAGES`, `RESPONSES`, `CHAT_COMPLETIONS`) and one `TESTKIT_<AGENT>_VERSION` per agent
//! (`CLAUDE`, `CODEX`, `OPENCODE`). `TESTKIT_CACHE_DIR` and `GITHUB_TOKEN` are optional.
use std::path::PathBuf;
use std::time::Duration;
use litellm_testkit::{
Agent, ClaudeCode, Codex, HttpFetch, Installer, Opencode, Outcome, Prompt, Session, Settings,
Target, Version, Wire,
};
use rstest::rstest;
const LIMIT: Duration = Duration::from_secs(180);
fn required(name: &str) -> String {
std::env::var(name).unwrap_or_else(|_| panic!("{name} must be set to run the live tests"))
}
fn model_var(wire: Wire) -> &'static str {
match wire {
Wire::Messages => "TESTKIT_MODEL_MESSAGES",
Wire::Responses => "TESTKIT_MODEL_RESPONSES",
Wire::ChatCompletions => "TESTKIT_MODEL_CHAT_COMPLETIONS",
}
}
async fn drive(
agent: &impl Agent,
version_var: &str,
wire: Wire,
model: Option<&str>,
prompt: Prompt,
) -> Outcome {
let cache = std::env::var("TESTKIT_CACHE_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| std::env::temp_dir().join("litellm-testkit-cache"));
let installer = Installer::new(HttpFetch::from_env(), cache, Target::host().unwrap());
let installed = installer
.install(agent, &Version::parse(&required(version_var)).unwrap())
.await
.unwrap();
let settings = Settings {
base_url: required("TESTKIT_GATEWAY_URL"),
api_key: required("TESTKIT_GATEWAY_KEY"),
model: model.map_or_else(|| required(model_var(wire)), str::to_owned),
wire,
};
let home = tempfile::tempdir().unwrap();
let session = Session::prepare(agent, &installed, settings, home.path()).unwrap();
session.run(agent, &prompt, LIMIT).await.unwrap()
}
fn text_prompt() -> Prompt {
Prompt {
text: "Reply with the single word: pong".to_owned(),
allow_tools: false,
}
}
fn tool_prompt() -> Prompt {
Prompt {
text: "Run the shell command 'echo tool-ok' and reply with exactly its output.".to_owned(),
allow_tools: true,
}
}
#[rstest]
#[case::claude_messages(&ClaudeCode, "TESTKIT_CLAUDE_VERSION", Wire::Messages)]
#[case::codex_responses(&Codex, "TESTKIT_CODEX_VERSION", Wire::Responses)]
#[case::opencode_chat(&Opencode, "TESTKIT_OPENCODE_VERSION", Wire::ChatCompletions)]
#[case::opencode_responses(&Opencode, "TESTKIT_OPENCODE_VERSION", Wire::Responses)]
#[case::opencode_messages(&Opencode, "TESTKIT_OPENCODE_VERSION", Wire::Messages)]
#[ignore = "needs a live gateway, see the module docs"]
#[tokio::test]
async fn plain_prompt_gets_an_answer_and_token_usage(
#[case] agent: &impl Agent,
#[case] version_var: &str,
#[case] wire: Wire,
) {
let outcome = drive(agent, version_var, wire, None, text_prompt()).await;
assert!(outcome.succeeded(), "{outcome:?}");
assert!(outcome.text.to_lowercase().contains("pong"), "{outcome:?}");
assert!(outcome.usage.output_tokens > 0, "{outcome:?}");
}
#[rstest]
#[case::claude_messages(&ClaudeCode, "TESTKIT_CLAUDE_VERSION", Wire::Messages)]
#[case::codex_responses(&Codex, "TESTKIT_CODEX_VERSION", Wire::Responses)]
#[case::opencode_chat(&Opencode, "TESTKIT_OPENCODE_VERSION", Wire::ChatCompletions)]
#[case::opencode_responses(&Opencode, "TESTKIT_OPENCODE_VERSION", Wire::Responses)]
#[case::opencode_messages(&Opencode, "TESTKIT_OPENCODE_VERSION", Wire::Messages)]
#[ignore = "needs a live gateway, see the module docs"]
#[tokio::test]
async fn tool_use_is_reported_and_its_result_reaches_the_answer(
#[case] agent: &impl Agent,
#[case] version_var: &str,
#[case] wire: Wire,
) {
let outcome = drive(agent, version_var, wire, None, tool_prompt()).await;
assert!(outcome.succeeded(), "{outcome:?}");
assert!(!outcome.tool_calls.is_empty(), "{outcome:?}");
assert!(outcome.text.contains("tool-ok"), "{outcome:?}");
}
#[rstest]
#[case::claude_messages(&ClaudeCode, "TESTKIT_CLAUDE_VERSION", Wire::Messages)]
#[case::codex_responses(&Codex, "TESTKIT_CODEX_VERSION", Wire::Responses)]
#[case::opencode_chat(&Opencode, "TESTKIT_OPENCODE_VERSION", Wire::ChatCompletions)]
#[case::opencode_responses(&Opencode, "TESTKIT_OPENCODE_VERSION", Wire::Responses)]
#[case::opencode_messages(&Opencode, "TESTKIT_OPENCODE_VERSION", Wire::Messages)]
#[ignore = "needs a live gateway, see the module docs"]
#[tokio::test]
async fn model_the_gateway_rejects_is_reported_as_an_error(
#[case] agent: &impl Agent,
#[case] version_var: &str,
#[case] wire: Wire,
) {
let outcome = drive(
agent,
version_var,
wire,
Some("testkit-no-such-model"),
text_prompt(),
)
.await;
assert!(!outcome.succeeded(), "{outcome:?}");
assert!(!outcome.errors.is_empty(), "{outcome:?}");
}