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:
heaven 2026-08-14 20:58:43 +03:00
parent 4e63b85cf3
commit ce33a8bcac

205
eval/dovodka/d1_gemini.py Normal file
View 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__)