Локальный фейковый эндпоинт спасает, когда код уже написан, а тратить деньги и квоту реального провайдера на прогон автотестов не хочется: мок-сервер отвечает в формате OpenAI API (это де-факто стандартная схема запроса `POST /v1/chat/completions` и ответа с полем `choices`), поэтому ваш клиент, SDK или пайплайн видит привычный интерфейс, но за ним стоит ваш собственный код. Гайд рассчитан на Python 3.9+ и любой компьютер — Linux, macOS или Windows, — где есть интернет только для установки пакетов; после установки сервер работает полностью офлайн.

Установка выглядит так:
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install fastapi uvicorn
Почему это работает: виртуальное окружение изолирует пакеты проекта, чтобы мок-сервер не конфликтовал с другими вашими библиотеками.

1. Создайте файл `mock_llm.py` в пустой папке. Это единственный файл, который нужен: в нём будет и роутер, и логика ответа.
2. Опишите две «ручки»: `GET /v1/models` — список доступных моделей, и `POST /v1/chat/completions` — генерация ответа. Клиенты при старте часто проверяют список моделей, поэтому без первой ручки вторая может даже не вызваться.
Пример:
# mock_llm.py
import time
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class ChatRequest(BaseModel):
model: str = "mock-model"
messages: list = []
temperature: float = 0.0
stream: bool = False
@app.get("/v1/models")
def models():
return {"object": "list",
"data": [{"id": "mock-model", "object": "model", "owned_by": "local"}]}
@app.post("/v1/chat/completions")
def chat(req: ChatRequest):
user_text = req.messages[-1]["content"] if req.messages else ""
time.sleep(0.3) # имитируем задержку сети
return {
"id": "chatcmpl-mock-1",
"object": "chat.completion",
"created": int(time.time()),
"model": req.model,
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": f"ЭХО: {user_text}"},
"finish_reason": "stop"
}],
"usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}
}
Почему это работает: `pydantic`-модель `ChatRequest` автоматически проверяет тело запроса, а `time.sleep(0.3)` даёт вашим тестам реалистичную задержку, чтобы таймауты и спиннеры в UI вели себя как с настоящим провайдером.
3. Запустите сервер командой ниже из той же папки.
Пример:
uvicorn mock_llm:app --host 127.0.0.1 --port 8000 --reload
Ожидаемый результат — в терминале строки вида `Uvicorn running on http://127.0.0.1:8000` и `Application startup complete`. Флаг `--reload` перезапускает сервер при сохранении файла: удобно, пока правите ответы.
4. Проверьте, что сервер отвечает. **Пример:**
curl -s http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"mock-model","messages":[{"role":"user","content":"привет"}]}'
Ожидаемый результат — JSON с `"content": "ЭХО: привет"`. Если увидели именно эхо, значит связка «запрос → валидация → ответ» работает. Дальше можно менять текст ответа на что угодно: например, возвращать фиксированную JSON-строку, чтобы тестировать парсинг.
5. Подключите свой клиент. Направьте SDK на локальный адрес: у OpenAI-совместимых клиентов для этого есть параметр `base_url`, а ключ можно передать любой непустой, чтобы клиент не падал на проверке.
Пример:
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:8000/v1", api_key="test")
r = client.chat.completions.create(model="mock-model",
messages=[{"role": "user", "content": "привет"}])
print(r.choices[0].message.content) # ЭХО: привет
Почему это работает: клиент просто формирует HTTP-запрос по адресу `base_url + /chat/completions`. Обратите внимание на `/v1` в конце `base_url` — он не должен дублироваться дальше в коде.
6. Добавьте режимы для негативных тестов. Мок ценен тем, что умеет ломаться по команде: возвращать код `429` при определённом слове в промпте, отдавать `500` каждый третий запрос или «зависать» на 10 секунд для проверки таймаута. Реализуется это условиями внутри `chat()` — например, если в тексте есть подстрока `SLOW`, увеличивайте `time.sleep` до 10.
7. Проверьте сценарии со стримингом отдельно. Если клиент ставит `"stream": true`, а сервер отвечает обычным JSON, вы получите ошибку разбора. Начните с `stream=False` и только потом добавляйте `StreamingResponse`, отдающий кусочки в формате `data: {...}` с финальным `data: [DONE]`.
8. Прогоните автотесты против мока. Здесь появляется главный выигрыш: тесты становятся детерминированными и бесплатными. Если вы тестируете поиск по документам, посмотрите гайд Локальный RAG: как поднять Qdrant или Chroma на своём мини-ПК — мок-LLM удобно поставить рядом с локальной векторной базой, чтобы проверять весь путь «запрос → поиск → ответ» без внешних сервисов. А если ваш агент вызывает инструменты, пригодится материал MCP: что это и как подключить первый MCP-сервер: MCP — это протокол, по которому модель получает доступ к внешним инструментам, и мок умеет возвращать заранее заданный вызов инструмента, чтобы проверить обработку на своей стороне.
Чек-лист перед тем, как считать мок готовым:

