feat(jarvis): auto-switch vers 3.1 quand modèle non-protégé épuisé + clean debug
Browse filesBehavior :
- Si modèle non-3.1 hit PerDay : on switch automatiquement vers
gemini-3.1-flash-lite (modèle protégé, 500 req/j) + nouvelle clé,
on rebuild LLM + agent, on continue le flow sans interruption.
- set_current_model(3.1) + set_current_gemini_key(new) → le dropdown
affiche ✅ devant 3.1 au prochain refresh (via je_chat.change ou
chat.chatbot.change selon l'onglet).
- Côté flow LLM Chatbot ET côté flow Jarvis (run_jarvis_flow).
Cleanup :
- Retire toute la diag debug injectée dans les erreurs UI ([MARKER-
JARVIS-FINAL-YIELD], État du pool, build_model_choices output...).
- Simplifie build_pool_diag_md (garde la version simple, utilisable
pour debug si besoin) et build_model_choices (retire les wraps
try/except qui servaient à pinpoint le 'Button' has no _id —
problème résolu en aval avec _get_app_module).
|
@@ -360,77 +360,27 @@ def _masked_key(key: str) -> str:
|
|
| 360 |
|
| 361 |
|
| 362 |
def build_pool_diag_md() -> str:
|
| 363 |
-
"""Bloc Markdown de DIAGNOSTIC du pool —
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
Contenu :
|
| 367 |
-
- Clé Gemini courante (masquée)
|
| 368 |
-
- Modèle actif
|
| 369 |
-
- Couples (clé, modèle) blown aujourd'hui
|
| 370 |
-
- Clés invalides de la session
|
| 371 |
-
"""
|
| 372 |
-
import traceback
|
| 373 |
lines = ["**État du pool Gemini** :"]
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
except Exception as e:
|
| 380 |
-
lines.append(f"- ❌ inspect _CURRENT_GEMINI_KEY: {type(e).__name__}: {e}")
|
| 381 |
-
try:
|
| 382 |
-
_cm = _CURRENT_MODEL
|
| 383 |
-
lines.append(f"- type(_CURRENT_MODEL)=`{type(_cm).__name__}` repr=`{_cm!r}`")
|
| 384 |
-
except Exception as e:
|
| 385 |
-
lines.append(f"- ❌ inspect _CURRENT_MODEL: {type(e).__name__}: {e}")
|
| 386 |
-
try:
|
| 387 |
-
if _CURRENT_GEMINI_KEY:
|
| 388 |
-
lines.append(f"- Clé courante : `{_masked_key(_CURRENT_GEMINI_KEY)}`")
|
| 389 |
-
else:
|
| 390 |
-
lines.append("- Clé courante : *(aucune)*")
|
| 391 |
-
except Exception as e:
|
| 392 |
-
lines.append(f"- ❌ Clé courante step: {type(e).__name__}: {e}\n TB: {traceback.format_exc()[-500:]}")
|
| 393 |
-
try:
|
| 394 |
-
lines.append(f"- Modèle actif : `{_CURRENT_MODEL or '(aucun)'}`")
|
| 395 |
-
except Exception as e:
|
| 396 |
-
lines.append(f"- ❌ Modèle actif step: {type(e).__name__}: {e}\n TB: {traceback.format_exc()[-500:]}")
|
| 397 |
today = _today_utc_str()
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
if d == today and v]
|
| 401 |
-
except Exception as e:
|
| 402 |
-
blown_today = []
|
| 403 |
-
lines.append(f"- ❌ blown_today comprehension: {type(e).__name__}: {e}\n TB: {traceback.format_exc()[-500:]}")
|
| 404 |
if blown_today:
|
| 405 |
lines.append("- **Blown aujourd'hui** :")
|
| 406 |
for k, m in blown_today:
|
| 407 |
-
|
| 408 |
-
lines.append(f" - `{_masked_key(k)}` / `{m}`")
|
| 409 |
-
except Exception as e:
|
| 410 |
-
lines.append(f" - ❌ blown entry: {type(e).__name__}: {e}")
|
| 411 |
else:
|
| 412 |
lines.append("- **Blown aujourd'hui** : *(aucun)*")
|
| 413 |
if _INVALID_KEYS:
|
| 414 |
lines.append("- **Clés marquées invalides (session)** :")
|
| 415 |
for k in _INVALID_KEYS:
|
| 416 |
lines.append(f" - `{_masked_key(k)}`")
|
| 417 |
-
# DIAG : état in-memory par modèle Gemini (rapide, sûr)
|
| 418 |
-
today = _today_utc_str()
|
| 419 |
-
lines.append("- **État par modèle Gemini** (debug) :")
|
| 420 |
-
for key in GEMINI_NATIVE_REQUIRED:
|
| 421 |
-
if _CURRENT_GEMINI_KEY:
|
| 422 |
-
blown = _BLOWN_TODAY.get((_CURRENT_GEMINI_KEY, key, today), False)
|
| 423 |
-
else:
|
| 424 |
-
blown = False
|
| 425 |
-
lines.append(f" - `{key}` : blown_on_current_key={blown}")
|
| 426 |
-
# DIAG : résultat de build_model_choices() — chaque étape wrappée,
|
| 427 |
-
# un [ERR ...] indique la step qui foire dans le contexte Jarvis.
|
| 428 |
-
try:
|
| 429 |
-
lines.append("- **build_model_choices() output** :")
|
| 430 |
-
for lbl, k in build_model_choices():
|
| 431 |
-
lines.append(f" - `{k}` → `{lbl}`")
|
| 432 |
-
except Exception as exc:
|
| 433 |
-
lines.append(f"- *(build_model_choices raised : {type(exc).__name__}: {exc})*")
|
| 434 |
return "\n".join(lines)
|
| 435 |
|
| 436 |
|
|
@@ -564,53 +514,20 @@ def build_model_choices(for_chatbot: bool = False) -> list[tuple[str, str]]:
|
|
| 564 |
|
| 565 |
Marquage server-side (les deux dropdowns) :
|
| 566 |
- ✅ devant le modèle courant (_CURRENT_MODEL)
|
| 567 |
-
- suffixe `— épuisé sur cette clé` pour les modèles
|
| 568 |
-
la clé
|
| 569 |
-
|
| 570 |
-
DIAG (Phase 13) : chaque étape est wrappée en try/except pour
|
| 571 |
-
identifier le coupable du mystérieux 'Button' has no attribute
|
| 572 |
-
'_id' qui empêche le refresh des dropdowns Jarvis.
|
| 573 |
"""
|
| 574 |
import re as _re
|
| 575 |
out: list[tuple[str, str]] = []
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
continue
|
| 586 |
-
try:
|
| 587 |
-
# Étape 1 : check si modèle Gemini natif
|
| 588 |
-
is_gemini = key in GEMINI_NATIVE_REQUIRED
|
| 589 |
-
except Exception as e:
|
| 590 |
-
out.append((f"[ERR is_gemini {key!r}: {type(e).__name__}: {e}]", str(key)))
|
| 591 |
-
continue
|
| 592 |
-
try:
|
| 593 |
-
# Étape 2 : check si blown sur clé courante
|
| 594 |
-
is_blown = is_gemini and is_model_blown_on_current_key(key)
|
| 595 |
-
except Exception as e:
|
| 596 |
-
out.append((f"[ERR is_blown {key!r}: {type(e).__name__}: {e}]", str(key)))
|
| 597 |
-
continue
|
| 598 |
-
try:
|
| 599 |
-
# Étape 3 : décoration label
|
| 600 |
-
if is_blown:
|
| 601 |
-
base = _re.sub(r"\s*\(.*?\)\s*$", "", str(label)).strip()
|
| 602 |
-
decorated = f"{base} — épuisé sur cette clé"
|
| 603 |
-
elif key == _CURRENT_MODEL:
|
| 604 |
-
decorated = f"✅ {label}"
|
| 605 |
-
else:
|
| 606 |
-
decorated = label
|
| 607 |
-
except Exception as e:
|
| 608 |
-
out.append((f"[ERR decorate {key!r}: {type(e).__name__}: {e}]", str(key)))
|
| 609 |
-
continue
|
| 610 |
-
try:
|
| 611 |
-
out.append((decorated, key))
|
| 612 |
-
except Exception as e:
|
| 613 |
-
out.append((f"[ERR append {key!r}: {type(e).__name__}: {e}]", str(key)))
|
| 614 |
return out
|
| 615 |
|
| 616 |
|
|
@@ -1181,6 +1098,35 @@ def chat_with_agent(message: str, history: list[dict], api_key: str, model: str,
|
|
| 1181 |
if is_per_day_quota_exhausted(e, expected_model=model):
|
| 1182 |
if current_gemini_key:
|
| 1183 |
mark_gemini_key_blown(current_gemini_key, model)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1184 |
if (model == GEMINI_POOL_PROTECTED_MODEL
|
| 1185 |
and is_per_day_quota_exhausted(e, expected_model=model)):
|
| 1186 |
switched = False
|
|
@@ -1265,11 +1211,7 @@ def chat_with_agent(message: str, history: list[dict], api_key: str, model: str,
|
|
| 1265 |
f"({len(progress_full)})</summary>\n\n"
|
| 1266 |
f"{(chr(10)*2).join(progress_full)}\n\n</details>"
|
| 1267 |
)
|
| 1268 |
-
|
| 1269 |
-
diag = "\n\n---\n" + build_pool_diag_md()
|
| 1270 |
-
except Exception:
|
| 1271 |
-
diag = ""
|
| 1272 |
-
yield f"❌ Erreur agent : {e}" + diag + err_block, _NOOP_FILE
|
| 1273 |
return
|
| 1274 |
|
| 1275 |
# Viz : iframe interactif embarqué dans un gr.HTML séparé.
|
|
|
|
| 360 |
|
| 361 |
|
| 362 |
def build_pool_diag_md() -> str:
|
| 363 |
+
"""Bloc Markdown de DIAGNOSTIC du pool — utilisable pour debug.
|
| 364 |
+
Non injecté dans les erreurs UI par défaut."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
lines = ["**État du pool Gemini** :"]
|
| 366 |
+
if _CURRENT_GEMINI_KEY:
|
| 367 |
+
lines.append(f"- Clé courante : `{_masked_key(_CURRENT_GEMINI_KEY)}`")
|
| 368 |
+
else:
|
| 369 |
+
lines.append("- Clé courante : *(aucune)*")
|
| 370 |
+
lines.append(f"- Modèle actif : `{_CURRENT_MODEL or '(aucun)'}`")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
today = _today_utc_str()
|
| 372 |
+
blown_today = [(k, m) for (k, m, d), v in _BLOWN_TODAY.items()
|
| 373 |
+
if d == today and v]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
if blown_today:
|
| 375 |
lines.append("- **Blown aujourd'hui** :")
|
| 376 |
for k, m in blown_today:
|
| 377 |
+
lines.append(f" - `{_masked_key(k)}` / `{m}`")
|
|
|
|
|
|
|
|
|
|
| 378 |
else:
|
| 379 |
lines.append("- **Blown aujourd'hui** : *(aucun)*")
|
| 380 |
if _INVALID_KEYS:
|
| 381 |
lines.append("- **Clés marquées invalides (session)** :")
|
| 382 |
for k in _INVALID_KEYS:
|
| 383 |
lines.append(f" - `{_masked_key(k)}`")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
return "\n".join(lines)
|
| 385 |
|
| 386 |
|
|
|
|
| 514 |
|
| 515 |
Marquage server-side (les deux dropdowns) :
|
| 516 |
- ✅ devant le modèle courant (_CURRENT_MODEL)
|
| 517 |
+
- suffixe `— épuisé sur cette clé` pour les modèles Gemini natifs
|
| 518 |
+
blown sur la clé courante (le JS ajoute ❌ + grisage CSS)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 519 |
"""
|
| 520 |
import re as _re
|
| 521 |
out: list[tuple[str, str]] = []
|
| 522 |
+
for key, label in ALL_MODELS.items():
|
| 523 |
+
if key in GEMINI_NATIVE_REQUIRED and is_model_blown_on_current_key(key):
|
| 524 |
+
base = _re.sub(r"\s*\(.*?\)\s*$", "", str(label)).strip()
|
| 525 |
+
decorated = f"{base} — épuisé sur cette clé"
|
| 526 |
+
elif key == _CURRENT_MODEL:
|
| 527 |
+
decorated = f"✅ {label}"
|
| 528 |
+
else:
|
| 529 |
+
decorated = label
|
| 530 |
+
out.append((decorated, key))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 531 |
return out
|
| 532 |
|
| 533 |
|
|
|
|
| 1098 |
if is_per_day_quota_exhausted(e, expected_model=model):
|
| 1099 |
if current_gemini_key:
|
| 1100 |
mark_gemini_key_blown(current_gemini_key, model)
|
| 1101 |
+
# AUTO-BASCULE vers le modèle protégé (3.1, 500 req/j)
|
| 1102 |
+
# quand un modèle non-protégé est épuisé.
|
| 1103 |
+
if (model != GEMINI_POOL_PROTECTED_MODEL
|
| 1104 |
+
and is_per_day_quota_exhausted(e, expected_model=model)):
|
| 1105 |
+
try:
|
| 1106 |
+
next_key_for_protected = pick_unblown_gemini_key(GEMINI_POOL_PROTECTED_MODEL)
|
| 1107 |
+
if next_key_for_protected:
|
| 1108 |
+
current_gemini_key = next_key_for_protected
|
| 1109 |
+
model = GEMINI_POOL_PROTECTED_MODEL
|
| 1110 |
+
set_current_gemini_key(current_gemini_key)
|
| 1111 |
+
set_current_model(model)
|
| 1112 |
+
llm = _build_llm(
|
| 1113 |
+
model, api_key,
|
| 1114 |
+
use_thinking=use_thinking,
|
| 1115 |
+
gemini_key_override=current_gemini_key,
|
| 1116 |
+
)
|
| 1117 |
+
agent = build_jdm_agent(
|
| 1118 |
+
client=get_client(), llm=llm
|
| 1119 |
+
)
|
| 1120 |
+
switch_msg = (
|
| 1121 |
+
f"\n\n*🔄 Quota quotidien épuisé sur ce "
|
| 1122 |
+
f"modèle — bascule automatique sur "
|
| 1123 |
+
f"`{GEMINI_POOL_PROTECTED_MODEL}` (500 req/j).*"
|
| 1124 |
+
)
|
| 1125 |
+
current_progress = "\n\n".join(progress_live)
|
| 1126 |
+
yield current_progress + switch_msg, _NOOP_FILE
|
| 1127 |
+
continue
|
| 1128 |
+
except Exception:
|
| 1129 |
+
pass
|
| 1130 |
if (model == GEMINI_POOL_PROTECTED_MODEL
|
| 1131 |
and is_per_day_quota_exhausted(e, expected_model=model)):
|
| 1132 |
switched = False
|
|
|
|
| 1211 |
f"({len(progress_full)})</summary>\n\n"
|
| 1212 |
f"{(chr(10)*2).join(progress_full)}\n\n</details>"
|
| 1213 |
)
|
| 1214 |
+
yield f"❌ Erreur agent : {e}" + err_block, _NOOP_FILE
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1215 |
return
|
| 1216 |
|
| 1217 |
# Viz : iframe interactif embarqué dans un gr.HTML séparé.
|
|
@@ -1467,6 +1467,52 @@ def run_jarvis_flow(
|
|
| 1467 |
if is_per_day_quota_exhausted(e, expected_model=model):
|
| 1468 |
if _mark_blown_fn and current_gemini_key:
|
| 1469 |
_mark_blown_fn(current_gemini_key, model)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1470 |
if (model == _PROTECTED
|
| 1471 |
and is_per_day_quota_exhausted(e, expected_model=model)):
|
| 1472 |
switched = False
|
|
@@ -1572,32 +1618,10 @@ def run_jarvis_flow(
|
|
| 1572 |
f"({len(progress_full)})</summary>\n\n"
|
| 1573 |
f"{(chr(10)*2).join(progress_full)}\n\n</details>"
|
| 1574 |
)
|
| 1575 |
-
# Diag : on récupère app via sys.modules (déjà
|
| 1576 |
-
# chargé par le processus principal Gradio). PAS
|
| 1577 |
-
# de `from app import` qui re-déclenche l'évaluation
|
| 1578 |
-
# du module dans un worker fork → bug Gradio
|
| 1579 |
-
# 'Button' has no '_id' à l'instanciation des
|
| 1580 |
-
# composants (problème connu fork+gradio context).
|
| 1581 |
-
diag = ""
|
| 1582 |
-
try:
|
| 1583 |
-
import sys as _sys
|
| 1584 |
-
# Sur HF Spaces, app.py est lancé comme __main__,
|
| 1585 |
-
# pas 'app'. On cherche les deux.
|
| 1586 |
-
app_mod = _sys.modules.get('__main__')
|
| 1587 |
-
if app_mod is None or not hasattr(app_mod, 'build_pool_diag_md'):
|
| 1588 |
-
app_mod = _sys.modules.get('app')
|
| 1589 |
-
if app_mod is None:
|
| 1590 |
-
diag = "\n\n---\n*(diag : ni __main__ ni app dans sys.modules)*"
|
| 1591 |
-
elif not hasattr(app_mod, 'build_pool_diag_md'):
|
| 1592 |
-
diag = "\n\n---\n*(diag : module trouvé mais sans build_pool_diag_md)*"
|
| 1593 |
-
else:
|
| 1594 |
-
diag = "\n\n---\n" + app_mod.build_pool_diag_md()
|
| 1595 |
-
except Exception as _ce:
|
| 1596 |
-
diag = f"\n\n---\n*(diag raised : {type(_ce).__name__}: {_ce})*"
|
| 1597 |
yield (
|
| 1598 |
[{"role": "user", "content": user_display},
|
| 1599 |
{"role": "assistant",
|
| 1600 |
-
"content": f"❌ Erreur agent : {e}" +
|
| 1601 |
last_file_path, _read_file_preview(last_file_path),
|
| 1602 |
)
|
| 1603 |
return
|
|
|
|
| 1467 |
if is_per_day_quota_exhausted(e, expected_model=model):
|
| 1468 |
if _mark_blown_fn and current_gemini_key:
|
| 1469 |
_mark_blown_fn(current_gemini_key, model)
|
| 1470 |
+
# AUTO-BASCULE vers le modèle protégé (3.1, 500 req/j)
|
| 1471 |
+
# quand un modèle non-protégé est épuisé. L'utilisateur
|
| 1472 |
+
# continue son flow sans interruption — on rebuild
|
| 1473 |
+
# LLM + agent avec _PROTECTED et la même clé (ou une
|
| 1474 |
+
# autre si celle-ci est aussi blown pour 3.1).
|
| 1475 |
+
if (model != _PROTECTED
|
| 1476 |
+
and is_per_day_quota_exhausted(e, expected_model=model)
|
| 1477 |
+
and _app is not None):
|
| 1478 |
+
try:
|
| 1479 |
+
pick_fn = _app.pick_unblown_gemini_key
|
| 1480 |
+
# Cherche une clé utilisable pour _PROTECTED
|
| 1481 |
+
# (peut être la même que current_gemini_key
|
| 1482 |
+
# car le quota est par-modèle).
|
| 1483 |
+
next_key_for_protected = pick_fn(_PROTECTED)
|
| 1484 |
+
if next_key_for_protected:
|
| 1485 |
+
current_gemini_key = next_key_for_protected
|
| 1486 |
+
model = _PROTECTED # mute le modèle local
|
| 1487 |
+
try:
|
| 1488 |
+
_app.set_current_gemini_key(current_gemini_key)
|
| 1489 |
+
_app.set_current_model(model)
|
| 1490 |
+
except Exception:
|
| 1491 |
+
pass
|
| 1492 |
+
llm = build_llm_fn(
|
| 1493 |
+
model, api_key,
|
| 1494 |
+
use_thinking=use_thinking,
|
| 1495 |
+
gemini_key_override=current_gemini_key,
|
| 1496 |
+
)
|
| 1497 |
+
agent = build_agent_fn(
|
| 1498 |
+
client=get_client_fn(), llm=llm
|
| 1499 |
+
)
|
| 1500 |
+
_add_line(
|
| 1501 |
+
f"*🔄 Quota quotidien épuisé sur ce "
|
| 1502 |
+
f"modèle — bascule automatique sur "
|
| 1503 |
+
f"`{_PROTECTED}` (500 req/j) pour "
|
| 1504 |
+
f"continuer le travail.*"
|
| 1505 |
+
)
|
| 1506 |
+
yield (
|
| 1507 |
+
[{"role": "user", "content": user_display},
|
| 1508 |
+
{"role": "assistant",
|
| 1509 |
+
"content": "\n\n".join(progress_live)}],
|
| 1510 |
+
last_file_path,
|
| 1511 |
+
_read_file_preview(last_file_path),
|
| 1512 |
+
)
|
| 1513 |
+
continue
|
| 1514 |
+
except Exception:
|
| 1515 |
+
pass # bascule indisponible → erreur finale
|
| 1516 |
if (model == _PROTECTED
|
| 1517 |
and is_per_day_quota_exhausted(e, expected_model=model)):
|
| 1518 |
switched = False
|
|
|
|
| 1618 |
f"({len(progress_full)})</summary>\n\n"
|
| 1619 |
f"{(chr(10)*2).join(progress_full)}\n\n</details>"
|
| 1620 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1621 |
yield (
|
| 1622 |
[{"role": "user", "content": user_display},
|
| 1623 |
{"role": "assistant",
|
| 1624 |
+
"content": f"❌ Erreur agent : {e}" + err_block}],
|
| 1625 |
last_file_path, _read_file_preview(last_file_path),
|
| 1626 |
)
|
| 1627 |
return
|