170 lines
10 KiB
Python
170 lines
10 KiB
Python
#!/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)
|