- name
- llmposter
- description
- Mock HTTP server for LLM provider APIs (OpenAI, Anthropic, Gemini, Responses API). Use when writing integration tests that need deterministic, controllable LLM API responses without calling real providers. Supports fixture-based request matching, SSE streaming, failure injection, auth simulation, VCR record/replay modes, and all four provider response formats.
- license
- AGPL-3.0-or-later
- metadata
- {"author":"SkillDoAI","version":"0.5.0","ecosystem":"rust","generated-by":"skilldo/claude-haiku-4-5-20251001 + review:claude-haiku-4-5-20251001"}
# llmposter
Mock HTTP server for LLM provider APIs. Clients point their base URL at llmposter and interact using real API paths — no code changes beyond the URL swap. Fixtures define request matchers and canned responses for Anthropic (`POST /v1/messages`), OpenAI (`POST /v1/chat/completions`, `POST /v1/completions`), Gemini (non-streaming `POST /v1beta/models/{model}:generateContent`, streaming `POST /v1beta/models/{model}:streamGenerateContent`), and Responses API (`POST /v1/responses`). Response format automatically matches provider (no prefixing). New in 0.5.0: VCR record/replay modes for recording live upstream responses and deterministic replay; new endpoints `GET /health`, `GET /v1/models`, `POST /v1/embeddings`, `POST /v1/moderations`.
## Imports
```rust
// Core types (re-exported at crate root)
use llmposter::{Fixture, Provider, ServerBuilder, MockServer, CapturedRequest};
// Fixture sub-types
use llmposter::fixture::FailureConfig;
// VCR types (0.5.0+, gated by "record" feature)
use llmposter::record::VcrMode;
```
```toml
[dependencies]
llmposter = "0.5.0"
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.13", default-features = false, features = ["json"] }
serde_json = "1"
# Optional: Disable default features to minimize dependencies
# llmposter = { version = "0.5.0", default-features = false, features = ["watch"] }
# Optional: UI debug interface
# llmposter = { version = "0.5.0", features = ["ui"] }
```
**Default features:** `oauth`, `watch`, `jsonpath`, `record`
**Optional features:** `ui`, `templating`
## Core Patterns
### Quick Start
```rust
mod quick_start {
use llmposter::{Fixture, ServerBuilder};
#[tokio::test]
async fn test_basic_mock_server() -> Result<(), Box<dyn std::error::Error>> {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_user_message("hello")
.respond_with_content("Hi from Claude mock!"),
)
.build()
.await?;
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/v1/messages", server.url()))
.json(&serde_json::json!({
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "hello world"}]
}))
.send()
.await?;
assert_eq!(resp.status(), 200);
let body: serde_json::Value = resp.json().await?;
assert_eq!(body["type"], "message");
assert_eq!(body["content"][0]["text"], "Hi from Claude mock!");
assert_eq!(body["stop_reason"], "end_turn");
assert!(body["id"].as_str().unwrap().starts_with("msg-llmposter-"));
assert!(body["usage"]["input_tokens"].as_u64().unwrap_or(0) > 0);
Ok(())
}
}
```
### Tool Use Response
```rust
mod tool_use_example {
use llmposter::fixture::ToolCall;
use llmposter::{Fixture, ServerBuilder};
#[tokio::test]
async fn test_tool_use_response() -> Result<(), Box<dyn std::error::Error>> {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_user_message("weather")
.respond_with_tool_calls(vec![ToolCall {
name: "get_weather".to_string(),
arguments: serde_json::json!({
"location": "London",
"unit": "celsius"
}),
}]),
)
.build()
.await?;
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/v1/messages", server.url()))
.json(&serde_json::json!({
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "What's the weather?"}]
}))
.send()
.await?;
assert_eq!(resp.status(), 200);
let body: serde_json::Value = resp.json().await?;
assert_eq!(body["stop_reason"], "tool_use");
assert_eq!(body["content"][0]["type"], "tool_use");
assert_eq!(body["content"][0]["name"], "get_weather");
let tool_id = body["content"][0]["id"].as_str().unwrap();
assert!(tool_id.starts_with("toolu_llmposter_"));
assert_eq!(body["content"][0]["input"]["location"], "London");
Ok(())
}
}
```
### SSE Streaming
```rust
mod streaming_example {
use llmposter::{Fixture, ServerBuilder};
#[tokio::test]
async fn test_streaming_response() -> Result<(), Box<dyn std::error::Error>> {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_user_message("hello")
.respond_with_content("Hello world")
.with_streaming(Some(0), Some(5)), // latency=0ms, chunk_size=5 chars
)
.build()
.await?;
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/v1/messages", server.url()))
.json(&serde_json::json!({
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "hello"}],
"stream": true
}))
.send()
.await?;
assert_eq!(resp.status(), 200);
let text = resp.text().await?;
assert!(text.contains("event: message_start"));
assert!(text.contains("event: content_block_delta"));
assert!(text.contains("event: message_stop"));
Ok(())
}
}
```
### Error Simulation
```rust
mod error_simulation {
use llmposter::{Fixture, ServerBuilder};
#[tokio::test]
async fn test_rate_limit_error() -> Result<(), Box<dyn std::error::Error>> {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_user_message("rate limit")
.with_error(429, "Rate limit exceeded"),
)
.build()
.await?;
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/v1/messages", server.url()))
.json(&serde_json::json!({
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "rate limit test"}]
}))
.send()
.await?;
assert_eq!(resp.status(), 429);
let body: serde_json::Value = resp.json().await?;
assert_eq!(body["error"]["message"], "Rate limit exceeded");
Ok(())
}
}
```
### Failure Injection (Latency)
```rust
mod latency_injection {
use llmposter::fixture::FailureConfig;
use llmposter::{Fixture, ServerBuilder};
use std::time::Instant;
#[tokio::test]
async fn test_latency_injection() -> Result<(), Box<dyn std::error::Error>> {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.respond_with_content("delayed response")
.with_failure(FailureConfig {
latency_ms: Some(200),
..Default::default()
}),
)
.build()
.await?;
let start = Instant::now();
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/v1/messages", server.url()))
.json(&serde_json::json!({
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "test"}]
}))
.send()
.await?;
assert_eq!(resp.status(), 200);
assert!(start.elapsed().as_millis() >= 180);
let body: serde_json::Value = resp.json().await?;
assert_eq!(body["content"][0]["text"], "delayed response");
Ok(())
}
}
```
### Corrupt Body (Overloaded Simulation)
```rust
mod corrupt_body_example {
use llmposter::fixture::FailureConfig;
use llmposter::{Fixture, ServerBuilder};
#[tokio::test]
async fn test_corrupt_body() -> Result<(), Box<dyn std::error::Error>> {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.respond_with_content("should not appear")
.with_failure(FailureConfig {
corrupt_body: Some(true),
..Default::default()
}),
)
.build()
.await?;
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/v1/messages", server.url()))
.json(&serde_json::json!({
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "test"}]
}))
.send()
.await?;
assert_eq!(resp.status(), 200);
let text = resp.text().await?;
assert_eq!(text, "overloaded");
Ok(())
}
}
```
### Provider Filtering
```rust
mod provider_filtering {
use llmposter::{Fixture, Provider, ServerBuilder};
#[tokio::test]
async fn test_provider_filters() -> Result<(), Box<dyn std::error::Error>> {
View on GitHub