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で見る