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) {