Продолжаем разбираться с тем, как можно эффективно работать с большими языковыми моделями, используя доступное оборудование.
В этой части мы перейдём к организации распределённого инференса с помощью vLLM и обеспечим доступ к нему через Ray Serve. А ещё выясним, как запустить модель Gemma 3 в Ray-кластере и как проверить работу нашего OpenAI-совместимого эндпойнта с JWT-аутентификацией.

Если вы ещё не читали первую часть, стоит начать с неё. Там описано «железо» моей домашней лаборатории и процесс подготовки всего необходимого для развёртывания распределённого инференса с Ray Serve и vLLM.
Написание скрипта vLLM для шардирования, распределения вычислений и Ray Serve для вывода API
Начнём с ключевых концепций шардирования, параллелизации вычислений и принципов работы API.
Что такое vLLM и почему он выбран
vLLM — это фреймворк, который поддерживает параллельный инференс больших языковых моделей. Он предоставляет:
Нативную поддержку шардирования (tensor parallel): разбивает весовые тензоры модели между GPU в рамках одной ноды, позволяя каждому ускорителю обрабатывать свою часть вычислений одновременно.
Пайплайн-параллелизм (pipeline parallel): разбивает модель на последовательные блоки-слои и распределяет их по разным нодам: первые слои выполняются на GPU ноды А, следующие — на GPU ноды B и так далее. При этом пока нода А обрабатывает следующий запрос, нода B уже догоняет и завершает предыдущий — конвейер остаётся загруженным.
Интеграцию с NVIDIA Collective Communications Library (NCCL), которая обеспечивает сверхбыстрый обмен промежуточными активациями и градиентами между GPU и между нодами (в том числе по RDMA/InfiniBand), автоматически группируя все устройства в единый вычислительный пул.
Стриминговые ответы: отдаёт результат по мере генерации, как в OpenAI ChatCompletion API, чтобы клиенты видели ответ постепенно.
Вместе это даёт нам:
Объединённую видеопамять: все VRAM-карты работают как одно целое, суммируя свои ресурсы.
Постоянную загрузку: тензорные операции на каждой карте идут параллельно, а конвейерные этапы никогда не простаивают.
Горизонтальное масштабирование «из коробки»: независимо от того, 1 или 8 GPU на ноде и каково общее количество нод, vLLM + NCCL автоматически развернут любую большую модель сразу на десятках карт.
Распределение инференса и параметры шардирования
В конфиге vLLM задаём:
Tensor parallel size — сколько «кусочков» весов распределять по GPU внутри узла.
Pipeline parallel size — на сколько стадий дробим модель по нодам.
gpu_memory_utilization и cpu_offload_gb — параметры, которые помогают эффективно расходовать видеопамять и часть объёма оперативной памяти хоста.
Так мы балансируем память и производительность под свой кластер.
Что такое Ray Serve
Ray Serve — это компонент фреймворка Ray, который предоставляет микросервисную архитектуру для инференса:
Позволяет развернуть FastAPI-приложение и автоматически управлять его репликами.
Делает возможной горизонтальную масштабируемость — каждая реплика может быть на отдельном узле, если нужно.
Может эффективно работать с GPU, когда под каждый сервис зарезервирована часть видеокарт.
Мы используем Ray Serve, чтобы предоставить внешний HTTP API, совместимый с OpenAI-протоколами (ChatCompletion). Через этот API клиенты смогут делать запросы к модели, развёрнутой в vLLM.
Как это работает вместе
Внутри узла. Tensor Parallelism дробит модель на куски, загружает их на все локальные GPU и с помощью NCCL синхронизирует параметры.
Между узлами. Pipeline Parallelism превращает несколько серверов с GPU в конвейер: каждый узел обрабатывает свою «стадию» модели, передавая промежуточные данные дальше — так все карты работают без простоев.
Внешний интерфейс. Ray Serve оборачивает всю эту мощь в единый HTTP-сервис по OpenAI-протоколу. Он разбивает входящий запрос на нужные шард-группы, отправляет их на соответствующие GPU и ноды, а затем собирает полученные фрагменты в единый осмысленный ответ.
Описания скриптов
Дисклеймер: я не Python-разработчик, поэтому скрипты далеко не production-ready и написаны больше для ознакомительных целей.
Скрипт serve.py
Первый скрипт — serve.py. В нём:
инициируется vLLM с заданными параметрами (tensor parallel, pipeline parallel и так далее);
создаётся FastAPI-приложение с эндпойнтами для ChatCompletion — стримингового и обычного;
добавляется JWT-аутентификация для контроля доступа.
Ray Serve оборачивает это приложение и распределяет HTTP-запросы по рабочим процессам.
Скрипт auth.py
Второй скрипт — auth.py, который:
работает с JWT — создание и проверка токенов, роль пользователя;
берёт загрузку пользователя (логин, роль, хэш пароля) из переменных окружения (например, USER_LIST);
предоставляет вспомогательные функции для проверки роли (admin/user) и времени жизни токена.
Этот модуль подключается внутри
serve.py
, чтобы проверять токены при обращении к эндпойнтам. Его реализация максимально упрощена (то же касается хранения логинов, паролей и ролей) и служит только демонстрационным примером.
Промежуточные итоги
Теперь у нас есть два скрипта для дальнейшего использования:
serve.py
— основной сервер, где Ray Serve запускает FastAPI-приложение с vLLM под капотом;auth.py
— вспомогательная логика JWT-аутентификации, используемая в серверном скрипте.
Благодаря этим двум модулям мы получаем распределённый инференс, шардирование больших языковых моделей и удобный API для взаимодействия с ними. Теперь нужно собрать скрипты в Docker-образ и развернуть всё в KubeRay.
Настройка KubeRay
В этом разделе мы разберёмся, как подготовить Docker-образ, поместить его в Registry и развернуть Ray Cluster с нужными параметрами. Это поможет организовать распределённый инференс, использовать CephFS или другое хранилище, настраивать CPU- и GPU-ресурсы и так далее. В примерах я беру за основу официальный Docker-образ Ray, но вы можете легко заменить его на любой другой совместимый базовый образ.
Подготовка Docker-образа и загрузка в Registry
1. Базовый образ Ray:
Берём официальный образ Ray соответствующей версии, например
rayproject/ray:2.44.0-py310-cu124
. Он уже содержит нужные компоненты Ray, CUDA-библиотеки и Python 3.10.
2. Добавляем скрипты и пакеты:
Помещаем
serve.py
,auth.py
(упаковывая всё в zip-архив).Устанавливаем
vLLM
,httpx
,PyJWT
и другие зависимости.
3. Сборка:
Пишем Dockerfile (dockerfile.ray), в котором описываем добавление zip-файла и установку pip-зависимостей.
Собираем образ с помощью Make (Makefile) командой:
make package-container
4. Загрузка образа:
Пушим результат в нужный Registry — DockerHub, GitLab Registry и так далее:
docker push gitlab.example.com:5050/it-operations/k8s-config/vllm-0.8.4-ray-2.44.0-py310-cu124-serve:1.1.7
Убеждаемся, что Kubernetes-узлы или кластер имеют доступ к этому Registry. Кстати, в Deckhouse уже есть возможность добавить внутренний registry, а также авторизацию для него.
Настройка KubeRay Cluster
Для удобства описываем Ray Cluster с помощью Helm-чарта и values-файла. Основная идея такая:
head (головной узел) — содержит Ray Head Pod с установленным Ray Dashboard, Autoscaler, если он нужен, и обеспечивает взаимодействие с worker'ами. Для обеспечения режима HA есть отдельная опция
gcsFaultToleranceOptions
— подробнее о её работе расскажу чуть позже.worker — 1+ подов, каждый из которых может работать на CPU или GPU.
additionalWorkerGroups — дополнительные группы worker'ов с другими конфигурациями (например, меньше памяти, более слабый GPU).
Ниже приведён пример ap-values.yaml с основными настройками.
1. image
image:
repository: gitlab.example.com:5050/it-operations/k8s-config/vllm-0.8.4-ray-2.44.0-py310-cu124-serve
tag: 1.1.7
pullPolicy: IfNotPresent
Здесь:
repository
иtag
— это Docker-образ, который мы собрали на предыдущем шаге;pullPolicy: IfNotPresent
означает, что Kubernetes не будет заново скачивать образ, если он уже есть локально.
2. imagePullSecrets
imagePullSecrets:
- name: regcred
Если Registry приватный, нужно прописать секрет (credential).
3. common
common:
containerEnv:
- name: HF_HOME
value: "/data/model-cache"
- name: DTYPE
value: "bfloat16"
- name: GPU_MEMORY_UTIL
value: "0.98"
- name: MAX_MODEL_LEN
value: "131072"
containerEnv
задаёт переменные окружения, которые попадут и в head, и в worker:
DTYPE
(например, float16) — тип данных при инференсе;GPU_MEMORY_UTIL
— доля видеопамяти, которую можно занимать при работе vLLM;MAX_MODEL_LEN
— максимально допустимая длина токенов для модели (по необходимости).
4. head
head:
rayVersion: "2.44.0"
enableGcsFT: true
gcsFT:
# externalStorageNamespace: ray-gcs-backup
redisAddress: redis:6379
redisSecret:
name: redis-password-secret
key: password
labels:
component: ray-head
serviceAccountName: "sa-deepseek-cluster"
rayStartParams:
dashboard-host: "0.0.0.0"
num-cpus: "0"
metrics-export-port: "8080"
containerEnv:
- name: RAY_PROMETHEUS_HOST
value: https://prometheus.d8-monitoring:9090
envFrom:
- secretRef:
name: auth-config
resources:
limits:
cpu: "6"
memory: "8G"
requests:
cpu: "3"
memory: "4G"
volumes:
- name: model-cache
persistentVolumeClaim:
claimName: model-cache-pvc
volumeMounts:
- mountPath: /data/model-cache
name: model-cache
Здесь:
rayVersion
— указывает, какая версия Ray используется Autoscaler'ом, если включён in-tree Autoscaling;serviceAccountName
— ServiceAccount, который нужно дать поду. Он может понадобиться для доступа к PV, Secret и так далее;envFrom
— берём переменные из Secret auth-config. Это логины, пароли, JWT-ключи;resources
— запросы и лимиты CPU/RAM для head-пода. В примере: лимит 6 CPU, 8GiB RAM;volumes
/volumeMounts
— примонтированный CephFS (через PVC model-cache-pvc) на путь/data/model-cache
, чтобы модель была доступна для head.
HA-режим GCS (GCS Fault Tolerance)
Чтобы защитить метаданные Ray-кластера от единственной точки отказа (GCS Head), мы включили опциональный режим репликации через Redis:
enableGcsFT: true
— кастомный флаг, добавленный вручную, который заставляет Helm-шаблон сгенерировать секциюgcsFaultToleranceOptions
. На момент написания статьи этого флага ещё нет в официальном чарте RayCluster.gcsFT.redisAddress: redis:6379
— адрес нашего Redis-сервиса, где будут храниться дубли GCS-метаданных.gcsFT.redisSecret.name/key
— Kubernetes Secret с учётными данными доступа к Redis, чтобы пароль не «плавал» в YAML.
Принцип работы
При старте Ray-кластера GCS head теперь дублирует свои служебные данные — информацию о запущенных задачах и состояние акторов — не только во внутреннее хранилище, но и в Redis. Если оригинальный head-под выходит из строя, резервный head подхватывает сохранённые в Redis метаданные и продолжает обслуживание без потери состояния.
5. worker
worker:
groupName: rtx-3090
replicas: 2
minReplicas: 2
maxReplicas: 2
resources:
limits:
cpu: "16"
memory: "24G"
nvidia.com/gpu: "1"
requests:
cpu: "8"
memory: "12G"
nvidia.com/gpu: "1"
nodeSelector:
node.deckhouse.io/group: "w-gpu"
tolerations:
- key: "dedicated.apiac.ru"
operator: "Equal"
value: "w-gpu"
effect: "NoExecute"
volumes:
- name: model-cache
persistentVolumeClaim:
claimName: model-cache-pvc
volumeMounts:
- mountPath: /data/model-cache
name: model-cache
Здесь:
groupName
— название группы worker'ов (rtx-3090
);replicas
иminReplicas
/maxReplicas
— сколько подов worker будет по умолчанию и какой допускается диапазон;resources
— лимиты CPU/Memory и GPU. Например, 1 карта на под;nodeSelector
иtolerations
— размещаем worker'ы на узлах с лейбломnode.deckhouse.io/group=w-gpu
;volumes
/volumeMounts
— аналогично head монтируем PVC для общей модели.
6. additionalWorkerGroups
additionalWorkerGroups:
rtx-3060:
disabled: true
replicas: 1
minReplicas: 1
maxReplicas: 1
nodeSelector:
node.deckhouse.io/group: "w-gpu-3060"
...
Можно добавить сколько угодно дополнительных групп, каждая — со своими ресурсами, лейблами, affinities и так далее. Если пока не хотим их запускать, ставим disabled: true
, как в примере.
Развёртывание Ray Application. Запуск модели Gemma 3 и тестирование API
Gemma 3 — одна из передовых LLM от Google, доступных на Hugging Face. Она умеет работать с очень длинными текстами (до 128K токенов) и спроектирована так, чтобы эффективно использовать память и быстро отвечать. Этот пример подходит для демонстрации работы, но вы можете использовать аналогичные шаги для развёртывания других доступных в открытом доступе LLM.
В этом разделе разберёмся, как выбрать и запустить модель в Ray-кластере, а также как проверить работу нашего OpenAI-совместимого эндпойнта с JWT-аутентификацией.
Запуск модели через Ray Application
Для запуска модели в Ray-кластере мы используем Ray Application. Данный механизм указывает Ray Serve, какие файлы брать, какую модель загружать и под какими настройками параллелизации (tensor/pipeline) работать.
Пример JSON-запроса к Ray Dashboard
Отправляем POST-запрос на https://ray-dashboard.k8s.example.com/api/serve/applications/
с телом:
{
"applications": [
{
"import_path": "serve:model",
"name": "Gemma-3-12b",
"route_prefix": "/",
"autoscaling_config": {
"min_replicas": 1,
"initial_replicas": 1,
"max_replicas": 1
},
"deployments": [
{
"name": "VLLMDeployment",
"num_replicas": 1,
"ray_actor_options": {},
"deployment_ready_timeout_s": 1200
}
],
"runtime_env": {
"working_dir": "file:///home/ray/serve.zip",
"env_vars": {
"MODEL_ID": "google/gemma-3-12b-it",
"TENSOR_PARALLELISM": "1",
"PIPELINE_PARALLELISM": "2",
"MODEL_NAME": "gemma-3-12b",
"MAX_MODEL_LEN": "131072",
"MAX_NUM_SEQS": "256",
"ENABLE_ENFORCE_EAGER": "true",
"CPU_OFFLOAD_GB": "0",
"DTYPE": "auto",
"VLLM_USE_V1": "1",
"GPU_MEMORY_UTIL": "0.98"
}
}
}
]
}
Ключевые моменты JSON-запроса
1. import_path: "serve:model"
Указывает Ray, что в архиве (working_dir) находится Python-модуль
serve.py
, в котором определён объектmodel
.Именно он развёртывается как Ray Serve Deployment.
2. name: "Gemma-3-12b"
Имя приложения в Ray Dashboard, чтобы легко отличать его от других.
3. route_prefix: "/"
Базовый путь, по которому будет доступен сервис при наличии Ingress или сервисов.
4. autoscaling_config
min_replicas
,initial_replicas
,max_replicas
задают политику масштабирования. В примере — 1 реплика без автоскейла.
5. deployments
Здесь описан
VLLMDeployment
с num_replicas = 1. Это класс/обёртка vLLM вserve.py
.deployment_ready_timeout_s = 1200
даёт время (20 минут) на инициализацию модели. Это полезно при больших загрузках.
6. runtime_env
working_dir: "file:///home/ray/serve.zip"
говорит Ray, где лежит код — скриптыserve.py
,auth.py
.-
env_vars:
задаёт переменные окружения для vLLM:"MODEL_ID"
— название модели на Hugging Face, здесь"google/gemma-3-12b-it"
;"TENSOR_PARALLELISM"
и"PIPELINE_PARALLELISM"
— регламентируют шардирование и конвейерную параллельность ("TENSOR_PARALLELISM": "1"
— на каждом узле 1 GPU,"PIPELINE_PARALLELISM": "2"
— 2 GPU всего в кластере);"MODEL_NAME"
— отображается в ответах API как название модели;"MAX_MODEL_LEN"
— максимальная длина обрабатываемой последовательности в токенах, можно задать в чарте Ray Cluster;"MAX_NUM_SEQS"
— максимальное число одновременных сессий/запросов к модели;"ENABLE_ENFORCE_EAGER"
— принудительное включение eager-режима в vLLM для улучшения детерминированности и дебага;"CPU_OFFLOAD_GB"
— объём оперативной памяти (GB), выделяемой под офлоад части вычислений с GPU на CPU;"DTYPE"
— выбор формата данных (bfloat16/float32 и т.д.) для инференса, можно задать в чарте Ray Cluster;"VLLM_USE_V1"
— переключает движок на новую архитектуру vLLM V1 с единым диспетчером задач, менеджером KV-кэша и другими оптимизациями, включёнными по умолчанию;"GPU_MEMORY_UTIL"
— доля доступной видеопамяти (98 %) для использования моделью, можно задать в чарте Ray Cluster.
Результат деплоя и вид в Ray Dashboard
После успешного запроса Ray:
Извлекает файлы из
serve.zip
и создаёт Ray Serve Application под именемGemma-3-12b
.В разделе Deployments появляется
VLLMDeployment
. vLLM инициализируется, подгружаяGemma-3-12b
из Hugging Face.При желании можно просмотреть логи, где будет видно, как vLLM скачивает вес модели и запускает инференс.
С этого момента модель Gemma 3 готова к приёму запросов на OpenAI-совместимые эндпойнты (например, /v1/chat/completions
), используя конфигурацию параллелизации (tensor/pipeline), указанную в env_vars
.

