Local mock OpenAI-compatible server for development and tests.
Project description
myna
myna is a local FastAPI server that mimics core OpenAI-compatible REST API endpoints for
development and automated testing.
PyPI package: mock-myna (import path remains myna).
Run locally
uv sync
uv run uvicorn myna.main:app --reload --port 8000
SDK usage (Python and JS)
You can keep this in README for now. A dedicated docs page is only useful once this
section becomes large or versioned.
Python (openai SDK)
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 (openai SDK)
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);
Endpoints
GET /v1/modelsPOST /v1/chat/completionsPOST /v1/completionsPOST /v1/embeddingsPOST /v1/images/generationsPOST /v1/audio/speechPOST /v1/audio/transcriptionsPOST /v1/audio/translations
Scenario header
Use X-Mock-Scenario (or ?scenario=) 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
List models
curl http://localhost:8000/v1/models
Chat completion (non-streaming)
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."}
]
}'
Chat completion (streaming SSE)
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."}]
}'
JSON mode using tool schema
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model":"mock-chat-v1",
"response_format":{"type":"json_object"},
"messages":[{"role":"user","content":"Return structured output"}],
"tools":[
{
"type":"function",
"function":{
"name":"create_item",
"parameters":{
"type":"object",
"properties":{
"title":{"type":"string"},
"priority":{"type":"integer"},
"done":{"type":"boolean"}
}
}
}
}
]
}'
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 (url vs base64)
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 (wav or mp3)
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 examples
# deterministic 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"}]}'
Using Myna as pytest mock endpoint for your tool
If your code under test calls an LLM endpoint over HTTP, load the built-in Myna pytest fixtures and inject its base URL via env var/config.
# tests/conftest.py
pytest_plugins = ["myna.pytest_plugin"]
Or import fixtures directly in conftest.py:
from myna.pytest_plugin import myna, myna_base_url, myna_scenario
# app/tool.py
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 ""
# tests/test_tool.py
import os
from app.tool import summarize
def test_summarize_uses_mock_endpoint(myna_base_url):
os.environ["LLM_BASE_URL"] = myna_base_url
os.environ["LLM_API_KEY"] = "mock"
os.environ["LLM_MODEL"] = "mock-chat-v1"
out = summarize("hello from pytest")
assert "Mock response" in out
# tests/test_tool_errors.py
import os
import pytest
from app.tool import summarize
@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)
monkeypatch.setenv("LLM_API_KEY", "mock")
monkeypatch.setenv("LLM_MODEL", "mock-chat-v1")
# Use myna.headers() or myna.url_with_scenario() in your HTTP client path.
# For OpenAI SDK wrappers, pass scenario headers through your transport hook.
headers = myna.headers()
assert headers["X-Mock-Scenario"] == "error=rate_limit"
Provided fixtures:
myna_base_url: starts one Myna server per test session and returns/v1base URL.myna_scenario: optional indirect-param fixture for scenario strings.myna: helper object withbase_url,headers(...), andurl_with_scenario(...).
Tests
uv run pytest
Publish to GitHub and PyPI
1) Create git repo and push to GitHub
git init
git add .
git commit -m "Initial release: myna mock API server"
git branch -M main
git remote add origin <your-github-repo-url>
git push -u origin main
2) Configure PyPI trusted publishing
In PyPI:
- Create project
mock-myna(or claim name if available). - Go to project settings > Publishing.
- Add a trusted publisher with:
- Owner: your GitHub org/user
- Repository: your repo name
- Workflow:
.github/workflows/ci-publish.yml - Environment:
pypi
In GitHub:
- Repo settings > Environments > create environment
pypi.
3) Release by pushing a version tag
Version comes from pyproject.toml ([project].version).
git tag v0.1.0
git push origin v0.1.0
This triggers GitHub Actions to run lint/tests and publish to PyPI on tag push.
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.1.0.tar.gz.
File metadata
- Download URL: mock_myna-0.1.0.tar.gz
- Upload date:
- Size: 40.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5d1aedb7e93388dcdc71edb685e3a40682ec22726ac543f8efc47c7a9be985f6
|
|
| MD5 |
36efcd0546bb00fa524f846dc987fefd
|
|
| BLAKE2b-256 |
a61a58396910294aa757004d1aaf4444f336520a6b0ce7d9c1eedb8477a4cc9e
|
Provenance
The following attestation bundles were made for mock_myna-0.1.0.tar.gz:
Publisher:
ci-publish.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.1.0.tar.gz -
Subject digest:
5d1aedb7e93388dcdc71edb685e3a40682ec22726ac543f8efc47c7a9be985f6 - Sigstore transparency entry: 1122709250
- Sigstore integration time:
-
Permalink:
tijnschouten/myna@bde699ab1aee23761c900b44df77668810e6429f -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/tijnschouten
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci-publish.yml@bde699ab1aee23761c900b44df77668810e6429f -
Trigger Event:
push
-
Statement type:
File details
Details for the file mock_myna-0.1.0-py3-none-any.whl.
File metadata
- Download URL: mock_myna-0.1.0-py3-none-any.whl
- Upload date:
- Size: 16.6 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 |
0a0fd93a476af418e610039ebfd0e3be58e204d080d80aa1b2dab313b6314b85
|
|
| MD5 |
671c3c27790b19c81d4f52f10d682b87
|
|
| BLAKE2b-256 |
0eff3607700f14b3684f4d42b90bf435da95f1108188d210d18972924b8f59e1
|
Provenance
The following attestation bundles were made for mock_myna-0.1.0-py3-none-any.whl:
Publisher:
ci-publish.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.1.0-py3-none-any.whl -
Subject digest:
0a0fd93a476af418e610039ebfd0e3be58e204d080d80aa1b2dab313b6314b85 - Sigstore transparency entry: 1122709270
- Sigstore integration time:
-
Permalink:
tijnschouten/myna@bde699ab1aee23761c900b44df77668810e6429f -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/tijnschouten
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci-publish.yml@bde699ab1aee23761c900b44df77668810e6429f -
Trigger Event:
push
-
Statement type: