Skip to content

Commit 16587ec

Browse files
authored
Merge pull request MODSetter#1292 from MODSetter/dev
temp(hotpatch): block github_coplot custom provider
2 parents f02909e + b067c92 commit 16587ec

1 file changed

Lines changed: 76 additions & 2 deletions

File tree

surfsense_backend/app/services/llm_service.py

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
import logging
23

34
import litellm
@@ -32,6 +33,39 @@
3233
logger = logging.getLogger(__name__)
3334

3435

36+
# Providers that require an interactive OAuth / device-flow login before
37+
# issuing any completion. LiteLLM implements these with blocking sync polling
38+
# (requests + time.sleep), which would freeze the FastAPI event loop if
39+
# invoked from validation. They are never usable from a headless backend,
40+
# so we reject them at the edge.
41+
_INTERACTIVE_AUTH_PROVIDERS: frozenset[str] = frozenset(
42+
{
43+
"github_copilot",
44+
"github-copilot",
45+
"githubcopilot",
46+
"copilot",
47+
}
48+
)
49+
50+
# Hard upper bound for a single validation call. Must exceed the ChatLiteLLM
51+
# request timeout (30s) by a small margin so a well-behaved provider never
52+
# trips the watchdog, while any pathological/blocking provider is killed.
53+
_VALIDATION_TIMEOUT_SECONDS: float = 35.0
54+
55+
56+
def _is_interactive_auth_provider(
57+
provider: str | None, custom_provider: str | None
58+
) -> bool:
59+
"""Return True if the given provider triggers interactive OAuth in LiteLLM."""
60+
for raw in (custom_provider, provider):
61+
if not raw:
62+
continue
63+
normalized = raw.strip().lower().replace(" ", "_")
64+
if normalized in _INTERACTIVE_AUTH_PROVIDERS:
65+
return True
66+
return False
67+
68+
3569
class LLMRole:
3670
AGENT = "agent" # For agent/chat operations
3771
DOCUMENT_SUMMARY = "document_summary" # For document summarization
@@ -93,6 +127,25 @@ async def validate_llm_config(
93127
- is_valid: True if config works, False otherwise
94128
- error_message: Empty string if valid, error description if invalid
95129
"""
130+
# Reject providers that require interactive OAuth/device-flow auth.
131+
# LiteLLM's github_copilot provider (and similar) uses a blocking sync
132+
# Authenticator that polls GitHub for up to several minutes and prints a
133+
# device code to stdout. Running it on the FastAPI event loop will freeze
134+
# the entire backend, so we refuse them up front.
135+
if _is_interactive_auth_provider(provider, custom_provider):
136+
msg = (
137+
"Provider requires interactive OAuth/device-flow authentication "
138+
"(e.g. github_copilot) and cannot be used in a hosted backend. "
139+
"Please choose a provider that authenticates via API key."
140+
)
141+
logger.warning(
142+
"Rejected LLM config validation for interactive-auth provider "
143+
"(provider=%r, custom_provider=%r)",
144+
provider,
145+
custom_provider,
146+
)
147+
return False, msg
148+
96149
try:
97150
# Build the model string for litellm
98151
if custom_provider:
@@ -153,9 +206,30 @@ async def validate_llm_config(
153206

154207
llm = SanitizedChatLiteLLM(**litellm_kwargs)
155208

156-
# Make a simple test call
209+
# Run the test call in a worker thread with a hard timeout. Some
210+
# LiteLLM providers have synchronous blocking code paths (e.g. OAuth
211+
# authenticators that call time.sleep and requests.post) that would
212+
# otherwise freeze the asyncio event loop. Offloading to a thread and
213+
# bounding the wait keeps the server responsive even if a provider
214+
# misbehaves.
157215
test_message = HumanMessage(content="Hello")
158-
response = await llm.ainvoke([test_message])
216+
try:
217+
response = await asyncio.wait_for(
218+
asyncio.to_thread(llm.invoke, [test_message]),
219+
timeout=_VALIDATION_TIMEOUT_SECONDS,
220+
)
221+
except TimeoutError:
222+
logger.warning(
223+
"LLM config validation timed out after %ss for model: %s",
224+
_VALIDATION_TIMEOUT_SECONDS,
225+
model_string,
226+
)
227+
return (
228+
False,
229+
f"Validation timed out after {int(_VALIDATION_TIMEOUT_SECONDS)}s. "
230+
"The provider is unreachable or requires interactive "
231+
"authentication that is not supported by the backend.",
232+
)
159233

160234
# If we got here without exception, the config is valid
161235
if response and response.content:

0 commit comments

Comments
 (0)