Тестирование API и аутентификации
1. Создание пользователей
В Kubernetes создаём Secret (auth.yaml) с JWT-ключом, параметрами истечения токена, а также списком пользователей (логины, роли, хэши паролей).
Пример auth.yaml:
apiVersion: v1
kind: Secret
metadata:
name: auth-config
namespace: kuberay-projects
type: Opaque
data:
JWT_KEY: ZGVmYXVsdF9qd3RfS2V5
ACCESS_TOKEN_EXPIRE_MINUTES: NjA=
SKIP_EXP_CHECK: ZmFsc2U=
HUGGING_FACE_HUB_TOKEN: ""
USER_LIST: QUxJQ0UsIEJPQg==
ALICE_USERNAME: YWxpY2U=
ALICE_HASHED_PASSWORD: ZmFrZWhhc2hlZGhhc2g=
ALICE_ROLE: YWRtaW4=
BOB_USERNAME: Ym9i
BOB_HASHED_PASSWORD: ZmFrZWhhc2hlZGhhc2g=
BOB_ROLE: Z3Vlc3Q=
В переменных окружения ALICE_HASHED_PASSWORD и так далее хранится уже «соль + хэш» пароля.
2. Генерация пароля и хэша
Используем Python-скрипт gen_pwd.py, где с помощью secrets.token_hex
и hashlib.sha256
вычисляем соли и хэши паролей. Затем записываем хэшированный пароль в значение строки PASSWORD
для конкретного пользователя, например ALICE_HASHED_PASSWORD
. Соль записываем в строку JWT_KEY
. Это нужно, чтобы auth.py
мог сравнивать введённые данные при аутентификации.
В качестве демонстрации у нас для всех паролей и ключа JWT одинаковая соль, но в реальных условиях, конечно же, так делать не нужно.
3. Авторизация и получение JWT
Выполняем запрос на /token
, передавая username / password в формате form-data
:
curl --location 'https://openai-api.example.com/token' \
--form 'username="admin"' \
--form 'password="password"'
В ответ:
{
"access_token": "<токен>",
"token_type": "bearer"
}
4. Вызов инференса
Теперь пробуем вызвать через API сам инференс, задав один из самых сложных вопросов во вселенной:
curl --location 'https://openai-api.example.com/v1/chat/completions' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <токен>' \
--data '{
"model": "Gemma-3-12b",
"messages": [
{
"role": "user",
"content": "Сколько четвергов в марте 2025 года?"
}
],
"stream": false,
"max_tokens": 2000,
"temperature": 0.85,
"top_p": 0.95,
"top_k": 50,
"repetition_penalty": 1.0
}'
Получаем ответ, по которому видим, что ИИ успешно справился с задачей — хотя другие модели с трудом отвечали на этот вопрос (это не шутка):
{
"id": "chatcmpl-1745243015.727946",
"object": "chat.completion",
"created": 1745243015,
"model": "Gemma-3-12b",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "В марте 2025 года будет **четыре** четверга.\n\nВот даты:\n\n* 6 марта\n* 13 марта\n* 20 марта\n* 27 марта"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 23,
"completion_tokens": 46,
"total_tokens": 69
}
}

