Skip to main content

Local mock OpenAI-compatible server for development and tests.

Project description

Myna

Myna is a local mock server for OpenAI-compatible APIs.

Use it when you want your application to talk to a predictable HTTP endpoint during local development or tests without calling a real model provider.

Package name: mock-myna
Import path: myna
PyPI | GHCR

What it provides

  • OpenAI-style REST endpoints under /v1
  • Stable mock responses for local development
  • Scenario controls for delays and error injection
  • A pytest plugin that starts the server and captures outgoing requests
  • One-shot seeded responses for parser and failure-path tests

Structured JSON mode

For chat completions with response_format.type set to json_object or json_schema, Myna inspects the provided JSON Schema and generates stable mock values by schema shape.

  • string fields default to "lorem ipsum"
  • String format values are recognized for date, date-time, email, uri/url, and uuid
  • integer, number, and boolean fields return typed values
  • array fields are populated from their items schema
  • object fields are populated from properties
  • Dict-like schemas using additionalProperties generate a small mock map with generated values
  • anyOf and oneOf prefer non-null variants when both typed and nullable branches are present

Supported endpoints

  • GET /v1/models
  • POST /v1/chat/completions
  • POST /v1/completions
  • POST /v1/embeddings
  • POST /v1/images/generations
  • POST /v1/audio/speech
  • POST /v1/audio/transcriptions
  • POST /v1/audio/translations

Quick start

Run locally

uv sync
uv run uvicorn myna.main:app --reload --port 8000

The server will be available at http://localhost:8000/v1.

Run with Docker

docker build -t myna .
docker run --rm -p 8000:8000 myna

Using it with the OpenAI SDK

Point your client at the local /v1 base URL and use any placeholder API key.

Python

from openai import OpenAI

client = OpenAI(
    api_key="mock",
    base_url="http://localhost:8000/v1",
)

resp = client.chat.completions.create(
    model="mock-chat-v1",
    messages=[{"role": "user", "content": "Say hello"}],
)

print(resp.choices[0].message.content)

JavaScript / TypeScript

import OpenAI from "openai";

const client = new OpenAI({
  apiKey: "mock",
  baseURL: "http://localhost:8000/v1",
});

const resp = await client.chat.completions.create({
  model: "mock-chat-v1",
  messages: [{ role: "user", content: "Say hello" }],
});

console.log(resp.choices[0]?.message?.content);

Example requests

List models

curl http://localhost:8000/v1/models

Chat completion

curl http://localhost:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model":"mock-chat-v1",
    "messages":[
      {"role":"system","content":"You are concise."},
      {"role":"user","content":"Give me one sentence about Amsterdam."}
    ]
  }'

Streaming chat completion

curl -N http://localhost:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model":"mock-chat-v1",
    "stream":true,
    "messages":[{"role":"user","content":"Stream this response."}]
  }'

Legacy completions

curl http://localhost:8000/v1/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"mock-chat-v1","prompt":"Write a short greeting."}'

Embeddings

curl http://localhost:8000/v1/embeddings \
  -H "Content-Type: application/json" \
  -d '{
    "model":"mock-embedding-v1",
    "input":["first text","second text"]
  }'

Image generation

curl http://localhost:8000/v1/images/generations \
  -H "Content-Type: application/json" \
  -d '{"prompt":"a cat in a bike basket","response_format":"url"}'

curl http://localhost:8000/v1/images/generations \
  -H "Content-Type: application/json" \
  -d '{"prompt":"a cat in a bike basket","response_format":"b64_json"}'

Audio speech

curl http://localhost:8000/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{"model":"mock-tts-v1","input":"Hello world","response_format":"wav"}' \
  --output speech.wav

curl http://localhost:8000/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{"model":"mock-tts-v1","input":"Hello world","response_format":"mp3"}' \
  --output speech.mp3

Audio transcription and translation

curl http://localhost:8000/v1/audio/transcriptions \
  -F "file=@sample.wav" \
  -F "model=mock-asr-v1"

curl http://localhost:8000/v1/audio/translations \
  -F "file=@sample.wav" \
  -F "model=mock-asr-v1"

Scenario controls

Use X-Mock-Scenario or the scenario query parameter to inject delays and failures.

curl http://localhost:8000/v1/chat/completions \
  -H "Authorization: Bearer test" \
  -H "X-Mock-Scenario: delay=500,error=rate_limit" \
  -H "Content-Type: application/json" \
  -d '{"model":"mock-chat-v1","messages":[{"role":"user","content":"hi"}]}'

Examples:

# auth error
curl http://localhost:8000/v1/chat/completions \
  -H "X-Mock-Scenario: error=auth" \
  -H "Content-Type: application/json" \
  -d '{"model":"mock-chat-v1","messages":[{"role":"user","content":"hello"}]}'

# delay + server error
curl http://localhost:8000/v1/chat/completions \
  -H "X-Mock-Scenario: delay=1200,error=server" \
  -H "Content-Type: application/json" \
  -d '{"model":"mock-chat-v1","messages":[{"role":"user","content":"hello"}]}'

# truncate SSE stream without [DONE]
curl -N http://localhost:8000/v1/chat/completions \
  -H "X-Mock-Scenario: stream_truncate" \
  -H "Content-Type: application/json" \
  -d '{"model":"mock-chat-v1","stream":true,"messages":[{"role":"user","content":"hello"}]}'

