From 7e3d1e347c4a7fa5a4294b1b38a1e9c53ce44b4f Mon Sep 17 00:00:00 2001 From: subochev Date: Mon, 5 Oct 2026 20:01:06 +0300 Subject: [PATCH] =?UTF-8?q?fix:=204xx=20(400/401/404/...)=20=D1=82=D0=B5?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D1=8C=20=D1=82=D1=80=D0=B8=D0=B3=D0=B3=D0=B5?= =?UTF-8?q?=D1=80=D0=B8=D1=82=20failover=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=BE=20=D0=BF=D1=80=D0=BE=D0=B1=D1=80=D0=B0=D1=81=D1=8B=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Раньше 4xx-ответы upstream'а (напр. 400 'Model is unavailable' от opencode.ai) передавались клиенту как есть — следующие upstream'ы в пуле не пробовались. В твоей модели assistant после deepseek-v4-flash-opencode с 400 оставались ещё три upstream'а (minimax-m3, minimax-m3-2, deepseek-v4-flash), которые могли бы ответить — но прокси молча возвращал 400 и сдавался. Теперь любой 4xx — failover-триггер (как 5xx/429/402), тело ошибки логируется, но клиенту не возвращается. Если все upstream'ы 4xx — клиент получит 503 'all upstreams failed', а не 400 от самого слабого звена пула. Memory 2733 обновлена. --- .../kotlin/pw/binom/llmproxy/Main.kt | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/commonMain/kotlin/pw/binom/llmproxy/Main.kt b/src/commonMain/kotlin/pw/binom/llmproxy/Main.kt index c32df96..5113a33 100644 --- a/src/commonMain/kotlin/pw/binom/llmproxy/Main.kt +++ b/src/commonMain/kotlin/pw/binom/llmproxy/Main.kt @@ -276,27 +276,34 @@ private suspend fun handleChat( setBody(forwarded.toString()) }.execute { resp -> upstreamStatus = resp.status.value - if (upstreamStatus >= 500 || upstreamStatus == 429 || upstreamStatus == 402) { - val rest = backoff.recordFailure(up) - metrics.record(modelName, up, statusClass(upstreamStatus)) - log.warn { - "[llm-proxy] model=$modelName upstream=${up.id} вернул $upstreamStatus → фейловер" + - (rest?.let { " (backoff: отдых ${it.toIsoString()})" } ?: "") + if (upstreamStatus in 400..599) { + // 4xx/5xx — upstream не смог обработать запрос: 5xx/429/402 — сбой + // провайдера (учёт в backoff); 400/401/404/etc. — ошибка либо + // запроса, либо состояния апстрима (например, "Model is unavailable"), + // но в обоих случаях следующий upstream в пуле может сработать, и + // пробуем его. Тело ошибки логируем, но клиенту не возвращаем — + // иначе пользователь увидит 400 от самого слабого звена пула, + // хотя другие upstream'ы могли бы ответить. + val errorBody = runCatching { resp.body() }.getOrDefault("") + if (upstreamStatus >= 500 || upstreamStatus == 429 || upstreamStatus == 402) { + val rest = backoff.recordFailure(up) + metrics.record(modelName, up, statusClass(upstreamStatus)) + log.warn { + "[llm-proxy] model=$modelName upstream=${up.id} вернул $upstreamStatus " + + "errorBody=${errorBody.take(500)} → фейловер" + + (rest?.let { " (backoff: отдых ${it.toIsoString()})" } ?: "") + } + } else { + metrics.record(modelName, up, statusClass(upstreamStatus)) + log.warn { + "[llm-proxy] model=$modelName upstream=${up.id} вернул $upstreamStatus " + + "errorBody=${errorBody.take(500)} → фейловер" + } } failed.add(up.id) failover = true return@execute } - if (upstreamStatus in 400..499) { - // 4xx — JSON-тело, не стрим: читаем безопасно и отдаём клиенту как есть. - val errorBody = runCatching { resp.body() }.getOrDefault("") - log.warn { "[llm-proxy] chat model=$modelName upstream=${up.id} вернул $upstreamStatus errorBody=${errorBody.take(500)}" } - metrics.record(modelName, up, statusClass(upstreamStatus)) - val ct = resp.headers["Content-Type"] ?: "application/json" - call.respondText(errorBody, ContentType.parse(ct), HttpStatusCode.fromValue(upstreamStatus)) - responded = true - return@execute - } responded = true backoff.recordSuccess(up) if (clientWantsStream) {