Skip to content

Не читается ответ от LLM¤

Проблема обнаружена 12.11.2025.

Взаимодействие¤

Фронтенд и бекенд¤

OpenWebUI состоит из двух частей, фронта и бека, которые являются отдельными компонентами и взаимодействуют между собой. Взаимодействие происходит:

  • через HTTP запросы на бекенд
  • через WebSocket
--- config: layout: elk look: handDrawn theme: 'base' --- graph LR f((Frontend)) -- HTTP --> b(Backend) f <== WebSocket ==> b

Бекенд и LLM¤

Интуитивный вариант работы с LLM (и, судя по коду, это возможно)

sequenceDiagram participant Backend as Backend participant LLM as LLM Backend ->> LLM: HTTP request LLM ->> Backend: HTTP response

Код ориентирован на такой вариант: Бекенд посылает HTTP запрос, LLM модель обрабатывает его и возвращают ответы в виде SSE (Server-Sent Events).

sequenceDiagram participant Backend as Backend participant LLM as LLM Backend ->> LLM: HTTP request activate LLM LLM -->> Backend: HTTP response LLM -->> Backend: SSE Stream deactivate LLM

При этом, сервер не закрывает соединение, как можно было бы ожидаеть, но посылает сигнал о завершении и ничего неделает.

sequenceDiagram participant Backend as Backend participant LLM as LLM Backend ->> LLM: HTTP request activate LLM LLM -->> Backend: HTTP response LLM -->> Backend: SSE Stream activate LLM LLM --x Backend: SSE Stream (finish) LLM --> Backend: Ожидание deactivate LLM deactivate LLM
--- config: layout: elk look: handDrawn theme: 'base' --- graph LR f((Frontend)) -- HTTP --> b(Backend) f <== WebSocket ==> b b -- HTTP SSE Stream --- llm@{shape: cloud, label: LLM}

Передача данных с LLM на Frontend¤

--- config: layout: elk look: handDrawn theme: 'base' --- graph LR subgraph Frontend direction TB f[Web Interface] ~~~ wsf((WebSocket)) end subgraph Backend direction TB b[Web Framework] ~~~ c((Coroutines)) end subgraph LLM direction TB llm@{shape: cloud, label: LLM} end Frontend ~~~ Backend Backend ~~~ LLM
  1. Фронтенд посылает HTTP запрос в chat/completions на бекенд
  2. Бекенд посылает HTTP запрос в LLM, получает HTTP ответ
  3. Бекенд передаёт соединение во внутреннюю задачу обработки
  4. Возвращает HTTP ответ на фронтенд, закрывает соединение с фронтом
--- config: look: handDrawn theme: 'base' --- graph TB subgraph Frontend direction TB f[Web Interface] ~~~ wsf((WebSocket)) end subgraph Backend direction TB b[Web Framework] ~~~ c((Coroutines)) end subgraph LLM direction TB llm@{shape: cloud, label: LLM} end f -- HTTP Request --> b b -- HTTP Request --> llm llm -- HTTP Response --> b b -- HTTP Response --> f b -- Connection Object --> c llm ~~~ c c ~~~ wsf
  1. LLM пишет в соединение ответ по спецификации SSE (Server-Sent Events)
  2. Фоновая задача бекенда читает стрим SSE и тут же отправляет их на Websocket
  3. На фронте в пользовательском интерфейсе изменяется UI, согласно сообщениям Websocket
--- config: look: handDrawn theme: 'base' --- graph TD subgraph Backend direction TB b[Web Framework] ~~~ c((Coroutines)) end subgraph Frontend direction TB f[Web Interface] ~~~ wsf((WebSocket)) end subgraph LLM direction TB llm@{shape: cloud, label: LLM} end f ~~~ b b ~~~ llm llm ~~~ b llm -- SSE Stream --> c c -- Messages --> wsf

Полную схему можно изобразить таким образом