Симптом: `curl: (7) Failed to connect to 127.0.0.1 port 8000` или `Connection refused`.Это значит, что никто не слушает порт. Проверьте, что окно с `uvicorn` ещё открыто и в нём нет трейсбека после старта. Затем убедитесь, что порт не занят: команда `lsof -i :8000` (Linux/macOS) или `netstat -ano | findstr :8000` (Windows) покажет процесс. Решение — либо остановить чужой процесс, либо поднять мок на другом порту: `uvicorn mock_llm:app --port 8010` и поменять `base_url` на `http://127.0.0.1:8010/v1`.
Симптом: `404 Not Found` в ответ на запрос к чату.Почти всегда это неверный путь: либо в `base_url` нет `/v1`, либо он есть и клиент добавляет свой второй `/v1`, получая `/v1/v1/chat/completions`. Решение — распечатать итоговый URL (`base_url` плюс путь) и сверить со списком ручек в коде мока. Быстрая проверка: `curl http://127.0.0.1:8000/v1/models` должен вернуть JSON, а не 404.
Симптом: `422 Unprocessable Entity` с описанием поля.Мок отклонил тело запроса, потому что структура не совпала со схемой. Частая причина — клиент шлёт `messages` как объект, а не список, или добавляет новые поля. Решение: посмотрите текст ошибки, там указано поле и причина; затем либо приведите запрос к схеме, либо ослабьте требования в `ChatRequest`, добавив значения по умолчанию или `dict` вместо строгого типа.
Симптом: ответ приходит целиком, хотя клиент ждал поток.Клиент поставил `"stream": true`, а мок вернул обычный JSON. Решение: либо временно отключите стриминг у клиента, либо реализуйте отдачу чанками и завершайте поток маркером `[DONE]`.
Нужен ли настоящий API-ключ, чтобы мок заработал?
Нет. Достаточно передать любую непустую строку, например `test`, потому что проверка ключа — забота сервера, а ваш мок её не делает. Если клиент требует ключ из переменной окружения, задайте её локально, например `OPENAI_API_KEY=test`.
Можно ли пользоваться мок-сервером без интернета?
Да, после установки `fastapi` и `uvicorn` сервер работает офлайн, потому что он ничего не запрашивает наружу. Это удобно в закрытых контурах и на машинах без доступа к внешним API.
Чем мок отличается от локальной модели вроде Ollama?
Мок не думает: он возвращает то, что вы в него заложили, и всегда одинаково. Локальная модель генерирует настоящий текст, но её ответы менее предсказуемы и требуют ресурсов. Для автотестов берут мок, для демо качества ответов — локальную модель.
Подходит ли такой сервер для проверки агентов и вызовов инструментов?
Да. Верните в ответе заранее заданный вызов инструмента, и ваш код обработает его как настоящий. Если инструменты у вас описаны через MCP, начните с гайда MCP: что это и как подключить первый MCP-сервер, а затем подставьте мок как источник ответов.