picoclaw/pkg/providers
Amir Mamaghani 71134babb9
feat(telegram): stream LLM responses via sendMessageDraft (#1101)
* feat(telegram): stream LLM responses in real-time via sendMessageDraft

Implements real-time token streaming to Telegram using the sendMessageDraft
API (telego v1.6.0). Instead of showing only a "Thinking..." placeholder
until the full response arrives, users now see partial LLM output appear
in the chat as it's generated.

The streaming pipeline threads through all layers:

- StreamingProvider interface (providers/types.go): opt-in ChatStream()
  method that receives an onChunk callback with accumulated text
- OpenAI-compatible SSE streaming (openai_compat/provider.go): parses
  SSE events with stream:true, handles text deltas and tool call assembly
- Anthropic native streaming (anthropic/provider.go): uses SDK's
  NewStreaming() for direct Anthropic API connections
- HTTPProvider delegation (http_provider.go): delegates ChatStream to
  the underlying openai_compat provider
- StreamingCapable + Streamer interfaces (channels/interfaces.go):
  opt-in channel capability like TypingCapable/PlaceholderCapable
- Telegram streamer (telegram/telegram.go): BeginStream returns a
  telegramStreamer that throttles sendMessageDraft calls (3s/200 chars)
  with graceful degradation on API errors
- StreamDelegate bridge (bus/bus.go): decouples agent loop from channel
  manager without tight imports
- Manager integration (manager.go): implements StreamDelegate, tracks
  streamActive state, coordinates with placeholder editing
- Agent loop (loop.go): uses ChatStream when both provider and channel
  support streaming, cancels stream on tool calls, skips PublishOutbound
  when Finalize already delivered the message

Graceful degradation:
- Bots without forum/topics mode: first sendMessageDraft error sets
  failed=true, subsequent Updates become no-ops, Finalize still delivers
  via SendMessage. User sees normal non-streaming behavior.
- Non-streaming providers: type assertion fails, falls back to Chat()
- Config opt-out: streaming.enabled (default true) in telegram config

Closes #1098

* fix(telegram): delete placeholder message when streaming delivers response

When streaming was active, the "Thinking..." placeholder message stayed
in the chat because preSend only deleted the tracking entry without
removing the actual Telegram message. Now preSend deletes the placeholder
via the new MessageDeleter interface when streamActive is set.

* refactor(streaming): remove dead code and simplify streaming wiring

- Delete unused Anthropic ChatStream/parseStream (-131 lines) — factory
  creates HTTPProvider for all OpenAI-compat providers including OpenRouter
- Simplify runLLMIteration from 4 to 3 return values (remove unused
  streamed bool)
- Replace managerStreamer struct with finalizeHookStreamer using embedding
  (Update/Cancel promoted, only Finalize overridden)

* fix(streaming): skip streamer acquisition when SendResponse is false

Heartbeat messages set SendResponse=false but the streaming path
was unconditionally acquiring a streamer, causing HEARTBEAT_OK to
leak to Telegram via streamer.Finalize().

* fix(streaming): guard streamer for non-sendable messages, add streaming config

Skip streamer acquisition for heartbeat (NoHistory=true), preventing
HEARTBEAT_OK from leaking to Telegram via streamer.Finalize().

Add streaming.enabled to Telegram defaults and example config.

* feat(telegram): stream LLM responses in real-time via sendMessageDraft

Implements real-time token streaming to Telegram using the sendMessageDraft
API (telego v1.6.0). Instead of showing only a "Thinking..." placeholder
until the full response arrives, users now see partial LLM output appear
in the chat as it's generated.

The streaming pipeline threads through all layers:

- StreamingProvider interface (providers/types.go): opt-in ChatStream()
  method that receives an onChunk callback with accumulated text
- OpenAI-compatible SSE streaming (openai_compat/provider.go): parses
  SSE events with stream:true, handles text deltas and tool call assembly
- Anthropic native streaming (anthropic/provider.go): uses SDK's
  NewStreaming() for direct Anthropic API connections
- HTTPProvider delegation (http_provider.go): delegates ChatStream to
  the underlying openai_compat provider
- StreamingCapable + Streamer interfaces (channels/interfaces.go):
  opt-in channel capability like TypingCapable/PlaceholderCapable
- Telegram streamer (telegram/telegram.go): BeginStream returns a
  telegramStreamer that throttles sendMessageDraft calls (3s/200 chars)
  with graceful degradation on API errors
- StreamDelegate bridge (bus/bus.go): decouples agent loop from channel
  manager without tight imports
- Manager integration (manager.go): implements StreamDelegate, tracks
  streamActive state, coordinates with placeholder editing
- Agent loop (loop.go): uses ChatStream when both provider and channel
  support streaming, cancels stream on tool calls, skips PublishOutbound
  when Finalize already delivered the message

Graceful degradation:
- Bots without forum/topics mode: first sendMessageDraft error sets
  failed=true, subsequent Updates become no-ops, Finalize still delivers
  via SendMessage. User sees normal non-streaming behavior.
- Non-streaming providers: type assertion fails, falls back to Chat()
- Config opt-out: streaming.enabled (default true) in telegram config

Closes #1098

* fix(telegram): delete placeholder message when streaming delivers response

When streaming was active, the "Thinking..." placeholder message stayed
in the chat because preSend only deleted the tracking entry without
removing the actual Telegram message. Now preSend deletes the placeholder
via the new MessageDeleter interface when streamActive is set.

* refactor(streaming): remove dead code and simplify streaming wiring

- Delete unused Anthropic ChatStream/parseStream (-131 lines) — factory
  creates HTTPProvider for all OpenAI-compat providers including OpenRouter
- Simplify runLLMIteration from 4 to 3 return values (remove unused
  streamed bool)
- Replace managerStreamer struct with finalizeHookStreamer using embedding
  (Update/Cancel promoted, only Finalize overridden)

* fix(streaming): skip streamer acquisition when SendResponse is false

Heartbeat messages set SendResponse=false but the streaming path
was unconditionally acquiring a streamer, causing HEARTBEAT_OK to
leak to Telegram via streamer.Finalize().

* fix(streaming): guard streamer for non-sendable messages, add streaming config

Skip streamer acquisition for heartbeat (NoHistory=true), preventing
HEARTBEAT_OK from leaking to Telegram via streamer.Finalize().

Add streaming.enabled to Telegram defaults and example config.

* fix(picoclaw): add missing closing brace for StreamingProvider interface

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve golangci-lint formatting issues

Fix gci import ordering in telegram and anthropic provider, and break
long function signature in openai_compat provider to satisfy golines.

* fix: address code review feedback on streaming PR

- Deduplicate Streamer interface: alias channels.Streamer to bus.Streamer
  to prevent type drift across packages
- Increase SSE scanner buffer to 10MB max to handle large single-line
  responses that exceed bufio.Scanner's 64KB default
- Switch draftID generation from math/rand to crypto/rand for
  collision-resistant random IDs
- Add context cancellation check in SSE parsing loop so cancelled
  streams stop processing immediately
- Log Finalize failures with chat_id and content length for debugging
  silent message delivery failures

* feat: make streaming throttle interval and min growth configurable

Move hardcoded streamThrottleInterval (3s) and streamMinGrowth (200)
into StreamingConfig so they can be tuned per deployment via config
or environment variables.

* fix(telegram): use parseTelegramChatID in DeleteMessage and BeginStream

These two functions called undefined parseChatID. Use
parseTelegramChatID with _ for the unused threadID instead of adding
a wrapper function. Fixes all three CI checks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(streaming): set streamActive only after successful Finalize

Move onFinalize hook to run after Streamer.Finalize succeeds, so that
if Finalize fails the streamActive flag stays false and the regular
placeholder fallback path remains available.

Addresses review feedback from @alexhoshina.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 21:04:14 +08:00
..
anthropic fix(anthropic): skip tool calls with empty names to prevent API errors (#1739) 2026-03-18 21:55:01 +08:00
anthropic_messages fix(provider): skip empty anthropic tool names (#1772) 2026-03-19 21:35:17 +08:00
azure fix(provider/azure): lint err 2026-03-15 12:45:11 +08:00
common feat(provider): add support for azure openai provider (#1422) 2026-03-14 22:52:34 +08:00
openai_compat feat(telegram): stream LLM responses via sendMessageDraft (#1101) 2026-03-20 21:04:14 +08:00
protocoltypes feat(providers): add Media field to Message struct for vision support 2026-03-03 16:27:29 +08:00
antigravity_provider.go fix: handle ignored io.ReadAll errors across codebase 2026-03-05 20:56:38 +09:00
antigravity_provider_test.go fix(antigravity): normalize tool calls to avoid empty function names 2026-02-17 20:05:35 +05:30
claude_cli_provider.go fix(claude_cli): surface stdout in error when CLI exits non-zero 2026-03-12 22:01:56 -04:00
claude_cli_provider_integration_test.go feat(fmt): Run formatters 2026-02-18 21:48:23 +02:00
claude_cli_provider_test.go remove wrapper methods 2026-03-01 18:24:11 +11:00
claude_provider.go feat(fmt): Run formatters 2026-02-18 21:48:23 +02:00
claude_provider_test.go feat(fmt): Fix formatting 2026-02-20 20:03:11 +02:00
codex_cli_credentials.go refactor: centralize environment variable key constants (#1730) 2026-03-18 18:03:24 +08:00
codex_cli_credentials_test.go enable dogsled 2026-02-25 21:14:19 +11:00
codex_cli_provider.go remove wrapper methods 2026-03-01 18:24:11 +11:00
codex_cli_provider_integration_test.go feat(fmt): Run formatters 2026-02-18 21:48:23 +02:00
codex_cli_provider_test.go refactor: update model name and add VolcEngine coding plan (#1412) 2026-03-12 16:10:29 +08:00
codex_provider.go Add model-native search (prefer_native) for OpenAI/Codex (#1618) 2026-03-18 11:55:30 +08:00
codex_provider_test.go Add model-native search (prefer_native) for OpenAI/Codex (#1618) 2026-03-18 11:55:30 +08:00
cooldown.go style: fix gofmt formatting in cooldown files 2026-02-13 12:26:44 -03:00
cooldown_test.go refactor(modernize): apply safe modernize fixes 2026-02-27 16:35:07 +08:00
error_classifier.go fix(pkg):do regex precompile insteadd on the fly (#911) 2026-03-01 23:06:31 +11:00
error_classifier_test.go fix(pkg):do regex precompile insteadd on the fly (#911) 2026-03-01 23:06:31 +11:00
factory.go feat(providers): add LongCat model provider support (#1317) 2026-03-12 02:34:42 +08:00
factory_provider.go feat(provider): add Alibaba Coding Plan and regional Qwen endpoints (#1748) 2026-03-19 22:07:30 +08:00
factory_provider_test.go feat(provider): add Alibaba Coding Plan and regional Qwen endpoints (#1748) 2026-03-19 22:07:30 +08:00
factory_test.go feat(providers): add LongCat model provider support (#1317) 2026-03-12 02:34:42 +08:00
fallback.go feat(config): support multiple API keys for failover (#1707) 2026-03-19 00:57:20 +08:00
fallback_multikey_test.go feat(config): support multiple API keys for failover (#1707) 2026-03-19 00:57:20 +08:00
fallback_test.go feat(config): support multiple API keys for failover (#1707) 2026-03-19 00:57:20 +08:00
github_copilot_provider.go fix: improve error handling in GitHub Copilot provider (#919) 2026-03-01 22:58:41 +11:00
http_provider.go feat(telegram): stream LLM responses via sendMessageDraft (#1101) 2026-03-20 21:04:14 +08:00
legacy_provider.go fix:Openrouter in providers and modellist 2026-02-25 14:44:49 +08:00
model_ref.go feat(provider): add Alibaba Coding Plan and regional Qwen endpoints (#1748) 2026-03-19 22:07:30 +08:00
model_ref_test.go feat(provider): add Alibaba Coding Plan and regional Qwen endpoints (#1748) 2026-03-19 22:07:30 +08:00
tool_call_extract.go feat(fmt): Run formatters 2026-02-18 21:48:23 +02:00
toolcall_utils.go enable dupl check 2026-03-01 18:17:32 +11:00
types.go feat(telegram): stream LLM responses via sendMessageDraft (#1101) 2026-03-20 21:04:14 +08:00