diff --git a/eval/dovodka/d1_gemini.py b/eval/dovodka/d1_gemini.py new file mode 100644 index 00000000..e077c04a --- /dev/null +++ b/eval/dovodka/d1_gemini.py @@ -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__)