Skip to content

Commit 52074a1

Browse files
committed
Replace magic numbers with named constants and configurable LLM timeout
- Extract SESSION_TTL, SESSION_GC_INTERVAL, SSE_KEEPALIVE_INTERVAL in main.py - Add LLM_TIMEOUT_MS env var (default 60000) to Config with settings_db override - Pass configurable timeout to all LLM providers instead of hardcoded 60s
1 parent 67149f5 commit 52074a1

7 files changed

Lines changed: 27 additions & 10 deletions

File tree

app/assistant/service.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,23 +140,27 @@ def _detect_db_type(db_url: str) -> str:
140140
def _build_provider(config: Config) -> LLMProvider:
141141
provider = config.llm_provider.lower()
142142
base_url = config.llm_base_url or _PROVIDER_BASE_URLS.get(provider)
143+
timeout = config.llm_timeout_ms / 1000
143144

144145
if provider == "anthropic":
145146
return AnthropicProvider(
146147
api_key=config.llm_api_key,
147148
model=config.llm_model,
148149
base_url=base_url,
150+
timeout=timeout,
149151
)
150152

151153
if provider == "ollama":
152154
return OllamaProvider(
153155
model=config.llm_model,
154156
base_url=base_url,
157+
timeout=timeout,
155158
)
156159

157160
# All others: OpenAI-compatible /v1/chat/completions
158161
return ChatCompletionsProvider(
159162
api_key=config.llm_api_key,
160163
model=config.llm_model,
161164
base_url=base_url,
165+
timeout=timeout,
162166
)

app/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class Config:
1818
llm_model: str
1919
llm_base_url: str | None
2020
openai_api_mode: str
21+
llm_timeout_ms: int
2122
chat_history_enabled: bool
2223
chat_history_limit: int
2324

@@ -44,6 +45,7 @@ def load(cls) -> "Config":
4445
llm_model = os.getenv("LLM_MODEL", "gpt-5.4-mini")
4546
llm_base_url: str | None = os.getenv("LLM_BASE_URL") or None
4647
openai_api_mode = os.getenv("OPENAI_API_MODE", "chat").lower()
48+
llm_timeout_ms = int(os.getenv("LLM_TIMEOUT_MS", "60000"))
4749
chat_history_enabled = True
4850
chat_history_limit = 10
4951

@@ -74,6 +76,9 @@ def load(cls) -> "Config":
7476
v = settings_db.get_app_setting("enable_explanations")
7577
if v:
7678
enable_explanations = v.lower() == "true"
79+
v = settings_db.get_app_setting("llm_timeout_ms")
80+
if v:
81+
llm_timeout_ms = int(v)
7782
v = settings_db.get_app_setting("chat_history_enabled")
7883
if v:
7984
chat_history_enabled = v.lower() == "true"
@@ -108,6 +113,7 @@ def load(cls) -> "Config":
108113
llm_model=llm_model,
109114
llm_base_url=llm_base_url,
110115
openai_api_mode=openai_api_mode,
116+
llm_timeout_ms=llm_timeout_ms,
111117
chat_history_enabled=chat_history_enabled,
112118
chat_history_limit=chat_history_limit,
113119
)

app/llm/providers/anthropic.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@
1414

1515

1616
class AnthropicProvider(LLMProvider):
17-
def __init__(self, api_key: str, model: str, base_url: str | None) -> None:
17+
def __init__(self, api_key: str, model: str, base_url: str | None, timeout: float = 60) -> None:
1818
self.api_key = api_key
1919
self.model = model
2020
self.base_url = (base_url or "https://api.anthropic.com").rstrip("/")
21+
self.timeout = timeout
2122

2223
async def generate(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]:
2324
if not self.api_key:
@@ -47,7 +48,7 @@ async def generate(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]:
4748
}
4849

4950
url = f"{self.base_url}/v1/messages"
50-
async with httpx.AsyncClient(timeout=60) as client:
51+
async with httpx.AsyncClient(timeout=self.timeout) as client:
5152
response = await client.post(url, headers=headers, json=payload)
5253
response.raise_for_status()
5354
data = response.json()

