From 3007174a61cbad9145794f24c9d7f9251fdf1d39 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:11:34 -0700 Subject: [PATCH] fix: prevent 400 format errors from triggering compression loop on Codex Responses API (#6751) The error classifier's generic-400 heuristic only extracted err_body_msg from the nested body structure (body['error']['message']), missing the flat body format used by OpenAI's Responses API (body['message']). This caused descriptive 400 errors like 'Invalid input[index].name: string does not match pattern' to appear generic when the session was large, misclassifying them as context overflow and triggering an infinite compression loop. Added flat-body fallback in _classify_400() consistent with the parent classify_api_error() function's existing handling at line 297-298. --- agent/error_classifier.py | 3 +++ tests/agent/test_error_classifier.py | 32 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/agent/error_classifier.py b/agent/error_classifier.py index b227932ad..0f1450113 100644 --- a/agent/error_classifier.py +++ b/agent/error_classifier.py @@ -596,6 +596,9 @@ def _classify_400( err_obj = body.get("error", {}) if isinstance(err_obj, dict): err_body_msg = (err_obj.get("message") or "").strip().lower() + # Responses API (and some providers) use flat body: {"message": "..."} + if not err_body_msg: + err_body_msg = (body.get("message") or "").strip().lower() is_generic = len(err_body_msg) < 30 or err_body_msg in ("error", "") is_large = approx_tokens > context_length * 0.4 or approx_tokens > 80000 or num_messages > 80 diff --git a/tests/agent/test_error_classifier.py b/tests/agent/test_error_classifier.py index da248f821..c59735589 100644 --- a/tests/agent/test_error_classifier.py +++ b/tests/agent/test_error_classifier.py @@ -507,6 +507,38 @@ class TestClassifyApiError: assert result.reason == FailoverReason.format_error assert result.retryable is False + def test_400_flat_body_descriptive_not_context_overflow(self): + """Responses API flat body with descriptive error + large session → format error. + + The Codex Responses API returns errors in flat body format: + {"message": "...", "type": "..."} without an "error" wrapper. + A descriptive 400 must NOT be misclassified as context overflow + just because the session is large. + """ + e = MockAPIError( + "Invalid 'input[index].name': string does not match pattern.", + status_code=400, + body={"message": "Invalid 'input[index].name': string does not match pattern.", + "type": "invalid_request_error"}, + ) + result = classify_api_error(e, approx_tokens=200000, context_length=400000, num_messages=500) + assert result.reason == FailoverReason.format_error + assert result.retryable is False + + def test_400_flat_body_generic_large_session_still_context_overflow(self): + """Flat body with generic 'Error' message + large session → context overflow. + + Regression: the flat-body fallback must not break the existing heuristic + for genuinely generic errors from providers that use flat bodies. + """ + e = MockAPIError( + "Error", + status_code=400, + body={"message": "Error"}, + ) + result = classify_api_error(e, approx_tokens=100000, context_length=200000) + assert result.reason == FailoverReason.context_overflow + # ── Peer closed + large session ── def test_peer_closed_large_session(self):