Skip to main content

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.

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)

Source distribution for mock-myna 0.2.1
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)

Table of built distributions (wheels) for mock-myna 0.2.1
File Interpreter ABI Platform Actions
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page