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.
소스 정보
- 저장소
- SkillDoAI/llmposter
- 최근 소스 활동
- 2026년 10월 17일 08:53
- 감지된 SKILL.md 언어
- 영어
- 스타
- 2
- 포크
- 1
설치 방법
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
소스 파일 검토
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
SKILL.md 표시 중
SKILL.md
소스 지침 · 읽기 전용 미리보기- 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>> {
GitHub에서 보기이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기