diff --git a/gateway/platforms/telegram.py b/gateway/platforms/telegram.py index 304c5625d..ad7c8f3d6 100644 --- a/gateway/platforms/telegram.py +++ b/gateway/platforms/telegram.py @@ -901,9 +901,8 @@ class TelegramAdapter(BasePlatformAdapter): pass # best-effort truncation return SendResult(success=True, message_id=message_id) # Flood control / RetryAfter — short waits are retried inline, - # long waits (>5s) return a failure so the caller can decide - # whether to wait or degrade gracefully. (grammY auto-retry - # pattern: maxDelaySeconds threshold.) + # long waits return a failure immediately so streaming can fall back + # to a normal final send instead of leaving a truncated partial. retry_after = getattr(e, "retry_after", None) if retry_after is not None or "retry after" in err_str: wait = retry_after if retry_after else 1.0 @@ -912,12 +911,7 @@ class TelegramAdapter(BasePlatformAdapter): self.name, wait, ) if wait > 5.0: - # Long wait — return failure immediately so callers - # (progress edits, stream consumer) aren't blocked. - return SendResult( - success=False, - error=f"flood_control:{wait}", - ) + return SendResult(success=False, error=f"flood_control:{wait}") await asyncio.sleep(wait) try: await self._bot.edit_message_text( diff --git a/gateway/stream_consumer.py b/gateway/stream_consumer.py index 2ceb0fb1d..4a3cf744a 100644 --- a/gateway/stream_consumer.py +++ b/gateway/stream_consumer.py @@ -174,12 +174,12 @@ class GatewayStreamConsumer: self._already_sent = True self._last_sent_text = text else: - # Edit not supported by this adapter — stop streaming, - # let the normal send path handle the final response. - # Without this guard, adapters like Signal/Email would - # flood the chat with a new message every edit_interval. + # If an edit fails mid-stream (especially Telegram flood control), + # stop progressive edits and let the normal final send path deliver + # the complete answer instead of leaving the user with a partial. logger.debug("Edit failed, disabling streaming for this adapter") self._edit_supported = False + self._already_sent = False else: # Editing not supported — skip intermediate updates. # The final response will be sent by the normal path. diff --git a/tools/transcription_tools.py b/tools/transcription_tools.py index 976a59d40..1a7acee9b 100644 --- a/tools/transcription_tools.py +++ b/tools/transcription_tools.py @@ -127,8 +127,11 @@ def is_stt_enabled(stt_config: Optional[dict] = None) -> bool: def _has_openai_audio_backend() -> bool: - """Return True when OpenAI audio can use direct credentials or the managed gateway.""" - return bool(resolve_openai_audio_api_key() or resolve_managed_tool_gateway("openai-audio")) + """Return True when OpenAI audio can use config credentials, env credentials, or the managed gateway.""" + stt_config = _load_stt_config() + openai_cfg = stt_config.get("openai", {}) + cfg_api_key = openai_cfg.get("api_key", "") + return bool(cfg_api_key or resolve_openai_audio_api_key() or resolve_managed_tool_gateway("openai-audio")) def _find_binary(binary_name: str) -> Optional[str]: @@ -577,13 +580,20 @@ def transcribe_audio(file_path: str, model: Optional[str] = None) -> Dict[str, A def _resolve_openai_audio_client_config() -> tuple[str, str]: """Return direct OpenAI audio config or a managed gateway fallback.""" + stt_config = _load_stt_config() + openai_cfg = stt_config.get("openai", {}) + cfg_api_key = openai_cfg.get("api_key", "") + cfg_base_url = openai_cfg.get("base_url", "") + if cfg_api_key: + return cfg_api_key, (cfg_base_url or OPENAI_BASE_URL) + direct_api_key = resolve_openai_audio_api_key() if direct_api_key: return direct_api_key, OPENAI_BASE_URL managed_gateway = resolve_managed_tool_gateway("openai-audio") if managed_gateway is None: - message = "Neither VOICE_TOOLS_OPENAI_KEY nor OPENAI_API_KEY is set" + message = "Neither stt.openai.api_key in config nor VOICE_TOOLS_OPENAI_KEY/OPENAI_API_KEY is set" if managed_nous_tools_enabled(): message += ", and the managed OpenAI audio gateway is unavailable" raise ValueError(message)