From b5666c4d68e3113d42530e12b224e081d47ff503 Mon Sep 17 00:00:00 2001 From: Luis Novo Date: Sun, 19 Oct 2025 11:37:24 -0300 Subject: [PATCH] Fix/increase fix: increase API client timeouts for transformation operations timeouts (#170) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: increase API client timeouts for transformation operations - Increase frontend timeout from 30s to 300s (5 minutes) - Increase Streamlit API client timeout from 30s to 300s - Add API_CLIENT_TIMEOUT environment variable for configurability - Add ESPERANTO_LLM_TIMEOUT environment variable documentation - Update .env.example with comprehensive timeout documentation Fixes #131 - API timeout errors during transformation generation Transformations now have sufficient time to complete on slower hardware (Ollama, LM Studio) without frontend timeout errors. Users can now configure timeouts for both the API client layer (API_CLIENT_TIMEOUT) and the LLM provider layer (ESPERANTO_LLM_TIMEOUT) to accommodate their specific hardware and network conditions. * docs: add timeout configuration documentation - Add comprehensive timeout troubleshooting section to common-issues.md - Add FAQ entry about timeout errors during transformations - Document API_CLIENT_TIMEOUT and ESPERANTO_LLM_TIMEOUT usage - Provide specific timeout recommendations for different hardware/network scenarios - Link to GitHub issue #131 for reference * chore: bump * refactor: improve timeout configuration with validation and consistency Based on PR review feedback, this commit addresses several improvements: **Timeout Validation:** - Add validation to ensure timeout values are between 30s and 3600s - Invalid values fall back to default 300s with warning logs - Handles edge cases (negative, zero, invalid strings) **Fix Hard-coded Timeouts:** - Replace all hard-coded timeout values in api/client.py - ask_simple: 300s → self.timeout - execute_transformation: 120s → self.timeout - embed_content: 120s → self.timeout - create_source: 300s → self.timeout - rebuild_embeddings: Uses smart logic (2x timeout, max 3600s) **Improved Documentation:** - Add clarifying comments about ms vs seconds (frontend vs backend) - Document that frontend uses 300000ms = backend 300s - Add inline documentation for rebuild_embeddings timeout logic **Development Dependencies:** - Add pytest>=8.0.0 to dev dependencies for future test coverage This makes timeout configuration more robust, consistent, and user-friendly while maintaining backward compatibility. --- .env.example | 33 ++++++++++++++ api/client.py | 46 ++++++++++++++----- docs/troubleshooting/common-issues.md | 66 +++++++++++++++++++++++++++ docs/troubleshooting/faq.md | 21 +++++++++ frontend/src/lib/api/client.ts | 6 ++- pyproject.toml | 3 +- uv.lock | 38 ++++++++++++++- 7 files changed, 198 insertions(+), 15 deletions(-) diff --git a/.env.example b/.env.example index dd809b5..4dff16a 100644 --- a/.env.example +++ b/.env.example @@ -19,6 +19,39 @@ # Only set this if you need to override the auto-detection (e.g., reverse proxy scenarios). API_URL=http://localhost:5055 +# API CLIENT TIMEOUT (in seconds) +# Controls how long the frontend/Streamlit UI waits for API responses +# Increase this if you're using slow AI providers or hardware (Ollama on CPU, remote LM Studio, etc.) +# Default: 300 seconds (5 minutes) - sufficient for most transformation/insight operations +# +# Common scenarios: +# - Fast cloud APIs (OpenAI, Anthropic): 300 seconds is more than enough +# - Local Ollama on GPU: 300 seconds should work fine +# - Local Ollama on CPU: Consider 600 seconds (10 minutes) or more +# - Remote LM Studio over slow network: Consider 900 seconds (15 minutes) +# - Very large documents: May need 900+ seconds +# +# API_CLIENT_TIMEOUT=300 + +# ESPERANTO LLM TIMEOUT (in seconds) +# Controls the timeout for AI model API calls at the Esperanto library level +# This is separate from API_CLIENT_TIMEOUT and applies to the actual LLM provider requests +# Only increase this if you're experiencing timeouts during model inference itself +# Default: 60 seconds (built into Esperanto) +# +# Important: This should generally be LOWER than API_CLIENT_TIMEOUT to allow proper error handling +# +# Common scenarios: +# - Fast cloud APIs (OpenAI, Anthropic, Groq): 60 seconds is sufficient +# - Local Ollama with small models: 120-180 seconds may help +# - Local Ollama with large models on CPU: 300+ seconds +# - Remote or self-hosted LLMs: 180-300 seconds depending on hardware +# +# Note: If transformations complete but you see timeout errors, increase API_CLIENT_TIMEOUT first. +# Only increase ESPERANTO_LLM_TIMEOUT if the model itself is timing out during inference. +# +# ESPERANTO_LLM_TIMEOUT=60 + # SECURITY # Set this to protect your Open Notebook instance with a password (for public hosting) # OPEN_NOTEBOOK_PASSWORD= diff --git a/api/client.py b/api/client.py index d628b25..016d31c 100644 --- a/api/client.py +++ b/api/client.py @@ -15,7 +15,24 @@ class APIClient: def __init__(self, base_url: Optional[str] = None): self.base_url = base_url or os.getenv("API_BASE_URL", "http://127.0.0.1:5055") - self.timeout = 30.0 + # Timeout increased to 5 minutes (300s) to accommodate slow LLM operations + # (transformations, insights) on slower hardware (Ollama, LM Studio, remote APIs) + # Configurable via API_CLIENT_TIMEOUT environment variable (in seconds) + timeout_str = os.getenv("API_CLIENT_TIMEOUT", "300.0") + try: + timeout_value = float(timeout_str) + # Validate timeout is within reasonable bounds (30s - 3600s / 1 hour) + if timeout_value < 30: + logger.warning(f"API_CLIENT_TIMEOUT={timeout_value}s is too low, using minimum of 30s") + timeout_value = 30.0 + elif timeout_value > 3600: + logger.warning(f"API_CLIENT_TIMEOUT={timeout_value}s is too high, using maximum of 3600s") + timeout_value = 3600.0 + self.timeout = timeout_value + except ValueError: + logger.error(f"Invalid API_CLIENT_TIMEOUT value '{timeout_str}', using default 300s") + self.timeout = 300.0 + # Add authentication header if password is set self.headers = {} password = os.getenv("OPEN_NOTEBOOK_PASSWORD") @@ -117,9 +134,9 @@ class APIClient: "answer_model": answer_model, "final_answer_model": final_answer_model, } - # Use 5 minute timeout for long-running ask operations + # Use configured timeout for long-running ask operations return self._make_request( - "POST", "/api/search/ask/simple", json=data, timeout=300.0 + "POST", "/api/search/ask/simple", json=data, timeout=self.timeout ) # Models API methods @@ -199,9 +216,9 @@ class APIClient: "input_text": input_text, "model_id": model_id, } - # Use extended timeout for transformation operations + # Use configured timeout for transformation operations return self._make_request( - "POST", "/api/transformations/execute", json=data, timeout=120.0 + "POST", "/api/transformations/execute", json=data, timeout=self.timeout ) # Notes API methods @@ -251,8 +268,8 @@ class APIClient: "item_type": item_type, "async_processing": async_processing, } - # Use extended timeout for embedding operations - return self._make_request("POST", "/api/embed", json=data, timeout=120.0) + # Use configured timeout for embedding operations + return self._make_request("POST", "/api/embed", json=data, timeout=self.timeout) def rebuild_embeddings( self, @@ -261,15 +278,20 @@ class APIClient: include_notes: bool = True, include_insights: bool = True ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: - """Rebuild embeddings in bulk.""" + """Rebuild embeddings in bulk. + + Note: This operation can take a long time for large databases. + Consider increasing API_CLIENT_TIMEOUT to 600-900s for bulk rebuilds. + """ data = { "mode": mode, "include_sources": include_sources, "include_notes": include_notes, "include_insights": include_insights, } - # Use extended timeout for rebuild operations (up to 10 minutes) - return self._make_request("POST", "/api/embeddings/rebuild", json=data, timeout=600.0) + # Use double the configured timeout for bulk rebuild operations (or configured value if already high) + rebuild_timeout = max(self.timeout, min(self.timeout * 2, 3600.0)) + return self._make_request("POST", "/api/embeddings/rebuild", json=data, timeout=rebuild_timeout) def get_rebuild_status(self, command_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Get status of a rebuild operation.""" @@ -347,8 +369,8 @@ class APIClient: if transformations: data["transformations"] = transformations - # Use 5 minute timeout for source creation (especially PDF processing with OCR) - return self._make_request("POST", "/api/sources/json", json=data, timeout=300.0) + # Use configured timeout for source creation (especially PDF processing with OCR) + return self._make_request("POST", "/api/sources/json", json=data, timeout=self.timeout) def get_source(self, source_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]: """Get a specific source.""" diff --git a/docs/troubleshooting/common-issues.md b/docs/troubleshooting/common-issues.md index 2195233..82deaa2 100644 --- a/docs/troubleshooting/common-issues.md +++ b/docs/troubleshooting/common-issues.md @@ -195,6 +195,72 @@ This document covers the most frequently encountered issues when installing, con - Use lower-tier models for testing - Check provider rate limits +### API Timeout Errors During Transformations + +**Problem**: Timeout errors when running transformations or generating insights, even though the operation completes successfully. + +**Symptoms**: +- "timeout of 30000ms exceeded" in React frontend +- "Failed to connect to API: timed out" in Streamlit UI +- Transformation completes after a few minutes, but error appears after 30-60 seconds +- Common with local models (Ollama), remote LM Studio, or slow hardware + +**Solutions**: + +1. **Increase API client timeout** (recommended): + ```bash + # Add to your .env file + API_CLIENT_TIMEOUT=600 # 10 minutes (600 seconds) + ``` + + This controls how long the frontend/UI waits for API responses. Default is 300 seconds (5 minutes). + +2. **Adjust timeout based on your setup**: + ```bash + # Fast cloud APIs (OpenAI, Anthropic, Groq) + API_CLIENT_TIMEOUT=300 # 5 minutes (default) + + # Local Ollama on GPU + API_CLIENT_TIMEOUT=600 # 10 minutes + + # Local Ollama on CPU or slow hardware + API_CLIENT_TIMEOUT=1200 # 20 minutes + + # Remote LM Studio over slow network + API_CLIENT_TIMEOUT=900 # 15 minutes + ``` + +3. **Increase LLM provider timeout if needed**: + ```bash + # Add to your .env file if the model itself is timing out + ESPERANTO_LLM_TIMEOUT=180 # 3 minutes (default is 60s) + ``` + + Only increase this if you see errors during actual model inference, not just HTTP timeouts. + +4. **Use faster models for testing**: + - Test with cloud APIs first to verify setup + - Try smaller local models (e.g., `gemma2:2b` instead of `llama3:70b`) + - Preload models before running transformations: `ollama run model-name` + +5. **Restart services after configuration changes**: + ```bash + # For Docker + docker compose down + docker compose up -d + + # For source installation + make stop-all + make start-all + ``` + +**Important Notes**: +- `API_CLIENT_TIMEOUT` should be HIGHER than `ESPERANTO_LLM_TIMEOUT` for proper error handling +- If transformations complete successfully after refresh, you only need to increase `API_CLIENT_TIMEOUT` +- First time running a model may be slower due to model loading + +**Related GitHub Issue**: [#131](https://github.com/lfnovo/open-notebook/issues/131) + ### Memory and Performance Issues **Problem**: Application running slowly or crashing due to memory issues. diff --git a/docs/troubleshooting/faq.md b/docs/troubleshooting/faq.md index 04bde6e..1a7ae80 100644 --- a/docs/troubleshooting/faq.md +++ b/docs/troubleshooting/faq.md @@ -338,6 +338,27 @@ tar -xzf backup-20240101.tar.gz ## Troubleshooting +### Why do I get timeout errors even though transformations complete successfully? + +**Cause**: The default client timeout (5 minutes) may be too short for slow AI providers or hardware. + +**Quick fix**: +```bash +# Add to your .env file +API_CLIENT_TIMEOUT=600 # 10 minutes for slow hardware +``` + +**When this happens**: +- Using local Ollama models on CPU +- Using remote LM Studio over slow network +- First transformation after starting (model loading) +- Very large documents +- Slower hardware configurations + +**Detailed solutions**: See [Common Issues - API Timeout Errors](./common-issues.md#api-timeout-errors-during-transformations) + +**Note**: If transformations complete after you refresh the page, you only need to increase `API_CLIENT_TIMEOUT`, not `ESPERANTO_LLM_TIMEOUT`. + ### My question isn't answered here. What should I do? 1. **Check the troubleshooting guide**: [Common Issues](./common-issues.md) diff --git a/frontend/src/lib/api/client.ts b/frontend/src/lib/api/client.ts index 65fbb07..8599b74 100644 --- a/frontend/src/lib/api/client.ts +++ b/frontend/src/lib/api/client.ts @@ -3,8 +3,12 @@ import { getApiUrl } from '@/lib/config' // API client with runtime-configurable base URL // The base URL is fetched from the API config endpoint on first request +// Timeout increased to 5 minutes (300000ms = 300s) to accommodate slow LLM operations +// (transformations, insights generation) especially on slower hardware (Ollama, LM Studio) +// Note: Frontend uses milliseconds (300000ms), backend uses seconds (300s) - both equal 5 minutes +// To configure: Set API_CLIENT_TIMEOUT=600 in .env for 10 minutes (600s = 600000ms) export const apiClient = axios.create({ - timeout: 30000, + timeout: 300000, // 300 seconds = 5 minutes headers: { 'Content-Type': 'application/json', }, diff --git a/pyproject.toml b/pyproject.toml index 2da49c5..3c94dd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "open-notebook" -version = "1.0.5" +version = "1.0.6" description = "An open source implementation of a research assistant, inspired by Google Notebook LM" authors = [ {name = "Luis Novo", email = "lfnovo@gmail.com"} @@ -55,6 +55,7 @@ dev = [ "types-requests>=2.32.0.20241016", "ipywidgets>=8.1.5", "pre-commit>=4.0.1", + "pytest>=8.0.0", ] [build-system] diff --git a/uv.lock b/uv.lock index f091184..e2b8de5 100644 --- a/uv.lock +++ b/uv.lock @@ -1232,6 +1232,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/c6/fa760e12a2483469e2bf5058c5faff664acf66cadb4df2ad6205b016a73d/imageio_ffmpeg-0.6.0-py3-none-win_amd64.whl", hash = "sha256:02fa47c83703c37df6bfe4896aab339013f62bf02c5ebf2dce6da56af04ffc0a", size = 31246824, upload-time = "2025-01-16T21:34:28.6Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "ipykernel" version = "7.0.1" @@ -2199,7 +2208,7 @@ wheels = [ [[package]] name = "open-notebook" -version = "1.0.5" +version = "1.0.6" source = { editable = "." } dependencies = [ { name = "ai-prompter" }, @@ -2238,6 +2247,7 @@ dev = [ { name = "ipywidgets" }, { name = "mypy" }, { name = "pre-commit" }, + { name = "pytest" }, { name = "ruff" }, { name = "types-requests" }, ] @@ -2276,6 +2286,7 @@ requires-dist = [ { name = "podcast-creator", specifier = ">=0.7.0" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.0.1" }, { name = "pydantic", specifier = ">=2.9.2" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.5.5" }, { name = "surreal-commands", specifier = ">=1.0.13" }, @@ -2589,6 +2600,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "podcast-creator" version = "0.7.0" @@ -2936,6 +2956,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, ] +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0"