Freeze the gemini backfill driver before its first purchase: per-attempt journal, exponential backoff, per-cell cap and the order threshold of twelve of sixteen
This commit is contained in:
parent
4e63b85cf3
commit
ce33a8bcac
1 changed files with 205 additions and 0 deletions
205
eval/dovodka/d1_gemini.py
Normal file
205
eval/dovodka/d1_gemini.py
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Д1 — ДОБИВКА `gemini-3.1-pro-preview`: брошенный замер эксп-22 §6/§6.3. Платный.
|
||||
|
||||
Что это закрывает. Эксп-22 объявил полную редакторскую панель, но gemini отдал не все клетки, и
|
||||
пробел остался незакрытым — владелец назвал это «брошенным замером». Гипотеза эксп-04 «проза
|
||||
gemini — аутлаер» держится на этом замере и без него не закрывается.
|
||||
|
||||
⚠ ВЕРДИКТ «МОДЕЛЬ НЕ ОТДАЁТ» ПРИНИМАЕТСЯ ТОЛЬКО С ЖУРНАЛОМ. Заказ формулирует прямо: журнал
|
||||
КАЖДОЙ попытки (время · код ответа · стоимость) файлом в сырьё, окно ≥24 часов, экспоненциальный
|
||||
бэкофф на 5xx, поштучно. «После N отказов бросил» = невыполнение заказа. Поэтому здесь журнал
|
||||
пишется ДО и ПОСЛЕ каждой попытки, а не собирается в память и не теряется при падении процесса.
|
||||
|
||||
⚠ СКРЫТАЯ ТАРИФИКАЦИЯ ×4.5. У gemini размышление тарифицируется, но в `completion_tokens` НЕ
|
||||
видно — разница живёт только в `total_tokens` (`roster.REASONING_BILLING` = `additive_total`).
|
||||
Потолок клетки считается по total, иначе прибор покажет вчетверо меньше потраченного.
|
||||
|
||||
База — та же якорная, что у всех армов эксп-22 (черновик `deepseek-v4-flash`), поэтому клетки
|
||||
сравнимы с прочей редакторской панелью без поправок.
|
||||
|
||||
Запуск: d1_gemini.py --dry план и состояние, $0
|
||||
d1_gemini.py --run один проход по недостающим клеткам (платный)
|
||||
d1_gemini.py --status журнал попыток и вердикт
|
||||
d1_gemini.py --selftest
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path("/home/ubuntu/projects/textmachine")
|
||||
ZONE = Path(__file__).resolve().parent
|
||||
for d in ("dovodka", "editor_tier", "tenant_panel", "role_topology", "bank_arbitration"):
|
||||
sys.path.insert(0, str(REPO / "eval" / d))
|
||||
|
||||
|
||||
def _load(name: str, path: Path):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
sys.modules[name] = mod
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
C = _load("contour_d1", ZONE / "contour.py")
|
||||
MONEY, PR, ROSTER, ED22, P22 = C.MONEY, C.PR, C.ROSTER, C.ED22, C.P22
|
||||
|
||||
MODEL = "gemini-3.1-pro-preview"
|
||||
PHASE = "ФД-E"
|
||||
PFX = "dv-e"
|
||||
N_TARGET = 16 # полная панель эксп-22
|
||||
N_ENOUGH = 12 # ≥12/16 — порог, при котором заказ разрешает судить
|
||||
CELL_CAP_USD = 0.30 # лимит цены КЛЕТКИ, считается по total_tokens
|
||||
BACKOFF = (30, 120, 480, 1800) # экспоненциальный бэкофф на 5xx, секунды
|
||||
JOURNAL = MONEY.OUT / "d1-attempts.jsonl"
|
||||
|
||||
|
||||
def units() -> list[dict]:
|
||||
"""16 единиц эксп-22 — те же, поверх которых куплена вся редакторская панель."""
|
||||
return [u for u in P22.M.units_zh() if u["uid"] not in C.EXCLUDED][:N_TARGET]
|
||||
|
||||
|
||||
def tag(uid: str) -> str:
|
||||
return f"{PFX}-r1-{uid}"
|
||||
|
||||
|
||||
def text_of(uid: str) -> str:
|
||||
f = MONEY.OUT / f"{tag(uid)}.json"
|
||||
if not f.exists():
|
||||
return ""
|
||||
return json.loads(f.read_text(encoding="utf-8")).get("content") or ""
|
||||
|
||||
|
||||
def log_attempt(rec: dict) -> None:
|
||||
"""Журнал попытки — ДОПИСЫВАЕТСЯ немедленно. Вердикт без него заказ не принимает."""
|
||||
JOURNAL.parent.mkdir(parents=True, exist_ok=True)
|
||||
with JOURNAL.open("a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
|
||||
|
||||
|
||||
def attempts() -> list[dict]:
|
||||
if not JOURNAL.exists():
|
||||
return []
|
||||
return [json.loads(x) for x in JOURNAL.read_text(encoding="utf-8").splitlines() if x.strip()]
|
||||
|
||||
|
||||
def msgs_for(u: dict) -> list[dict]:
|
||||
"""Редакторский промт поверх ЯКОРНОГО черновика — как у всей панели эксп-22."""
|
||||
draft = C.base_draft(u["uid"])
|
||||
law = PR.law_block(PR.terms_in(u["source"]))
|
||||
return PR.editor_msgs(u["source"], draft, law)
|
||||
|
||||
|
||||
def cmd_run() -> None:
|
||||
"""Один проход по недостающим клеткам. Повторные проходы набирают окно ≥24 часов."""
|
||||
led = MONEY.Guarded(PHASE, default_expect=MONEY.model_expect(MODEL))
|
||||
missing = [u for u in units() if not text_of(u["uid"]).strip()]
|
||||
print(f"Д1: есть {N_TARGET - len(missing)}/{N_TARGET}, добираю {len(missing)}")
|
||||
for u in missing:
|
||||
uid = u["uid"]
|
||||
draft = C.base_draft(uid)
|
||||
if not draft.strip():
|
||||
print(f" {uid}: якорной базы нет — клетка невозможна")
|
||||
continue
|
||||
for i, pause in enumerate((0, *BACKOFF)):
|
||||
if pause:
|
||||
print(f" {uid}: бэкофф {pause}s перед попыткой {i + 1}")
|
||||
time.sleep(pause)
|
||||
t0 = time.time()
|
||||
try:
|
||||
rec = MONEY.purchase(led, tag(uid), MODEL, ED22.client(MODEL),
|
||||
ROSTER.call_kwargs(MODEL, msgs_for(u)),
|
||||
role="edit", uid=uid, arm="R1")
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as e: # noqa: BLE001
|
||||
code = getattr(getattr(e, "response", None), "status_code", None)
|
||||
log_attempt(dict(uid=uid, attempt=i + 1, ok=False, code=code,
|
||||
err=type(e).__name__, msg=str(e)[:200],
|
||||
secs=round(time.time() - t0, 1), cost_usd=0.0))
|
||||
print(f" {uid}: попытка {i + 1} — {type(e).__name__} код {code}")
|
||||
# ⚠ Бэкофф только на 5xx и на сетевые: 4xx повтором не лечится, и молотить им
|
||||
# вендора значит жечь квоту на заведомо неудачных вызовах.
|
||||
if code and 400 <= code < 500 and code != 429:
|
||||
break
|
||||
continue
|
||||
if rec.get("skipped"):
|
||||
log_attempt(dict(uid=uid, attempt=i + 1, ok=False, code="GUARD",
|
||||
secs=round(time.time() - t0, 1), cost_usd=0.0))
|
||||
raise SystemExit(f"⛔ ГАРД ОТКАЗАЛ на {tag(uid)} — СТОП и вопрос владельцу")
|
||||
cost = rec.get("cost_usd", 0.0)
|
||||
log_attempt(dict(uid=uid, attempt=i + 1, ok=True, code=200,
|
||||
finish=rec.get("finish"), secs=round(time.time() - t0, 1),
|
||||
cost_usd=cost, chars=len(rec.get("content") or "")))
|
||||
if cost > CELL_CAP_USD:
|
||||
print(f" ⚠ {uid}: клетка ${cost:.4f} выше лимита ${CELL_CAP_USD} — объявить")
|
||||
break
|
||||
MONEY.report()
|
||||
|
||||
|
||||
def cmd_status() -> None:
|
||||
us = units()
|
||||
have = [u for u in us if text_of(u["uid"]).strip()]
|
||||
at = attempts()
|
||||
ok = [a for a in at if a.get("ok")]
|
||||
fail = [a for a in at if not a.get("ok")]
|
||||
print(f"Д1 · собрано {len(have)}/{len(us)} · порог заказа ≥{N_ENOUGH}")
|
||||
print(f" попыток всего {len(at)} · удачных {len(ok)} · отказов {len(fail)}")
|
||||
if at:
|
||||
span = (max(a.get("ts", 0) for a in at) - min(a.get("ts", 0) for a in at)) / 3600
|
||||
print(f" окно журнала: {span:.1f} ч (заказ требует ≥24 ч до вердикта «не отдаёт»)")
|
||||
codes: dict = {}
|
||||
for a in fail:
|
||||
codes[str(a.get("code"))] = codes.get(str(a.get("code")), 0) + 1
|
||||
if codes:
|
||||
print(f" коды отказов: {codes}")
|
||||
if len(have) >= N_ENOUGH:
|
||||
print(" [OK ] порог взят — ось идёт в судейство")
|
||||
else:
|
||||
print(f" ⚠ порога нет: вердикт «модель не отдаёт» принимается ТОЛЬКО с журналом ≥24 ч "
|
||||
f"и снимком вендор-статуса. «После N отказов бросил» = невыполнение заказа.")
|
||||
|
||||
|
||||
def cmd_dry() -> None:
|
||||
us = units()
|
||||
have = sum(1 for u in us if text_of(u["uid"]).strip())
|
||||
exp = MONEY.model_expect(MODEL)
|
||||
sp = MONEY.Guarded(PHASE).spent()
|
||||
print(f"Д1 gemini · единиц {len(us)} · есть {have} · надо {len(us) - have}")
|
||||
print(f" ожидание клетки ${exp:.5f} · лимит клетки ${CELL_CAP_USD}")
|
||||
print(f" {PHASE}: потрачено ${sp:.4f} · потолок ${MONEY.CEILINGS[PHASE]:.2f} · "
|
||||
f"смета ${(len(us) - have) * exp:.4f}")
|
||||
print(f" журнал попыток: {JOURNAL} ({len(attempts())} записей)")
|
||||
|
||||
|
||||
def cmd_selftest() -> int:
|
||||
bad = 0
|
||||
|
||||
def ck(n: str, ok: bool, d: str = "") -> None:
|
||||
nonlocal bad
|
||||
bad += not ok
|
||||
print(f"[{'OK ' if ok else 'ПРОВАЛ'}] {n}" + (f" {d}" if d else ""))
|
||||
|
||||
us = units()
|
||||
ck("единиц ровно 16 — полная панель эксп-22", len(us) == N_TARGET, f"их {len(us)}")
|
||||
ck("тег покрыт глобом кассы ФД-E", tag("u").startswith("dv-e-"))
|
||||
ck("база — ЯКОРНАЯ, та же что у всей панели",
|
||||
all(C.base_draft(u["uid"]).strip() for u in us[:3]))
|
||||
ck("тарификация gemini объявлена скрытой (additive_total)",
|
||||
ROSTER.REASONING_BILLING.get(MODEL) == "additive_total",
|
||||
str(ROSTER.REASONING_BILLING.get(MODEL)))
|
||||
ck("бэкофф экспоненциальный и длинный", BACKOFF[-1] >= 1800 and len(BACKOFF) >= 4,
|
||||
str(BACKOFF))
|
||||
ck("порог заказа ниже полной панели", N_ENOUGH < N_TARGET, f"{N_ENOUGH} < {N_TARGET}")
|
||||
print(f"\n{'Д1 ГОТОВ' if not bad else f'ПРОВАЛОВ: {bad}'}")
|
||||
return bad
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
a = sys.argv[1] if len(sys.argv) > 1 else "--dry"
|
||||
fn = {"--dry": cmd_dry, "--run": cmd_run, "--status": cmd_status,
|
||||
"--selftest": lambda: sys.exit(1 if cmd_selftest() else 0)}.get(a)
|
||||
fn() if fn else print(__doc__)
|
||||
Loading…
Add table
Reference in a new issue