Мониторинг в Ray Dashboard
В Ray Dashboard видим загруженность GPU: скорость ~40 token/s в зависимости от конкретной модели и конфигурации.


А логи отображают процесс инференса, можно анализировать ошибки или задержки.

Пришло время для тестов
Чтобы понять, как быстро и надёжно работает модель, я провёл два вида тестов с помощью LLMPerf — один на скорость и нагрузку, другой на корректность ответов под нагрузкой.
Как я тестировал
Тест скорости и нагрузки:
Скрипт
token_benchmark_ray.py
.Менял число параллельных запросов (1, 4, 8, 16, 20), длину контекста (от 512 до 32 768 токенов) и способ сэмплинга (greedy, Top-K, Top-P, их сочетание).
Для каждого случая я запускал от 30 до 100 запросов и фиксировал:
время на токен (
inter_token_latency_s
);время до первого токена (
ttft_s
);общую задержку (
end_to_end_latency_s
);скорость (
throughput_token_per_s
);p50/p95/p99 в каждой метрике.
Тест стабильности и точности:
Скрипт
llm_correctness.py
.Отправлял до 300 запросов при 20 параллельных сессиях и смотрел, стали ли ответы «уезжать» от ожидаемого или уходить в ошибку. Фиксировал
error_rate
иmismatch_rate
.
Особенности окружения
CUDA Graph для Gemma-3-12B пришлось отключить (
ENABLE_ENFORCE_EAGER=true
), иначе модель падала. На Dolphin3.0 с графом производительность была ≈ 40 t/s.
Если у вас есть идеи, как вернуть CUDA Graph для Gemma-3 или другие способы ускорить эту модель, напишите, пожалуйста, в комментариях!
Тесты шли через внешний OpenAI API, поэтому в задержку добавлялся сетевой оверхед.
Все тесты на одной и той же конфигурации с постоянными GPU и RAM, чтобы сравнения были честными.
Результаты тестирования
Базовая производительность: модель выдаёт новый токен каждые ≈ 0,116 с, скорость генерации ≈ 10 tok/s, время до первого токена ≈ 3,2 с.
Параллелизм: оптимально использовать до 8 одновременных сессий — при дальнейшем росте latency начинает расти непропорционально.
Контекст: безопасный предел — до ≈ 2 000–4 000 токенов; при больших окнах существенно падает скорость (до 6 tok/s на 8 192 токенах и ниже) и возрастает риск ошибок.
Сэмплинг: выбор Top-K или Top-P даёт незначительное замедление (< 0,005 с/токен), но может улучшить качество генерации.
Надёжность: при 20 concurrent запросах error_rate = 0 %, mismatch_rate < 1 % — модель отвечает стабильно, без падений.
Все подробные результаты и исходные скрипты доступны в репозитории.
Промежуточные итоги
Успешно подключили Gemma 3 из Hugging Face, развернув её в Ray-кластере с vLLM «под капотом».
API
/v1/chat/completions
проверен — он выдаёт JSON-ответ, совместимый с OpenAI-протоколом.JWT-аутентификация ограничивает доступ к этому эндпойнту, пользователи хранятся в Secret.
Провели ряд тестов производительности с помощью LLMPerf. Средняя межтокеновая задержка составила ≈0,116 с, а throughput — ≈10 т/с, что наглядно подтверждает стабильность и корректность работы инференса. Не так быстро, как хотелось бы, но я продолжаю исследовать возможности улучшить результат.
В следующей части
Итак, мы протестировали API. Дальше хочется подключить удобный интерфейс. В следующей, заключительной, статье мы рассмотрим, как развернуть OpenWebUI — бесплатный веб-интерфейс для взаимодействия с LLM. Подробно разберём его основные возможности и настройки, а также посмотрим, как связать OpenWebUI с нашим Ray-кластером, чтобы общаться с моделью через OpenAI-совместимый API.