Skip to main content

Deterministic mock server for LLM APIs. Test your AI code without spending tokens.

Project description

stubllm

Deterministic mock server for LLM APIs. Test your AI code without spending tokens.

CI PyPI Python License: MIT

Works with: OpenAI · Anthropic · Google Gemini


30-second quickstart

# 1. Install
pip install stubllm

# 2. Create a fixture
mkdir fixtures
cat > fixtures/chat.yaml << 'EOF'
fixtures:
  - name: "greeting"
    match:
      provider: openai
      messages:
        - role: user
          content:
            contains: "hello"
    response:
      content: "Hello! How can I help you today?"
EOF

# 3. Start the server
stubllm serve --port 8765

# 4. Point your code at it
export OPENAI_BASE_URL=http://localhost:8765/v1/
python your_app.py  # no real API calls, no tokens spent

Why stubllm?

stubllm Real API Ollama
Cost Free Paid Free
Speed <1ms 1-30s 5-30s
Deterministic
Works offline
No GPU needed
Pytest integration
Fixtures / record-replay
CI-friendly Slow/expensive Heavy

Installation

pip install stubllm

# With pytest support
pip install "stubllm[pytest]"

Fixture format

Fixtures are YAML (or JSON) files that map request patterns to responses.

Basic text response

fixtures:
  - name: "greeting"
    match:
      provider: openai          # openai | anthropic | gemini | any
      endpoint: /v1/chat/completions   # optional
      model: "gpt-4o"           # optional
      messages:
        - role: user
          content:
            contains: "hello"   # exact | contains | regex
    response:
      content: "Hello! How can I help you today?"
      usage:
        prompt_tokens: 10
        completion_tokens: 12
        total_tokens: 22

Tool call response

fixtures:
  - name: "weather_tool"
    match:
      provider: openai
      messages:
        - role: user
          content:
            contains: "weather"
      tools_present: true        # only match when tools are provided
    response:
      tool_calls:
        - id: "call_abc123"
          type: function
          function:
            name: "get_weather"
            arguments: '{"location": "Amsterdam"}'

Streaming with delay

fixtures:
  - name: "slow_story"
    match:
      messages:
        - role: user
          content:
            contains: "story"
    response:
      content: "Once upon a time..."
      stream_chunk_delay_ms: 50   # simulate realistic streaming speed

Error response

fixtures:
  - name: "rate_limit"
    match:
      messages:
        - role: user
          content:
            contains: "trigger_error"
    response:
      content: '{"error": {"message": "Rate limit exceeded", "type": "rate_limit_error"}}'
      http_status: 429

Content match strategies

# Exact match (highest priority)
content:
  exact: "Hello, world!"

# Substring match (case-insensitive)
content:
  contains: "weather"

# Regular expression
content:
  regex: "tell me.*joke"

Matching priority

Higher specificity = higher priority. When multiple fixtures match, the most specific wins:

  1. exact message content (score: +10)
  2. contains message content (score: +5)
  3. regex message content (score: +4)
  4. model specified (score: +2)
  5. tools_present specified (score: +2)
  6. provider specified (score: +1)
  7. Fallback (no match criteria)

Pytest plugin

Basic setup

# conftest.py — nothing needed, stubllm auto-registers as a pytest plugin
# The `stubllm_server` fixture is available automatically after installing stubllm
# test_my_app.py
import openai
from stubllm.pytest_plugin import use_fixtures

@use_fixtures("fixtures/chat.yaml")
def test_greeting(stubllm_server):
    client = openai.OpenAI(
        base_url=stubllm_server.openai_url,  # includes /v1/ — openai SDK needs this
        api_key="test-key"
    )
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": "hello world"}]
    )
    assert "Hello" in response.choices[0].message.content
    assert stubllm_server.call_count == 1

Assertion helpers

def test_with_assertions(stubllm_server):
    # ... make some calls ...

    # Assert specific prompt was sent
    stubllm_server.assert_called_with_prompt("hello")

    # Assert number of calls
    assert stubllm_server.call_count == 2

    # Inspect all calls
    for call in stubllm_server.calls:
        print(call["path"], call["body"])

    # Assert last call path
    stubllm_server.assert_last_call_path("/v1/chat/completions")

Multiple fixture files

@use_fixtures("fixtures/chat.yaml", "fixtures/tools.yaml")
def test_combined(stubllm_server):
    ...  # both fixture files are active for this test

Multi-provider support

OpenAI

import openai

