feat(gateway): typed send-error classification (SendResult.error_kind) (#50342)

Add a platform-neutral send-failure vocabulary so consumers can branch on a
typed category instead of substring-matching the raw provider message.

- base.py: SEND_ERROR_KINDS + classify_send_error() (too_long / bad_format /
  forbidden / not_found / rate_limited / transient / unknown), and an optional
  SendResult.error_kind field (defaults None — fully backward compatible).
- telegram.py: populate error_kind on send() failures; message_too_long keeps
  its existing error token plus error_kind='too_long'.

Purely additive: no behavioral change to the existing degrade-and-deliver
paths (MarkdownV2->plain-text fallback, overflow split, retry classification
all untouched). 22 new tests + 210 adapter regression tests green.
This commit is contained in:
Teknium 2026-06-21 12:34:22 -07:00 committed by GitHub
parent 6bbacc2238
commit c0409a87ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 244 additions and 2 deletions

View file

@ -72,6 +72,7 @@ from gateway.platforms.base import (
MessageType,
ProcessingOutcome,
SendResult,
classify_send_error,
cache_image_from_bytes,
cache_audio_from_bytes,
cache_video_from_bytes,
@ -2763,6 +2764,7 @@ class TelegramAdapter(BasePlatformAdapter):
except Exception as e:
logger.error("[%s] Failed to send Telegram message: %s", self.name, e, exc_info=True)
err_str = str(e).lower()
error_kind = classify_send_error(e)
# Message too long — content exceeded 4096 chars. Return failure so
# stream consumer enters fallback mode and sends the remainder.
if "message_too_long" in err_str or "too long" in err_str:
@ -2770,7 +2772,7 @@ class TelegramAdapter(BasePlatformAdapter):
"[%s] send() content too long, falling back to new-message continuation",
self.name,
)
return SendResult(success=False, error="message_too_long")
return SendResult(success=False, error="message_too_long", error_kind="too_long")
# TimedOut usually means the request may have reached Telegram —
# mark as non-retryable so _send_with_retry() doesn't re-send.
# Exceptions: a wrapped ConnectTimeout (no connection established)
@ -2780,7 +2782,12 @@ class TelegramAdapter(BasePlatformAdapter):
is_timeout = (_to and isinstance(e, _to)) or "timed out" in err_str
is_connect_timeout = self._looks_like_connect_timeout(e)
is_pool_timeout = self._looks_like_pool_timeout(e)
return SendResult(success=False, error=str(e), retryable=(is_connect_timeout or is_pool_timeout or not is_timeout))
return SendResult(
success=False,
error=str(e),
retryable=(is_connect_timeout or is_pool_timeout or not is_timeout),
error_kind=error_kind,
)
async def send_or_update_status(
self,