From 1172b9b67f3175c33b10355e67c1ed9dc9e19359 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 25 Feb 2026 07:27:39 +0000 Subject: [PATCH] Add API timeout to all backends + mesh-aware system prompt All three LLM backends (Google, OpenAI, Anthropic) now wrap API calls in asyncio.wait_for() using config.timeout (default 30s). Previously Gemini could hang indefinitely with grounding+AFC enabled. Router catches TimeoutError with user-friendly "request timed out" message. Empty context buffer now injects "[No recent mesh traffic observed yet.]" so the LLM knows the capability exists even when buffer is empty. Default system prompt updated to mention mesh awareness. Co-Authored-By: Claude Opus 4.6 --- meshai/backends/anthropic_backend.py | 17 ++++++++++----- meshai/backends/google_backend.py | 32 +++++++++++++++++++--------- meshai/backends/openai_backend.py | 9 +++++++- meshai/config.py | 5 ++++- meshai/router.py | 8 +++++++ 5 files changed, 54 insertions(+), 17 deletions(-) diff --git a/meshai/backends/anthropic_backend.py b/meshai/backends/anthropic_backend.py index 06cdc88..9cef0f2 100644 --- a/meshai/backends/anthropic_backend.py +++ b/meshai/backends/anthropic_backend.py @@ -1,5 +1,6 @@ """Anthropic (Claude) LLM backend with rolling summary memory.""" +import asyncio import logging from typing import Optional @@ -114,17 +115,23 @@ class AnthropicBackend(LLMBackend): final_messages = messages try: - response = await self._client.messages.create( - model=self.config.model, - max_tokens=max_tokens, - system=enhanced_system, - messages=final_messages, + response = await asyncio.wait_for( + self._client.messages.create( + model=self.config.model, + max_tokens=max_tokens, + system=enhanced_system, + messages=final_messages, + ), + timeout=self.config.timeout, ) # Extract text from response content = response.content[0].text if response.content else "" return content.strip() + except asyncio.TimeoutError: + logger.error(f"Anthropic API timed out after {self.config.timeout}s") + raise except Exception as e: logger.error(f"Anthropic API error: {e}") raise diff --git a/meshai/backends/google_backend.py b/meshai/backends/google_backend.py index 23dbd1c..27810f1 100644 --- a/meshai/backends/google_backend.py +++ b/meshai/backends/google_backend.py @@ -1,5 +1,6 @@ """Google Gemini LLM backend with rolling summary memory and Google Search grounding.""" +import asyncio import logging from typing import Optional @@ -53,15 +54,20 @@ class GoogleBackend(LLMBackend): prompt = _SUMMARIZE_PROMPT.format(conversation=conversation) try: - response = await self._client.aio.models.generate_content( - model=self.config.model, - contents=prompt, - config=types.GenerateContentConfig( - max_output_tokens=150, - temperature=0.3, + response = await asyncio.wait_for( + self._client.aio.models.generate_content( + model=self.config.model, + contents=prompt, + config=types.GenerateContentConfig( + max_output_tokens=150, + temperature=0.3, + ), ), + timeout=self.config.timeout, ) return response.text.strip() if response.text else f"Previous conversation: {len(messages)} messages." + except asyncio.TimeoutError: + logger.warning(f"Summary generation timed out after {self.config.timeout}s") except Exception as e: logger.warning(f"Failed to generate summary: {e}") return f"Previous conversation: {len(messages)} messages about various topics." @@ -112,14 +118,20 @@ class GoogleBackend(LLMBackend): tools=tools if tools else None, ) - response = await self._client.aio.models.generate_content( - model=self.config.model, - contents=contents, - config=config, + response = await asyncio.wait_for( + self._client.aio.models.generate_content( + model=self.config.model, + contents=contents, + config=config, + ), + timeout=self.config.timeout, ) return response.text.strip() if response.text else "" + except asyncio.TimeoutError: + logger.error(f"Google API timed out after {self.config.timeout}s") + raise except Exception as e: logger.error(f"Google API error: {e}") raise diff --git a/meshai/backends/openai_backend.py b/meshai/backends/openai_backend.py index d776daf..5060c91 100644 --- a/meshai/backends/openai_backend.py +++ b/meshai/backends/openai_backend.py @@ -1,5 +1,6 @@ """OpenAI-compatible LLM backend with rolling summary memory.""" +import asyncio import logging from typing import Optional @@ -134,11 +135,17 @@ class OpenAIBackend(LLMBackend): if getattr(self.config, 'web_search', False): request_kwargs["extra_body"] = {"features": {"web_search": True}} - response = await self._client.chat.completions.create(**request_kwargs) + response = await asyncio.wait_for( + self._client.chat.completions.create(**request_kwargs), + timeout=self.config.timeout, + ) content = response.choices[0].message.content return content.strip() if content else "" + except asyncio.TimeoutError: + logger.error(f"OpenAI API timed out after {self.config.timeout}s") + raise except Exception as e: logger.error(f"OpenAI API error: {e}") raise diff --git a/meshai/config.py b/meshai/config.py index fd0bb68..a51841b 100644 --- a/meshai/config.py +++ b/meshai/config.py @@ -98,7 +98,10 @@ class LLMConfig: system_prompt: str = ( "You are a helpful assistant on a Meshtastic mesh network. " "Keep responses VERY brief - under 250 characters total. " - "Be concise but friendly. No markdown formatting." + "Be concise but friendly. No markdown formatting. " + "You can passively observe recent mesh traffic when available. " + "If asked about mesh activity and no recent traffic is shown below, " + "say you haven't observed any traffic yet rather than claiming you lack access." ) use_system_prompt: bool = True # Toggle to disable sending system prompt web_search: bool = False # Enable web search (Open WebUI feature) diff --git a/meshai/router.py b/meshai/router.py index d2ec57b..324f74e 100644 --- a/meshai/router.py +++ b/meshai/router.py @@ -1,5 +1,6 @@ """Message routing logic for MeshAI.""" +import asyncio import logging import re from dataclasses import dataclass @@ -159,6 +160,10 @@ class MessageRouter: "\n\n--- Recent mesh traffic (for context only, not messages to you) ---\n" + context_block ) + else: + system_prompt += ( + "\n\n[No recent mesh traffic observed yet.]" + ) try: response = await self.llm.generate( @@ -166,6 +171,9 @@ class MessageRouter: system_prompt=system_prompt, max_tokens=500, ) + except asyncio.TimeoutError: + logger.error("LLM request timed out") + response = "Sorry, request timed out. Try again." except Exception as e: logger.error(f"LLM generation error: {e}") response = "Sorry, I encountered an error. Please try again."