textmachine/eval/tenant_panel/roster.py

163 lines
11 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-v4-flash": ("https://api.deepseek.com/v1", "DEEPSEEK_API_KEY",
0.14, 0.0028, 0.28, "draft",
"api-docs.deepseek.com/quick_start/pricing, live 08.08"),
"deepseek-v4-pro": ("https://api.deepseek.com/v1", "DEEPSEEK_API_KEY",
0.435, 0.003625, 0.87, "editor",
"api-docs.deepseek.com/quick_start/pricing, live 08.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`
# признана НЕ УСТАНОВЛЕННОЙ (quirks 3б) ⇒ едем вендор-дефолтом;
# 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}")