CodeCommunity commited on
Commit
e2ff048
·
verified ·
1 Parent(s): ba7b35d

Update app/core/model_loader.py

Browse files
Files changed (1) hide show
  1. app/core/model_loader.py +136 -123
app/core/model_loader.py CHANGED
@@ -7,15 +7,23 @@ import random
7
  from datetime import datetime
8
  from dotenv import load_dotenv
9
  from groq import Groq
 
10
 
11
  load_dotenv()
12
  logger = logging.getLogger(__name__)
13
 
14
  STATS_FILE = "usage_stats.json"
15
 
16
- # Fixed: Updated to active Groq production model IDs to prevent 404 errors
17
- DEFAULT_MODEL = "openai/gpt-oss-20b"
18
- FALLBACK_MODEL = "openai/gpt-oss-120b"
 
 
 
 
 
 
 
19
 
20
  class LLMSingleton:
21
  _instance = None
@@ -33,25 +41,36 @@ class LLMSingleton:
33
  if self._instance is not None:
34
  raise Exception("Singleton instance already exists!")
35
 
36
- # 1. Load and sanitize API Key
37
- raw_key = os.getenv("GROQ_API_KEY", "")
38
- self.api_key = raw_key.strip().strip('"').strip("'") if raw_key else ""
 
39
 
40
- # Debug check for API key loading status
41
- if self.api_key:
42
- logger.info(f"✅ Loaded GROQ_API_KEY successfully! (Length: {len(self.api_key)}, Starts with: {self.api_key[:4]})")
43
- self.client = Groq(api_key=self.api_key)
 
 
 
 
44
  else:
45
- logger.error(" GROQ_API_KEY is empty or missing from environment variables!")
46
- self.client = None
47
 
48
- # 2. Load and sanitize Model Name
49
- raw_model = os.getenv("GROQ_MODEL", DEFAULT_MODEL)
50
- clean_model = raw_model.strip().strip('"').strip("'") if raw_model else ""
51
- self.model_name = clean_model if clean_model else DEFAULT_MODEL
52
- logger.info(f"🔑 Groq Client initialized with model target: {self.model_name}")
53
 
54
- # 3. Threading locks and usage stats setup
 
 
 
 
 
 
 
55
  self._stats_lock = threading.Lock()
56
  self._rpm_lock = threading.Lock()
57
  self.stats = self._load_stats()
@@ -60,6 +79,9 @@ class LLMSingleton:
60
  self.minute_window_start = time.time()
61
  self.requests_this_minute = 0
62
 
 
 
 
63
  def _load_stats(self):
