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.
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ab49337cf7cb09752bd5f9b248295beaea4736072a79095b2de30d54586a58d2
|
|
| MD5 |
b59681bd53f89cccfccec352312355d7
|
|
| BLAKE2b-256 |
98646c1a533c9970ddcbaab46fc9132e2f9946214a14e44f197c88f290922fb4
|
Provenance
The following attestation bundles were made for mock_myna-0.2.1.tar.gz:
Publisher:
ci-release.yml on tijnschouten/myna
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mock_myna-0.2.1.tar.gz -
Subject digest:
ab49337cf7cb09752bd5f9b248295beaea4736072a79095b2de30d54586a58d2 - Sigstore transparency entry: 1154998526
- Sigstore integration time:
-
Permalink:
tijnschouten/myna@4acf11a571f10c15ef75b666ee20e6db1013969c -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/tijnschouten
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci-release.yml@4acf11a571f10c15ef75b666ee20e6db1013969c -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0773cb2d9b1f364b6634cc334f32e19686cc4e8e729aa41fd0ebd0df90e6ebf1
|
|
| MD5 |
3c08ec37236d65f7b4a20fc55f75d0b4
|
|
| BLAKE2b-256 |
0cf3cf22468cb8edf939873847648264624ba162547242f101a74f418b0981b6
|
Provenance
The following attestation bundles were made for mock_myna-0.2.1-py3-none-any.whl:
Publisher:
ci-release.yml on tijnschouten/myna
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mock_myna-0.2.1-py3-none-any.whl -
Subject digest:
0773cb2d9b1f364b6634cc334f32e19686cc4e8e729aa41fd0ebd0df90e6ebf1 - Sigstore transparency entry: 1154998529
- Sigstore integration time:
-
Permalink:
tijnschouten/myna@4acf11a571f10c15ef75b666ee20e6db1013969c -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/tijnschouten
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci-release.yml@4acf11a571f10c15ef75b666ee20e6db1013969c -
Trigger Event:
push
-
Statement type: