#!/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: """Журнал попытки — ДОПИСЫВАЕТСЯ немедленно. Вердикт без него заказ не принимает. ⚠ ВРЕМЯ ОБЯЗАТЕЛЬНО. Первая редакция писала длительность вызова, но не МЕТКУ ВРЕМЕНИ, и `--status` считал окно журнала как 0.0 ч на любом числе попыток. Заказ принимает вердикт «модель не отдаёт» ТОЛЬКО по журналу за ≥24 ч — без метки времени доказать окно нечем, то есть журнал не выполнял свою единственную задачу. """ rec.setdefault("ts", time.time()) 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)} — СТОП и вопрос владельцу") # ⚠ ОТКАЗ, ПОЙМАННЫЙ КАССОЙ, — ТОЖЕ ПОПЫТКА. `purchase` перехватывает ошибку API # сама и пишет карантинную запись `finish="error"`, поэтому мой `except` не # срабатывает и журнал показывал «отказов 0» при двух реально ошибившихся клетках. # Вердикт заказа держится на журнале, значит журнал обязан видеть и этот путь. if rec.get("finish") == "error" or not (rec.get("content") or "").strip(): log_attempt(dict(uid=uid, attempt=i + 1, ok=False, code=rec.get("finish") or "empty", err=str(rec.get("error"))[:200], secs=round(time.time() - t0, 1), cost_usd=rec.get("cost_usd", 0.0))) print(f" {uid}: попытка {i + 1} — касса вернула {rec.get('finish')}") continue 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)}") # ⚠ Окно считается ТОЛЬКО по записям с меткой времени. Записи первой редакции её не несли, # и `.get("ts", 0)` давал минимум 0 — то есть отсчёт от эпохи и «окно 496 315 часов». # Число выглядело солидно и означало ровно ничего. ts = [a["ts"] for a in at if a.get("ts")] if len(ts) >= 2: print(f" окно журнала: {(max(ts) - min(ts)) / 3600:.2f} ч по {len(ts)} записям с меткой " f"(заказ требует ≥24 ч ДО вердикта «не отдаёт»)") elif at: print(f" окно журнала: записей с меткой времени {len(ts)} — окно не доказуемо") 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__)