--- config: look: handDrawn theme: 'base' --- graph TD subgraph Frontend direction TB f[Web Interface] ~~~ wsf((WebSocket)) end subgraph Backend direction TB b[Web Framework] ~~~ c((Coroutines)) end subgraph LLM direction TB llm@{shape: cloud, label: LLM} end f -- HTTP Request --> b b -- HTTP Request --> llm llm -- HTTP Response --> b b -- HTTP Response --> f b -- Connection Object --> c llm -- SSE Stream --> c c -- Messages --> wsf

В виде диаграммы последовательности:

--- config: layout: elk look: handDrawn theme: 'forest' --- sequenceDiagram participant ui as Web Interface participant websocket@{label: WebSocket, type: entity} participant fw as Web Framework participant Coroutines@{label: Coroutines, type: entity} participant llm as LLM ui ->> fw: HTTP Request activate ui activate fw fw ->> llm: HTTP Request activate llm llm ->> fw: HTTP Response fw ->> Coroutines: Connection Object activate Coroutines fw ->> ui: HTTP Response deactivate ui deactivate fw llm -->> Coroutines: SSE Stream Coroutines -->> websocket: Messages activate ui activate websocket websocket <<-->> ui: Влияет на UI deactivate websocket deactivate ui llm -->> Coroutines: SSE Stream (finish) Coroutines --x llm: Close Connection deactivate Coroutines deactivate llm

Предпологаемые места проблемы¤

Я предполагаю, что проблема начинается, когда перед LLM встаёт Proxy LLM. Nginx либо режет заголовки на входе и тем самым заставляет отвечать LLM не в режиме SSE,

--- title Nginx режет внешние заголовки --- graph LR b((Backend)) -- Give me answer with SSE --> p([Proxy LLM]) p -- Give me answer with HTTP --> llm@{shape: cloud, label: LLM} llm -- HTTP Response --> p p -- HTTP Response --> b b -- Connection --> c(Coroutines) c -- Connection is closed, no messages --> w([WebSocket])

либо режет соединение, мешая SSE.

--- title Nginx режет соединение --- graph LR b((Backend)) -- HTTP Request --> p([Proxy LLM]) p -- HTTP Request --> llm@{shape: cloud, label: LLM} llm -- HTTP Response + SSE Stream --> p p -- HTTP Response NO SSE Stream --> b b -- Connection --> c(Coroutines) c -- Connection is closed, no messages --> w([WebSocket])

Эта проблема имеет вариации. Например nginx режет внешине или внутренние заголовки, тем самым "путая карты" логике, которая работает с соединением (Coroutines), обработка ответа падает "молча", так как код OpenWebUI весьма плохо написан. Этот вариант выловить будет сложнее.

Поиск решения¤

13.11.2025

Обнаружил в коде, что в зависимости от заголовков ответа от LLM, выбирается способ обработки ответа.

r = await session.request(
    method="POST",
    url=request_url,
    data=payload,
    headers=headers,
    cookies=cookies,
    ssl=AIOHTTP_CLIENT_SESSION_SSL,
)

# Check if response is SSE
if "text/event-stream" in r.headers.get("Content-Type", ""):
    streaming = True
    return StreamingResponse(
        r.content,
        status_code=r.status,
        headers=dict(r.headers),
        background=BackgroundTask(
            cleanup_response, response=r, session=session
        ),
    )
else:
    try:
        response = await r.json()
    except Exception as e:
        log.error(e)
        response = await r.text()

Это исключает вариант, в котором режутся заголовки на входе.

Необходимо посмотреть в тесте какие заголовки приходят со стороны Proxy LLM.

Ответ от Proxy LLM¤

На сложныз запросах к LLM через прокси становится понятно, что Nginx ожидает полного ответа от LLM и отдаёт ответ без SSE, целиком, с заголовком Content-Type: application/json.

