chore(ai-chat): guard-пороги в env + единый маркер дегенерации
Три хвоста детектора петель (#444): - Пороги детектора дегенерации теперь конфигурируются из env (по образцу AI_CHAT_FINAL_STEP_LOCKDOWN): AI_CHAT_DEGENERATION_REPEATED_LINES / _PERIOD_MAX_LEN / _PERIOD_MIN_REPEATS / _CHECK_STEP. Резолвер читает сырую строку (пусто = unset → компилируемый дефолт), требует ≥1, иначе безопасный откат к дефолту (0/отриц. сломал бы детектор). Оператор перенастраивает анти-babble-guard без редеплоя. - Единый маркер: при дегенерации live-стрим показывает нейтральное «Response stopped.» (клиент не отличает от ручного Stop), а персист-баннер после refetch падал в дженерик. Классифицировал OUTPUT_DEGENERATION_ERROR на клиенте под тот же заголовок «Response stopped.» + деталь про петлю — live и refetch больше не расходятся. - STEP_LIMIT_NO_ANSWER_MARKER: вместо хардкодной русской строки в content — локаль-нейтральная английская (базовая локаль = i18n-ключ этого репо, читаема моделью на реплее); не-русские юзеры больше не видят русский текст. Тесты: env-пороги гоняются против реальных форм повторов с mutation-проверкой (поднятый checkStep глушит burst; сниженный repeatedLines триггерит короткий ран); клиентская классификация — новый кейс в error-message.test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,23 @@ describe("describeChatError", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("classifies a token-degeneration abort under the SAME 'Response stopped.' marker the live view shows (#495)", () => {
|
||||
// The exact reason the server persists in metadata.error on a degeneration
|
||||
// abort (ai-chat.service OUTPUT_DEGENERATION_ERROR). Live, this event shows
|
||||
// the neutral "Response stopped." notice; the persisted banner MUST match it
|
||||
// so live and refetch never disagree.
|
||||
const view = describeChatError(
|
||||
"Output degeneration detected (repeated token loop)",
|
||||
t,
|
||||
);
|
||||
expect(view.title).toBe("Response stopped.");
|
||||
expect(view.detail).toBe(
|
||||
"The answer was stopped automatically because the model fell into a repeated output loop.",
|
||||
);
|
||||
// Regression guard: it must NOT fall through to the generic heading.
|
||||
expect(view.title).not.toBe("Something went wrong");
|
||||
});
|
||||
|
||||
it("classifies a dropped connection (ECONNRESET) as a lost-connection error", () => {
|
||||
expect(
|
||||
describeChatError("Cannot connect to API: read ECONNRESET", t).title,
|
||||
|
||||
@@ -39,6 +39,22 @@ export function describeChatError(
|
||||
};
|
||||
}
|
||||
|
||||
// Our own token-degeneration abort (#444): the server aborts a runaway
|
||||
// repetition loop and persists this exact reason in metadata.error. LIVE, the
|
||||
// same abort surfaces as the neutral "Response stopped." notice (the client
|
||||
// cannot tell it from a manual Stop mid-stream), so the persisted banner must
|
||||
// read the SAME "Response stopped." marker — otherwise the live view and a
|
||||
// later refetch show two different texts for one event. The detail explains the
|
||||
// loop-guard cause without contradicting the shared heading.
|
||||
if (/output degeneration detected|repeated token loop/i.test(msg)) {
|
||||
return {
|
||||
title: t("Response stopped."),
|
||||
detail: t(
|
||||
"The answer was stopped automatically because the model fell into a repeated output loop.",
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (/"statusCode"\s*:\s*403\b/.test(msg)) {
|
||||
return {
|
||||
title: t("AI chat is disabled"),
|
||||
|
||||
Reference in New Issue
Block a user