#!/usr/bin/env python3 """ФАЗА A — СКРИН СИЛЬНОГО ТИРА. Платный (потолок $1.10), идёт через кассу пака. Пороги НЕ свои: берутся у эксп-22 побайтно (`screen.PRICE_CAP/CYR_MIN/ECHO_MAX`, `gates`, `verdict`). Порог, взятый из прошлого пака без правки, — единственная защита от подгонки состава под желаемый результат, а состав здесь и есть предмет спора: пак существует потому, что эксп-22 срезал сильный тир. ⚠ ОСОБЫЙ РЕЖИМ `kimi-k3`, объявлен ДО покупок. По отношению «цена клетки / цена выхода» внутри Kimi он ожидается ~$0.25/ед — вчетверо выше порога роли, — и `k2.6` уже отдал пустой выход на трёх клетках из четырёх при 100% доли размышления. Полный скрин был бы покупкой предсказуемого провала примерно за доллар. Берётся ОДНА клетка редакторской роли с жёстким потолком выхода: этого хватает и на ценовой вердикт, и на второй замер отказного режима. Отдай он против ожидания годный дешёвый выход — досняли бы полным скрином отдельным решением. """ from __future__ import annotations import json import sys from pathlib import Path REPO = Path("/home/ubuntu/projects/textmachine") ZONE = Path(__file__).resolve().parent sys.path.insert(0, str(REPO / "eval" / "tenant_panel")) sys.path.insert(0, str(REPO / "eval" / "role_topology")) def _load(name: str, path: Path): """⚠ Модули соседнего пака грузятся ПО ПУТИ, а не `import <имя>`. При прямом запуске своя папка стоит в sys.path первой, и `import screen` находит ЭТОТ файл, а не скрин эксп-22 — молчаливый само-импорт. Ловилось трижды подряд (ростер, касса, скрин), поэтому правило одно: всё чужое — только явной загрузкой.""" import importlib.util # noqa: PLC0415 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 TP = REPO / "eval" / "tenant_panel" S22 = _load("screen22", TP / "screen.py") MONEY = _load("money_et", ZONE / "money.py") ROSTER = MONEY.ROSTER_ET PHASE = "ФA" NEW = list(ROSTER.NEW) # gpt-5.6-terra · kimi-k3 · grok-4.5 · glm-5.2 SPECIAL = {"kimi-k3": dict(units=1, roles=("editor",), max_out=4000)} ROLES = ("translator", "editor") def client(model: str): """Соединение по РАСШИРЕННОЙ таблице. Клиент эксп-22 знает только свои 11 слагов и падает KeyError на новом кандидате — тот же класс, что уронил его собственный скрин на середине.""" import os # noqa: PLC0415 from dotenv import load_dotenv # noqa: PLC0415 from openai import OpenAI # noqa: PLC0415 load_dotenv(REPO / "eval" / ".env") base, env, *_ = ROSTER.CANDIDATES[model] key = os.environ.get(env) if not key: raise SystemExit(f"нет ключа {env} для {model}") return OpenAI(base_url=base, api_key=key, timeout=600.0, max_retries=1) def tag(model: str, role: str, uid: str) -> str: return f"et-scr-{model}-{role}-{uid[:6]}" def plan() -> list[tuple[str, str, dict]]: """Что будет куплено. Печатается ДО покупок и лежит во фризе.""" us = S22.units() # те же две единицы, что скринил эксп-22 out = [] for m in ROSTER.by_new_price(): sp = SPECIAL.get(m, {}) for role in sp.get("roles", ROLES): for u in us[:sp.get("units", len(us))]: out.append((m, role, u)) return out def cmd_plan() -> None: tot = 0.0 print(f"{'модель':16s}{'роль':12s}{'ед.':>4s}{'ожидание $':>12s}") for m in ROSTER.by_new_price(): sp = SPECIAL.get(m, {}) n = len(sp.get("roles", ROLES)) * sp.get("units", len(S22.units())) exp = ROSTER.EXPECT_PER_UNIT[m] * n tot += exp print(f"{m:16s}{'/'.join(sp.get('roles', ROLES)):12s}{n:4d}{exp:12.3f}" + (" ← особый режим" if m in SPECIAL else "")) print(f"{'ИТОГО':16s}{'':12s}{'':4s}{tot:12.3f} потолок {MONEY.CEILINGS[PHASE]:.2f}") if tot > MONEY.CEILINGS[PHASE]: print("⛔ смета выше потолка фазы — СТОП, не резать молча") raise SystemExit(1) def cmd_run() -> None: led = MONEY.Guarded(PHASE) for m, role, u in plan(): t = tag(m, role, u["uid"]) if (MONEY.OUT / f"{t}.json").exists(): continue msgs = S22.msgs_for(role, u) kw = ROSTER.call_kwargs(m, msgs, max_out=SPECIAL.get(m, {}).get("max_out", 16000)) rec = MONEY.purchase(led, t, m, client(m), kw, role=role, uid=u["uid"]) if rec is None: print(f" {t}: не куплено") continue print(f" {t}: ${rec.get('cost_usd', 0):.5f} finish={rec.get('finish')} " f"знаков={len(rec.get('content') or '')}") MONEY.report() def _share(rec: dict) -> float: """Доля размышления в ТАРИФИЦИРУЕМОМ выходе — по таблице биллинга ЭТОГО пака. ⚠ Гейты эксп-22 считают знаменатель своей таблицей режимов, где новых слагов нет, и берут выход без размышления. Для `grok-4.5` (аддитивный биллинг) это давало долю 227.6% — величину, которой не бывает. На вердикт не влияло (цена берётся из кассы), но печаталось неверное число, а числа без смысла в отчёте стоять не должны. """ out = ROSTER.billed_output(rec["model"], rec.get("completion_tokens", 0), rec.get("reasoning_tokens", 0), rec.get("total_tokens", 0), rec.get("prompt_tokens", 0)) return round(rec.get("reasoning_tokens", 0) / max(1, out), 3) def verdict_editor(rows: list[dict]) -> tuple[str, list[str]]: """ПРЕ-РЕГ-критерий этого пака: он скринит РЕДАКТОРОВ. ⚠ Расхождение реализации с пре-регом, объявляю. Пре-рег (план, зафиксирован до покупок) называет пороги: цена РЕДАКТОРА ≤$0.06/ед · кириллица ≥0.5 · эхо ≤0.15 · формат · непустой выход. Переводческого порога в нём нет — пак переводчиков не выбирает, эту роль эксп-22 уже закрыл. Реализация же переиспользовала `verdict` эксп-22 целиком, вместе с переводческим порогом $0.02, и именно он снял `gpt-5.6-terra` и `grok-4.5` — обоих кандидатов, ради которых пак и существует. Снимать критерий после того, как увидел результат, — подгонка, поэтому оба вердикта печатаются РЯДОМ: пре-рег-критерий как несущий, полный критерий эксп-22 как справка. """ if not rows: return "НЕ ЗАМЕРЕН", ["клеток нет"] why = [] ed = [r for r in rows if r["role"] == "editor"] fmt_bad = sum(1 for r in ed if r["md"] or r["preamble"] or r["empty"] or r["truncated"]) if fmt_bad > 1: why.append(f"Г1 формат: нарушений в {fmt_bad} клетках") if any(r["han_share"] > S22.ECHO_MAX for r in ed): why.append("Г2 эхо выше боевого порога") if any(r["refusal"] for r in ed): why.append("Г3 отказ на чувствительном фрагменте") if any(r["cyr_share"] < S22.CYR_MIN and not r["empty"] for r in ed): why.append("Г4 выход не на целевом языке") import statistics as st # noqa: PLC0415 pe = [r["cost"] for r in ed] if pe and st.median(pe) > S22.PRICE_CAP["editor"]: why.append(f"Г5 цена editor: ${st.median(pe):.4f} > ${S22.PRICE_CAP['editor']}") if any(r["truncated"] and r["empty"] for r in ed): why.append("Г6 размышление съело бюджет, выход пуст") return ("ПРОВАЛ" if why else "прошёл"), why def result() -> dict: """Оба вердикта: пре-рег-критерий пака (несущий) и полный критерий эксп-22 (справка).""" import statistics as st # noqa: PLC0415 res = {} for m in NEW: rows = [] for f in MONEY.OUT.glob(f"et-scr-{m}-*.json"): rec = json.loads(f.read_text(encoding="utf-8")) u = next((x for x in S22.units() if x["uid"] == rec.get("uid")), None) g = S22.gates(rec, u["source"] if u else "") g["reasoning_share"] = _share(rec) rows.append(g) v22, why22 = S22.verdict(rows) ved, whyed = verdict_editor(rows) pt = [r["cost"] for r in rows if r["role"] == "translator"] pe = [r["cost"] for r in rows if r["role"] == "editor"] res[m] = dict(verdict=ved, why=whyed, verdict_full22=v22, why_full22=why22, n=len(rows), price_translator=(st.median(pt) if pt else None), price_editor=(st.median(pe) if pe else None), reasoning_share=(max((r["reasoning_share"] for r in rows), default=0.0)), han=sum(r["han_chars"] for r in rows)) return res def cmd_score() -> None: rs = result() print("ВЕРДИКТ ПО ПРЕ-РЕГ-КРИТЕРИЮ ПАКА (редакторская роль) — несущий") print(f"{'модель':16s}{'вердикт':>10s}{'ед.':>4s}{'$/ед edit':>11s}{'размышл':>9s} причины") for m, r in rs.items(): print(f"{m:16s}{r['verdict']:>10s}{r['n']:4d}{(r['price_editor'] or 0):11.5f}" f"{r['reasoning_share']:9.1%} {'; '.join(r['why'])}") ok = [m for m, r in rs.items() if r["verdict"] == "прошёл"] print(f"\nпрошли фазу A: {ok or '—'}") print("\nСПРАВКА — полный критерий эксп-22 (обе роли, включая переводческий порог $0.02,") print("которого в пре-реге этого пака нет; печатается, чтобы разница не пряталась):") for m, r in rs.items(): print(f" {m:16s}{r['verdict_full22']:>10s} $/ед transl " f"{(r['price_translator'] or 0):.5f} {'; '.join(r['why_full22'])}") def selfcheck() -> int: """До покупок: пороги те же, план в потолке, теги уникальны.""" bad = 0 if (S22.PRICE_CAP, S22.CYR_MIN, S22.ECHO_MAX) != ({"translator": 0.02, "editor": 0.06}, 0.5, 0.15): print("[ПРОВАЛ] пороги эксп-22 изменились — состав пака стал бы подгонкой") bad += 1 tags = [tag(m, r, u["uid"]) for m, r, u in plan()] if len(tags) != len(set(tags)): print("[ПРОВАЛ] теги покупок не уникальны — клетки перетрут друг друга") bad += 1 if not all(t.startswith("et-scr-") for t in tags): print("[ПРОВАЛ] тег вне глоба фазы — расход будет невидим кассе") bad += 1 for m in NEW: if m not in ROSTER.THINK_ET: print(f"[ПРОВАЛ] боевая конфигурация размышления не объявлена: {m}") bad += 1 if "kimi-k3" not in SPECIAL: print("[ПРОВАЛ] особый режим kimi-k3 потерян") bad += 1 print(f"скрин: клеток в плане {len(tags)} · провалов {bad}") return bad if __name__ == "__main__": a = sys.argv[1:] or ["--plan"] fn = {"--plan": cmd_plan, "--run": cmd_run, "--score": cmd_score, "--selfcheck": lambda: sys.exit(1 if selfcheck() else 0)}.get(a[0]) if fn: fn() else: print(__doc__)