Skip to main content

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/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

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 /v1 base URL.
  • myna_scenario: optional indirect-param fixture for scenario strings.
  • myna: helper object with base_url, headers(...), and url_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


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.1.0.tar.gz (40.8 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.1.0-py3-none-any.whl (16.6 kB view details)

Uploaded Python 3

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

Hashes for mock_myna-0.1.0.tar.gz
Algorithm Hash digest
SHA256 5d1aedb7e93388dcdc71edb685e3a40682ec22726ac543f8efc47c7a9be985f6
MD5 36efcd0546bb00fa524f846dc987fefd
BLAKE2b-256 a61a58396910294aa757004d1aaf4444f336520a6b0ce7d9c1eedb8477a4cc9e

See more details on using hashes here.

Provenance

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

Publisher: ci-publish.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.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

Hashes for mock_myna-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0a0fd93a476af418e610039ebfd0e3be58e204d080d80aa1b2dab313b6314b85
MD5 671c3c27790b19c81d4f52f10d682b87
BLAKE2b-256 0eff3607700f14b3684f4d42b90bf435da95f1108188d210d18972924b8f59e1

See more details on using hashes here.

Provenance

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

Publisher: ci-publish.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