textmachine/eval/editor_tier/roster.py

170 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""РОСТЕР ПАКА «СИЛЬНЫЙ ТИР РЕДАКТОРСКОЙ РОЛИ». $0.
Пак закрывает дыру эксп-22: промт разрешал до 16 кандидатов и требовал у КАЖДОГО провайдера два
тира — дешёвый и сильный; сессия срезала до 11 и снесла сильный тир четырёх провайдеров. Отсюда
вывод «`deepseek-v4-pro` побеждает» верен только ВНУТРИ панели. Здесь берутся ровно те четверо.
Ростер НЕ дублирует эксп-22, а расширяет его: `tenant_panel.roster` остаётся источником для армов,
которые пак переиспользует (якорь `deepseek-v4-flash`, боевой редактор `deepseek-v4-pro`,
пограничный `glm-5`). Дублирование прайс-таблицы уже стоило эксп-22 падения прогона на середине
скрина, и повторять его незачем.
Цены — с ВЕНДОР-страниц, снятых 09.08.2026, а не из памяти и не из оценки. Слаги — живой листинг
`/models` того же дня (`role_topology/list_models.py`): все четыре присутствуют.
"""
from __future__ import annotations
import sys
from pathlib import Path
REPO = Path("/home/ubuntu/projects/textmachine")
sys.path.insert(0, str(REPO / "eval" / "tenant_panel"))
sys.path.insert(0, str(REPO / "eval" / "role_topology"))
# ⚠ Ростер эксп-22 грузится ПО ПУТИ, а не `import roster`. Причина механическая и поймана первым
# же прогоном: файл называется так же, и при прямом запуске Python находит сам себя — циклический
# импорт с пустым модулем. Явная загрузка под своим именем снимает зависимость от порядка путей.
def _load(name: str, path: Path):
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
R22 = _load("roster22", REPO / "eval" / "tenant_panel" / "roster.py")
# (base_url, env-ключ, $/M вход, $/M кэш, $/M выход, роль, провенанс цены)
NEW: dict[str, tuple] = {
"gpt-5.6-terra": ("https://api.openai.com/v1", "OPENAI_API_KEY",
2.00, 0.20, 12.00, "editor",
"developers.openai.com/api/docs/pricing, live 09.08 (standard tier)"),
"kimi-k3": ("https://api.moonshot.ai/v1", "KIMI_API_KEY",
3.00, 0.30, 15.00, "editor",
"platform.moonshot.ai (сохранено prices/kimi_k3.html 08.08): "
"cache-miss 3.00 / cache-hit 0.30 / out 15.00"),
"grok-4.5": ("https://api.x.ai/v1", "XAI_API_KEY",
2.00, 0.30, 6.00, "editor",
"docs.x.ai/docs/models, live 09.08; тариф <200k контекста — наш случай (вход ~4.5k)"),
"glm-5.2": ("https://api.z.ai/api/paas/v4", "ZAI_API_KEY",
1.40, 0.26, 4.40, "editor",
"docs.z.ai/guides/overview/pricing (сохранено prices/zai.html 08.08)"),
}
# Полная таблица = переиспользуемые армы эксп-22 + новые кандидаты. Пересечений быть не должно:
# если вендор завёл слаг, уже известный эксп-22, это надо заметить, а не тихо перекрыть.
_dup = set(NEW) & set(R22.CANDIDATES)
if _dup:
raise SystemExit(f"слаг есть и в ростере эксп-22, и в новом: {_dup}")
CANDIDATES: dict[str, tuple] = {**R22.CANDIDATES, **NEW}
# Режим биллинга размышления наследуется; у новых моделей он НЕ ОБЪЯВЛЕН вендором и будет снят
# замером в фазе A. До замера считаем консервативно — как аддитивный, то есть дороже.
REASONING_BILLING = {**R22.REASONING_BILLING,
"grok-4.5": "additive"} # grok-4.3 аддитивен (quirks); семейство то же
# ⚠ ОЖИДАНИЕ ЦЕНЫ РЕДАКТОРСКОЙ КЛЕТКИ — из ЗАМЕРОВ эксп-22, не из прайса. Считается как
# отношение «замеренная цена клетки / цена выхода» внутри ТОГО ЖЕ семейства, потому что отношение
# определяется поведением размышления, а оно у вендора своё. У Kimi отношение вчетверо хуже всех:
# `k2.6` жёг бюджет на размышление и отдавал пустой выход.
EXPECT_PER_UNIT = {
"gpt-5.6-terra": 12.00 * 0.00364, # отношение luna: 0.0043745 / 1.20
"kimi-k3": 15.00 * 0.01690, # отношение k2.6: 0.0677265 / 4.00 ⚠ ~$0.25
"grok-4.5": 6.00 * 0.00581, # отношение grok-4.3: 0.0145165 / 2.50
"glm-5.2": 4.40 * 0.00407, # отношение glm-5: 0.013033 / 3.20
}
def cost(model: str, prompt_tokens: int, cached: int, completion_tokens: int,
reasoning_tokens: int = 0, total_tokens: int = 0) -> float:
"""Цена вызова по СВОЕЙ таблице. Считает `tenant_panel.roster`, но по расширенным CANDIDATES —
иначе новые слаги дают KeyError на середине прогона (тот самый отказ эксп-22 §2.2). Сигнатура
повторяет исходную побайтно: касса зовёт её позиционно."""
saved_c, saved_b = R22.CANDIDATES, R22.REASONING_BILLING
R22.CANDIDATES, R22.REASONING_BILLING = CANDIDATES, REASONING_BILLING
try:
return R22.cost(model, prompt_tokens, cached, completion_tokens,
reasoning_tokens, total_tokens)
finally:
R22.CANDIDATES, R22.REASONING_BILLING = saved_c, saved_b
# БОЕВАЯ КОНФИГУРАЦИЯ РАЗМЫШЛЕНИЯ новых кандидатов — из quirks и поведения СОСЕДА ПО СЕМЕЙСТВУ,
# не из вкуса. Гардрейл DeepSeek (не гасить) новых моделей не касается: их тут нет.
THINK_ET = {
"gpt-5.6-terra": ("effort", "low"), # как luna того же семейства (эксп-22 §0.5)
"kimi-k3": ("none", ""), # ручки эффорта у Kimi в quirks не описано (эксп-22 §2.3)
"grok-4.5": ("none", ""), # grok-4.3: reasoning-off только явным none; дефолт low
"glm-5.2": ("extra_body_disable", ""), # как glm-5/glm-4.7 — так шёл арм B эксп-21
}
OPENAI_FAMILY_ET = R22.OPENAI_FAMILY | {"gpt-5.6-terra"}
FIXED_TEMP_ET = {**R22.FIXED_TEMP, "kimi-k3": 1.0} # у k2.6 иное → HTTP 400; семейство то же
def billed_output(model: str, completion: int, reasoning: int, total: int, prompt: int) -> int:
"""Тарифицируемый выход по таблице ЭТОГО пака (у эксп-22 новых слагов нет)."""
sv = R22.REASONING_BILLING
R22.REASONING_BILLING = REASONING_BILLING
try:
return R22.billed_output(model, completion, reasoning, total, prompt)
finally:
R22.REASONING_BILLING = sv
def by_new_price() -> list[str]:
"""Порядок замера — по ВОЗРАСТАНИЮ цены выхода: кончится потолок — непокрытыми останутся
дорогие, и это будет объявлено числом, а не скрыто."""
return sorted(NEW, key=lambda m: NEW[m][4])
def call_kwargs(model: str, msgs: list[dict], max_out: int = 16000,
temperature: float = 0.3) -> dict:
"""Тело вызова в боевой конфигурации. Считает `tenant_panel.roster`, но по РАСШИРЕННЫМ
таблицам — иначе новый слаг падает с KeyError в `THINK`."""
sv = (R22.CANDIDATES, R22.THINK, R22.OPENAI_FAMILY, R22.FIXED_TEMP)
R22.CANDIDATES = CANDIDATES
R22.THINK = {**R22.THINK, **THINK_ET}
R22.OPENAI_FAMILY, R22.FIXED_TEMP = OPENAI_FAMILY_ET, FIXED_TEMP_ET
try:
return R22.call_kwargs(model, msgs, max_out, temperature)
finally:
R22.CANDIDATES, R22.THINK, R22.OPENAI_FAMILY, R22.FIXED_TEMP = sv
def selfcheck() -> int:
"""Проверки, которые обязаны пройти ДО первой покупки."""
bad = 0
U = (4500, 0, 2500, 1000, 8000)
for m in NEW:
c = cost(m, *U)
if not c > 0:
print(f"[ПРОВАЛ] цена не считается: {m}")
bad += 1
for m in ("deepseek-v4-pro", "glm-5", "deepseek-v4-flash"):
if m not in CANDIDATES:
print(f"[ПРОВАЛ] переиспользуемый арм пропал из таблицы: {m}")
bad += 1
# цена унаследованных моделей не должна поехать от расширения таблицы
for m in ("deepseek-v4-pro", "glm-5", "grok-4.3"):
if abs(cost(m, *U) - R22.cost(m, *U)) > 1e-12:
print(f"[ПРОВАЛ] цена унаследованной модели поехала: {m}")
bad += 1
for m in NEW:
if m not in THINK_ET:
print(f"[ПРОВАЛ] боевая конфигурация размышления не объявлена: {m}")
bad += 1
kw = call_kwargs(m, [{"role": "user", "content": "x"}])
if "model" not in kw:
print(f"[ПРОВАЛ] тело вызова не строится: {m}")
bad += 1
if m in OPENAI_FAMILY_ET and "temperature" in kw:
print(f"[ПРОВАЛ] OpenAI-семейству шлётся temperature: {m}")
bad += 1
print(f"ростер: кандидатов {len(CANDIDATES)} (новых {len(NEW)}) · провалов {bad}")
return bad
if __name__ == "__main__":
sys.exit(1 if selfcheck() else 0)