From a725be51915bb16aa326c11cee3c0985adad49e6 Mon Sep 17 00:00:00 2001 From: "Claude (backend session)" Date: Sat, 8 Aug 2026 20:04:06 +0300 Subject: [PATCH] Re-freeze phase 2 before its bulk purchases: bound the ledger guard by per-model price and allow a tenant subset per process --- eval/tenant_panel/itog22.py | 213 ++++++++++++++++++++++++++++++++++++ eval/tenant_panel/money.py | 68 ++++++++++-- eval/tenant_panel/panel.py | 19 +++- 3 files changed, 286 insertions(+), 14 deletions(-) create mode 100644 eval/tenant_panel/itog22.py diff --git a/eval/tenant_panel/itog22.py b/eval/tenant_panel/itog22.py new file mode 100644 index 00000000..68e4160a --- /dev/null +++ b/eval/tenant_panel/itog22.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +"""ПЕРЕ-СЧЁТ ВСЕХ НЕСУЩИХ ЧИСЕЛ ЭКСП-22 ИЗ СЫРЬЯ. $0. + +Зачем. Эксп-21 сдал отчёт, несущие числа которого НЕ ИМЕЛИ пути воспроизведения в дереве: часть +бралась из stdout прогонов, утраченных инцидентом. Число, которое нельзя пере-снять командой из +репозитория, ничем не отличается от числа по памяти. Здесь каждый блок ИТОГа печатается заново из +персистированных артефактов, с провенансом — из какого прохода и по какой формуле. + +Ничего не покупается и не судится: только счёт по тому, что уже лежит на диске. + +Запуск: eval/.venv/bin/python eval/tenant_panel/itog22.py +""" +from __future__ import annotations + +import json +import statistics as st +import sys +from pathlib import Path + +REPO = Path("/home/ubuntu/projects/textmachine") +ZONE = Path(__file__).resolve().parent +sys.path.insert(0, str(ZONE)) +sys.path.insert(0, str(REPO / "eval" / "role_topology")) + +import bank as BANK # noqa: E402 +import battery as B # noqa: E402 +import judge as JU # noqa: E402 +import material as M # noqa: E402 +import money as MONEY # noqa: E402 +import panel as P2 # noqa: E402 +import roster as R # noqa: E402 + +OUT = MONEY.OUT + + +def h(t: str) -> None: + print(f"\n═══ {t} ═══") + + +def contamination() -> None: + h("Ф0.2 КОНТАМИНАЦИЯ СРЕЗА (порог: узнавание ≥3/5 = непригоден)") + f = OUT / "contamination.json" + if not f.exists(): + print(" пробы нет") + return + for r in json.loads(f.read_text(encoding="utf-8")): + v = "НЕПРИГОДЕН" if r["recognized"] * 5 >= 3 * max(1, r["n"]) else "следов нет" + print(f" {r['book']:14s} {r['model']:22s} узнано {r['recognized']}/{r['n']} · " + f"клоуз {r['cloze']}/{r['n']} {v} [{r['kind']}]") + + +def material() -> None: + h("Ф0.2 МАТЕРИАЛ") + zs = M.units_zh() + allch = M.chapters(all_=True) + print(f" книга {M.BOOK['title']} · 首发 {M.BOOK['first_pub']} · глав скачано {len(allch)} · " + f"снято гейтом прав {sorted(M.se_dropped(allch))}") + print(f" zh-единиц {len(zs)} · знаков {min(len(u['source']) for u in zs)}…" + f"{max(len(u['source']) for u in zs)} · медиана " + f"{sorted(len(u['source']) for u in zs)[len(zs) // 2]} · глав-источников " + f"{len({u['chapter'] for u in zs})} · макс. близость {M._pairwise_max_sim(zs):.3f}") # noqa: SLF001 + b = BANK.PR.bank() + tot = sum(sum(u["source"].count(t) for u in zs) for t in b) + print(f" банк {len(b)} терминов · знаменатель покрытия {tot} упоминаний · signed=false") + + +def screen() -> None: + h("Ф1 СКРИН — по КАЖДОМУ кандидату") + res = P2.screen_result() + if not res: + print(" скрина нет") + return + print(f" {'модель':26s}{'вердикт':>11s}{'$/ед transl':>13s}{'$/ед edit':>11s} причины") + for m in R.by_out_price(): + r = res.get(m, {}) + print(f" {m:26s}{r.get('verdict', '—'):>11s}" + f"{(r.get('price_translator') or 0):13.5f}{(r.get('price_editor') or 0):11.5f}" + f" {'; '.join(r.get('why', []))}") + + +def echo_rate() -> None: + h("Ф2 (в) ЭХО-МИНА — доля единиц, потребовавших ре-гена") + for m in P2.tenants(): + nz, tz = P2.regen_rate(m, en=False) + ne, te = P2.regen_rate(m, en=True) + print(f" {m:26s} zh {nz}/{tz} = {nz / max(1, tz):.0%} · en {ne}/{te} = {ne / max(1, te):.0%}") + + +def battery_canon() -> None: + h("Ф2 (г) БАТАРЕЯ И ПОКРЫТИЕ КАНОНА (zh, детерминированно, шума нет)") + zh, cards = M.units_zh(), B.load_cards() + print(f" {'арм':32s}{'ед.':>5s}{'ханьцзы':>9s}{'типогр':>8s}{'потеря':>8s}" + f"{'канон':>14s}{'покрытие':>10s}") + for m in P2.tenants(): + for kind in ("D", "E"): + rows, k, s = [], 0, 0 + for u in zh: + t = P2.draft_text(m, u["uid"]) if kind == "D" else P2.edit_text(m, u["uid"]) + if not t.strip(): + continue + rows.append(B.run_unit(B.Unit(source=u["source"], draft=t, final=t, cards=cards))) + kk, ss = BANK.coverage(u["source"], t) + k += kk + s += ss + if rows: + print(f" {kind + '[' + m + ']':32s}{len(rows):5d}" + f"{sum(r['cjk_leak']['han_chars'] for r in rows):9d}" + f"{sum(r['typography']['violations'] for r in rows):8d}" + f"{sum(r['numbers']['lost_n'] for r in rows):8d}" + f"{f'{k}/{s}':>14s}{(k / s if s else 0):>10.3f}") + + +def economics() -> None: + h("Ф2 (д) ЦЕНА/ЕДИНИЦА И ПРОЕКЦИЯ НА КНИГУ (1500 единиц)") + zh = M.units_zh() + print(f" {'жилец':26s}{'черновик':>11s}{'редактор':>11s}{'связка':>11s}{'книга $':>10s}") + for m in P2.tenants(): + dc = [P2.draft_cost(m, u["uid"]) for u in zh] + ec = [] + for u in zh: + f = OUT / f"{P2.tag_edit(m, u['uid'])}.json" + if f.exists(): + ec.append(json.loads(f.read_text(encoding="utf-8")).get("cost_usd", 0.0)) + if not dc: + continue + d50, e50 = st.median(dc), (st.median(ec) if ec else 0.0) + print(f" {m:26s}{d50:11.5f}{e50:11.5f}{d50 + e50:11.5f}{(d50 + e50) * 1500:10.2f}") + print(" ⚠ DeepSeek объявил на прайс-странице 08.08 повышение цен — проекция едет с оговоркой") + + +def judged(pass_: str) -> None: + h(f"СУДЕЙСТВО, проход {pass_}") + JU.setup(pass_) + import absjudge as AJ # noqa: PLC0415 + data = AJ._by_unit() # noqa: SLF001 + if not data: + print(" голосов нет") + return + AJ.controls() + print() + AJ.score() + + +def money_two_ways() -> None: + h("ДЕНЬГИ — ДВА НЕЗАВИСИМЫХ ПУТИ") + led = {ph: MONEY.Guarded(ph).spent() for ph in ("Ф0", "Ф1", "Ф2", "Ф3")} + # второй путь: пере-счёт из сырых usage × прайс, правило биллинга выписано заново + def indep(rec: dict) -> float: + m = rec.get("model") + if m not in R.CANDIDATES: + return rec.get("cost_usd", 0.0) + _, _, pin, pc, pout, _t, _s = R.CANDIDATES[m] + pt, cd = rec.get("prompt_tokens", 0), rec.get("cached_tokens", 0) + ct, rt = rec.get("completion_tokens", 0), rec.get("reasoning_tokens", 0) + tt = rec.get("total_tokens", 0) + mode = R.REASONING_BILLING.get(m) + out = ct + rt if mode == "additive" else ( + max(tt - pt, ct + rt) if mode == "additive_total" else ct) + return (pt - cd) / 1e6 * pin + cd / 1e6 * pc + out / 1e6 * pout + tot2 = {ph: 0.0 for ph in led} + empty_paid = [] + for ph, globs in MONEY.TAGS.items(): + for g in globs: + for f in OUT.glob(g): + r = json.loads(f.read_text(encoding="utf-8")) + tot2[ph] += indep(r) + if r.get("cost_usd", 0) > 0 and not (r.get("content") or "").strip(): + empty_paid.append((f.name, r["cost_usd"])) + print(f" {'фаза':6s}{'касса':>12s}{'пере-счёт':>12s}{'расхождение':>13s}{'потолок':>10s}") + for ph in ("Ф0", "Ф1", "Ф2", "Ф3"): + mark = " ⛔ ПРОБОЙ" if led[ph] > MONEY.CEILINGS[ph] + 1e-9 else "" + print(f" {ph:6s}{led[ph]:12.6f}{tot2[ph]:12.6f}{led[ph] - tot2[ph]:13.6f}" + f"{MONEY.CEILINGS[ph]:10.2f}{mark}") + print(f" {'ПАК':6s}{sum(led.values()):12.6f}{sum(tot2.values()):12.6f}" + f"{sum(led.values()) - sum(tot2.values()):13.6f}{MONEY.PACK_CEILING:10.2f}") + if empty_paid: + print(f" оплачено за ПУСТОЙ выход: {len(empty_paid)} вызовов " + f"${sum(c for _n, c in empty_paid):.5f}") + for n, c in sorted(empty_paid, key=lambda x: -x[1])[:5]: + print(f" {n} ${c:.5f}") + + +def agent_runs() -> None: + h("АГЕНТ-ЗАПУСКИ") + rs = JU.runs_used() + print(f" израсходовано {len(rs)} из {JU.CAP}") + by: dict[str, int] = {} + for r in rs: + by[r["pass_"]] = by.get(r["pass_"], 0) + 1 + for p, n in sorted(by.items()): + print(f" {p:6s} {n}") + + +def main() -> None: + print("ИТОГ ЭКСП-22 — ВСЕ НЕСУЩИЕ ЧИСЛА, ПЕРЕ-СНЯТЫЕ ИЗ СЫРЬЯ ($0)") + material() + contamination() + screen() + echo_rate() + battery_canon() + economics() + for p in ("p2zh", "p2en", "p3", "dva"): + try: + judged(p) + except Exception as e: # noqa: BLE001 + print(f"\n═══ СУДЕЙСТВО {p} ═══\n не считается: {type(e).__name__}: {e}") + money_two_ways() + agent_runs() + print("\nВСЁ ВЫШЕ ПЕРЕ-СНЯТО ИЗ СЫРЬЯ. Чисел без пути воспроизведения в отчёте быть не должно.") + + +if __name__ == "__main__": + main() diff --git a/eval/tenant_panel/money.py b/eval/tenant_panel/money.py index 7e8cb8f6..980709dd 100644 --- a/eval/tenant_panel/money.py +++ b/eval/tenant_panel/money.py @@ -53,8 +53,14 @@ TAGS = { # проекционный гард остановил скрин перед `gemini-3.1-pro-preview` — ОБЯЗАТЕЛЬНЫМ кандидатом Ф3 # по промту. Его цена — единственная неизвестная в смете Ф3 (оценка $0.80 из $1.60), и без замера # половина фазы стояла бы на прайс-странице вместо провода. Пакетный потолок $5.00 не двигался. -CEILINGS = {"Ф0": 0.40, "Ф1": 1.05, "Ф2": 2.00, "Ф3": 1.60} -PACK_CEILING = 5.00 +# ⚠ ПОТОЛКИ Ф3 И ПАКА ПОДНЯТЫ 08.08 СЛОВОМ ВЛАДЕЛЬЦА, объявлено ДО покупок Ф3: $1.60 → $3.00 и +# $5.00 → $6.50. Причина — замер, а не желание: обязательный кандидат промта +# `gemini-3.1-pro-preview` тарифицирует 11 144–12 010 выходных токенов на ответ в 2 508 (скрытое +# размышление видно ТОЛЬКО в `total_tokens`), то есть $0.152 за редакторскую единицу и $2.44 за +# 16 единиц — больше всего прежнего потолка фазы. Альтернатива «подмножество единиц» владельцем +# отклонена в пользу полной мощности панели. +CEILINGS = {"Ф0": 0.40, "Ф1": 1.05, "Ф2": 2.00, "Ф3": 3.00} +PACK_CEILING = 6.50 class Pack: @@ -76,6 +82,31 @@ class Pack: return self._led.afford() +def model_expect(model: str) -> float: + """Ожидаемая цена следующего вызова ИМЕННО ЭТОЙ модели. + + ⚠ Заведено после того, как потолок Ф1 был пробит ($1.087758 против $1.05). Механизм пробоя: + проекционный гард брал p95 УЖЕ КУПЛЕННЫХ вызовов — то есть прошлое, в котором новой модели + ещё не было. `gemini-3.1-pro-preview` оказался в семь раз дороже любого предшественника, и + оценка «как раньше» его не удержала. Здесь ожидание берётся по СВОИМ записям этой модели, + а при их отсутствии считается из прайса по медианным токенам пака — консервативно, с + множителем на скрытое размышление у провайдеров класса `additive_total`. + """ + mine = [json.loads(f.read_text(encoding="utf-8")).get("cost_usd", 0.0) + for f in OUT.glob("tp*.json") + if f.name.count("SELFTEST") == 0] + prices = [c for c, m in ((json.loads(f.read_text(encoding="utf-8")).get("cost_usd", 0.0), + json.loads(f.read_text(encoding="utf-8")).get("model")) + for f in OUT.glob("tp*.json")) if m == model and c > 0] + if len(prices) >= 2: + return max(prices) + if model not in ROSTER.CANDIDATES: + return max(mine or [0.02]) + _, _, pin, _pc, pout, _t, _s = ROSTER.CANDIDATES[model] + hidden = 5.0 if ROSTER.REASONING_BILLING.get(model) in ("additive_total", "additive") else 1.5 + return 4500 / 1e6 * pin + 3000 * hidden / 1e6 * pout + + class Guarded: """Фазовая касса, которая не пускает покупку, пробивающую ЛЮБОЙ из двух потолков.""" @@ -85,19 +116,27 @@ class Guarded: self.ceiling = CEILINGS[phase] self._led = BUY.Ledger(self.name, self.ceiling, TAGS[phase], default_expect=default_expect) self._pack = Pack() + self.next_model: str | None = None def spent(self) -> float: return self._led.spent() def expect(self) -> float: self._led.spent() - return self._led.expect() + base = self._led.expect() + return max(base, model_expect(self.next_model)) if self.next_model else base def afford(self) -> tuple[bool, str]: - ok, why = self._led.afford() - if not ok: - return ok, why - return self._pack.afford() + sp = self.spent() + exp = self.expect() + if sp + exp > self.ceiling: + return False, (f"[СТОП {self.name}] потрачено ${sp:.6f} + ожидание ${exp:.6f} " + f"= ${sp + exp:.6f} > потолок ${self.ceiling:.2f}") + psp, pexp = self._pack.spent(), max(self._pack.expect(), exp) + if psp + pexp > PACK_CEILING: + return False, (f"[СТОП ПАК] потрачено ${psp:.6f} + ожидание ${pexp:.6f} " + f"> потолок пака ${PACK_CEILING:.2f}") + return True, "" @lru_cache(maxsize=64) @@ -152,6 +191,7 @@ def purchase(led: Guarded, tag: str, model: str, client, kw: dict, **extra) -> d if not any(tag.startswith(p.split("*")[0]) for p in TAGS[led.phase]): raise SystemExit(f"⛔ тег {tag!r} не покрыт глобом кассы {led.phase} — впиши в TAGS") _refuse_unfrozen() + led.next_model = model # гард считает ожидание по ЭТОЙ модели rec = {} for k in range(4): rec = BUY.purchase(led, tag, model, client, kw, **extra) @@ -197,10 +237,20 @@ def selftest() -> int: f.unlink() # Проекционный гард обязан ОТКАЗЫВАТЬ, когда ожидание пробивает потолок. g = Guarded("Ф0") - g._led.ceiling = 0.0 # noqa: SLF001 - ok, why = g.afford() + g.ceiling = 0.0 + ok, _why = g.afford() bad += ok print(f"[{'OK ' if not ok else 'ПРОВАЛ'}] проекционный гард отказывает при нулевом потолке") + # ⚠ РЕГРЕССИЯ НА ПРОБОЙ ПОТОЛКА Ф1: ожидание обязано считаться по ЭТОЙ модели, а не по + # p95 прошлого. Дорогая новая модель не должна проходить гард только потому, что раньше + # таких дорогих вызовов не было. + g2 = Guarded("Ф0") + g2.ceiling = g2.spent() + 0.05 + g2.next_model = "gemini-3.1-pro-preview" + ok2, _ = g2.afford() + bad += ok2 + print(f"[{'OK ' if not ok2 else 'ПРОВАЛ'}] гард видит цену дорогой НОВОЙ модели " + f"(${model_expect('gemini-3.1-pro-preview'):.4f}), а не p95 прошлого") # Прайс: КАЖДАЯ модель ростера обязана считаться кассой, иначе покупка падает на середине. for m in ROSTER.CANDIDATES: try: diff --git a/eval/tenant_panel/panel.py b/eval/tenant_panel/panel.py index 47102cf1..02bc04e1 100644 --- a/eval/tenant_panel/panel.py +++ b/eval/tenant_panel/panel.py @@ -178,9 +178,18 @@ def cmd_plan() -> None: print("⛔ СМЕТА НЕ ВЛЕЗАЕТ — резать панель по объявленному правилу или пинг владельцу") -def cmd_drafts() -> None: +def _selected(argv: list[str]) -> list[str]: + """Подмножество жильцов для параллельного прогона. Клетки идемпотентны по тегу, поэтому + несколько процессов не мешают друг другу; касса читает диск, так что расход виден всем. + ⚠ Гонка ограничена сверху: перерасход не больше (процессов−1)×цена одного вызова.""" + only = [a.split("=", 1)[1] for a in argv if a.startswith("--only=")] + ts = tenants() + return [m for m in ts if m in only] if only else ts + + +def cmd_drafts(argv: list[str] | None = None) -> None: led = MONEY.Guarded("Ф2", default_expect=0.01) - for m in tenants(): + for m in _selected(argv or []): for u, en in units_all(): rec = buy_draft(led, m, u, en) if rec.get("skipped"): @@ -193,9 +202,9 @@ def cmd_drafts() -> None: print(f"потрачено Ф2 ${led.spent():.6f}") -def cmd_edit() -> None: +def cmd_edit(argv: list[str] | None = None) -> None: led = MONEY.Guarded("Ф2", default_expect=0.01) - for m in tenants(): + for m in _selected(argv or []): for u, en in units_all(): rec = buy_edit(led, m, u, en) if rec.get("no_draft"): @@ -299,6 +308,6 @@ def cmd_score() -> None: if __name__ == "__main__": a = sys.argv[1:] or ["--plan"] - fn = {"--plan": cmd_plan, "--drafts": cmd_drafts, "--edit": cmd_edit, + fn = {"--plan": cmd_plan, "--drafts": lambda: cmd_drafts(a), "--edit": lambda: cmd_edit(a), "--floor": cmd_floor, "--score": cmd_score}.get(a[0]) fn() if fn else print(__doc__)