client = openai.OpenAI(
    base_url="http://localhost:8765/v1/",  # note: /v1/ required — the OpenAI SDK does not add it
    api_key="test-key"
)
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "hello"}]
)

Anthropic

import anthropic

client = anthropic.Anthropic(
    base_url="http://localhost:8765",  # Anthropic SDK adds /v1/ itself
    api_key="test-key"
)
message = client.messages.create(
    model="claude-opus-4-6",
    max_tokens=1024,
    messages=[{"role": "user", "content": "hello"}]
)

Google Gemini

import google.generativeai as genai

genai.configure(
    api_key="test-key",
    client_options={"api_endpoint": "localhost:8765"}
)
model = genai.GenerativeModel("gemini-pro")
response = model.generate_content("hello")

Streaming

All providers support streaming. Fixtures work identically — streaming is controlled by the stream: true parameter in the request, not the fixture.

# OpenAI streaming
stream = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "hello"}],
    stream=True,
)
for chunk in stream:
    print(chunk.choices[0].delta.content, end="", flush=True)

Control streaming speed in fixtures:

response:
  content: "A long streaming response..."
  stream_chunk_delay_ms: 20   # default: 20ms between chunks

Record and replay

Record real API interactions for later replay:

# Start in record mode (proxies to real OpenAI, saves fixtures)
stubllm record \
  --target https://api.openai.com \
  --fixture-dir ./recorded_fixtures

# Run your app against the recording proxy
OPENAI_BASE_URL=http://localhost:8765/v1/ python your_app.py

# Fixtures are saved to ./recorded_fixtures/
ls recorded_fixtures/
# recorded_hello_world_1706000000.yaml
# recorded_weather_query_1706000001.yaml

Recorded fixtures are sanitized (API keys removed) and can be committed to your repo.


CLI reference

# Start server (auto-loads ./fixtures/ if it exists)
stubllm serve

# Custom port and fixture directory
stubllm serve --port 9000 --fixture-dir ./my-fixtures

# Multiple fixture directories
stubllm serve --fixture-dir ./fixtures/openai --fixture-dir ./fixtures/anthropic

# Individual fixture files
stubllm serve --fixture-file chat.yaml --fixture-file tools.yaml

# Record mode
stubllm record --target https://api.openai.com --fixture-dir ./recorded

# Version
stubllm --version

Structured output (JSON schema)

When response_format: { type: "json_schema" } is set, stubllm validates that the fixture response is valid JSON. If it's not, it wraps the content automatically.

fixtures:
  - name: "structured"
    match:
      provider: openai
    response:
      content: '{"name": "Alice", "age": 30}'  # must be valid JSON

Project structure

stubllm/
├── src/stubllm/
│   ├── fixtures/       # YAML/JSON loading, Pydantic models, matching engine
│   ├── providers/      # OpenAI, Anthropic, Gemini endpoint handlers
│   ├── streaming/      # SSE streaming simulation
│   ├── recorder/       # Record-and-replay proxy
│   ├── pytest_plugin/  # pytest fixtures and @use_fixtures decorator
│   ├── server.py       # FastAPI app factory
│   └── cli.py          # click CLI
├── tests/              # >80% coverage
└── examples/           # Working examples (basic + advanced)

Contributing

git clone https://github.com/your-org/stubllm
cd stubllm
pip install -e ".[dev]"
pytest tests/ -v

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

stubllm-0.1.0.tar.gz (62.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

stubllm-0.1.0-py3-none-any.whl (27.0 kB view details)

Uploaded Python 3

File details

Details for the file stubllm-0.1.0.tar.gz.

File metadata

  • Download URL: stubllm-0.1.0.tar.gz
  • Upload date:
  • Size: 62.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for stubllm-0.1.0.tar.gz
Algorithm Hash digest
SHA256 b0108d4e8fc2c07db48f7f19ac031e5e6a59f50a15c39a7b222c9989a984a462
MD5 7d5831baaf5bc35cab5d4c9728bcb265
BLAKE2b-256 52938894e3580758a12a3ba26a4a9d7d07a3a93a6ba68fab5e06028a49a36ea3

See more details on using hashes here.

File details

Details for the file stubllm-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: stubllm-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 27.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for stubllm-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6a88e7e8cb576347e7a8dc3bef31315c799bf64f050c57ef90ed08801dde66c4
MD5 c3ca258299027bdcb04b2a48bc7f3281
BLAKE2b-256 2673f8dc242adfa527b213369aa4f55d1f9b4f1daaf10bf83ecbfa89acf783c3

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page