163 lines
11 KiB
Python
163 lines
11 KiB
Python
#!/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 (05–06.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}")
|