Metadata-Version: 2.4
Name: s-modelkit
Version: 0.0.3
Summary: Каталог моделей провайдера как ДАННЫЕ: живой резолв, кеш на диске с TTL, доступность по тиру подписки, лимиты per-model. Отказ «модель не по тарифу» — до сети и словами сервера.
Author: Dmitry
License: MIT
License-File: LICENSE
Keywords: catalog,entitlement,llm,models,quota,subscription-tier
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.11
Requires-Dist: s-corekit>=0.0.2
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.8; extra == 'dev'
Description-Content-Type: text/markdown

# s-modelkit — каталог моделей провайдера как ДАННЫЕ

Ни один клиент не хранит список моделей константой в коде: каталог **резолвится
живьём**, кешируется на диске, знает про **тир подписки** и **лимиты**, а отказ
«модель не по твоему тарифу» приходит **до сети** и словами сервиса.

```
потребители (навыки чатов, gateway, accountpoolkit)
                |
             modelkit          ← знание о моделях: состав, доступность, лимиты
                |
             corekit           ← основание (LimitSpec / QuotaScope / AccessVerdict)
```

## Зачем

Реальный замер (Grok, 2026-08-07). В коде навыка жила таблица:

```python
MODEL_MODES = {"grok-3": …, "grok-4": …}   # имён grok-3/grok-4 у сервиса НЕТ
```

А сервис на своём каталоге отвечает так:

```json
{"modes":[{"id":"fast","availability":{"available":{}}},
          {"id":"expert","availability":{"requiresUpgrade":{"minimumSubscriptionTier":"TIER_SUPERGROK_LITE"}}}],
 "defaultModeId":"fast"}
```

Зашитая таблица врала в обе стороны: придумывала несуществующие имена и «знала»
доступность, которую сервис сообщает открытым текстом. Запрос `expert` уходил в
сеть и возвращался кадром `stream_error: entitlement`, который снаружи выглядит
как «пустой ответ».

## Как пользоваться

```python
from modelkit import ModelSpec, catalog_of, choose, load_catalog, save_catalog, is_fresh, touch

DECLARATION = catalog_of("grok", [ModelSpec(code="fast", aliases=("grok-3",))])

catalog = load_catalog(DECLARATION, path)          # декларация + снимок + научённое
if not is_fresh(catalog):                          # ленивая сеть: только когда надо
    catalog = touch(catalog, models=fetch_live())  # fetch_live() — забота вызывающего
    save_catalog(catalog, path)

pick = choose(catalog, "expert", tier_id="free")
if not pick.ok:
    raise SystemExit(pick.message)
    # grok: модель `expert` требует тариф `TIER_SUPERGROK_LITE` (тир аккаунта: `free`).
    # Доступны: fast.
send(model=pick.request_key)                        # на провод — ключ, не отображаемое имя
```

## Три решения, ради которых кит существует

**1. Доступность — три состояния, не флаг.** `AVAILABLE` / `REQUIRES_TIER` /
`UNKNOWN`. Схлопнуть «не проверяли» в «недоступна» значит однажды спрятать от
человека рабочую модель. Та же логика, что у `alive` в реестре адресов.

**2. `request_key` отдельно от `code`.** Отображаемое имя и то, что уходит на
провод, — разные вещи; их смешение у google-flow давало 400/404.

**3. Декларация, живой ответ и научённое не сливаются.** Состав берётся из живого
ответа либо из декларации; с диска накладывается только доступность и отметки
времени. Иначе вчерашний кеш начнёт затенять каталог, и модель, добавленная
сервисом сегодня, не появится у пользователя никогда.

## Что кит НЕ делает

Не ходит в сеть и не знает про конкретного провайдера: каталог добывает
вызывающий (только он умеет говорить со своим сервисом), кит принимает уже
разобранный результат. Не зависит от `accountpoolkit` — каталог нужен ему самому,
и обратная зависимость дала бы цикл; поэтому тир приезжает строкой `tier_id`.

## Лицензия

MIT.
