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 <noreply@anthropic.com>
This commit is contained in:
Ubuntu 2026-02-25 07:27:39 +00:00
commit 1172b9b67f
5 changed files with 54 additions and 17 deletions

View file

@ -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(
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

View file

@ -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(
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(
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

View file

@ -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

View file

@ -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)

View file

@ -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."