feat(telegram): native DM draft streaming (4.30 — vercel/chat#340)#140
Conversation
vercel/chat#340) Port of upstream chat@4.30.0's Telegram private-chat draft streaming (changeset 5461ea9, PR vercel/chat#340). Adapter (src/chat_sdk/adapters/telegram/adapter.py): - Add `stream()`: DMs get native draft streaming via the `sendMessageDraft` Bot API method. The draft bubble updates in place as chunks arrive, throttled to `options.update_interval_ms` (default TELEGRAM_DEFAULT_STREAM_UPDATE_INTERVAL_MS = 250ms), then a regular `sendMessage` persists the final text. Returns `None` for non-DM threads BEFORE consuming any chunks so the SDK's built-in post+edit fallback handles groups, supergroups, and channels. - Add `with_telegram_markdown_fallback()`: shared retry-without-`parse_mode` path now wraps `post_message`, `edit_message`, `send_document`, and `send_attachment`. When Telegram rejects MarkdownV2 with a "can't parse entities" 400, the call retries once with the plain-text rendering (or the original text when stripping markup left nothing). - Add `render_plain_text_message`, `resolve_telegram_fallback_text`, `create_draft_id` (int32-wrapping, wall-clock-seeded), and `_create_telegram_document_form_data` (form bodies are single-use, so the retry rebuilds them). Core (src/chat_sdk/thread.py, src/chat_sdk/types.py): - `Adapter.stream` / `BaseAdapter.stream` may now return `RawMessage | None`. `None` (returned before any chunk is consumed) delegates to the post+edit fallback for the current thread. - `Thread._handle_stream` checks `if raw_result is not None`; on `None` it closes the unused wrapper generator and falls through to the fallback. The existing `RawMessage.text` override (Teams cancellation reconciliation) is preserved inside the non-None branch. Documented divergence (docs/UPSTREAM_SYNC.md): unlike upstream's `thread.ts`, we do NOT seed `update_interval_ms` with the thread default. Our hand-rolled Teams native streaming path treats any non-`None` value as a caller override of its 1500ms quota-protecting throttle; seeding 500ms would silently drop Teams DM throttling below the platform's ~1 req/sec quota. Adapters apply their own default when the field is `None`, and `_fallback_stream` already substitutes `self._streaming_update_interval_ms`, so the fallback path stays observably identical to upstream. Tests: all 12 upstream `it()` blocks ported to tests/test_telegram_streaming.py plus the `stream returns null` fallback test to tests/test_thread_faithful.py, with Python-only coverage for the default-interval, whitespace-rejection, and empty-fallback-text branches. Parity note: this is a partial 4.30.0 slice; UPSTREAM_PARITY stays at 4.29.0 (the rest of the 4.30 wave is tracked separately). https://claude.ai/code/session_013zwTcMek5rNqBTQvs2oF64
|
Warning Review limit reached
More reviews will be available in 59 minutes and 46 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request implements native DM draft streaming and markdown parse-error fallback for the Telegram adapter, porting features from upstream vercel/chat#340. Specifically, it introduces the stream method in TelegramAdapter to update draft bubbles in real-time for private chats using sendMessageDraft, while delegating non-DM threads back to the SDK's built-in post+edit fallback by returning None. It also adds a robust fallback mechanism (with_telegram_markdown_fallback) that retries message posts, edits, and attachments without a parse mode if Telegram rejects MarkdownV2 entities. Corresponding updates were made to Thread._handle_stream to handle None return values from adapter streams, and comprehensive tests were added to validate these behaviors. I have no feedback to provide as there are no review comments.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d797f2c532
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if not self.is_dm(thread_id): | ||
| return None |
There was a problem hiding this comment.
Delegate username chats to fallback streaming
When the thread id is a valid Telegram channel/supergroup username such as telegram:@announcements, is_dm() returns true because the chat id does not start with -, so this new gate does not delegate to the SDK fallback. sendMessageDraft only works for private numeric chat ids, so the first draft call is rejected, draft updates are disabled, and the stream is consumed until a single final message is posted instead of using the post+edit fallback for non-DMs. Gate native drafts on a positive numeric chat id.
Useful? React with 👍 / 👎.
| final_postable: AdapterPostableMessage = ( | ||
| PostableMarkdown(markdown=accumulated) | ||
| if stream_uses_markdown | ||
| else self.resolve_telegram_fallback_text(accumulated, _markdown_to_plain_text(accumulated)) |
There was a problem hiding this comment.
Finalize markdown before persisting DM streams
When a DM stream ends with markdown the renderer repaired for the draft preview, for example chunks ending in **bold, flush_draft() has just sent the repaired renderer.render() text, but the final sendMessage is built from raw accumulated. Telegram then persists literal/unrepaired markup while users just saw the repaired draft, and Thread.stream records the raw accumulator because the returned RawMessage.text is unset. Use renderer.finish() or otherwise return the text that was actually sent for the final markdown send.
Useful? React with 👍 / 👎.
0.4.30 wave. Cherry-picks 8f290d1 onto current main: native DM draft streaming via sendMessageDraft + the core
stream()->RawMessage|Nonewidening + None-handling in Thread._handle_stream. No version bump. Independent review needed.