# OpenAI Responses API локально: какие серверы совместимы и как проверить tools

Источник: https://modelium.tech/zhurnal/openai-responses-api-lokalno/

Локальный endpoint /v1/responses есть у Ollama, LM Studio, llama.cpp и vLLM, но совпадение адреса не означает полной совместимости с облачным Responses API. Начните с текстового запроса, затем отдельно проверьте ответ с function tool и сценарий продолжения диалога. В Ollama предыдущий response ID не поддерживается; возможность модели предложить инструмент ещё не означает, что приложение его выполнило.

**Проверено по документации 25 сентября 2026 года.** Для llama.cpp использована закреплённая Git-ревизия, для vLLM — документация релиза 0.30.0. Modelium не запускал модели, не проверял скорость и не устанавливал перечисленные серверы. Команды ниже — сценарий проверки читателем, а не наш успешный прогон.

## Что именно совместимо с OpenAI Responses API?

В Responses API приложение отправляет `input` на `/v1/responses` и получает структуру `output`; если модель предлагает функцию, это отдельный элемент `function_call` с именем, аргументами и `call_id`. **Приложение**, а не модель, проверяет аргументы, выполняет разрешённое действие и передаёт `function_call_output` с соответствующим `call_id` следующим запросом [s1](#src-s1). При многошаговой работе состояние можно вести вручную, сохраняя элементы истории, либо использовать серверный `previous_response_id`, если реализация его действительно поддерживает [s2](#src-s2).

«OpenAI-compatible» не значит «всё работает одинаково». Официальная документация Ollama поддерживает `POST /v1/responses`, tools и streaming начиная с версии **0.13.3**, но прямо исключает stateful `previous_response_id` и `conversation` [s3](#src-s3). LM Studio описывает тот же маршрут и пример продолжения с `previous_response_id` [s4](#src-s4). В закреплённом README llama.cpp endpoint есть, но запрос преобразуется в Chat Completions: это важнее названия маршрута при переносе сложного агента [s5](#src-s5). vLLM 0.30.0 перечисляет Responses endpoints для **текстовых генеративных моделей**; наличие маршрута в документации не подтверждает, что ваша модель выполняет вызов инструмента [s6](#src-s6).

**Выбор по задаче:** если клиент просто передаёт текст, сначала проверьте текстовый ответ на выбранном runtime. Если клиент полагается на хранение истории по `previous_response_id`, не подставляйте Ollama без изменения логики: там нужен клиентский учёт истории [s2](#src-s2)[s3](#src-s3). Если нужен инструмент, тестируйте цикл «предложение → разрешённое действие → результат» на **своей** модели, а не только HTTP 200 [s1](#src-s1)[s7](#src-s7). Универсальную совместимость, качество и скорость эти документы не доказывают.

## Как подготовить локальный сервер, модель и железо?

Сначала выберите и установите **один** runtime, затем загрузите совместимую модель по инструкции самого runtime. [Ollama](/zapusk/ollama/) подойдёт для простого старта; [LM Studio llmster](/zhurnal/lm-studio-llmster-server/) — отдельный путь для headless-сервера; [llama.cpp](/zhurnal/llama-cpp/) и [vLLM](/zhurnal/vllm/) требуют собственного выбора формата и способа запуска. Это разные серверы: настраивайте точное имя **реально установленной** модели, не копируйте в запрос названия из чужого примера.

Официальные адреса тестовых endpoint: Ollama `http://127.0.0.1:11434/v1/responses`, LM Studio `http://127.0.0.1:1234/v1/responses`, llama.cpp `http://127.0.0.1:8080/v1/responses`; vLLM обычно использует порт сервера, который настроили при запуске [s3](#src-s3)[s4](#src-s4)[s5](#src-s5)[s6](#src-s6). `127.0.0.1` означает машину **процесса-клиента**: внутри контейнера или удалённой IDE это не обязательно хост с моделью. Проверьте адрес из той же среды, где работает ваш клиент; не публикуйте API-порт в интернет ради теста.

Размер файла весов **не является** минимальной RAM/VRAM. Чтобы выбрать модель, оцените её фактическое размещение и контекст на своём устройстве; если серверу не хватает памяти, возьмите меньший вариант и повторите. Документация по endpoint не заменяет тест на конкретной модели, железе и версии.

## Как проверить сам `/v1/responses` без облачного ключа?

Ниже — один тестовый запрос в **уже запущенный** локальный сервер. Укажите ваш URL, точный тег/ID модели и только безвредный текст. Установленный API-ключ конкретного сервера или прокси может требовать дополнительного заголовка; приведённый локальный запрос не пытается обойти настроенную аутентификацию. Ollama игнорирует заполнитель `api_key` у совместимого SDK на локальном сервере; это **не** ключ OpenAI и не обещание защиты порта [s3](#src-s3).

# Пример для Ollama; при выборе другого runtime замените адрес и точный model ID.export RESPONSES_URL='http://127.0.0.1:11434/v1/responses'export MODEL='вставьте-точный-тег-из-локального-списка'python3 - <<'PY'import json, os, urllib.request, urllib.parseurl = os.environ['RESPONSES_URL']model = os.environ['MODEL']parsed = urllib.parse.urlsplit(url)if (parsed.scheme != 'http' or parsed.hostname not in ('127.0.0.1', 'localhost')        or parsed.path != '/v1/responses' or not parsed.port or parsed.username or parsed.password        or model.startswith('вставьте-')):    raise SystemExit('Укажите локальный URL /v1/responses и существующий model ID')payload = {'model': model, 'input': 'Ответь одним словом: готово'}request = urllib.request.Request(url, data=json.dumps(payload).encode(), headers={'Content-Type': 'application/json'})with urllib.request.build_opener(urllib.request.ProxyHandler({})).open(request, timeout=90) as response:    result = json.load(response)print(json.dumps({'id': result.get('id'), 'output': result.get('output')}, ensure_ascii=False, indent=2))PY

**Критерий:** HTTP 200 с JSON и содержательным ответом в `output`, а не только работающий порт или `id`. Ответ другой модели, облачного endpoint или ошибка 404/401 тест не проходят. Для SDK `base_url` обычно указывает на `/v1`, а сам SDK добавляет `/responses`; в сыром HTTP выше используется полный путь [s3](#src-s3)[s5](#src-s5). Не передавайте реальные документы, ключи или данные клиентов в smoke-тест.

## Как отдельно проверить tools и хранение истории?

Следующий тест использует те же `RESPONSES_URL` и `MODEL`, что в предыдущей команде. Он просит модель **предложить**, но не выполнить функцию над синтетическим кодом. Если runtime или конкретная модель не умеют такой ответ, скрипт завершится с ошибкой — это результат проверки, а не повод выдавать обычный текст за вызов [s1](#src-s1)[s7](#src-s7).

python3 - <<'PY'import json, os, urllib.request, urllib.parseurl = os.environ['RESPONSES_URL']model = os.environ['MODEL']parsed = urllib.parse.urlsplit(url)if (parsed.scheme != 'http' or parsed.hostname not in ('127.0.0.1', 'localhost')        or parsed.path != '/v1/responses' or not parsed.port or parsed.username or parsed.password        or model.startswith('вставьте-')):    raise SystemExit('Нужны локальный URL /v1/responses и существующий model ID')payload = {    'model': model,    'input': 'Вызови lookup_demo с code DEMO-01. Не придумывай результат.',    'tools': [{'type': 'function', 'name': 'lookup_demo',               'description': 'Находит только тестовый код',               'parameters': {'type': 'object', 'properties': {'code': {'type': 'string'}},                              'required': ['code'], 'additionalProperties': False}}],}request = urllib.request.Request(url, data=json.dumps(payload).encode(), headers={'Content-Type': 'application/json'})with urllib.request.build_opener(urllib.request.ProxyHandler({})).open(request, timeout=90) as response:    result = json.load(response)calls = [item for item in result.get('output', []) if item.get('type') == 'function_call']if len(calls) != 1 or calls[0].get('name') != 'lookup_demo' or not calls[0].get('call_id'):    raise SystemExit('Тест не пройден: нет одного структурного вызова lookup_demo с call_id')args = json.loads(calls[0].get('arguments', '{}'))if args != {'code': 'DEMO-01'}:    raise SystemExit('Тест не пройден: аргументы вызова не совпали с тестовыми')print('Вызов предложен; call_id:', calls[0]['call_id'], '; действие НЕ выполнялось')PY

**Критерий:** в структурном `output` ровно один `function_call` с именем, проверенными аргументами и `call_id`. Текст «я вызвал функцию» или JSON внутри обычного ответа **не равен** tool call [s1](#src-s1). Этот скрипт намеренно **не исполняет** предложенное действие и не доказывает работоспособность полного агентного цикла. Не передавайте модели доступ к рабочей базе; полный безопасный цикл с проверкой аргументов и ответом инструмента описан [отдельно](/zhurnal/tool-calling-lokalnyh-modeley/) [s1](#src-s1)[s7](#src-s7).

Не делайте тест `previous_response_id` общим обязательным критерием для всех четырёх серверов: на Ollama это официально неподдерживаемое поле [s3](#src-s3). В LM Studio документация показывает stateful-продолжение с ID предыдущего ответа, но пример `resp_123` — **заполнитель**: отправлять нужно настоящий `id` предыдущего ответа своего сервера [s4](#src-s4). У OpenAI есть и ручной способ передачи истории; при переносе на локальный runtime сверяйте фактическое поведение ответа, а не только подпись метода SDK [s2](#src-s2). Для llama.cpp и vLLM здесь **не утверждается** одинаковая с LM Studio семантика хранения или повторного воспроизведения tool calls: их проверяют отдельно на своей версии [s5](#src-s5)[s6](#src-s6).

## Почему запрос проходит, но агент не работает?

**404:** сверьте порт, `/v1/responses` и версию runtime. В Ollama маршрут описан начиная с 0.13.3; переключение клиента на `/v1/chat/completions` меняет формат протокола и не считается проверкой Responses [s3](#src-s3). **Ошибка model not found:** проверьте точный локальный ID, запуск модели и место выполнения клиента. **Ответ без **`**function_call**`**:** сначала отделите сетевую доступность от способности модели предложить инструмент; проверьте схему, ограничения своей модели и трассу приложения [s1](#src-s1)[s7](#src-s7). **Ошибка продолжения:** не переиспользуйте `previous_response_id` между серверами и не передавайте его в Ollama; поддержка состояния различается [s2](#src-s2)[s3](#src-s3)[s4](#src-s4).

Для отката остановите тестовый сервер способом своей установки и удалите только специально созданные тестовые данные после проверки пути. Если использовали настоящий документ или открыли endpoint в локальную сеть, оцените доступ отдельно: наличие локальной модели не гарантирует изоляцию. Следующий практический шаг — [проверить полный цикл tool calling](/zhurnal/tool-calling-lokalnyh-modeley/), включая разрешения и передачу результата модели [s1](#src-s1)[s7](#src-s7).

## Источники

1. [OpenAI — Function calling, официальный протокол](https://developers.openai.com/api/docs/guides/function-calling.md) — получено 2026-09-25
2. [OpenAI — Conversation state, способы продолжения](https://developers.openai.com/api/docs/guides/conversation-state.md) — получено 2026-09-25
3. [Ollama — OpenAI compatibility, Responses API](https://docs.ollama.com/api/openai-compatibility.md) — получено 2026-09-25
4. [LM Studio — OpenAI-compatible Responses endpoint](https://lmstudio.ai/docs/developer/openai-compat/responses.md) — получено 2026-09-25
5. [llama.cpp — llama-server README, закреплённая ревизия](https://raw.githubusercontent.com/ggml-org/llama.cpp/217f81c266a7b7c986ee3d2c58e1cccee05a0744/tools/server/README.md) — получено 2026-09-25
6. [vLLM 0.30.0 — OpenAI-compatible server](https://docs.vllm.ai/en/v0.30.0/serving/online_serving/openai_compatible_server/) — получено 2026-09-25
7. [Ollama — Tool calling, цикл приложения](https://docs.ollama.com/capabilities/tool-calling.md) — получено 2026-09-25
