230 lines
13 KiB
Python
230 lines
13 KiB
Python
#!/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__)
|