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.
stringfields default to"lorem ipsum"- String
formatvalues are recognized fordate,date-time,email,uri/url, anduuid integer,number, andbooleanfields return typed valuesarrayfields are populated from theiritemsschemaobjectfields are populated fromproperties- Dict-like schemas using
additionalPropertiesgenerate a small mock map with generated values anyOfandoneOfprefer non-nullvariants when both typed and nullable branches are present
Supported endpoints
GET /v1/modelsPOST /v1/chat/completionsPOST /v1/completionsPOST /v1/embeddingsPOST /v1/images/generationsPOST /v1/audio/speechPOST /v1/audio/transcriptionsPOST /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, orNonemyna.requests: all captured requests for the current testmyna.clear_requests(): clear capture history
Captured request records include:
method,path,query,headers,content_typejsonfor JSON payloadsformandfilesfor form and multipart uploadsbody_textandbody_base64for 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_urlmyna.headers(...)myna.path_with_scenario(...)myna.url_with_scenario(...)myna.last_requestmyna.requestsmyna.clear_requests()myna.next_response(...)myna.clear_seeded_responses()myna_urlmyna_base_urlmyna_scenario
Scope notes:
- Use
mynawhen you need request capture or seeded responses. - Use
myna_urlwhen you only want the resolved base URL plus themynahelper lifecycle. - Use
myna_base_urlfor a session-scoped server when request inspection is not needed.
Development
Run tests
uv run pytest
Changelog
Release history lives in CHANGELOG.md.
Metadata
Release files for mock-myna 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| mock_myna-0.2.1.tar.gz | 47.0 kB | With provenance | Details for mock_myna-0.2.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| mock_myna-0.2.1-py3-none-any.whl | Python 3 | none | any | Details for mock_myna-0.2.1-py3-none-any.whl |
Total release size: 67.9 kB
Release files / mock_myna-0.2.1.tar.gz
| Download URL | mock_myna-0.2.1.tar.gz |
|---|---|
| Size | 47.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ab49337cf7cb09752bd5f9b248295beaea4736072a79095b2de30d54586a58d2
|
|
BLAKE2b-256 checksum How to use checksums |
98646c1a533c9970ddcbaab46fc9132e2f9946214a14e44f197c88f290922fb4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 22, 2026.
Transparency logRelease files / mock_myna-0.2.1-py3-none-any.whl
| Download URL | mock_myna-0.2.1-py3-none-any.whl |
|---|---|
| Size | 20.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0773cb2d9b1f364b6634cc334f32e19686cc4e8e729aa41fd0ebd0df90e6ebf1
|
|
BLAKE2b-256 checksum How to use checksums |
0cf3cf22468cb8edf939873847648264624ba162547242f101a74f418b0981b6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 22, 2026.
Transparency logSecurity for mock-myna 0.2.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for mock_myna-0.2.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 22, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.