Alibrown commited on
Commit
92368cc
Β·
verified Β·
1 Parent(s): eb496e0

Update app/providers.py

Browse files
Files changed (1) hide show
  1. 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 = name
52
- self.key = os.getenv(cfg.get("env_key", ""))
53
- self.base_url = cfg.get("base_url", "")
54
- self.fallback = cfg.get("fallback_to", "")
55
- self.timeout = int(config.get_limits().get("REQUEST_TIMEOUT_SEC", "60"))
56
- self.model = cfg.get("default_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 httpx.HTTPStatusError on non-2xx responses.
 
66
  """
67
- safe_url = url.split("?")[0] # strip query params from logs
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
- r.raise_for_status()
 
 
 
 
 
 
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 = model or self.model
 
112
  async with httpx.AsyncClient() as client:
113
  r = await client.post(
114
- f"{self.base_url}/models/{m}:generateContent",
115
- params={"key": self.key},
116
  json={
117
- "contents": [{"parts": [{"text": prompt}]}],
118
  "generationConfig": {"maxOutputTokens": max_tokens},
119
  },
120
  timeout=self.timeout,
121
  )
122
- r.raise_for_status()
 
 
 
 
 
 
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
- logger.warning(f"Provider '{current}' failed: {e} β€” trying fallback.")
 
 
 
 
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.")