LLM-эндпоинт для Go, Java и .NET: таймауты и стриминг
LLM-эндпоинт для Go, Java и .NET часто ломается не в коде, а в мелочах: таймаутах, SSE-стриминге, прокси и настройках TLS.

Где команды теряют время
Проблема обычно начинается не с модели, а с ожиданий. Команда меняет base_url на общий OpenAI-совместимый эндпоинт, например api.rullm.com, оставляет прежние SDK и ждет одинакового поведения во всех сервисах. На практике один и тот же запрос в Go проходит за 20 секунд, в Java висит до обрыва, а в .NET слишком рано закрывает стрим.
Снаружи это легко принять за сбой у провайдера или за "нестабильную" модель. Логи только путают картину: один клиент пишет timeout, другой сообщает о reset соединения, третий возвращает пустой поток без явной ошибки. Из-за этого команды сравнивают промпты, модели и маршруты, хотя причина обычно гораздо ближе.
Чаще всего время уходит на мелкие различия в HTTP-настройках. Они выглядят безобидно, пока сервис не попадает под нагрузку или в корпоративную сеть с прокси, DPI и своими TLS-правилами. Тогда одинаковая бизнес-логика ведет себя по-разному просто потому, что у клиентов разные значения по умолчанию.
Обычно ломаются четыре места: таймауты на соединение и чтение настраивают отдельно, но часто задают только один; SSE-стриминг идет кусками, а клиент или прокси буферизует ответ и убивает "живой" вывод; пул соединений и keep-alive по-разному работают под серией коротких запросов; корпоративный прокси меняет TLS, режет длинные ответы или закрывает idle-соединения.
Особенно неприятны ошибки, которые видны только в бою. На тестовом стенде все выглядит нормально: мало запросов, короткие ответы, прямой выход в интернет. После релиза появляются редкие таймауты, обрывы стрима на 40-й секунде и жалобы, что "LLM иногда молчит". На деле модель отвечает, но клиент не дождался, прокси закрыл соединение или библиотека решила, что поток уже мертв.
У команды с тремя сервисами это обычно выглядит похоже: Go-сервис стабилен, потому что разработчик явно настроил transport и connection pool; Java-сервис страдает из-за неверного read timeout; .NET-сервис спотыкается на стриминге через прокси. Снаружи кажется, что общий LLM-эндпоинт для Go, Java и .NET ведет себя непредсказуемо. На самом деле клиенты по-разному обращаются с одним и тем же HTTP-каналом.
Что должно совпасть у всех клиентов
Один и тот же LLM-эндпоинт работает предсказуемо только тогда, когда Go, Java и .NET ходят в него по одним правилам. На практике команды часто меняют только base_url, а все остальное оставляют на дефолтах SDK. Потом один сервис спокойно стримит ответ, второй ждет весь JSON целиком, а третий падает на прокси.
Если вы используете OpenAI-совместимый шлюз вроде RU LLM, этого обычно хватает для первого запуска: меняете base_url на api.rullm.com и сохраняете текущий код. Но одинаковое поведение требует сверить весь контракт запроса, а не только адрес.
Единый контракт для всех сервисов
Сначала зафиксируйте общую матрицу настроек и раздайте ее всем командам. В ней должны совпадать точный base_url, путь запроса, заголовок Authorization, формат ответа, лимиты по времени, правила ретраев, работа через корпоративный прокси и проверка TLS.
Даже небольшое расхождение меняет поведение. Один клиент может слать запрос на /v1/chat/completions, другой на старый путь, а третий добавит лишний slash в base_url. Снаружи это выглядит как одна интеграция, но в логах вы получите три разных сценария.
Со стримингом расхождение видно еще быстрее. Если Go и .NET читают SSE по мере прихода чанков, а Java-клиент буферизует ответ до конца, пользователи увидят разную задержку при одном и том же промпте. Для поддержки это один из самых неприятных случаев: модель одна, тариф один, а жалобы разные.
С ретраями лучше быть жесткими. После сетевого сбоя повторно отправлять запрос можно не всегда. Если один сервис автоматически ретраит стриминговый вызов, а другой нет, вы получите дубли, лишние токены в биллинге и спорные записи в аудит-трейлах.
Отдельно опишите прокси и сертификаты. В закрытых контурах именно здесь теряют часы: Go доверяет одному хранилищу CA, Java живет со своим truststore, .NET зависит от настроек ОС и контейнера. Пока это не сведено к одному правилу, одинакового поведения не будет, даже если код почти совпадает.
Как сравнивать таймауты в Go, Java и .NET
Одинаковые цифры в конфиге не значат одинаковое поведение. Один клиент считает таймаутом весь запрос целиком, другой только соединение, третий ожидание первого байта. Из-за этого одна и та же модель может стабильно стримить в одном сервисе и обрываться в другом через 60 или 100 секунд.
Если у команды один LLM-эндпоинт для Go, Java и .NET, сравнивайте не только значения, но и точку применения таймаута. Часто это важнее самой цифры. Полезно свести все настройки в одну таблицу: connect timeout, TLS handshake, ожидание заголовков, чтение body, общий request timeout.
Что ломает стриминг
В Go частая ошибка очень простая: ставят http.Client.Timeout = 60 * time.Second и ждут, что этого хватит для SSE. Не хватит. Этот таймаут ограничивает всю жизнь запроса, включая чтение потока. Если модель отвечает долго и присылает токены постепенно, клиент все равно оборвет стрим по общему лимиту.
В Java путаница обычно живет в двух местах. Команда меняет таймаут в SDK, но забывает про транспортный слой, например OkHttp или Apache HttpClient. В итоге приложение думает, что дало 5 минут, а нижний слой закрывает соединение через 30 секунд без новых данных.
В .NET похожая ловушка есть у HttpClient.Timeout. Для длинного streaming-запроса он почти всегда мешает. Сервис может нормально работать на коротких ответах, а потом получать TaskCanceledException на длинных генерациях и искать проблему в модели, хотя виноват клиент.
Практическое правило простое:
- для соединения держите короткий лимит, часто 3-10 секунд;
- для чтения стрима ставьте длинный лимит или отдельную отмену через token;
- не задавайте общий timeout на весь запрос, если клиент читает SSE минутами.
Такой разбор быстро убирает ложные версии. Если Go-сервис обрывает поток ровно через 60 секунд, Java через 30, а .NET через 100, проблема почти наверняка не в API и не в маршрутизации модели, а в разных уровнях таймаутов.
Практичный ориентир
Для стриминга лучше мыслить не одним числом, а цепочкой ожиданий. Сначала клиент должен быстро открыть TCP и TLS. Потом он должен дождаться заголовков. После этого начинается длинное чтение, где сервер присылает токены, служебные события или keep-alive.
Простой пример: Go-воркер, Java-бэкенд и .NET-кабинет идут в api.rullm.com с одинаковым промптом. Если падает только .NET, сначала смотрите HttpClient.Timeout, а не логи модели. Если сыпется только Go, проверьте общий http.Client.Timeout. Если ломается Java, ищите второй таймаут в транспортном слое.
Хорошая настройка для всех трех стеков выглядит скучно, и это скорее плюс: короткий таймаут на соединение, длинное чтение, явная отмена по бизнес-логике и никакой магии в глобальном request timeout.
Как не сломать стриминг
Стриминг чаще всего ломается не в модели, а в клиенте или где-то по пути до него. Один сервис ждет события построчно, другой пытается сначала собрать весь ответ целиком, и команда часами ищет проблему не там.
Для OpenAI-совместимого SSE клиент должен читать text/event-stream как поток строк, а не как обычный JSON-ответ. Если библиотека или ваш обертчик буферизует весь body до конца, пользователь не увидит токены по мере генерации. Он получит весь текст сразу или упрется в таймаут.
Пустые строки тоже важны. Они разделяют события. Если парсер отбрасывает их без правил, поток легко разобрать неверно. Отдельно проверьте маркер [DONE]: одни клиенты считают его обычной строкой, другие ждут после него еще данные и зависают при закрытии соединения.
Что проверить первым
Если вы настраиваете LLM-эндпоинт для Go, Java и .NET, начните с самого короткого маршрута. Отправьте стрим-запрос напрямую в общий эндпоинт, без корпоративного прокси, API gateway и лишних middleware. Так вы быстро поймете, где именно рвется поток.
Проверьте четыре вещи:
- читает ли клиент поток построчно, без ожидания полного тела ответа;
- отдает ли библиотека события сразу, а не после внутреннего буфера;
- корректно ли обрабатываются пустые строки и
[DONE]; - пишете ли вы в лог время до первого токена и причину обрыва.
Время до первого токена обычно говорит больше, чем общий таймаут. Если первый токен приходит за 700 мс напрямую в api.rullm.com, а через внутренний прокси только через 8 секунд, дело чаще всего в буферизации ответа, сжатии или попытке middleware прочитать body целиком для аудита.
Типичный сбой
Распространенный сценарий такой: команда включает логирование ответа на уровне reverse proxy, и прокси начинает копить куски потока. Формально запрос успешен, но стрим уже не стрим. Пользователь видит длинную паузу, а потом большой кусок текста одним блоком.
Исправление обычно простое. Уберите буферизацию ответа, отключите middleware, которые переписывают body, и проверьте, не ломает ли соединение TLS inspection. Только потом возвращайте прокси в цепочку по одному слою. Так быстрее найти причину, чем разбирать все сразу.
Что проверить в прокси и TLS
Если один сервис видит эндпоинт, а второй нет, причина часто не в JSON и не в токене. Для LLM-эндпоинта для Go, Java и .NET сбои нередко приходят из прокси, хранилищ сертификатов и правил обхода прокси.
HTTP_PROXY, HTTPS_PROXY и NO_PROXY похожи на общий стандарт, но клиенты читают их не одинаково. Go чаще берет эти переменные из окружения без дополнительного кода. Java и .NET нередко ждут явную настройку прокси в коде, конфиге приложения или параметрах запуска. В итоге один сервис идет напрямую, другой через корпоративный прокси, а третий вообще не открывает соединение.
NO_PROXY тоже дает сюрпризы. Один клиент сравнивает только хост, другой учитывает порт, а запись с лишней схемой просто не срабатывает. Если команда переводит сервисы на единый адрес, например api.rullm.com, стоит проверить не только значение переменной, но и реальный маршрут трафика.
Корпоративный сертификат ломает TLS-рукопожатие чаще, чем кажется. Прокси с инспекцией трафика подменяет сертификат на свой, и браузер это переживает легче, чем серверный код. Java хранит доверенные центры сертификации отдельно, .NET обычно смотрит в системное хранилище, а контейнер может жить со своим набором сертификатов. Поэтому у Go запрос проходит, у Java падает TLS, а .NET работает только на машине разработчика.
Прокси влияет не только на сертификаты. Он может закрывать idle-соединения через 30-60 секунд, менять keep-alive, добавлять лишнюю задержку и чаще рвать длинные ответы. На коротком POST это почти незаметно. На SSE-стриминге это быстро превращается в обрывы, повторы и странные таймауты.
Порядок проверки
- Сначала отправьте обычный POST без стриминга и получите короткий ответ.
- Проверьте, какой маршрут использует каждый клиент: прямой или через прокси.
- Убедитесь, что корпоративный сертификат есть в нужном хранилище у Java, .NET и внутри контейнера.
- Только потом включайте стриминг и смотрите, не буферизует ли прокси ответ и не закрывает ли соединение раньше времени.
Команды часто тратят день на поиск "несовместимости SDK", хотя проблема обычно проще. Go подхватил NO_PROXY из окружения, Java нет, а .NET доверяет другому набору сертификатов. Когда вы выравниваете прокси и TLS, остальная диагностика идет заметно быстрее.
Единый сценарий настройки
Если у вас три клиента на Go, Java и .NET, не настраивайте их сразу "по уму". Так команды часто маскируют проблему ретраями, пулами и разными таймаутами. Лучше взять один и тот же короткий OpenAI-совместимый запрос и прогнать его по одному сценарию.
Для такого теста хватит простого запроса с коротким промптом, маленьким max_tokens и temperature=0. Зафиксируйте одинаковые модель, заголовки и тело запроса. Если вы проверяете общий эндпоинт вроде api.rullm.com, меняйте только base_url, а все остальное оставляйте прежним.
- Сначала отправьте обычный запрос без стриминга. Цель здесь не в скорости, а в совпадении поведения. Сохраните HTTP-логи: метод, путь, статус, заголовки, время ответа и текст ошибки, если она есть.
- Потом сравните ответы между тремя клиентами. Если Go получает 200, Java уходит в таймаут, а .NET режет тело ответа, проблема почти всегда в клиентской конфигурации, а не в модели.
- После этого включите SSE с тем же запросом. Смотрите не только на общий ответ, но и на время первого токена.
- Дальше прогоните тот же тест через прокси, который реально стоит на пути в проде. Если стриминг ломается только через прокси, вы уже сузили поиск до сети и промежуточного слоя.
- И только потом добавляйте ретраи, пул соединений, keep-alive и небольшую нагрузку.
Полезно сразу складывать результат в простую таблицу: статус, общее время ответа, время первого токена, размер ответа и текст ошибки. Через полчаса у вас будет честная карта различий между Go, Java и .NET. Это почти всегда полезнее, чем сразу смотреть на код и гадать, где именно клиент ведет себя иначе.
Пример команды с тремя сервисами
У одной команды три сервиса, и каждый по-своему работает с моделью. Go-сервис отвечает в чате поддержки и отдает текст сразу, по мере генерации. Java-сервис проверяет ответ перед отправкой клиенту, поэтому ему чаще нужен обычный ответ без стриминга. Сервис на .NET собирает итоговый отчет по диалогу, и его запросы живут дольше остальных.
Все три сервиса ходят в один OpenAI-совместимый эндпоинт, но используют разные библиотеки и разные HTTP-клиенты. Из-за этого одна и та же проблема выглядит как три разных сбоя. В Go команда видит context deadline exceeded, в Java кажется, что модель слишком медленная, а в .NET отчет иногда обрывается на последнем блоке текста.
Если команда использует единый шлюз вроде RU LLM, код и SDK часто можно не менять, достаточно заменить base_url. Но это не убирает разницу в сетевых настройках. Именно там обычно и теряют часы.
Одна таблица вместо трех расследований
Удобнее всего свести настройки в одну короткую таблицу и сравнить не SDK, а поведение запросов. Обычно хватает нескольких строк: общий timeout на весь запрос, timeout на установку соединения, режим SSE, поведение прокси и буферизация ответа, правила retry.
После этого ложные симптомы быстро исчезают. У Go-сервиса можно оставить стриминг и дать длинное чтение, потому что оператору поддержки нужен ответ сразу. Java-сервису часто проще выключить SSE совсем: он проверяет текст целиком, и поток только усложняет обработку. Для .NET-отчета обычно нужен самый длинный лимит чтения, потому что итоговый текст больше и приходит дольше.
На практике команда часто находит совсем не то, что искала. Проблема оказывается не в модели и не в провайдере, а в том, что Java-прокси буферизует SSE, Go ждет первый токен 15 секунд, а .NET закрывает длинное соединение раньше, чем отчет собрался полностью.
Когда у всех трех сервисов есть одна таблица таймаутов и одно правило по стримингу, разбор инцидентов становится короче. Вместо трех отдельных версий про "нестабильный API" команда видит простой факт: эндпоинт один, а ожидания клиентов были разными.
Частые ошибки и ложные следы
Когда команда подключает общий LLM-эндпоинт для Go, Java и .NET, она часто ищет причину там, где ее нет. Модель кажется "медленной" или "ломаной", хотя сбой дает таймаут, пул соединений или корпоративный прокси.
Самая частая ловушка - один общий таймаут на 30 секунд для всего запроса. Для короткого JSON-ответа этого иногда хватает. Для длинного SSE-стрима нет. Клиент получает первые токены, потом соединение обрывается ровно на одной и той же секунде, и команда начинает менять модель, температуру или провайдера. Гораздо полезнее проверить deadline клиента, idle timeout прокси и лимиты ingress.
Не меньше времени съедает привычка создавать новый HTTP-клиент на каждый запрос. В Go вы теряете переиспользование соединений. В Java быстро упираетесь в настройки пула. В .NET растет число лишних сокетов и TLS-рукопожатий. На тестах это может не всплыть, а под нагрузкой стрим начинает подвисать кусками.
Частые симптомы выглядят так:
- обрыв случается примерно через 30 секунд;
- первый запрос проходит, а потом задержка растет;
- ответ дублируется после повторной попытки;
- один сервис стримит нормально, а соседний с тем же
base_urlнет.
У каждого такого симптома есть более приземленная причина, чем "плохая модель". Если приложение повторяет streaming-запрос после частичного ответа, пользователь может увидеть дублированный текст, а биллинг посчитает две отдельные попытки. Повторять такой запрос безопасно только тогда, когда приложение умеет явно разруливать частичный вывод.
После замены SDK команды часто забывают проверить сырые заголовки. Для обычного ответа и для стрима цепочка отличается. Если клиент или прокси переписывает Accept и Content-Type, поведение меняется тихо: сервер может вернуть не тот формат, а промежуточный слой начнет буферизовать ответ вместо живого потока.
На практике много ложных следов дает именно сеть. Команда меняет base_url на api.rullm.com и сохраняет старые правила прокси, TLS inspection и response buffering. Формально SDK совместим, код почти не менялся, но соединение режет слой между приложением и API. Если обрыв повторяется на одной и той же стадии, полезнее снять сырые заголовки и тайминги соединения, чем снова переключать модель.
Быстрая проверка перед релизом
Перед релизом полезно сделать один короткий прогон, который ловит большую часть типичных сбоев. Смысл простой: убрать различия между клиентами и проверить сеть, таймауты и поведение стриминга.
Начните с одного test prompt для Go, Java и .NET. Текст должен быть коротким и одинаковым, например запрос на 20-30 токенов без сложной логики. Если ответы расходятся уже здесь, проблема почти всегда не в модели, а в конфиге клиента или окружения.
Отдельно проверьте base_url. Во всех средах он должен быть одним и тем же, без "почти одинаковых" значений. Если одна служба ходит в старый адрес, а две другие в новый, вы получите три разных набора ошибок и потратите лишнее время на ложные следы. Для команд, которые используют RU LLM, это обычно означает один и тот же api.rullm.com во всех сервисах и одинаковый формат OpenAI-совместимого запроса.
Перед запуском достаточно зафиксировать несколько вещей: один prompt и одни параметры запроса для всех клиентов, один base_url для нужных сред, явные таймауты на соединение и чтение, два прогона без стриминга и со стримингом, а также HTTP-лог первой ошибки с кодом ответа, заголовками и телом.
Таймауты лучше задавать явно, даже если SDK уже что-то ставит по умолчанию. Иначе Go может оборвать долгий ответ по общему deadline, Java зависнет на чтении, а .NET отвалится по своему HttpClient.Timeout. Когда все три значения прописаны руками, сравнение сразу становится честнее.
Стриминг проверяйте отдельно. Сначала убедитесь, что обычный ответ без stream=true проходит стабильно. Потом включайте SSE и смотрите не только на финальный текст, но и на то, приходят ли чанки без пауз, не режет ли их прокси и не закрывает ли клиент соединение раньше времени.
Самая частая ошибка перед релизом выглядит скучно: команда сохраняет только текст исключения. Этого мало. Нужен первый HTTP-лог с методом, URL, статусом, временем до ошибки и телом ответа. Один такой лог почти всегда полезнее десяти скриншотов из мониторинга.
Что сделать дальше
Если у вас один LLM-эндпоинт для Go, Java и .NET, сведите все настройки в одну таблицу. В ней должны быть одинаково названы base_url, таймаут на соединение, таймаут чтения, общий дедлайн запроса, режим стриминга, адрес прокси и источник сертификатов. Когда эти поля лежат рядом, спор о "странном SDK" обычно заканчивается быстро.
Не пытайтесь выровнять все библиотеки до мелочей. Достаточно договориться о нескольких общих полях и одном формате для фиксации отклонений. Тогда Go, Java и .NET перестают жить по разным правилам.
Потом закрепите ответственность. Частая причина срывов простая: таймауты меняет одна команда, прокси настраивает другая, а сертификаты обновляет третья. В итоге каждый уверен, что проблема не у него, и часы уходят на проверку не того слоя.
- Соберите общую матрицу настроек для Go, Java и .NET и храните ее рядом с кодом.
- Назначьте одного владельца для таймаутов, прокси и сертификатов, даже если внедряют их разные команды.
- Держите два smoke-теста: обычный ответ без стриминга и ответ со стримингом SSE.
- Прогоняйте оба теста через тот же прокси и тот же TLS-контур, что и в проде.
- Фиксируйте результат после каждого изменения SDK, корпоративного прокси или цепочки сертификатов.
Если нужен OpenAI-совместимый эндпоинт в РФ, RU LLM удобно использовать как быстрый тест на совместимость: в типовом случае команда меняет base_url на api.rullm.com и продолжает работать с теми же SDK, кодом и промптами. А для систем с требованиями 152-ФЗ имеет смысл заранее проверить, где хранятся логи и бэкапы, как маскируются PII и как проходит аудит запросов. У RU LLM эти механизмы встроены на уровне платформы: хранение в РФ, маскирование PII, метки AI-Law и аудит-трейлы в каждом запросе.
Хороший порог перед релизом простой: оба smoke-теста проходят во всех трех клиентах, через один и тот же прокси, с теми же сертификатами и без ручных правок в коде в последний момент.
Часто задаваемые вопросы
Почему один и тот же запрос работает в Go, но висит в Java?
Почти всегда дело не в модели, а в разных HTTP-настройках. В Java часто срабатывает read timeout или таймаут транспорта, даже если в SDK вы поставили больше.
Сравните не только числа в конфиге, но и то, на каком слое они действуют: соединение, TLS, ожидание заголовков и чтение тела. Один и тот же запрос легко ведет себя по-разному, если нижний клиент закрывает сокет раньше.
Можно ли просто сменить base_url и оставить дефолты SDK?
Для первого запуска этого часто хватает, но для стабильной работы — нет. base_url меняет только адрес, а таймауты, стриминг, ретраи, прокси и TLS остаются разными.
Если вы идете через OpenAI-совместимый шлюз вроде RU LLM, сначала действительно удобно заменить только base_url. Потом сразу сверьте общий контракт запроса у всех сервисов, иначе отличия всплывут уже под нагрузкой.
Какой таймаут чаще всего убивает SSE-стриминг?
Чаще всего стрим ломает общий таймаут на весь запрос. В Go это нередко http.Client.Timeout, в .NET — HttpClient.Timeout.
Для SSE держите короткий лимит на установку соединения и длинное чтение потока. Если вы ограничите всю жизнь запроса одним числом, клиент оборвет генерацию даже тогда, когда сервер честно присылает токены.
Нужен ли общий timeout на весь streaming-запрос?
Обычно не нужен. Для длинного стрима такой лимит мешает больше, чем помогает, потому что клиент может читать события минутами.
Лучше задайте короткий connect timeout, отдельно контролируйте чтение и отменяйте запрос по бизнес-логике через token или context. Так вы не срежете живой поток ровно на 30, 60 или 100 секунде.
Что проверить первым, если стрим приходит одним куском?
Начните с самого короткого маршрута: клиент напрямую к эндпоинту, без корпоративного прокси и лишних middleware. Если напрямую токены идут сразу, а через ваш слой приходят пачкой, проблему создает буферизация.
Часто виноваты reverse proxy, логирование тела ответа или middleware, которые пытаются прочитать весь body до конца. Для SSE клиент должен читать text/event-stream построчно, а не ждать полный JSON.
Почему .NET получает TaskCanceledException на длинных ответах?
Смотрите сначала на HttpClient.Timeout. На коротких ответах он может не мешать, а на длинной генерации дает TaskCanceledException и маскируется под сбой модели.
Еще проверьте, как сервис идет через прокси. .NET нередко нормально работает на машине разработчика, но в контейнере или закрытом контуре упирается в другой маршрут, сертификаты или раннее закрытие соединения.
Как понять, что виноват прокси, а не модель?
Проверьте маршрут трафика и повторите один и тот же запрос напрямую и через прокси. Если без прокси обычный POST и SSE проходят, а через прокси растет задержка первого токена или рвется поток, искать нужно в сети.
Еще один явный признак — обрыв на одной и той же стадии. Модель редко ломается ровно через одинаковое число секунд, а прокси и ingress так делают постоянно.
Нужно ли ретраить streaming-запросы?
По умолчанию лучше не ретраить стрим после частичного ответа. Иначе пользователь увидит дубли, а биллинг посчитает лишнюю попытку.
Повторяйте только те запросы, где приложение умеет распознать частичный вывод и безопасно склеить результат. Для обычного короткого запроса без стриминга ретраи проще и безопаснее.
Что логировать перед релизом, чтобы потом не гадать?
Сохраняйте не только текст исключения. Нужны метод, URL, статус, заголовки, время до ошибки и, если возможно, время до первого токена.
Такой лог сразу показывает, где искать проблему: в маршруте, таймауте, формате ответа или прокси. Один сырой HTTP-лог обычно полезнее набора скриншотов из мониторинга.
Как быстро выровнять поведение Go, Java и .NET с одним LLM-эндпоинтом?
Сведите в одну таблицу base_url, путь, таймаут соединения, чтение, режим стриминга, прокси, сертификаты и правила ретраев. Потом прогоните один короткий запрос без стриминга и тот же запрос со стримингом во всех трех клиентах.
Если Go, Java и .NET проходят оба smoke-теста через один и тот же прокси и TLS-контур, дальше жить станет проще. Когда один сервис все еще ведет себя иначе, разница почти всегда уже видна в этой таблице.