64
  default_stats = {
65
  "total_requests": 0,
@@ -121,12 +143,6 @@ class LLMSingleton:
121
  stats["remaining_rpm"] = max(0, self.rpm_limit - requests_this_minute)
122
  return stats
123
 
124
- def track_local_usage(self, input_chars: int = 0):
125
- with self._stats_lock:
126
- self.stats["local_model_requests"] += 1
127
- self.stats["input_tokens"] += input_chars // 4
128
- self._save_stats()
129
-
130
  def _reserve_request_slot(self) -> bool:
131
  with self._stats_lock:
132
  if self.stats["daily_requests_count"] >= 1000:
@@ -139,133 +155,130 @@ class LLMSingleton:
139
  self.requests_this_minute += 1
140
  return True
141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  def generate(self, prompt: str, max_tokens: int = 2048) -> str:
 
143
  self._check_daily_reset()
144
  self._check_rpm_window()
145
 
146
- if not self.api_key or not self.client:
147
- logger.error("Cannot generate: Missing GROQ_API_KEY")
148
- raise RuntimeError("MISSING_API_KEY")
149
-
150
  if not self._reserve_request_slot():
151
- logger.error("❌ Daily Quota Exceeded. Request blocked.")
152
  raise RuntimeError("QUOTA_EXCEEDED")
153
 
154
- logger.info(f"🤖 Generating with Groq ({self.model_name}). Prompt start: {prompt[:50]}...")
155
-
156
- retries = 0
157
- max_retries = 3
158
- base_delay = 2
159
- active_model = self.model_name
160
 
161
- while retries <= max_retries:
 
162
  try:
 
 
163
  with self._stats_lock:
164
- self.stats["input_tokens"] += len(prompt) // 4
 
165
  self._save_stats()
 
 
 
166
 
167
- chat_completion = self.client.chat.completions.create(
168
- model=active_model,
169
- messages=[
170
- {
171
- "role": "system",
172
- "content": "You are a senior Android code reviewer. You MUST return a valid JSON object matching the requested schema strictly.",
173
- },
174
- {"role": "user", "content": prompt},
175
- ],
176
- response_format={"type": "json_object"},
177
- max_tokens=max_tokens,
178
- temperature=0.2,
179
- )
180
-
181
- response_text = chat_completion.choices[0].message.content or ""
182
-
183
  with self._stats_lock:
184
  self.stats["successful_requests"] += 1
185
- if response_text:
186
- self.stats["output_tokens"] += len(response_text) // 4
187
  self._save_stats()
188
-
189
- return response_text.strip()
190
-
191
  except Exception as e:
192
- error_str = str(e)
193
-
194
- # Automatic Model Fallback handling if a 404/model_not_found occurs
195
- if "404" in error_str or "model_not_found" in error_str.lower():
196
- if active_model != FALLBACK_MODEL:
197
- logger.warning(
198
- f"⚠️ Model target '{active_model}' rejected (404). Falling back to '{FALLBACK_MODEL}'..."
199
- )
200
- active_model = FALLBACK_MODEL
201
- continue
202
-
203
- # Rate Limit handling (HTTP 429)
204
- if "429" in error_str or "rate_limit_exceeded" in error_str.lower():
205
- with self._stats_lock:
206
- self.stats["rate_limit_hits"] += 1
207
- self._save_stats()
208
-
209
- wait_time = (base_delay * (2**retries)) + random.uniform(0.5, 1.5)
210
- logger.warning(
211
- f"⚠️ Groq rate limit hit. Retrying in {wait_time:.2f}s... (Attempt {retries + 1}/{max_retries})"
212
- )
213
- time.sleep(wait_time)
214
- retries += 1
215
- else:
216
- with self._stats_lock:
217
- self.stats["errors"] += 1
218
- self._save_stats()
219
- logger.error(f"Groq generation failed: {e}")
220
- raise RuntimeError(f"GENERATION_FAILED: {e}")
221
-
222
- with self._stats_lock:
223
- self.stats["errors"] += 1
224
- self._save_stats()
225
 
226
- logger.error(" Max retries reached. Request failed.")
227
- raise RuntimeError("RATE_LIMIT_EXCEEDED")
228
 
229
  def generate_text(self, prompt: str) -> str:
 
230
  self._check_daily_reset()
231
  self._check_rpm_window()
232
 
233
- if not self.api_key or not self.client:
234
- return "Error: Missing API Key."
235
-
236
  if not self._reserve_request_slot():
237
  return "Error: Daily Quota Exceeded."
238
 
239
- try:
240
- with self._stats_lock:
241
- self.stats["input_tokens"] += len(prompt) // 4
242
- self._save_stats()
243
 
244
- chat_completion = self.client.chat.completions.create(
245
- model=self.model_name,
246
- messages=[
247
- {
248
- "role": "system",
249
- "content": "You are GitGud AI, an expert software architect.",
250
- },
251
- {"role": "user", "content": prompt},
252
- ],
253
- max_tokens=2048,
254
- temperature=0.3,
255
- )
256
-
257
- response_text = chat_completion.choices[0].message.content or ""
258
-
259
- with self._stats_lock:
260
- self.stats["successful_requests"] += 1
261
- self.stats["output_tokens"] += len(response_text) // 4
262
- self._save_stats()
263
 
264
- return response_text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
 
266
- except Exception as e:
267
- logger.error(f"Groq text generation failed: {e}")
268
- return f"Error generating content: {str(e)}"
269
 
270
  # Export global instance
271
  llm_engine = LLMSingleton.get_instance()
 
7
  from datetime import datetime
8
  from dotenv import load_dotenv
9
  from groq import Groq
10
+ import google.generativeai as genai
11
 
12
  load_dotenv()
13
  logger = logging.getLogger(__name__)
14
 
15
  STATS_FILE = "usage_stats.json"
16
 
17
+ # ====================== CONFIG ======================
18
+ # Gemini (Primary)
19
+ DEFAULT_GEMINI_MODEL = "gemini-2.5-pro" # Higher quality. Use "gemini-2.5-flash" if you want faster + cheaper
20
+ FALLBACK_GEMINI_MODEL = "gemini-2.0-flash"
21
+
22
+ # Groq (Fallback)
23
+ DEFAULT_GROQ_MODEL = "llama-3.3-70b-versatile"
24
+ FALLBACK_GROQ_MODEL = "openai/gpt-oss-120b"
25
+ # ====================================================
26
+
27
 
28
  class LLMSingleton:
29
  _instance = None
 
41
  if self._instance is not None:
42
  raise Exception("Singleton instance already exists!")
43
 
44
+ # ---------- Gemini ----------
45
+ self.gemini_key = os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY") or ""
46
+ self.gemini_key = self.gemini_key.strip().strip('"').strip("'")
47
+ self.gemini_model_name = os.getenv("GEMINI_MODEL", DEFAULT_GEMINI_MODEL).strip() or DEFAULT_GEMINI_MODEL
48
 
49
+ if self.gemini_key:
50
+ try:
51
+ genai.configure(api_key=self.gemini_key)
52
+ self.gemini_model = genai.GenerativeModel(self.gemini_model_name)
53
+ logger.info(f"✅ Gemini initialized → {self.gemini_model_name}")
54
+ except Exception as e:
55
+ logger.error(f"❌ Failed to init Gemini: {e}")
56
+ self.gemini_model = None
57
  else:
58
+ logger.warning("⚠️ GOOGLE_API_KEY / GEMINI_API_KEY not found")
59
+ self.gemini_model = None
60
 
61
+ # ---------- Groq (Fallback) ----------
62
+ raw_key = os.getenv("GROQ_API_KEY", "")
63
+ self.groq_key = raw_key.strip().strip('"').strip("'") if raw_key else ""
64
+ self.groq_model_name = os.getenv("GROQ_MODEL", DEFAULT_GROQ_MODEL).strip() or DEFAULT_GROQ_MODEL
 
65
 
66
+ if self.groq_key:
67
+ self.groq_client = Groq(api_key=self.groq_key)
68
+ logger.info(f"✅ Groq initialized → {self.groq_model_name}")
69
+ else:
70
+ logger.warning("⚠️ GROQ_API_KEY not found")
71
+ self.groq_client = None
72
+
73
+ # ---------- Stats & Rate limiting ----------
74
  self._stats_lock = threading.Lock()
75
  self._rpm_lock = threading.Lock()
76
  self.stats = self._load_stats()
 
79
  self.minute_window_start = time.time()
80
  self.requests_this_minute = 0
81
 
82
+ # ------------------------------------------------------------------
83
+ # Stats helpers
84
+ # ------------------------------------------------------------------
85
  def _load_stats(self):
86
  default_stats = {
87
  "total_requests": 0,
 
143
  stats["remaining_rpm"] = max(0, self.rpm_limit - requests_this_minute)
144
  return stats
145
 
 
 
 
 
 
 
146
  def _reserve_request_slot(self) -> bool:
147
  with self._stats_lock:
148
  if self.stats["daily_requests_count"] >= 1000:
 
155
  self.requests_this_minute += 1
156
  return True
157
 
158
+ # ------------------------------------------------------------------
159
+ # Core generation methods
160
+ # ------------------------------------------------------------------
161
+ def _call_gemini(self, prompt: str, system_prompt: str, max_tokens: int = 2048, json_mode: bool = False) -> str:
162
+ if not self.gemini_model:
163
+ raise RuntimeError("Gemini not available")
164
+
165
+ full_prompt = f"{system_prompt}\n\n{prompt}"
166
+
167
+ generation_config = {
168
+ "max_output_tokens": max_tokens,
169
+ "temperature": 0.2 if json_mode else 0.3,
170
+ }
171
+ if json_mode:
172
+ generation_config["response_mime_type"] = "application/json"
173
+
174
+ response = self.gemini_model.generate_content(
175
+ full_prompt,
176
+ generation_config=generation_config,
177
+ )
178
+ return (response.text or "").strip()
179
+
180
+ def _call_groq(self, prompt: str, system_prompt: str, max_tokens: int = 2048, json_mode: bool = False) -> str:
181
+ if not self.groq_client:
182
+ raise RuntimeError("Groq not available")
183
+
184
+ messages = [
185
+ {"role": "system", "content": system_prompt},
186
+ {"role": "user", "content": prompt},
187
+ ]
188
+
189
+ kwargs = {
190
+ "model": self.groq_model_name,
191
+ "messages": messages,
192
+ "max_tokens": max_tokens,
193
+ "temperature": 0.2 if json_mode else 0.3,
194
+ }
195
+ if json_mode:
196
+ kwargs["response_format"] = {"type": "json_object"}
197
+
198
+ completion = self.groq_client.chat.completions.create(**kwargs)
199
+ return (completion.choices[0].message.content or "").strip()
200
+
201
  def generate(self, prompt: str, max_tokens: int = 2048) -> str:
202
+ """Used for structured JSON responses (code review etc.)"""
203
  self._check_daily_reset()
204
  self._check_rpm_window()
205
 
 
 
 
 
206
  if not self._reserve_request_slot():
 
207
  raise RuntimeError("QUOTA_EXCEEDED")
208
 
209
+ system_prompt = (
210
+ "You are a senior Android code reviewer. "
211
+ "You MUST return a valid JSON object matching the requested schema strictly."
212
+ )
 
 
213
 
214
+ # Try Gemini first
215
+ if self.gemini_model:
216
  try:
217
+ logger.info(f"🤖 Generating with Gemini ({self.gemini_model_name})")
218
+ result = self._call_gemini(prompt, system_prompt, max_tokens, json_mode=True)
219
  with self._stats_lock:
220
+ self.stats["successful_requests"] += 1
221
+ self.stats["output_tokens"] += len(result) // 4
222
  self._save_stats()
223
+ return result
224
+ except Exception as e:
225
+ logger.warning(f"Gemini failed → falling back to Groq: {e}")
226
 
227
+ # Fallback to Groq
228
+ if self.groq_client:
229
+ try:
230
+ logger.info(f"🤖 Generating with Groq ({self.groq_model_name})")
231
+ result = self._call_groq(prompt, system_prompt, max_tokens, json_mode=True)
 
 
 
 
 
 
 
 
 
 
 
232
  with self._stats_lock:
233
  self.stats["successful_requests"] += 1
234
+ self.stats["output_tokens"] += len(result) // 4
 
235
  self._save_stats()
236
+ return result
 
 
237
  except Exception as e:
238
+ logger.error(f"Groq also failed: {e}")
239
+ raise RuntimeError(f"GENERATION_FAILED: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
 
241
+ raise RuntimeError("No LLM provider available")
 
242
 
243
  def generate_text(self, prompt: str) -> str:
244
+ """Used for normal chat / explanations"""
245
  self._check_daily_reset()
246
  self._check_rpm_window()
247
 
 
 
 
248
  if not self._reserve_request_slot():
249
  return "Error: Daily Quota Exceeded."
250
 
251
+ system_prompt = "You are GitGud AI, an expert software architect."
 
 
 
252
 
253
+ # Try Gemini first
254
+ if self.gemini_model:
255
+ try:
256
+ logger.info(f"🤖 Chat with Gemini ({self.gemini_model_name})")
257
+ result = self._call_gemini(prompt, system_prompt, max_tokens=2048, json_mode=False)
258
+ with self._stats_lock:
259
+ self.stats["successful_requests"] += 1
260
+ self.stats["output_tokens"] += len(result) // 4
261
+ self._save_stats()
262
+ return result
263
+ except Exception as e:
264
+ logger.warning(f"Gemini chat failed → falling back to Groq: {e}")
 
 
 
 
 
 
 
265
 
266
+ # Fallback to Groq
267
+ if self.groq_client:
268
+ try:
269
+ logger.info(f"🤖 Chat with Groq ({self.groq_model_name})")
270
+ result = self._call_groq(prompt, system_prompt, max_tokens=2048, json_mode=False)
271
+ with self._stats_lock:
272
+ self.stats["successful_requests"] += 1
273
+ self.stats["output_tokens"] += len(result) // 4
274
+ self._save_stats()
275
+ return result
276
+ except Exception as e:
277
+ logger.error(f"Groq chat also failed: {e}")
278
+ return f"Error generating content: {str(e)}"
279
+
280
+ return "Error: No LLM provider available (check API keys)."
281
 
 
 
 
282
 
283
  # Export global instance
284
  llm_engine = LLMSingleton.get_instance()