-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
fix: LLM timeouts, realtime endpoint override, Python run() parity #3879
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1248b42
249b7b8
e86f778
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -41,6 +41,38 @@ def _maybe_load_dotenv() -> None: | |||||||||||||||||||||||||||||||||||||||||||
| PORT = int(os.getenv('PORT', 8090)) | ||||||||||||||||||||||||||||||||||||||||||||
| NGROK_AUTH_TOKEN = os.getenv('NGROK_AUTH_TOKEN') | ||||||||||||||||||||||||||||||||||||||||||||
| PUBLIC = os.getenv('PUBLIC', 'false').lower() == 'true' | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| def _resolve_realtime_endpoint(): | ||||||||||||||||||||||||||||||||||||||||||||
| """Resolve the realtime WebSocket URL + auth headers. | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| Defaults to OpenAI so existing users with only ``OPENAI_API_KEY`` set keep | ||||||||||||||||||||||||||||||||||||||||||||
| working unchanged. Operators running Azure / a self-hosted realtime-capable | ||||||||||||||||||||||||||||||||||||||||||||
| gateway can point the voice path elsewhere without editing this module via: | ||||||||||||||||||||||||||||||||||||||||||||
| - ``PRAISONAI_REALTIME_URL`` full ``wss://...`` URL (takes precedence) | ||||||||||||||||||||||||||||||||||||||||||||
| - ``PRAISONAI_REALTIME_MODEL`` model for the default OpenAI URL | ||||||||||||||||||||||||||||||||||||||||||||
| - ``PRAISONAI_REALTIME_API_KEY`` bearer key (falls back to OPENAI_API_KEY) | ||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||
| explicit_url = os.getenv('PRAISONAI_REALTIME_URL') | ||||||||||||||||||||||||||||||||||||||||||||
| api_key = os.getenv('PRAISONAI_REALTIME_API_KEY') or OPENAI_API_KEY | ||||||||||||||||||||||||||||||||||||||||||||
| if explicit_url: | ||||||||||||||||||||||||||||||||||||||||||||
| headers = {"Authorization": f"Bearer {api_key}"} if api_key else {} | ||||||||||||||||||||||||||||||||||||||||||||
| if "openai.com" in explicit_url: | ||||||||||||||||||||||||||||||||||||||||||||
| headers["OpenAI-Beta"] = "realtime=v1" | ||||||||||||||||||||||||||||||||||||||||||||
| return explicit_url, headers | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+56
to
+62
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Reject plaintext custom endpoints when an API key is present. When Proposed validation+from urllib.parse import urlsplit
+
explicit_url = os.getenv('PRAISONAI_REALTIME_URL')
api_key = os.getenv('PRAISONAI_REALTIME_API_KEY') or OPENAI_API_KEY
if explicit_url:
+ parsed_url = urlsplit(explicit_url)
+ if api_key and parsed_url.scheme != "wss":
+ raise ValueError(
+ "PRAISONAI_REALTIME_URL must use wss:// when an API key is configured"
+ )
headers = {"Authorization": f"Bearer {api_key}"} if api_key else {}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| model = os.getenv( | ||||||||||||||||||||||||||||||||||||||||||||
| 'PRAISONAI_REALTIME_MODEL', 'gpt-4o-realtime-preview-2024-10-01' | ||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||
| f"wss://api.openai.com/v1/realtime?model={model}", | ||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||
| "Authorization": f"Bearer {api_key}", | ||||||||||||||||||||||||||||||||||||||||||||
| "OpenAI-Beta": "realtime=v1", | ||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+67
to
+72
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Do not send When no realtime key is configured, the default branch still creates Proposed fix+ if not api_key:
+ raise RuntimeError(
+ "PRAISONAI_REALTIME_API_KEY or OPENAI_API_KEY is required"
+ )
return (📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| SYSTEM_MESSAGE = ( | ||||||||||||||||||||||||||||||||||||||||||||
| "You are a helpful and bubbly AI assistant who loves to chat about " | ||||||||||||||||||||||||||||||||||||||||||||
| "anything the user is interested in and is prepared to offer them facts. " | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -313,12 +345,17 @@ async def handle_media_stream(websocket: WebSocket): | |||||||||||||||||||||||||||||||||||||||||||
| print("Client connected") | ||||||||||||||||||||||||||||||||||||||||||||
| await websocket.accept() | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| realtime_url, realtime_headers = _resolve_realtime_endpoint() | ||||||||||||||||||||||||||||||||||||||||||||
| async with websockets.connect( | ||||||||||||||||||||||||||||||||||||||||||||
| 'wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01', | ||||||||||||||||||||||||||||||||||||||||||||
| extra_headers={ | ||||||||||||||||||||||||||||||||||||||||||||
| "Authorization": f"Bearer {OPENAI_API_KEY}", | ||||||||||||||||||||||||||||||||||||||||||||
| "OpenAI-Beta": "realtime=v1" | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| realtime_url, | ||||||||||||||||||||||||||||||||||||||||||||
| extra_headers=realtime_headers, | ||||||||||||||||||||||||||||||||||||||||||||
| # Bounded connect / heartbeat / close so a dead upstream cannot | ||||||||||||||||||||||||||||||||||||||||||||
| # hold a live Twilio media leg (and phone number) indefinitely. | ||||||||||||||||||||||||||||||||||||||||||||
| open_timeout=10, | ||||||||||||||||||||||||||||||||||||||||||||
| ping_interval=20, | ||||||||||||||||||||||||||||||||||||||||||||
| ping_timeout=20, | ||||||||||||||||||||||||||||||||||||||||||||
| close_timeout=5, | ||||||||||||||||||||||||||||||||||||||||||||
| max_size=2 ** 20, # 1 MiB frame cap | ||||||||||||||||||||||||||||||||||||||||||||
| ) as openai_ws: | ||||||||||||||||||||||||||||||||||||||||||||
| await send_session_update(openai_ws) | ||||||||||||||||||||||||||||||||||||||||||||
| stream_sid = None | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,6 +31,37 @@ def generate(self, prompt): | |
| ProviderType = Union[ProviderClass, ProviderFactory] | ||
|
|
||
|
|
||
| # Default ceilings so an unresponsive provider cannot pin a request coroutine | ||
| # (or a bridge worker thread) indefinitely. Both are overridable per-call via | ||
| # kwargs, and the timeout is tunable for operators via PRAISONAI_LLM_TIMEOUT. | ||
| _DEFAULT_LLM_TIMEOUT_SECONDS = 60.0 | ||
| _DEFAULT_LLM_NUM_RETRIES = 2 | ||
|
|
||
|
|
||
| def default_llm_timeout() -> float: | ||
| """Resolve the default LLM call timeout, tolerating a bad env value.""" | ||
| import os # lazy: keep module top-level imports to stdlib typing/threading only | ||
| raw = os.getenv("PRAISONAI_LLM_TIMEOUT") | ||
| if not raw: | ||
| return _DEFAULT_LLM_TIMEOUT_SECONDS | ||
| try: | ||
| return float(raw) | ||
| except ValueError: | ||
| import logging | ||
| logging.getLogger(__name__).warning( | ||
| "Invalid PRAISONAI_LLM_TIMEOUT=%r; falling back to %.0fs", | ||
| raw, _DEFAULT_LLM_TIMEOUT_SECONDS, | ||
| ) | ||
| return _DEFAULT_LLM_TIMEOUT_SECONDS | ||
|
Comment on lines
+41
to
+55
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- registry.py relevant sections ---'
sed -n '1,90p' src/praisonai/praisonai/llm/registry.py
sed -n '260,315p' src/praisonai/praisonai/llm/registry.py
printf '%s\n' '--- LiteLLM dependency declarations ---'
rg -n -i 'litellm|llm_timeout|timeout' pyproject.toml setup.py setup.cfg requirements*.txt src/praisonai 2>/dev/null | head -200
printf '%s\n' '--- candidate lock/config files ---'
git ls-files | rg -i '(^|/)(pyproject\.toml|poetry\.lock|uv\.lock|requirements[^/]*|setup\.py|setup\.cfg|Pipfile|.*lock)$' | head -100Repository: MervinPraison/PraisonAI Length of output: 31254 🌐 Web query:
💡 Result: In LiteLLM, the Citations:
🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- declared project metadata ---'
sed -n '1,45p' src/praisonai/pyproject.toml
printf '%s\n' '--- locked LiteLLM package records ---'
sed -n '3365,3400p' src/praisonai/uv.lock
sed -n '4015,4035p' src/praisonai/poetry.lock
printf '%s\n' '--- repository tests and docs for PRAISONAI_LLM_TIMEOUT ---'
rg -n -C 4 'PRAISONAI_LLM_TIMEOUT|default_llm_timeout|_apply_default_timeout' src/praisonaiRepository: MervinPraison/PraisonAI Length of output: 8502 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from urllib.request import urlopen
from zipfile import ZipFile
from io import BytesIO
url = "https://files.pythonhosted.org/packages/7f/5c/1b5691575420135e90578543b2bf219497caa33cfd0af64cb38f30288450/litellm-1.83.14-py3-none-any.whl"
data = urlopen(url, timeout=20).read()
with ZipFile(BytesIO(data)) as wheel:
names = [
name for name in wheel.namelist()
if name.endswith((".py", ".pyi"))
and any(part in name for part in ("main.py", "utils.py", "llms/custom_httpx/http_handler.py"))
]
for name in names:
text = wheel.read(name).decode("utf-8", errors="replace")
lines = text.splitlines()
hits = [
i for i, line in enumerate(lines)
if "timeout" in line.lower()
and any(token in line.lower() for token in ("float(", "isfinite", "httpx.timeout", "timeout="))
]
if hits:
print(f"--- {name} ---")
shown = set()
for i in hits:
start, end = max(0, i - 3), min(len(lines), i + 4)
key = (start, end)
if key not in shown:
shown.add(key)
for n in range(start, end):
print(f"{n + 1}:{lines[n]}")
PYRepository: MervinPraison/PraisonAI Length of output: 2447 🏁 Script executed: #!/bin/bash
set -eu
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
curl -fsSLk --max-time 30 \
'https://files.pythonhosted.org/packages/7f/5c/1b5691575420135e90578543b2bf219497caa33cfd0af64cb38f30288450/litellm-1.83.14-py3-none-any.whl' \
-o "$tmp"
python3 - "$tmp" <<'PY'
from sys import argv
from zipfile import ZipFile
with ZipFile(argv[1]) as wheel:
for name in wheel.namelist():
if not name.endswith(".py"):
continue
text = wheel.read(name).decode("utf-8", errors="replace")
lines = text.splitlines()
hits = [
i for i, line in enumerate(lines)
if "timeout" in line.lower()
and any(token in line.lower() for token in (
"float(", "isfinite", "httpx.timeout", "timeout="
))
]
if hits:
print(f"--- {name} ---")
emitted = set()
for i in hits:
start, end = max(0, i - 3), min(len(lines), i + 4)
if (start, end) in emitted:
continue
emitted.add((start, end))
for n in range(start, end):
print(f"{n + 1}:{lines[n]}")
PYRepository: MervinPraison/PraisonAI Length of output: 50380 🏁 Script executed: #!/bin/bash
set -eu
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
curl -fsSLk --max-time 30 \
'https://files.pythonhosted.org/packages/7f/5c/1b5691575420135e90578543b2bf219497caa33cfd0af64cb38f30288450/litellm-1.83.14-py3-none-any.whl' \
-o "$tmp"
python3 - "$tmp" <<'PY'
import re
import sys
from zipfile import ZipFile
targets = [
"litellm/main.py",
"litellm/llms/custom_httpx/http_handler.py",
"litellm/utils.py",
]
patterns = re.compile(
r"def (?:completion|acompletion)|"
r"class HTTPHandler|"
r"httpx\.(?:Client|AsyncClient|Timeout)|"
r"timeout\s*=\s*|"
r"timeout\s*:"
)
with ZipFile(sys.argv[1]) as wheel:
for name in targets:
text = wheel.read(name).decode("utf-8", errors="replace")
lines = text.splitlines()
print(f"--- {name} ---")
for i, line in enumerate(lines):
if patterns.search(line):
start, end = max(0, i - 2), min(len(lines), i + 3)
print(f"{start + 1}-{end}")
for n in range(start, end):
print(f"{n + 1}:{lines[n]}")
PYRepository: MervinPraison/PraisonAI Length of output: 48785 Reject non-finite and non-positive timeout values.
🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| def _apply_default_timeout(completion_kwargs: Dict[str, Any]) -> Dict[str, Any]: | ||
| """Seed a default timeout + bounded retries. An explicit caller value wins.""" | ||
| completion_kwargs.setdefault("timeout", default_llm_timeout()) | ||
| completion_kwargs.setdefault("num_retries", _DEFAULT_LLM_NUM_RETRIES) | ||
| return completion_kwargs | ||
|
|
||
|
|
||
| class LLMProviderRegistry(PluginRegistry[ProviderType]): | ||
| """ | ||
| Registry for LLM providers. | ||
|
|
@@ -255,6 +286,7 @@ def _resolve_model_and_kwargs(self, prompt: str, **kwargs): | |
| completion_kwargs = { | ||
| k: v for k, v in {**self.config, **kwargs}.items() if k != "provider" | ||
| } | ||
| _apply_default_timeout(completion_kwargs) | ||
| messages = [{"role": "user", "content": prompt}] | ||
| return litellm, full_model, messages, completion_kwargs | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a caller supplies both a loose
model=orllm=argument and a different model incli_config,_apply_model_overridewrites the loose value directly intoconfig_listwhile the explicit value remains in acli_configkey that model selection does not read, causing the run to use the loose model despite the documented explicit-config precedence.Knowledge Base Used: PraisonAI CLI and API Entrypoints