#!/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)