Skip to main content

llmposter

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.

Source facts

Repository
SkillDoAI/llmposter
Last source activity
October 17, 2026 at 08:53
Detected SKILL.md language
English
Stars
2
Forks
1

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
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
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub