hermes-hub/tests/test_grok_quota.py
Hermes Team 0df7dba2ce feat(grok): подписка вместо кредитов — квота и модели заработали
Владелец был прав: «апи не нужен». Подтверждено на его аккаунте GROK PRO —
api.x.ai/v1/chat/completions вернул 200 и ответ модели grok-4.3 БЕЗ
покупки кредитов. Прежний 402 был целиком из-за того, что подключён был
другой аккаунт, без подписки. Моё утверждение про «разные кошельки»
окончательно снято.

1. Квота показывала «Н/Д» при живой подписке. Читались только
   prepaidBalance и onDemandCap — у подписчика оба нулевые. А расход
   подписки лежит в creditUsagePercent, и разбивка по продуктам в
   productUsage. Теперь оттуда и берётся: на живом аккаунте выходит
   14% за неделю, GrokChat 13%, GrokBuild 1% — ровно то, что владелец
   видит на grok.com. Корзина кредитов создаётся только когда они реально
   заведены: нули у подписчика — норма, а не повод рисовать пустое.

2. Выбор модели отвергал настоящие имена: «кэш моделей для grok пуст, а
   модель grok-4.5 не найдена». Причина — в _probe_provider грока не было
   вовсе, обнаружение знало только antigravity, codex, opencode и local.
   При этом api.x.ai/v1/models принимает тот же OAuth-токен и отдаёт 12
   моделей. Зонд добавлен; grok-4.5 теперь принимается, выдуманная
   grok-99-turbo — отклоняется.

Тесты: 428 passed, ruff чисто.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 20:29:08 +07:00

114 lines
5.5 KiB
Python
Raw Permalink 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.

"""Квота Grok читается у провайдера, а не показывается как «Н/Д».
Владелец спросил: «где закончились лимиты?» — и показал экран, где
еженедельный лимит SuperGrok израсходован на 14%. Но вызовы через Hub
падали с 402.
Разгадка: это ДВА РАЗНЫХ кошелька. Подписка SuperGrok покрывает чат
grok.com, а программный доступ списывается с кредитов API. Их у аккаунта
ноль — подтверждено биллинговым эндпоинтом.
Раньше _collect_grok_quota строила пустые корзины и никуда не ходила,
поэтому Hub показывал «Н/Д» и объяснить ничего не мог.
"""
from __future__ import annotations
import json
from unittest.mock import MagicMock, patch
from antigravity_provider.router.quota_collector import AccountQuotaService
def _billing(prepaid: float, cap: float, used: float, credit_pct=None, products=None) -> MagicMock:
config = {
"currentPeriod": {"start": "2026-08-19T00:00:00+00:00", "end": "2026-08-26T00:00:00+00:00"},
"prepaidBalance": {"val": prepaid},
"onDemandCap": {"val": cap},
"onDemandUsed": {"val": used},
}
if credit_pct is not None:
config["creditUsagePercent"] = credit_pct
if products:
config["productUsage"] = products
payload = {"config": config}
resp = MagicMock()
resp.__enter__.return_value.read.return_value = json.dumps(payload).encode("utf-8")
return resp
def _auth() -> dict:
return {"token": {"access_token": "tok"}}
def test_account_without_subscription_or_credits_says_so():
"""Ни подписки, ни кредитов — честная причина, а не пустые корзины.
Прежняя версия рисовала две корзины с нулями и утверждала, что подписка
SuperGrok «не пополняет кредиты». Это оказалось неверно: проверка на
живом аккаунте GROK PRO показала, что подписка даёт доступ и вызовы
проходят без покупки кредитов.
"""
with patch("antigravity_provider.router.quota_collector.urllib.request.urlopen",
return_value=_billing(0, 0, 0)):
snap = AccountQuotaService()._collect_grok_quota("grok-orch", _auth())
assert snap.source == "provider_api"
assert snap.buckets == []
assert "не вернул" in (snap.unavailable_reason or "")
def test_zero_cap_does_not_fabricate_percent():
"""При нулевом лимите корзина кредитов не создаётся вовсе.
Доля от нуля не определена, и показывать «0%» было бы выдумкой.
"""
with patch("antigravity_provider.router.quota_collector.urllib.request.urlopen",
return_value=_billing(0, 0, 0, credit_pct=5.0)):
snap = AccountQuotaService()._collect_grok_quota("grok-orch", _auth())
assert not any(b.id == "grok.on_demand" for b in snap.buckets)
def test_account_with_credits_reports_real_percent():
"""Когда лимит есть, доля считается по измеренным значениям."""
with patch("antigravity_provider.router.quota_collector.urllib.request.urlopen",
return_value=_billing(50, 200, 50)):
snap = AccountQuotaService()._collect_grok_quota("grok-orch", _auth())
on_demand = next(b for b in snap.buckets if b.id == "grok.on_demand")
assert on_demand.used_percent == 25.0
assert on_demand.remaining_percent == 75.0
assert snap.unavailable_reason is None
def test_subscription_usage_is_reported():
"""Расход подписки — то самое число, что владелец видит на grok.com.
Раньше читались только баланс и лимит трат, у подписчика оба нулевые,
и квота показывалась как «Н/Д» при живой подписке.
"""
products = [
{"product": "GrokChat", "usagePercent": 13.0},
{"product": "GrokBuild", "usagePercent": 1.0},
]
with patch("antigravity_provider.router.quota_collector.urllib.request.urlopen",
return_value=_billing(0, 0, 0, credit_pct=14.0, products=products)):
snap = AccountQuotaService()._collect_grok_quota("grok-orch", _auth())
weekly = next(b for b in snap.buckets if b.id == "grok.subscription.weekly")
assert weekly.used_percent == 14.0
assert weekly.remaining_percent == 86.0
assert snap.unavailable_reason is None
names = {b.display_name for b in snap.buckets}
assert {"GrokChat", "GrokBuild"} <= names, "разбивка по продуктам потеряна"
def test_zero_credits_not_shown_for_subscriber():
"""Нулевые кредиты у подписчика — норма, пустых корзин рисовать не надо."""
with patch("antigravity_provider.router.quota_collector.urllib.request.urlopen",
return_value=_billing(0, 0, 0, credit_pct=14.0)):
snap = AccountQuotaService()._collect_grok_quota("grok-orch", _auth())
assert not any(b.id == "grok.on_demand" for b in snap.buckets)