Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion agent/error_classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -1141,12 +1141,18 @@ def _build_error_msg(error: Exception, body: Any) -> str:


def _body_message_candidates(body: dict) -> Iterator[Any]:
"""Body message fields in priority order (OpenAI, flat, litellm/Bedrock proxy shapes)."""
"""Body message fields in priority order (OpenAI, flat, litellm/Bedrock proxy, FastAPI shapes)."""
yield _error_obj(body).get("message")
yield body.get("message")
yield body.get("errorMessage")
args = body.get("errorArgs")
yield args.get("reason") if isinstance(args, dict) else None
# FastAPI/Starlette relays and the Codex gateway answer {"detail": "..."} (or a nested
# OpenAI-ish object); without it a descriptive rejection reads as a bare 400 and the
# large-session heuristic sends it into compression (#81558). A list here is pydantic's
# validation shape, read by _oversized_message_content_rejection.
detail = body.get("detail")
yield detail.get("message") if isinstance(detail, dict) else detail if isinstance(detail, str) else None


def _from_cause_chain(error: Exception, pick: Callable[[Any], Any], default: Any) -> Any:
Expand Down
1 change: 1 addition & 0 deletions contributors/emails/nagornyy.o@gmail.com
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
i-Hun
22 changes: 22 additions & 0 deletions tests/agent/test_error_classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -1077,6 +1077,28 @@ def test_400_litellm_invalid_request_body_shape(self, caplog):
for r in caplog.records
), "Expected a distinct warning identifying the malformed-body 400"

def test_400_top_level_detail_body_is_not_a_bare_400_on_large_session(self):
"""FastAPI-style ``{"detail": "..."}`` bodies (Codex gateway, Starlette relays) →
the descriptive text is read, so the large-session heuristic does not route a
model entitlement/retirement rejection into compression (#81558, #106475).
``str(error)`` is the SDK's ``Error code: 400 - {...}`` form, exactly as on the wire.
Salvaged from #100783 (@i-Hun)."""
detail = "The 'gpt-5.5-codex' model is not supported when using Codex with a ChatGPT account."
large = dict(provider="openai-codex", model="gpt-5.5-codex",
approx_tokens=109_962, context_length=272_000, num_messages=223)
for body in ({"detail": detail}, {"detail": {"message": detail}}):
e = MockAPIError(f"Error code: 400 - {body!r}", status_code=400, body=body)
result = classify_api_error(e, **large) # type: ignore[arg-type]
assert result.reason is not FailoverReason.context_overflow, body
assert result.should_compress is False
assert result.should_fallback is True
assert result.message == detail
# Control: the genuinely bare body the heuristic exists for still compresses.
bare = classify_api_error(
MockAPIError("Error code: 400 - {'error': {'message': 'Error'}}", status_code=400,
body={"error": {"message": "Error"}}), **large) # type: ignore[arg-type]
assert bare.reason is FailoverReason.context_overflow


# ── Peer closed + large session ──

Expand Down
Loading