> curl -X POST 'https://s001tst-api-gchat.sibur.local/v1/chat/completions' \
    -H 'Content-Type: application/json' \
    -H "Authorization: Bearer $TOKEN" \
    --data '{"model":"GigaChat","messages":[{"role":"user","content":"Разработай бизнес план стартапа, который бы заинтересовал компанию СИБУР"}]}' \
    -k -vvv
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 10.2.39.228:443...
* Connected to s001tst-api-gchat.sibur.local (10.2.39.228) port 443 (#0)
* ALPN: offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN: server accepted http/1.1
* Server certificate:
*  subject: C=RU; ST=Moscow region; L=Skolkovo; O=LLCSIBUR; OU=IT; CN=s001tst-api-gchat.sibur.local
*  start date: Mar 21 09:34:07 2025 GMT
*  expire date: Mar 21 09:34:07 2026 GMT
*  issuer: C=RU; ST=Moscow; L=Moscow; O=LLC SIBUR; OU=Information Security; CN=SiburInfrastructureCAG1
*  SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
* using HTTP/1.1
> POST /v1/chat/completions HTTP/1.1
> Host: s001tst-api-gchat.sibur.local
> User-Agent: curl/7.88.1
> Accept: */*
> Content-Type: application/json
> Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI3WkpwSXBPT2lVdjQ3TzhWNFFWWGprSlNQRjFWTTZKcHdWaVE0NXh1YjQwIn0.eyJleHAiOjE3NjMwMjkwMjcsImlhdCI6MTc2MzAyNzIyNywianRpIjoiOGRhYzE1N2EtYTZhMS00NTg1LWFkNDgtNzkzMjRkZTBjZTc1IiwiaXNzIjoiaHR0cHM6Ly9pZC1lb3MtdHN0LnNpYnVyLmxvY2FsL3JlYWxtcy9zaWJ1ciIsImF1ZCI6WyJyYWctbGF5ZXItY2xpZW50IiwiYWNjb3VudCJdLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJyYWctbGF5ZXItY2xpZW50Iiwic2lkIjoiZjZhODVlNmYtOTYwNi00MWFkLWIwMzAtMWQ5ZWEwN2QyNWIyIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iLCJkZWZhdWx0LXJvbGVzLXNpYnVyIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJvcGVuaWQgZW1haWwgcHJvZmlsZSBvZmZsaW5lX2FjY2VzcyIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJuYW1lIjoiYTAwMS1haXNyY2gtZ2NodC10c3QiLCJncm91cHMiOlsiRzAwMWdnLXRzdC1naWdhY2hhdC10ZXN0LWNsaWVudCJdLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhMDAxLWFpc3JjaC1nY2h0LXRzdCIsImdpdmVuX25hbWUiOiJhMDAxLWFpc3JjaC1nY2h0LXRzdCJ9.mBsaCo0Ocvo-VVItZKqx-3LtWwCEbOWGj3MmFO04MVJQCJKxnzdHtDB_BC8gzdjuABYQxkPlCpx3vgBgMbT-Rrbd_Ser3x-DX06_EIqw7r2pv5P9mYGnzwOBc-omL_1zAmUksDufkeEiFoOrASl7a_m6s7qKNqmAJQrtbrRXBZmCm26994Nx7nUcRXBddGNyQ0IIhevtxLH9_rNGj8fBf7KNp8nbZeYSRauhk3CnTxhcoqQQ9tKGvPQGWeEDdIsiX4RnZJdCwIx1LP_nDU1P2tpMyxT2UA5tD176nQ5Ss-jdGOMv_TMZFOTpcoKJgmHtJt0OIAxt3tiLqvN0Rw4lOg> Content-Length: 197
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
< HTTP/1.1 200 OK
< Server: nginx/1.26.3
< Date: Thu, 13 Nov 2025 09:53:30 GMT
< Content-Type: application/json; charset=utf-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< Access-Control-Allow-Credentials: true
< Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization
< Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS
< Access-Control-Allow-Origin: https://beta.saluteai.sberdevices.ru
< X-Request-Id: ed3b0b9f-d5ef-4a06-bb95-ffbd74fe82a2
< X-Session-Id: 35300513-3127-4aac-82b1-c887e68e4744
< Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
< X-Frame-Options: DENY
< X-Content-Type-Options: nosniff
< Content-Security-Policy: default-src 'self';
< X-Permitted-Cross-Domain-Policies: none
< Referrer-Policy: strict-origin-when-cross-origin
< Permissions-Policy: geolocation=(), microphone=(), camera=()
< Cross-Origin-Embedder-Policy: require-corp
< Cross-Origin-Opener-Policy: same-origin
< Cross-Origin-Resource-Policy: same-origin
< Cache-Control: no-store
<
{"choices":[{"message":{"content":"### Бизнес-план стартапа для компании СИБУР\n\n#### Описание проекта\nНазвание: **«ЭкоПолимер»**\nЦель: Разработка и производство инновационного биоразлагаемого полимера на основе возобновляемого сырья, способного заменить традиционные полимеры в упаковочной отрасли, сельском хозяйстве и строительстве.\n\n**Краткое описание идеи:**\nНа фоне усиливающегося экологического давления и законодательных ограничений (например, запрет одноразового пластика), рынок нуждается в устойчивых альтернативах традиционному пластику. Наша разработка — экологически чистый биополимер, произведенный из отходов сельского хозяйства и побочных продуктов переработки углеводородов. Мы предлагаем продукцию с уникальными свойствами: высокая прочность, эластичность, способность разлагаться естественным образом в течение нескольких месяцев после утилизации, безопасность для окружающей среды и человека.\n\n---\n\n## Резюме проекта\n\n- Название проекта: ЭкоПолимер\n- Направление деятельности: Производство инновационных биоразлагаемых полимеров.\n- Ключевые преимущества продукта: Устойчивость, биоразлагаемость, низкая стоимость производства благодаря использованию возобновляемых источников сырья.\n- Целевые рынки: Упаковка пищевых продуктов, сельское хозяйство, строительство, промышленность товаров повседневного спроса.\n- Потенциальный партнер/инвестор: Компания СИБУР.\n\n---\n\n## 1. Общее описание бизнеса\n\nКомпания «ЭкоПолимер» занимается разработкой и производством устойчивого материала на основе биоразлагаемых полимеров, сочетающих свойства традиционного полиэтилена и полипропилена, но имеющих минимальное воздействие на окружающую среду.\n\n**Продуктовая линейка:**\n- Биопластиковые пленки для упаковки продуктов питания и напитков.\n- Биоразлагаемые материалы для сельскохозяйственной промышленности (мешки, агротекстиль).\n- Материалы для строительства (влагоизоляционные покрытия, панели для внутренних стен и фасадов зданий).\n\n---\n\n## 2. Анализ рынка\n\n**Спрос и тенденции рынка:**\n- Рост потребления пластиковых изделий растет ежегодно на 4–6%.\n- Экологические ограничения ужесточаются во многих странах мира (Европа, США, Китай, Россия). Доля традиционных пластиков будет сокращаться.\n- Увеличение потребительского интереса к продукции, имеющей экологический сертификат.\n\n**Конкуренты:**\n- Основные конкуренты находятся в сегменте обычных полиэтиленовых и полипропиленовых пленок, пакеты, упаковка и товары массового потребления.\n- В сегменте био-разлагаемых материалов конкурентов немного, однако есть перспектива появления новых игроков.\n\n**Целевой сегмент клиентов:**\n- Крупные производители пищевой продукции и напитков.\n- Производители бытовой химии и косметики.\n- Строительные компании.\n- Сельскохозяйственные предприятия.\n\n---\n\n## 3. Концепция и технология производства\n\n**Сырье и технологическая цепочка:**\n- Сырьем станут отходы сельскохозяйственных культур (свекла, картофель, зерновые культуры), побочные продукты нефтепереработки (метанол, этанол).\n- Используемая технология предполагает переработку сырьевых компонентов в биологически активные добавки и непосредственно в готовый продукт через экструзию и формовку.\n\n**Уникальность технологии:**\n- Высокое качество конечного продукта при низкой себестоимости производства.\n- Быстрая деградация (от 3 до 18 месяцев в зависимости от условий утилизации).\n- Возможность полной вторичной переработки.\n\n---\n\n## 4. Маркетинговая стратегия\n\n**Позиционирование продукта:**\n- Высокая экологичность и соответствие международным стандартам (ISO, ГОСТ).\n- Сертификация продукции по стандарту ISO 14001 (экологический менеджмент).\n- Удобство интеграции в существующие производственные процессы клиентов.\n\n**Каналы сбыта:**\n- Прямые продажи крупным производителям.\n- Продажа через дистрибьюторов и оптовые сети.\n- Партнерства с крупными торговыми сетями и поставщиками.\n\n**Стратегия продвижения:**\n- Активное продвижение бренда через PR-акции, участие в выставках и форумах (AgroWorld, PackExpo, CEEF).\n- Сотрудничество с университетами и научно-исследовательскими институтами (совместные исследования и разработки).\n- Проведение презентаций и семинаров для потенциальных клиентов.\n\n---\n\n## 5. Финансовый план\n\n**Инвестиции и источники финансирования:**\n- Необходимый объем инвестиций для запуска производства — около ₽2 млрд рублей.\n- Источники финансирования: инвестиции венчурных фондов, собственные средства основателей, привлечение партнеров-инвесторов (СИБУР).\n\n**Прогноз доходов и расходов:**\n- Годовой объем продаж первого года работы — ₽500 млн.\n- Второй год — ₽1,2 млрд.\n- Третий год — ₽2,5 млрд.\n\n**Окупаемость проекта:**\n- Срок окупаемости проекта — 3–4 года.\n\n---\n\n## 6. Планы сотрудничества с СИБУР\n\n**Преимущества для СИБУРа:**\n- Доступ к возобновляемым источникам сырья, возможность снизить углеродный след.\n- Расширение ассортимента продукции компании.\n- Создание устойчивого имиджа компании как лидера в области экологически чистых технологий.\n\n**Партнерские условия:**\n- Совместная разработка технологических решений.\n- Использование производственных мощностей СИБУРа.\n- Совместный маркетинг и продвижение на международных рынках.\n\n---\n\n## Заключение\n\nПроект «ЭкоПолимер» представляет собой уникальный стартап, ориентированный на решение актуальных экологических проблем, связанных с использованием пластиковых материалов. Работая вместе с компанией СИБУР, мы сможем создать новый качественный продукт мирового уровня, отвечающий требованиям современного рынка и одновременно обеспечивающий стабильную прибыльность нашего совместного бизнеса.","role":"assistant"},"index":0,"finish_reason":"stop"}],"created":1763027609,"model":"GigaChat:latest","object":"chat.completion",* Connection #0 to host s001tst-api-gchat.sibur.local left intact"usage":{"prompt_tokens":28,"completion_tokens":1141,"total_tokens":1169,"precached_prompt_tokens":0}}

Значит не бекенде отрабатывает та ветка, которая ожидает полного ответа от LLM. И, судя по всему, эта ветка поломана. В деве работает, потому, что LLM отвечает в режиме SSE.

Предложение изменить настроки Nginx¤

В текущем варианте работы прокси мы не можем получать ответ в режиме SSE. То есть пользователь не сможет получать ответ от LLM постепенно, но только весь ответ разом после долгого ожидания, поэтому было предложено сменить настройки прокси. Я изучил текущие настройки и предлагаю добавить в location /:

    # SSE
    proxy_buffering  off;    # Отключить прокси-буферизацию!
    proxy_cache  off;    # Отключить кеширование
    add_header 'Cache-Control' 'no-cache';
    proxy_read_timeout  1h;    # Для долгих SSE-соединений
    proxy_send_timeout  1h;    # Для долгих SSE-соединений
    proxy_set_header Connection '';    # Чтобы не возникало проблем с keep-alive соединениями
  1. По большому счёту, нужно просто отключить буферизацию и стримить ответ: proxy_buffering off;. Это основное. Уже этого, скорее всего хватит.
  2. Затем заголовок proxy_set_header Connection '';, может понадобится, может нет.
  3. Осталось контроль кеширования и увеличение таймаутов. Напрямую это к нашей проблеме не относится, но это в принципе нужно для правильной работы с ответами LLM.

Предложение озвучено в общем чате разработки RAG-слоя.

Продолжение поиска решения¤

14.11.2025

Добавил прокси в проект¤

Реализовал прокси написанный на FastAPI в проекте. Прокси можно запустить через

python -m fast_implementation.proxy

Реализовал механизм, который позволяет в существующем деплое добавлять написанный прокси. Для этого добавил в start.sh запуск этого прокси в зависимости от содержимого переменной окружения RUN_OWU_PROXY, добавил эту переменную в values.yml хелм чарта, она теперь читается из ConfigMap. Механизм будет работать в деве и в тесте, в продуктовую среду не добавлял.

Прокси запускается автоматически при запуске пода, если выставлены нужные параметры.

Ближайшие цели¤

Прокси мне даёт:

  1. Воспроизводит поведение на тесте, которое ломает OpenWebUI
  2. Позволяет видеть все вызовы со стороны OpenWebUI, вместе с заголовками.

Продолжаю дебаг. Ближайшие цели:

  1. Увидеть реальные запросы со стороны OpenWebUI. Есть вероятность, что способы общения с моделью отличается от наших представлений.
  2. Локализация проблемы в коде OpenWebUI. Буду добавлять логирование в код OpenWebUI и локализовывать потерю ответа от LLM.

Изучение вызовов со стороны OpenWebUI¤

Во время работы прокси я вижу следующую картину в ответе:

2025-11-14 11:11:24,538 | INFO | REQUEST: POST v1/chat/completions
Headers: {'host': 'localhost:9999', 'content-type': 'application/json', 'x-openwebui-user-name': 'Pavel', 'x-openwebui-user-id': '0823afed-eecb-42c0-a353-b296208b6570', 'x-openwebui-user-email': 'basspv@sibur.ru', 'x-openwebui-user-role': 'admin', 'x-openwebui-chat-id': 'c162fee3-4562-4e55-99a9-1e686908e5c8', 'accept': '*/*', 'accept-encoding': 'gzip, deflate, br', 'user-agent': 'Python/3.11 aiohttp/3.12.15', 'content-length': '228'}
Query: {}
Body: {"stream": true, "model": "infidelis/GigaChat-20B-A3B-instruct:q8_0", "messages": [{"role": "user", "content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option."}]}
2025-11-14 11:11:24,538 | INFO | Remove Host header
2025-11-14 11:11:24,538 | INFO | url='http://s001cd-0205:11434/v1/chat/completions'
2025-11-14 11:11:24,538 | INFO | Starting request...
2025-11-14 11:11:31,285 | INFO | RESPONSE: http://s001cd-0205:11434/v1/chat/completions
Status: 200
Headers: {'Content-Type': 'text/event-stream', 'Date': 'Fri, 14 Nov 2025 11:11:25 GMT', 'Transfer-Encoding': 'chunked'}
Body: data: {"id":"chatcmpl-490","object":"chat.completion.chunk","created":1763118685,"model":"infidelis/GigaChat-20B-A3B-instruct:q8_0","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"Cer"},"finish_reason":null}]}

data: {"id":"chatcmpl-490","object":"chat.completion.chunk","created":1763118685,"model":"infidelis/GigaChat-20B-A3B-instruct:q8_0","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"tain"},"finish_reason":null}]}

data: {"id":"chatcmpl-490","object":"chat.completion.chunk","created":1763118685,"model":"infidelis/GigaChat-20B-A3B-instruct:q8_0","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"ly"},"finish_reason":null}]}

data: {"id":"chatcmpl-490","object":"chat.completion.chunk","created":1763118685,"model":"infidelis/GigaChat-20B-A3B-instruct:q8_0","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"!"}...[TRUNCATED]

Просто перехваченный ответ на стороне прокси, полученный в полном объёме, будет содержать все переданные события SSE. В тесте я вижу уже собранный ответ от LLM в виде JSON. Получается, что Nginx не только работает как прокси, но и выполняет логику клиента.