Spaces:
Running
Running
Update app/providers.py
Browse files- app/providers.py +45 -17
app/providers.py
CHANGED
|
@@ -21,6 +21,11 @@
|
|
| 21 |
# anthropic β fails β openrouter β fails β RuntimeError
|
| 22 |
# Visited set prevents infinite loops.
|
| 23 |
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
# DEPENDENCY CHAIN (app/* only, no fundaments!):
|
| 25 |
# config.py β parses app/.pyfun β single source of truth
|
| 26 |
# providers.py β LLM + Search registry + fallback chain
|
|
@@ -48,12 +53,18 @@ class BaseProvider:
|
|
| 48 |
Subclasses only implement complete() β HTTP logic lives here.
|
| 49 |
"""
|
| 50 |
def __init__(self, name: str, cfg: dict):
|
| 51 |
-
self.name
|
| 52 |
-
self.key
|
| 53 |
-
self.base_url
|
| 54 |
-
self.fallback
|
| 55 |
-
self.timeout
|
| 56 |
-
self.model
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
async def complete(self, prompt: str, model: str, max_tokens: int) -> str:
|
| 59 |
"""Override in each provider subclass."""
|
|
@@ -62,9 +73,10 @@ class BaseProvider:
|
|
| 62 |
async def _post(self, url: str, headers: dict, payload: dict) -> dict:
|
| 63 |
"""
|
| 64 |
Shared HTTP POST β used by all providers.
|
| 65 |
-
Raises
|
|
|
|
| 66 |
"""
|
| 67 |
-
safe_url = url.split("?")[0] # strip query params
|
| 68 |
logger.debug(f"POST β {safe_url}")
|
| 69 |
async with httpx.AsyncClient() as client:
|
| 70 |
r = await client.post(
|
|
@@ -73,11 +85,16 @@ class BaseProvider:
|
|
| 73 |
json=payload,
|
| 74 |
timeout=self.timeout,
|
| 75 |
)
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
return r.json()
|
| 78 |
|
| 79 |
|
| 80 |
-
|
| 81 |
# =============================================================================
|
| 82 |
# SECTION 2 β LLM Provider Implementations
|
| 83 |
# Only the API-specific parsing logic differs per provider.
|
|
@@ -108,18 +125,25 @@ class GeminiProvider(BaseProvider):
|
|
| 108 |
"""Google Gemini API β generateContent endpoint."""
|
| 109 |
|
| 110 |
async def complete(self, prompt: str, model: str = None, max_tokens: int = 1024) -> str:
|
| 111 |
-
m
|
|
|
|
| 112 |
async with httpx.AsyncClient() as client:
|
| 113 |
r = await client.post(
|
| 114 |
-
|
| 115 |
-
params={"key": self.key},
|
| 116 |
json={
|
| 117 |
-
"contents":
|
| 118 |
"generationConfig": {"maxOutputTokens": max_tokens},
|
| 119 |
},
|
| 120 |
timeout=self.timeout,
|
| 121 |
)
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
return r.json()["candidates"][0]["content"]["parts"][0]["text"]
|
| 124 |
|
| 125 |
|
|
@@ -246,7 +270,11 @@ async def llm_complete(
|
|
| 246 |
logger.info(f"Response from provider: '{current}'")
|
| 247 |
return f"[{current}] {result}"
|
| 248 |
except Exception as e:
|
| 249 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
|
| 251 |
# Next in fallback chain from .pyfun
|
| 252 |
cfg = config.get_active_llm_providers().get(current, {})
|
|
@@ -335,4 +363,4 @@ def get(name: str) -> BaseProvider:
|
|
| 335 |
# =============================================================================
|
| 336 |
|
| 337 |
if __name__ == "__main__":
|
| 338 |
-
print("WARNING: Run via main.py β app.py, not directly.")
|
|
|
|
| 21 |
# anthropic β fails β openrouter β fails β RuntimeError
|
| 22 |
# Visited set prevents infinite loops.
|
| 23 |
#
|
| 24 |
+
# SECURITY NOTE:
|
| 25 |
+
# API keys are NEVER logged or included in exception messages.
|
| 26 |
+
# All errors are sanitized before propagation β only HTTP status codes
|
| 27 |
+
# and safe_url (query params stripped) are ever exposed in logs.
|
| 28 |
+
#
|
| 29 |
# DEPENDENCY CHAIN (app/* only, no fundaments!):
|
| 30 |
# config.py β parses app/.pyfun β single source of truth
|
| 31 |
# providers.py β LLM + Search registry + fallback chain
|
|
|
|
| 53 |
Subclasses only implement complete() β HTTP logic lives here.
|
| 54 |
"""
|
| 55 |
def __init__(self, name: str, cfg: dict):
|
| 56 |
+
self.name = name
|
| 57 |
+
self.key = os.getenv(cfg.get("env_key", ""))
|
| 58 |
+
self.base_url = cfg.get("base_url", "")
|
| 59 |
+
self.fallback = cfg.get("fallback_to", "")
|
| 60 |
+
self.timeout = int(config.get_limits().get("REQUEST_TIMEOUT_SEC", "60"))
|
| 61 |
+
self.model = cfg.get("default_model", "")
|
| 62 |
+
# Safe key hint for debug logs β never log the full key
|
| 63 |
+
self._key_hint = (
|
| 64 |
+
f"{self.key[:4]}...{self.key[-4:]}"
|
| 65 |
+
if self.key and len(self.key) > 8
|
| 66 |
+
else "***"
|
| 67 |
+
)
|
| 68 |
|
| 69 |
async def complete(self, prompt: str, model: str, max_tokens: int) -> str:
|
| 70 |
"""Override in each provider subclass."""
|
|
|
|
| 73 |
async def _post(self, url: str, headers: dict, payload: dict) -> dict:
|
| 74 |
"""
|
| 75 |
Shared HTTP POST β used by all providers.
|
| 76 |
+
Raises RuntimeError with sanitized message on non-2xx responses.
|
| 77 |
+
API keys are never included in raised exceptions or log output.
|
| 78 |
"""
|
| 79 |
+
safe_url = url.split("?")[0] # strip query params (may contain API keys)
|
| 80 |
logger.debug(f"POST β {safe_url}")
|
| 81 |
async with httpx.AsyncClient() as client:
|
| 82 |
r = await client.post(
|
|
|
|
| 85 |
json=payload,
|
| 86 |
timeout=self.timeout,
|
| 87 |
)
|
| 88 |
+
try:
|
| 89 |
+
r.raise_for_status()
|
| 90 |
+
except httpx.HTTPStatusError as e:
|
| 91 |
+
# Sanitize: only status code + safe_url, never headers or body
|
| 92 |
+
raise RuntimeError(
|
| 93 |
+
f"HTTP {e.response.status_code} from {safe_url}"
|
| 94 |
+
) from None
|
| 95 |
return r.json()
|
| 96 |
|
| 97 |
|
|
|
|
| 98 |
# =============================================================================
|
| 99 |
# SECTION 2 β LLM Provider Implementations
|
| 100 |
# Only the API-specific parsing logic differs per provider.
|
|
|
|
| 125 |
"""Google Gemini API β generateContent endpoint."""
|
| 126 |
|
| 127 |
async def complete(self, prompt: str, model: str = None, max_tokens: int = 1024) -> str:
|
| 128 |
+
m = model or self.model
|
| 129 |
+
safe_url = f"{self.base_url}/models/{m}:generateContent"
|
| 130 |
async with httpx.AsyncClient() as client:
|
| 131 |
r = await client.post(
|
| 132 |
+
safe_url,
|
| 133 |
+
params={"key": self.key}, # key in query param, never in logs
|
| 134 |
json={
|
| 135 |
+
"contents": [{"parts": [{"text": prompt}]}],
|
| 136 |
"generationConfig": {"maxOutputTokens": max_tokens},
|
| 137 |
},
|
| 138 |
timeout=self.timeout,
|
| 139 |
)
|
| 140 |
+
try:
|
| 141 |
+
r.raise_for_status()
|
| 142 |
+
except httpx.HTTPStatusError as e:
|
| 143 |
+
# safe_url has no key β params are NOT part of safe_url string
|
| 144 |
+
raise RuntimeError(
|
| 145 |
+
f"HTTP {e.response.status_code} from {safe_url}"
|
| 146 |
+
) from None
|
| 147 |
return r.json()["candidates"][0]["content"]["parts"][0]["text"]
|
| 148 |
|
| 149 |
|
|
|
|
| 270 |
logger.info(f"Response from provider: '{current}'")
|
| 271 |
return f"[{current}] {result}"
|
| 272 |
except Exception as e:
|
| 273 |
+
# Log only exception type + sanitized message β never raw {e}
|
| 274 |
+
# which may contain headers, keys, or response bodies
|
| 275 |
+
logger.warning(
|
| 276 |
+
f"Provider '{current}' failed: {type(e).__name__}: {e} β trying fallback."
|
| 277 |
+
)
|
| 278 |
|
| 279 |
# Next in fallback chain from .pyfun
|
| 280 |
cfg = config.get_active_llm_providers().get(current, {})
|
|
|
|
| 363 |
# =============================================================================
|
| 364 |
|
| 365 |
if __name__ == "__main__":
|
| 366 |
+
print("WARNING: Run via main.py β app.py, not directly.")
|