textmachine/eval/role_topology/prices.py

110 lines
8.6 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 — ЦЕНЫ КАНДИДАТОВ, снятые с прайс-страниц вендоров 06.08.2026.
Почему отдельным файлом с датой и URL. Гардрейл CLAUDE.md: цены берутся ТОЛЬКО с прайс-страницы
вендора, не по памяти; урок календаря D39.61: «слаг живой ≠ модель та же». Смета Ф1/Ф2 стоит на
этих числах, и если через неделю они разъедутся, должно быть видно, ЧТО именно устарело и откуда
бралось. Порядок в кортеже: (вход, кэш-хит, выход) долларов за 1M токенов.
⚠ ЧТО ЗДЕСЬ НЕ ПРОВЕРЕНО ЖИВЬЁМ: цена — это ЗАЯВЛЕНИЕ вендора, а не замер. Реальная стоимость
вызова считается из `usage` и может разойтись с прайсом (кэш, тарификация размышления, батч-тир).
Расхождение прайса с леджером — само по себе находка, и Ф1 обязана его напечатать.
"""
from __future__ import annotations
# (base_url, env-ключ, вход, кэш, выход, механизм гашения размышления, источник цены)
CANDIDATES: dict[str, tuple] = {
# DeepSeek — цены пост-катовера, сверены 25.07 (quirks 00, строка эндпоинтов).
"deepseek-v4-flash": ("https://api.deepseek.com/v1", "DEEPSEEK_API_KEY",
0.14, 0.0028, 0.28, None, "quirks 00 (25.07, вендор-прайс)"),
"deepseek-v4-pro": ("https://api.deepseek.com/v1", "DEEPSEEK_API_KEY",
0.435, 0.003625, 0.87, None, "quirks 00 (25.07, вендор-прайс)"),
# Z.AI — docs.z.ai/guides/overview/pricing, снято 06.08.
"glm-5": ("https://api.z.ai/api/paas/v4", "ZAI_API_KEY",
1.00, 0.20, 3.20, "extra_body_disable", "docs.z.ai/pricing 06.08"),
"glm-5-turbo": ("https://api.z.ai/api/paas/v4", "ZAI_API_KEY",
1.20, 0.24, 4.00, "extra_body_disable", "docs.z.ai/pricing 06.08"),
"glm-5.2": ("https://api.z.ai/api/paas/v4", "ZAI_API_KEY",
1.40, 0.26, 4.40, "extra_body_disable", "docs.z.ai/pricing 06.08"),
"glm-4.7": ("https://api.z.ai/api/paas/v4", "ZAI_API_KEY",
0.60, 0.11, 2.20, "extra_body_disable", "docs.z.ai/pricing 06.08"),
# xAI — docs.x.ai/docs/models, снято 06.08. Тариф <200k токенов (наши единицы много меньше).
"grok-4.3": ("https://api.x.ai/v1", "XAI_API_KEY",
1.25, 0.20, 2.50, "effort_none", "docs.x.ai/models 06.08"),
"grok-4.5": ("https://api.x.ai/v1", "XAI_API_KEY",
2.00, 0.30, 6.00, "effort_none", "docs.x.ai/models 06.08 (НОВЫЙ жилец)"),
# OpenAI — прайс сверен живьём 05.08 (эксп-20 §1.3), изменений против 04.08 не было.
"gpt-5.6-luna": ("https://api.openai.com/v1", "OPENAI_API_KEY",
0.20, 0.02, 1.20, "effort_none", "эксп-20 §1.3 (05.08, вендор-прайс)"),
"gpt-5.6-terra": ("https://api.openai.com/v1", "OPENAI_API_KEY",
2.00, 0.20, 12.00, "effort_none", "эксп-20 §1.3 (05.08, вендор-прайс)"),
# Gemini — ai.google.dev/gemini-api/docs/pricing, снято 06.08. Кэш-цена не выделена.
"gemini-3.6-flash": ("https://generativelanguage.googleapis.com/v1beta/openai",
"GEMINI_API_KEY", 1.50, 1.50, 7.50, "thinking_budget",
"ai.google.dev/pricing 06.08 (НОВЫЙ жилец)"),
"gemini-3.1-flash-lite": ("https://generativelanguage.googleapis.com/v1beta/openai",
"GEMINI_API_KEY", 0.25, 0.25, 1.50, "thinking_budget",
"ai.google.dev/pricing 06.08"),
"gemini-3.1-pro-preview": ("https://generativelanguage.googleapis.com/v1beta/openai",
"GEMINI_API_KEY", 2.00, 2.00, 12.00, "mandatory",
"ai.google.dev/pricing 06.08"),
# Kimi — platform.kimi.ai/docs/pricing/chat-k3, снято 06.08. Самый дорогой выход ростера.
"kimi-k3": ("https://api.moonshot.ai/v1", "KIMI_API_KEY",
3.00, 0.30, 15.00, None, "platform.kimi.ai/pricing 06.08 (НОВЫЙ жилец)"),
# Mistral — mistral.ai/pricing, снято 06.08. Кэш-тариф не опубликован ⇒ равен входу.
"mistral-large-latest": ("https://api.mistral.ai/v1", "MISTRAL_API_KEY",
2.00, 2.00, 6.00, None, "mistral.ai/pricing 06.08"),
}
# Учёт РАЗМЫШЛЕНИЯ в биллинге различается по провайдерам, и это не деталь, а деньги.
# subset — размышление ВНУТРИ completion_tokens (DeepSeek, OpenAI, Z.AI): считать нечего.
# additive — размышление СВЕРХ completion (xAI): billed = completion + reasoning.
# Источник: quirks 00, строка grok — «reasoning у xAI аддитивен к completion (billed =
# completion+reasoning)». ⚠ Поймано исполнением: скрин показал у grok-4.5 reasoning 3605 при
# completion 1737, то есть размышление БОЛЬШЕ ответа — при subset-формуле это невозможно, и
# первая редакция занижала цену grok примерно втрое.
# additive_total — размышление видно ТОЛЬКО в total_tokens (Gemini): OpenAI-совместимый слой
# Google отдаёт reasoning_tokens=0 и НЕ кладёт thinking в completion, поэтому оплаченный
# выход = total prompt. Источник: quirks 00 §D22.3 — «иначе недоучёт 146×/вызов».
# ⚠ Класс добавлен 07.08 по адверсариальному ревью: прошлая редакция знала только subset и
# additive, gemini падал в subset, и его размышление не оплачивалось в леджере вовсе. Величину
# недоучёта прошлого прогона восстановить НЕЛЬЗЯ: total_tokens тогда не персистился.
REASONING_BILLING = {"grok-4.3": "additive", "grok-4.5": "additive",
"gemini-3.6-flash": "additive_total",
"gemini-3.1-flash-lite": "additive_total",
"gemini-3.1-pro-preview": "additive_total"}
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":
# Пол — обычная формула: если total не записан (старый артефакт), не занижаем молча,
# но и не выдумываем. Отсутствие total_tokens видно по равенству двух путей.
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 out_price_order() -> list[str]:
"""Кандидаты по цене ВЫХОДА — она доминирует в наших ролях (размышление биллится как выход)."""
return sorted(CANDIDATES, key=lambda m: CANDIDATES[m][4])
if __name__ == "__main__":
print(f"{'модель':26s}{'вход':>8s}{'кэш':>8s}{'выход':>8s} источник")
print("-" * 86)
for m in out_price_order():
_, _, pin, pc, pout, _, src = CANDIDATES[m]
print(f"{m:26s}{pin:8.3f}{pc:8.3f}{pout:8.2f} {src}")