Pytest integration

Myna ships with a pytest plugin that starts a local server and gives you helpers for URL construction, scenario injection, request inspection, and response seeding.

Enable the plugin:

# tests/conftest.py
pytest_plugins = ["myna.pytest_plugin"]

Or import the fixtures directly:

from myna.pytest_plugin import myna, myna_base_url, myna_scenario, myna_url

Typical test setup

import os

from openai import OpenAI


def summarize(text: str) -> str:
    client = OpenAI(
        api_key=os.getenv("LLM_API_KEY", "mock"),
        base_url=os.getenv("LLM_BASE_URL", "http://localhost:8000/v1"),
    )
    resp = client.chat.completions.create(
        model=os.getenv("LLM_MODEL", "mock-chat-v1"),
        messages=[{"role": "user", "content": text}],
    )
    return resp.choices[0].message.content or ""


def test_summarize_uses_mock_endpoint(myna_base_url, monkeypatch):
    monkeypatch.setenv("LLM_BASE_URL", myna_base_url)
    monkeypatch.setenv("LLM_API_KEY", "mock")
    monkeypatch.setenv("LLM_MODEL", "mock-chat-v1")

    out = summarize("hello from pytest")
    assert "Mock response" in out

Scenario-aware tests

import pytest


@pytest.mark.parametrize("myna_scenario", ["error=rate_limit"], indirect=True)
def test_retry_path_with_rate_limit(myna, monkeypatch):
    monkeypatch.setenv("LLM_BASE_URL", myna.base_url)
    headers = myna.headers()

    assert headers["X-Mock-Scenario"] == "error=rate_limit"

If your code accepts a path instead of a full URL:

api_endpoint = myna.path_with_scenario("/audio/transcriptions", "error=rate_limit")
assert api_endpoint == "/audio/transcriptions?scenario=error%3Drate_limit"

Request capture

Use the myna fixture to inspect what your application actually sent:

  • myna.last_request: most recent request, or None
  • myna.requests: all captured requests for the current test
  • myna.clear_requests(): clear capture history

Captured request records include:

  • method, path, query, headers, content_type
  • json for JSON payloads
  • form and files for form and multipart uploads
  • body_text and body_base64 for raw-body assertions

Example:

def test_run_transcription_sends_correct_fields(myna, monkeypatch):
    monkeypatch.setenv("LLM_BASE_URL", myna.base_url)

    run_transcription(TEST_AUDIO)

    req = myna.last_request
    assert req is not None
    assert req.form["model"] == "whisper-mock"
    assert req.form["language"] == "nl"
    assert req.files["file"]["content_type"] == "audio/wav"

Internal instrumentation endpoints under /__myna power this feature. Requests to those internal endpoints are not added to the capture log.

One-shot seeded responses

For parser or error-path tests, seed the next matching response without patching your HTTP client:

def test_handles_empty_or_malformed_output(myna):
    myna.next_response(
        {"choices": [{"message": {"content": ""}}]},
        path="/chat/completions",
    )

    out = run_summary("input")
    assert out == ""

myna.next_response(...) matches by method and path and is consumed after one request.

Available helpers:

  • myna.base_url
  • myna.headers(...)
  • myna.path_with_scenario(...)
  • myna.url_with_scenario(...)
  • myna.last_request
  • myna.requests
  • myna.clear_requests()
  • myna.next_response(...)
  • myna.clear_seeded_responses()
  • myna_url
  • myna_base_url
  • myna_scenario

Scope notes:

  • Use myna when you need request capture or seeded responses.
  • Use myna_url when you only want the resolved base URL plus the myna helper lifecycle.
  • Use myna_base_url for a session-scoped server when request inspection is not needed.

Development

Run tests

uv run pytest

Changelog

Release history lives in CHANGELOG.md.

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

mock_myna-0.2.1.tar.gz (47.0 kB view details)

Uploaded Source

Built Distribution

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

mock_myna-0.2.1-py3-none-any.whl (20.9 kB view details)

Uploaded Python 3

File details

Details for the file mock_myna-0.2.1.tar.gz.

File metadata

  • Download URL: mock_myna-0.2.1.tar.gz
  • Upload date:
  • Size: 47.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for mock_myna-0.2.1.tar.gz
Algorithm Hash digest
SHA256 ab49337cf7cb09752bd5f9b248295beaea4736072a79095b2de30d54586a58d2
MD5 b59681bd53f89cccfccec352312355d7
BLAKE2b-256 98646c1a533c9970ddcbaab46fc9132e2f9946214a14e44f197c88f290922fb4

See more details on using hashes here.

Provenance

The following attestation bundles were made for mock_myna-0.2.1.tar.gz:

Publisher: ci-release.yml on tijnschouten/myna

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file mock_myna-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: mock_myna-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 20.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for mock_myna-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 0773cb2d9b1f364b6634cc334f32e19686cc4e8e729aa41fd0ebd0df90e6ebf1
MD5 3c08ec37236d65f7b4a20fc55f75d0b4
BLAKE2b-256 0cf3cf22468cb8edf939873847648264624ba162547242f101a74f418b0981b6

See more details on using hashes here.

Provenance

The following attestation bundles were made for mock_myna-0.2.1-py3-none-any.whl:

Publisher: ci-release.yml on tijnschouten/myna

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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