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