1 Commits

Author SHA1 Message Date
subochev 7e3d1e347c fix: 4xx (400/401/404/...) теперь триггерит failover вместо пробрасывания клиенту
Build LLM Proxy / Build and push (release) Successful in 59s
Раньше 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 обновлена.
2026-10-05 20:01:06 +03:00
+23 -16
View File
@@ -276,27 +276,34 @@ private suspend fun handleChat(
setBody(forwarded.toString()) setBody(forwarded.toString())
}.execute { resp -> }.execute { resp ->
upstreamStatus = resp.status.value upstreamStatus = resp.status.value
if (upstreamStatus >= 500 || upstreamStatus == 429 || upstreamStatus == 402) { if (upstreamStatus in 400..599) {
val rest = backoff.recordFailure(up) // 4xx/5xx — upstream не смог обработать запрос: 5xx/429/402 — сбой
metrics.record(modelName, up, statusClass(upstreamStatus)) // провайдера (учёт в backoff); 400/401/404/etc. — ошибка либо
log.warn { // запроса, либо состояния апстрима (например, "Model is unavailable"),
"[llm-proxy] model=$modelName upstream=${up.id} вернул $upstreamStatus → фейловер" + // но в обоих случаях следующий upstream в пуле может сработать, и
(rest?.let { " (backoff: отдых ${it.toIsoString()})" } ?: "") // пробуем его. Тело ошибки логируем, но клиенту не возвращаем —
// иначе пользователь увидит 400 от самого слабого звена пула,
// хотя другие upstream'ы могли бы ответить.
val errorBody = runCatching { resp.body<String>() }.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) failed.add(up.id)
failover = true failover = true
return@execute return@execute
} }
if (upstreamStatus in 400..499) {
// 4xx — JSON-тело, не стрим: читаем безопасно и отдаём клиенту как есть.
val errorBody = runCatching { resp.body<String>() }.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 responded = true
backoff.recordSuccess(up) backoff.recordSuccess(up)
if (clientWantsStream) { if (clientWantsStream) {