Spaces:
Sleeping
Sleeping
SuZeAI commited on
Commit ·
d47f0ef
1
Parent(s): 6cab4e8
Fail fast on hard Gemini quota instead of spamming every key
Browse filesA 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 +14 -3
- app/workers/tasks.py +10 -1
- tests/test_tryon.py +21 -0
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
return True
|
| 43 |
if resp.status_code >= 500:
|
| 44 |
return True
|
| 45 |
-
if resp.status_code == 400 and "api key" in
|
| 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 =
|
| 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
|