textmachine/eval/tenant_panel/roster.py

180 lines
14 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
"""Ф1 — РОСТЕР КАНДИДАТОВ И ЦЕНЫ. Снято 08.08.2026, каждая строка с провенансом.
Гардрейл CLAUDE.md: цены берутся с ПРАЙС-СТРАНИЦЫ ВЕНДОРА, не по памяти и не из соседнего пака.
Урок календаря D39.61 («слаг живой ≠ модель та же») требует ещё и живого `/models` в день старта —
он снят `role_topology/list_models.py`, снимок лежит рядом с сырьём.
⚠ ЧТО ЗДЕСЬ ЗАМЕР, А ЧТО ЗАЯВЛЕНИЕ. Цена — заявление вендора. Фактическая стоимость считается из
`usage` и сверяется вторым путём (`verify22.independent_price`). Расхождение прайса с леджером —
находка, а не повод молча поправить одну из сторон.
ЖИВАЯ СВЕРКА 08.08 УДАЛАСЬ по DeepSeek · Z.AI · Gemini · Kimi (curl, HTTP 200, страницы сохранены
в `~/books/tenant-panel/prices/`). НЕ УДАЛАСЬ по OpenAI (Cloudflare 403), xAI (308→403) и Mistral
(прайс рендерится JS) — там строки перенесены из эксп-21 с датой его сверки, и это объявлено.
Три поправки к `role_topology/prices.py`, найденные живой сверкой:
* у Gemini кэш-цена ВЫДЕЛЕНА вендором и втрое-десятикратно ниже входной ($0.20 против $2.00 у
3.1-pro; $0.15 против $1.50 у 3.6-flash; $0.025 против $0.25 у flash-lite) — эксп-21 ставил
кэш = входу и тем ЗАВЫШАЛ цену Gemini;
* у Z.AI появились `glm-4.7-flash` (бесплатный) и `glm-4.7-flashx` ($0.07/$0.40) — тир дешевле
боевого черновика; в live-`/models` их НЕТ, поэтому в ростер они не берутся, а проверяются
отдельной $0-пробой существования слага (урок quirks: «нет в /models ≠ не работает»);
* DeepSeek на прайс-странице 08.08 несёт объявление о ПОВЫШЕНИИ цен («significant increase
expected») — это бьёт по проекции себестоимости книги и обязано ехать с ИТОГом.
"""
from __future__ import annotations
# (base_url, env-ключ, вход, кэш-хит, выход, тир, провенанс цены)
CANDIDATES: dict[str, tuple] = {
# ⚠⚠ ПЕРЕ-ПИН DeepSeek 16.08.2026, ПОСЛЕ ПЕРЕЛОМА ПРАЙСА. Вендор ввёл TIME-BASED прайсинг с
# 16:00 UTC 16.08 (сноска прайс-страницы, факт-чек 15.08, зеркало в `backend/configs/models.yaml`).
# Пишем ПИКОВЫЕ цены — та же политика, что у бэкенда (D39.136): наша схема цены ПЛОСКАЯ и
# время суток не выражает, а пиковый пин даёт потолку сработать раньше, а не позже.
# ⚠ Цена этого решения названа числом: пиковые часы у вендора — 01:0004:00 и 06:0010:00 UTC,
# то есть 7 часов из 24; остальные 17 часов идут по ПОЛОВИНЕ. Значит на офф-пиковой покупке
# леджер ЗАВЫШАЕТ вдвое, и это осознанная переплата резерва ради простоты, а не измерение.
# Рост против июльского пина: flash ×3.1 вход / ×4.7 выход / ×5 кэш-хит; pro ×3.0 / ×4.6 / ×12.1.
# ⚠ ЧТО ЭТО ДВИГАЕТ СВЕРХ КАССЫ: правило ничьей эксп-22 («при равном качестве берём дешёвого»)
# отдало редакторскую роль `deepseek-v4-pro` при разрыве ×9 с альтернативами. `glm-5` живёт у
# Z.AI и не дорожал. Разрыв сжался, и правило обязано быть пере-считано — см. `plan.py --tier`.
"deepseek-v4-flash": ("https://api.deepseek.com/v1", "DEEPSEEK_API_KEY",
0.44, 0.014, 1.32, "draft",
"api-docs.deepseek.com/quick_start/pricing + news260813, "
"факт-чек 15.08, ПИК с 16:00 UTC 16.08"),
"deepseek-v4-pro": ("https://api.deepseek.com/v1", "DEEPSEEK_API_KEY",
1.32, 0.044, 3.96, "editor",
"api-docs.deepseek.com/quick_start/pricing + news260813, "
"факт-чек 15.08, ПИК с 16:00 UTC 16.08"),
"glm-4.7": ("https://api.z.ai/api/paas/v4", "ZAI_API_KEY",
0.60, 0.11, 2.20, "draft", "docs.z.ai/guides/overview/pricing, live 08.08"),
"glm-5": ("https://api.z.ai/api/paas/v4", "ZAI_API_KEY",
1.00, 0.20, 3.20, "editor", "docs.z.ai/guides/overview/pricing, live 08.08"),
"gpt-5.6-luna": ("https://api.openai.com/v1", "OPENAI_API_KEY",
0.20, 0.02, 1.20, "draft",
"эксп-20 §1.3 / эксп-21 (0506.08); живой ре-чек 08.08 — Cloudflare 403"),
"gemini-3.1-flash-lite": ("https://generativelanguage.googleapis.com/v1beta/openai",
"GEMINI_API_KEY", 0.25, 0.025, 1.50, "draft",
"ai.google.dev/gemini-api/docs/pricing, live 08.08"),
"gemini-3.6-flash": ("https://generativelanguage.googleapis.com/v1beta/openai",
"GEMINI_API_KEY", 1.50, 0.15, 7.50, "editor",
"ai.google.dev/gemini-api/docs/pricing, live 08.08"),
"gemini-3.1-pro-preview": ("https://generativelanguage.googleapis.com/v1beta/openai",
"GEMINI_API_KEY", 2.00, 0.20, 12.00, "editor",
"ai.google.dev/gemini-api/docs/pricing, live 08.08"),
"kimi-k2.6": ("https://api.moonshot.ai/v1", "KIMI_API_KEY",
0.95, 0.16, 4.00, "draft", "platform.kimi.ai/docs/pricing/chat-k26, live 08.08"),
"mistral-large-latest": ("https://api.mistral.ai/v1", "MISTRAL_API_KEY",
2.00, 2.00, 6.00, "draft",
"эксп-21 (06.08); живой ре-чек 08.08 — прайс рендерится JS"),
"grok-4.3": ("https://api.x.ai/v1", "XAI_API_KEY",
1.25, 0.20, 2.50, "editor",
"эксп-21 (06.08); живой ре-чек 08.08 — 308→403, Wayback свежее не имеет"),
}
# ⚠ РЕЗ ПО ПРАЙСУ, объявлен ДО замера. Промт разрешает срезать при >16 кандидатах и требует
# оставить каждому провайдеру хотя бы один слот. Снимаются те, кто ДОМИНИРОВАН одноимённым
# семейством: дороже при той же роли и без уникальной способности.
CUT = {
"gpt-5.6-terra": "выход $12.00 против $1.20 у luna того же семейства",
"kimi-k3": "выход $15.00 против $4.00 у k2.6 того же семейства",
"glm-5.1": "цена в цент совпадает с glm-5.2, семейство представлено glm-5 и glm-4.7",
"glm-5.2": "выход $4.40 против $3.20 у glm-5 — сильный тир Z.AI уже представлен",
"glm-5-turbo": "выход $4.00 против $3.20 у glm-5",
"grok-4.5": "выход $6.00 против $2.50 у grok-4.3",
"mistral-medium-latest": "цену вендора живьём подтвердить не удалось — в смету не берётся",
}
# Учёт размышления в биллинге (эксп-21 `prices.py`, пере-проверено прайс-страницами 08.08):
# subset — размышление внутри `completion_tokens` (DeepSeek, OpenAI, Z.AI, Kimi);
# additive — сверх completion (xAI);
# additive_total — видно только в `total_tokens` (Gemini: OpenAI-слой отдаёт reasoning 0).
# Gemini-страница 08.08 подтверждает дословно: «Output price (including thinking tokens)».
REASONING_BILLING = {"grok-4.3": "additive",
"gemini-3.6-flash": "additive_total",
"gemini-3.1-flash-lite": "additive_total",
"gemini-3.1-pro-preview": "additive_total"}
# БОЕВАЯ КОНФИГУРАЦИЯ РАЗМЫШЛЕНИЯ — из quirks 00, не из вкуса. `none` = ручку не шлём (вендор-
# дефолт), и это осознанный выбор, а не пропуск.
# deepseek — НИКОГДА не гасить (эхо-мина, гардрейл CLAUDE.md); у flash боевая ставит
# `reasoning_effort: low`, иначе размышление съедает бюджет (quirks §10); у pro ручка `low`
# ⚠ С 30.08 ДОХОДИТ ДО МОДЕЛИ (quirks 3г: серверная реакция 79 входных токенов при побайтно
# одинаковых messages) — прежняя причина «не установлена» БОЛЬШЕ НЕ ДЕЙСТВУЕТ. Едем всё равно
# вендор-дефолтом, но по другой причине: ВЕЛИЧИНА среза не замерена (дизайн конфаундирован
# дрейфом, exp23 §Д46.2), а цена ручки в эхе и качестве прозы не мерена ни разу ⇒ смена ступени
# у боевого редактора была бы обменом с неизвестным курсом. Менять — после интерливинг-замера;
# glm — гасится `extra_body {"thinking":{"type":"disabled"}}`, так шёл арм B эксп-21;
# luna — без явной ручки выжигает бюджет и отдаёт пустой `content` (провод-факт эксп-21 §8);
# gemini — конфиг размышления НЕ шлём: у 3.1-pro `thinking_budget:0` даёт HTTP 400, а у
# 3.x-flash поведение ручки не замерено, и гадать запрещено правилом двух направлений;
# grok — reasoning-off из РЕДАКТОРСКИХ ролей снят как no-op (D30.1) ⇒ вендор-дефолт `low`;
# kimi — ручки нет, зато принимает ТОЛЬКО `temperature: 1` (quirks, «Параметры сэмплинга»).
THINK = {
"deepseek-v4-flash": ("effort", "low"),
"deepseek-v4-pro": ("none", ""),
"glm-4.7": ("extra_body_disable", ""),
"glm-5": ("extra_body_disable", ""),
"gpt-5.6-luna": ("effort", "low"),
"gemini-3.1-flash-lite": ("none", ""),
"gemini-3.6-flash": ("none", ""),
"gemini-3.1-pro-preview": ("mandatory", ""),
"kimi-k2.6": ("none", ""),
"mistral-large-latest": ("none", ""),
"grok-4.3": ("none", ""),
}
OPENAI_FAMILY = {"gpt-5.6-luna"} # max_completion_tokens, temperature не шлём
FIXED_TEMP = {"kimi-k2.6": 1.0} # quirks: иное → HTTP 400 «only 1 is allowed»
def call_kwargs(model: str, msgs: list[dict], max_out: int = 16000,
temperature: float = 0.3) -> dict:
"""Тело вызова в боевой конфигурации модели. Одно место — чтобы квирк не разъехался по файлам."""
kw: dict = dict(model=model, messages=msgs)
if model in OPENAI_FAMILY:
kw["max_completion_tokens"] = max_out
else:
kw["max_tokens"] = max_out
kw["temperature"] = FIXED_TEMP.get(model, temperature)
mode, val = THINK[model]
if mode == "effort":
kw["extra_body"] = {"reasoning_effort": val}
elif mode == "effort_none":
kw["extra_body"] = {"reasoning_effort": "none"}
elif mode == "extra_body_disable":
kw["extra_body"] = {"thinking": {"type": "disabled"}}
return kw
def billed_output(model: str, completion_tokens: int, reasoning_tokens: int,
total_tokens: int = 0, prompt_tokens: int = 0) -> int:
mode = REASONING_BILLING.get(model)
if mode == "additive":
return completion_tokens + reasoning_tokens
if mode == "additive_total":
return max(total_tokens - prompt_tokens, completion_tokens + reasoning_tokens)
return completion_tokens
def cost(model: str, prompt_tokens: int, cached: int, completion_tokens: int,
reasoning_tokens: int = 0, total_tokens: int = 0) -> float:
_, _, pin, pcache, pout, _, _ = CANDIDATES[model]
out = billed_output(model, completion_tokens, reasoning_tokens, total_tokens, prompt_tokens)
return ((prompt_tokens - cached) / 1e6 * pin + cached / 1e6 * pcache + out / 1e6 * pout)
def by_out_price() -> list[str]:
"""Порядок замера — по ВОЗРАСТАНИЮ цены выхода: если потолок кончится, непокрытыми останутся
дорогие, и это будет объявлено числом, а не скрыто."""
return sorted(CANDIDATES, key=lambda m: CANDIDATES[m][4])
if __name__ == "__main__":
print(f"{'модель':26s}{'тир':>8s}{'вход':>8s}{'кэш':>8s}{'выход':>8s} провенанс")
print("-" * 110)
for m in by_out_price():
_, _, pin, pc, pout, tier, src = CANDIDATES[m]
print(f"{m:26s}{tier:>8s}{pin:8.3f}{pc:8.3f}{pout:8.2f} {src}")
print(f"\nсрезано по прайсу ({len(CUT)}):")
for m, why in sorted(CUT.items()):
print(f" {m:24s} {why}")