SuZeAI commited on
Commit
d47f0ef
·
1 Parent(s): 6cab4e8

Fail fast on hard Gemini quota instead of spamming every key

Browse files

A 429 with 'limit: 0' means the model is unavailable on the plan for every key
(e.g. free tier gives 0 image requests), so rotating through all keys just made
N redundant calls and still failed. Detect that case and stop after one call.
Surface a clear quota/billing message on the try-on task instead of a raw error.

app/services/gemini_keys.py CHANGED
@@ -37,12 +37,23 @@ def _parse_keys() -> List[str]:
37
 
38
 
39
  def _should_rotate(resp: requests.Response) -> bool:
40
- """True when the failure is plausibly key-related (so trying another key helps)."""
41
- if resp.status_code in (401, 403, 429):
 
 
 
 
 
 
 
 
 
 
 
42
  return True
43
  if resp.status_code >= 500:
44
  return True
45
- if resp.status_code == 400 and "api key" in resp.text.lower():
46
  return True
47
  return False
48
 
 
37
 
38
 
39
  def _should_rotate(resp: requests.Response) -> bool:
40
+ """True when the failure is plausibly key-related (so trying another key helps).
41
+
42
+ Rotating is only useful when a *different* key has spare capacity. A 429 that
43
+ reports a hard ``limit: 0`` means the model is not available on this plan for
44
+ ANY key (e.g. the free tier gives 0 image requests) — rotating just spams every
45
+ key for nothing, so we fail fast (1 call) in that case.
46
+ """
47
+ body = resp.text.lower()
48
+ if resp.status_code in (401, 403):
49
+ return True
50
+ if resp.status_code == 429:
51
+ if "limit: 0" in body:
52
+ return False # hard cap / model not on this plan — rotating won't help
53
  return True
54
  if resp.status_code >= 500:
55
  return True
56
+ if resp.status_code == 400 and "api key" in body:
57
  return True
58
  return False
59
 
app/workers/tasks.py CHANGED
@@ -272,8 +272,17 @@ def run_tryon(task_id: str):
272
  db.rollback()
273
  logger.error(f"Error in Virtual Try-On task {task_id}: {str(e)}", exc_info=True)
274
  if task:
 
 
 
 
 
 
 
 
 
275
  task.status = "FAILED"
276
- task.error_message = f"Lỗi hệ thống local try-on: {str(e)}"
277
  db.commit()
278
  return False
279
  finally:
 
272
  db.rollback()
273
  logger.error(f"Error in Virtual Try-On task {task_id}: {str(e)}", exc_info=True)
274
  if task:
275
+ err = str(e)
276
+ low = err.lower()
277
+ if "429" in err or "quota" in low or "resource_exhausted" in low or "limit: 0" in low:
278
+ friendly = (
279
+ "Đã hết quota tạo ảnh của nhà cung cấp (model sinh ảnh không khả dụng "
280
+ "trên gói miễn phí). Vui lòng bật billing hoặc đổi provider rồi thử lại."
281
+ )
282
+ else:
283
+ friendly = f"Tạo ảnh thử đồ thất bại: {err}"
284
  task.status = "FAILED"
285
+ task.error_message = friendly
286
  db.commit()
287
  return False
288
  finally:
tests/test_tryon.py CHANGED
@@ -200,6 +200,27 @@ def test_gemini_key_rotation_on_failure():
200
  assert "key=good-key" in calls[1]
201
 
202
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  def test_tryon_service_disabled_raises():
204
  """With no API key configured, generate() should raise a clear error (task -> FAILED)."""
205
  from app.services import tryon_service as ts_module
 
200
  assert "key=good-key" in calls[1]
201
 
202
 
203
+ def test_gemini_no_rotation_on_hard_quota():
204
+ """A 429 with 'limit: 0' (model not on plan) must fail fast — exactly one call, no rotation."""
205
+ from app.services import gemini_keys as gk_module
206
+
207
+ calls = []
208
+
209
+ def do_request(key):
210
+ calls.append(key)
211
+ mock_resp = MagicMock()
212
+ mock_resp.status_code = 429
213
+ mock_resp.text = '{"error":{"code":429,"message":"...limit: 0, model: gemini-2.5-flash-preview-image"}}'
214
+ return mock_resp
215
+
216
+ with patch.object(gk_module.gemini_keys, "_keys", ["k1", "k2", "k3", "k4", "k5"]), \
217
+ patch.object(gk_module.gemini_keys, "_idx", 0):
218
+ resp = gk_module.gemini_keys.request(do_request)
219
+ # request() returns the 429 (caller raise_for_status), and only ONE key was tried.
220
+ assert resp.status_code == 429
221
+ assert len(calls) == 1
222
+
223
+
224
  def test_tryon_service_disabled_raises():
225
  """With no API key configured, generate() should raise a clear error (task -> FAILED)."""
226
  from app.services import tryon_service as ts_module