app/llm/providers/chat_completions.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@
1212

1313

1414
class ChatCompletionsProvider(LLMProvider):
15-
def __init__(self, api_key: str, model: str, base_url: str | None) -> None:
15+
def __init__(self, api_key: str, model: str, base_url: str | None, timeout: float = 60) -> None:
1616
self.api_key = api_key
1717
self.model = model
1818
self.base_url = (base_url or "https://api.openai.com").rstrip("/")
19+
self.timeout = timeout
1920

2021
async def generate(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]:
2122
url = f"{self.base_url}/v1/chat/completions"
@@ -28,7 +29,7 @@ async def generate(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]:
2829
if self.api_key:
2930
headers["Authorization"] = f"Bearer {self.api_key}"
3031

31-
async with httpx.AsyncClient(timeout=60) as client:
32+
async with httpx.AsyncClient(timeout=self.timeout) as client:
3233
response = await client.post(url, headers=headers, json=payload)
3334
response.raise_for_status()
3435
data = response.json()

app/llm/providers/ollama.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66

77

88
class OllamaProvider(LLMProvider):
9-
def __init__(self, model: str, base_url: str | None) -> None:
9+
def __init__(self, model: str, base_url: str | None, timeout: float = 60) -> None:
1010
self.model = model
1111
self.base_url = (base_url or "http://localhost:11434").rstrip("/")
12+
self.timeout = timeout
1213

1314
async def generate(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]:
1415
url = f"{self.base_url}/api/chat"
@@ -18,7 +19,7 @@ async def generate(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]:
1819
"stream": False,
1920
"options": {"temperature": 0.1},
2021
}
21-
async with httpx.AsyncClient(timeout=60) as client:
22+
async with httpx.AsyncClient(timeout=self.timeout) as client:
2223
response = await client.post(url, json=payload)
2324
response.raise_for_status()
2425
data = response.json()

app/main.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,12 @@ def _build_app_state() -> tuple[Config, ToolRegistry]:
3535

3636
SUPPORTED_MCP_VERSIONS = {"2025-11-25", "2025-06-18", "2025-03-26"}
3737

38+
SESSION_TTL = 600 # seconds before idle session expires
39+
SESSION_GC_INTERVAL = 60 # seconds between garbage collection sweeps
40+
SSE_KEEPALIVE_INTERVAL = 15 # seconds between SSE keepalive pings
41+
3842
_sessions: Dict[str, Tuple[asyncio.Queue[str], float]] = {}
3943
_sessions_lock = asyncio.Lock()
40-
_session_ttl_seconds = 600
4144

4245

4346
def reload_config() -> None:
@@ -84,13 +87,13 @@ async def _enqueue(session_id: str, payload: Dict[str, Any]) -> bool:
8487

8588
async def _gc_sessions() -> None:
8689
while True:
87-
await asyncio.sleep(60)
90+
await asyncio.sleep(SESSION_GC_INTERVAL)
8891
now = time.time()
8992
async with _sessions_lock:
9093
expired = [
9194
session_id
9295
for session_id, (_, last_seen) in _sessions.items()
93-
if now - last_seen > _session_ttl_seconds
96+
if now - last_seen > SESSION_TTL
9497
]
9598
for session_id in expired:
9699
_sessions.pop(session_id, None)
@@ -296,7 +299,7 @@ async def event_stream():
296299
try:
297300
while True:
298301
try:
299-
message = await asyncio.wait_for(queue.get(), timeout=15)
302+
message = await asyncio.wait_for(queue.get(), timeout=SSE_KEEPALIVE_INTERVAL)
300303
yield f"event: message\ndata: {message}\n\n"
301304
except asyncio.TimeoutError:
302305
yield ": keepalive\n\n"

tests/test_mcp_api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ async def test_ui_routes_use_latest_runtime_config(
244244
llm_model="gpt-5.4-mini",
245245
llm_base_url=None,
246246
openai_api_mode="chat",
247+
llm_timeout_ms=60000,
247248
chat_history_enabled=True,
248249
chat_history_limit=10,
249250
)

0 commit comments

Comments
 (0)