# dsh-chat-cost

Стоимость токенов каждого чата в веб-интерфейсе [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh): сам чат, его субагенты и всё дерево сессий, по ценам из встроенного каталога нескольких провайдеров, с дописываемым JSONL-логом расхода в папке проекта.

[English](README.md) | [中文](README.zh.md) | Русский

[![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![DeepSeek Harness plugin](https://img.shields.io/badge/DeepSeek%20Harness-plugin-blueviolet)](#установка)
[![npm](https://img.shields.io/npm/v/dsh-chat-cost.svg)](https://www.npmjs.com/package/dsh-chat-cost)
[![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
[![providers](https://img.shields.io/badge/providers-7%20%C2%B7%20139%20models-informational)](#правила-расчёта)

![The readout in the composer: one line under the harness stats, the session priced from the cost log](https://raw.githubusercontent.com/igormel81/dsh-chat-cost/main/docs/readout.png)

![The price of one answer, under that answer: this turn's own spend, not a share of the session total](https://raw.githubusercontent.com/igormel81/dsh-chat-cost/main/docs/answer.png)

**Ключевые слова:** плагин DeepSeek Harness, плагин dsh, dsh-plugin, стоимость токенов, цена чата, расход субагентов, стоимость дерева сессий, учёт расходов LLM, расход токенов, лог расходов, JSONL, local-first, DeepSeek V4.1 Flash, DeepSeek V4 Pro, OpenAI GPT-5, Anthropic Claude, Google Gemini, Kimi K3 (Moonshot), xAI Grok, Mistral, цена чтения и записи кэша, пиковые и непиковые тарифы, плагин Cordis.

## Состояние

Работает целиком, и проверено на живом хосте, а не только тестами: встроенный каталог семи провайдеров, движок расчёта (пиковые тарифы, правила чтения и записи кэша), серверная половина, которая обходит дерево сессий, считает каждую сессию по интервалам и дописывает JSONL-лог расхода, расчёт стоимости по ходам из событий самой сессии, шесть инструментов бюджета (`cost_price`, `cost_history`, `cost_estimate`, `cost_plan`, `cost_mark`, `cost_scenarios`) и клиентский вывод на английском, китайском и русском — в композере для всего чата и под каждым завершённым ответом для этого ответа.

Известные ограничения — названы, а не спрятаны: расход отдают только живые сессии, поэтому субагентская сессия, оставшаяся лишь в хранилище, попадает в список без цены; у чата, известного только по логу, нет границ ходов, поэтому он показывает итог и не показывает отметки под ответами; опубликованные тарифы за длинный контекст у части моделей Gemini и Grok пока не применяются.

## Настройка

```yaml
- id: dsh-chat-cost
  name: dsh-chat-cost
  config:
    writeLog: true        # дописывать JSONL-лог расхода в папку проекта
    logDir: .dsh-cost      # имя каталога внутри папки проекта
    language: en          # en | zh | ru; не указывать — тогда локаль харнесса, затем браузер
    ledgerTailBytes: 2097152  # сколько байт лога читается обратно на сводку (2 МиБ)
```

## Возможности

- **Цены нескольких провайдеров.** Встроенный снимок каталога [models.dev](https://models.dev) покрывает семь провайдеров — DeepSeek, Moonshot, OpenAI, Anthropic, Google, xAI, Mistral — и все их модели с ценами (сегодня 139 моделей).
- **Честная арифметика.** DeepSeek считается по собственной официальной таблице с пиковыми и непиковыми часами; запись в кэш тарифицируется по цене записи там, где провайдер её публикует (Anthropic, OpenAI), иначе по цене входа; модель без цены показывает `—`, а не выдуманное число.
- **Разбивка по чату, субагентам и дереву сессий.** Подсказка при наведении отделяет этот чат от субагентов и показывает итог по дереву, разбивку токенов и задействованные модели.
- **Лог расхода в папке проекта.** `<проект>/.dsh-cost/cost.jsonl` — по одному объекту JSON на сессию за проход, с положением в дереве, четырьмя корзинами токенов, кумулятивной и дельта-стоимостью и источником цены. Внутри git-дерева при первой записи `.dsh-cost/` добавляется в `.gitignore` проекта; обычная папка не трогается — лог там уместен, а мусор нет.
- **Цены по интервалам.** Основа — JSONL-лог: каждый проход оценивает только токены, пришедшие с момента предыдущей записи, по тарифу, действующему в этот момент. Чат, пересекающий пиковую границу DeepSeek, сохраняет ту цену, по которой он реально тарифицировался, и не пересчитывается задним числом.
- **Ограниченное чтение с кэшем.** Сводка читает только хвост лога (`ledgerTailBytes`, по умолчанию 2 МиБ) и переиспользует кэш, пока размер и mtime файла не изменились; если хвост начинается с середины файла, плагин сообщает `truncated` и `skippedBytes`, а не делает вид, что знает всю историю.
- **Ничего не теряется молча.** Расход, не отнесённый ни к одному пункту плана, называется в подсказке (`$1.230 не отнесено ни к одному пункту плана`), а не растворяется в итоге.
- **Цена каждого ответа — под ответом.** У завершённого хода своя стоимость прямо в переписке: она считается по событиям этого хода, а не вырезается из общего итога, и обновляется в момент завершения хода. Виджет в композере делит один опрос с этими отметками, поэтому завершённый ответ обновляет и то и другое сразу.
- **Каждый чат, открытый или нет.** У неоткрытого чата нет живой сессии, поэтому виджет считает его по cost-логу и помечает цифру как записанную; чат, которого в логе ещё нет, показывает `—` с объяснением причины вместо того, чтобы исчезнуть. Стоит его открыть — он становится живым, считается точно, и недостающая запись дописывается.
- **Три языка.** Английский, китайский и русский: выбор идёт от настроек плагина к локали харнесса, затем к языку браузера.

## Установка

```sh
dsh plugin --profile web add dsh-chat-cost
```

Пакет является бандлом DSH: профиль сам подхватывает его слой патча (зависимость с объявленным `dsh.bundle` попадает в `dsh.profile.bundles`), править YAML руками не нужно. После установки перезапусти хост.

## Правила расчёта

| Правило | Поведение |
| --- | --- |
| DeepSeek | официальная таблица; пиковые часы 01:00–04:00 и 06:00–10:00 UTC по будням удваивают цену; у записи в кэш отдельного тарифа нет, считается по цене входа |
| Прочие провайдеры | встроенный снимок models.dev; цены чтения и записи кэша берутся как опубликованы |
| Чтение кэша без опубликованной цены | фолбэк на цену входа — это верхняя граница |
| Неизвестная модель | цены нет; в виджете `—`, в подсказке названа модель |
| Отнесение | основа — лог: записанный интервал сохраняет свою цену, оцениваются только новые токены |

В `data/prices.json` записано, откуда снимок взят (`source`) и когда сделан (`generatedAt`), поэтому возраст цены можно проверить, а не предполагать. Обновление: `npm run prices` (семь отобранных провайдеров) или `npm run prices:all` (все провайдеры каталога).

### Как считается число

Сводка строится от лога: записанные дельты суммируются из файла, а по текущему тарифу оцениваются только токены, пришедшие после последней записи. Поэтому чат, пересекающий пиковую границу, показывает реальную стоимость, а не задним числом удвоенную.

## Формат лога расхода

```json
{"ts":"2026-09-12T20:00:00.000Z","plugin":"dsh-chat-cost@0.6.8","rootSessionId":"root-1","sessionId":"child-1","parentSessionId":"root-1","depth":1,"kind":"subagent","provider":"moonshot","model":"kimi-k3","pricingSource":"catalog","tier":"flat","tokens":{"uncachedInput":5000,"cacheRead":0,"cacheWrite":0,"output":1000},"totalTokens":6000,"deltaTokens":{"uncachedInput":1000,"cacheRead":0,"cacheWrite":0,"output":200},"deltaTotalTokens":1200,"cumulativeUsd":0.014,"deltaUsd":0.002}
```

## Что и куда пишется

Всё живёт в папке проекта, рядом с той работой, которую описывает.

| Путь | Кто пишет | Что внутри |
| --- | --- | --- |
| `<проект>/.dsh-cost/cost.jsonl` | каждый проход | по объекту JSON на сессию за интервал: положение в дереве, четыре корзины токенов, дельта и кумулятив стоимости, тариф, по которому списано |
| `<проект>/.dsh-cost/plan.json` | `cost_plan`, `cost_scenarios` | посчитанный план работ: пункты, выбранные маршруты, что не влезло, сравнение маршрутов |
| `<проект>/.dsh-cost/budget.json` | `cost_plan {action: budget}` | денежный лимит и заметка к нему |
| `<проект>/.dsh-cost/plan.md` | `cost_plan` | тот же план документом для человека, на языке виджета |

Лог только дописывается: запись никогда не перезаписывается — именно это и делает возможным расчёт по интервалам. Удалите папку, и плагин начнёт с нуля: больше он нигде ничего не хранит.

## Приватность

Ни аккаунта, ни телеметрии, ни сервера. Во время работы плагин вообще не делает исходящих запросов: цены берутся из встроенного снимка, виджет обращается только к локальному маршруту хоста, а лог пишется в вашу же папку проекта. Единственная команда, выходящая в сеть, — `npm run prices`: она пересобирает каталог из models.dev и предназначена для мейнтейнера перед выпуском, а не для пользователя.

## Выпуск версии

```sh
npm test            # 142 тестов; проверки схем требуют профиля DSH с валидатором
npm run prices      # обновить встроенный каталог цен перед выпуском
npm version minor
npm publish --access public
for f in README.md README.zh.md README.ru.md; do echo "$f: $(git hash-object $f)"; done
```

### Проверка текстов (необязательно, для мейнтейнера)

Три README пишет ассистент, поэтому возможны две поломки, которых не видит ни один юнит-тест: невидимые символы и следы вставки из чата, а также факты, которые незаметно меняются при переписывании абзаца. Обе закрывает [`humanizer-ru`](https://github.com/Vladimir-Human/humanizer-ru) с опубликованными замерами ложных срабатываний, а `scripts/prose-check.mjs` его оборачивает:

```sh
uv tool install humanizer-ru
npm run prose            # артефакты (жёсткий гейт), сверка фактов с прошлым выпуском, мягкие признаки стиля
npm run prose -- --strict  # плюс падать, если инструмента нет, — для CI
```

Артефакты класса A валят прогон; маркеры класса B (невидимые символы и экзотические пробелы, доля ложных срабатываний у них по замерам инструмента мала, но не нулевая) печатаются для просмотра и никогда не валят проверку. Потеря термина из `scripts/prose-terms.txt` (имя пакета, путь лога, имена инструментов, два слота оболочки) — тоже; все остальные потерянные числа и цитаты печатаются, чтобы их оценил человек, потому что смена версии законно меняет числа. Мягкие признаки стиля только печатаются и никогда не валят сборку — сам инструмент отказывается считать их доказательством, и счётчик не должен решать за прозу. В `npm test` этого нет: набор должен запускаться у любого, кто поставил пакет, а Python-инструмент — не зависимость Node-плагина.

Релиз можно провести и через workflow `publish` (Actions → publish → Run workflow): он прогоняет тесты, отказывается публиковать версию, которая уже есть в реестре, и публикует с `--provenance` через доверенную публикацию npm — токен не нужен. Разовая настройка доверенного издателя описана в начале файла workflow.

Пакет является бандлом DSH: `dsh plugin --profile web add dsh-chat-cost` его устанавливает, а слой патча профиль подхватывает сам — для тех, у кого плагин уже стоит, выпуск меняет только версию. Поднимай `PLUGIN_VERSION` в `lib/index.js` вместе с версией пакета: он проставляется в каждой записи cost-лога, и по нему видно, каким релизом запись сделана. После правки любого README перезапиши хеши в `README.i18n.yaml` — до этого тест падает, пока три языка снова не сойдутся.

## Тесты

```sh
npm test
```

Покрывают движок цен (сопоставление маршрутов, канонизация идентификаторов, пиковые окна, правила кэша, неизвестные модели), свёртку по ходам (стримовый сэмпл заменяется финальным, шаги внутри хода суммируются, модель относится к тому ходу, который её использовал), чтение токенов для всех форм, которые отдаёт харнесс (плоская, обёртка `totals` из проекции, один шаг, конверт кэша), дисциплину контекста обеих половин (Cordis бросает исключение на любое свойство, которое плагин не объявил в inject, — это однажды уронило хост, а однажды веб-оболочку, поэтому исходники проверяются механически), записи лога и запись в настоящие файлы во временном каталоге, путь активации плагина (`apply` на стабе хоста: маршрут, шесть инструментов, запись по завершении хода, освобождение), одновременные записи и собранный клиентский бандл, отрендеренный с хранилищем хуков, как у React: проверяется, что все три языка объявляют одинаковый набор ключей, что каждая строка рендерится со своими аргументами и что выбор языка идёт по порядку настройки → локаль харнесса → браузер.

Две проверки выходят за пределы обычного клона: схемы инструментов прогоняются через валидатор самого харнесса, а набор загрузки поднимает плагин на настоящем Cordis, который использует харнесс (там чтение настроек из контекста вместо аргумента загрузчика бросает исключение). Обеим нужны пакеты, резолвящиеся только внутри профиля DSH, поэтому вне профиля они помечаются пропущенными, а не проходят молча. Третья проверка — вообще не набор: опубликованный тарбол проверяется прогоном того же набора внутри распакованного пакета. Полный прогон:

```sh
dsh plugin --profile web add link:$PWD
npm pack && tar xzf dsh-chat-cost-*.tgz -C ~/.dsh/profiles/web/pack-check \
  && (cd ~/.dsh/profiles/web/pack-check/package && npm test)
```

## Планирование под бюджет

Плагин превращает лог расхода в ограничение: план составляет модель, а плагин считает цену, укладывает план в денежный лимит и записывает, что реально потрачено.

| Инструмент | Назначение |
| --- | --- |
| `cost_price` | цены и тарифы кэша по провайдеру или модели — чтобы маршрут выбирался осознанно |
| `cost_history` | фактический расход по помеченным пунктам и моделям с разбросом P50/P90 — источник калибровки |
| `cost_estimate` | считает стоимость списка работ, укладывает в бюджет и говорит, что не влезло |
| `cost_plan` | пишет и читает `<проект>/.dsh-cost/plan.json` (плюс сгенерированный `plan.md`) и задаёт денежный лимит |
| `cost_mark` | открывает пункт плана: дальнейший расход привязывается к плану, а не угадывается по времени |
| `cost_scenarios` | сравнивает варианты маршрутизации — `quality`, `connected`, `economy`, `balanced` — и называет, какие модели стоит подключить под этот проект |

```
cost_plan   {action: write, budgetUsd: 20, units: [...]}   -> plan.json + plan.md, 20% отложено на переделки
cost_mark   {label: research}                              -> расход отсюда идёт в пункт research
cost_history {}                                            -> research: факт $1.84 против плана $2.10
cost_plan   {action: budget, budgetUsd: 25}                -> виджет покажет «≈ $0.42 / $25.00»
```

Два правила делают оценки пригодными, а не декоративными. Оценки — **диапазоны**: пункт, посчитанный по объявленным токенам, точен, всё остальное имеет P50 и P90 (по умолчанию вдвое больше ожидаемого объёма работ), и инструмент говорит, откуда взято число — из `declared` токенов, из `history` или из `bootstrap`-профиля. И бюджет держит **резерв 20%** на переделки: план без буфера — это ложь. Модель без цены даёт `—`, ничего не выдумывается.

Файлы плана и бюджета читаются-изменяются-записываются, поэтому каждая правка идёт через одну очередь на папку проекта: два одновременных вызова модели не потеряют работу друг друга. Обновление плана сохраняет записанное сравнение маршрутов, пока входы плана не изменились, и отбрасывает его с пометкой, когда они сдвинулись, — сравнение, посчитанное для других пунктов, это ложь. `plan.md` пишется на том языке, который показывает виджет.

### Какие модели стоит подключить

`cost_scenarios` считает один и тот же план в четырёх вариантах: **quality** (предпочтительный маршрут для каждого пункта), **connected** (самый дешёвый из уже перечисленных), **economy** (самая дешёвая подходящая модель всего каталога — возможно, это потребует подключить провайдера, которым ты не пользуешься) и **balanced** (предпочтительный маршрут для пунктов с пометкой `critical`, остальное — economy). Каждый вариант показывает ожидаемую и худшую сумму, укладывается ли он в бюджет и какие провайдеры ему нужны.

Дальше рекомендация называет источники расхода, сортирует по пунктам, сколько даст переключение, и перечисляет три самых дешёвых подходящих варианта с размером контекста и датой выхода. Пригодность определяется фактами каталога — reasoning, вызов инструментов, зрение, пределы контекста и вывода — и никогда оценкой качества, потому что в каталоге её нет. Бесплатные тарифы по умолчанию пропускаются, а дешёвая модель с заметно меньшим контекстом, чем у предпочтительного маршрута, помечается как более узкая, а не рекомендуется молча.

## Обновление

Плагин — зависимость профиля, поэтому обновление это операция pnpm в каталоге профиля, и одна деталь решает, сработает ли оно:

```sh
dsh plugin --profile web list                       # что установлено прямо сейчас
dsh plugin --profile web add dsh-chat-cost@latest   # обычный путь
dsh plugin --profile web update dsh-chat-cost       # только внутри объявленного диапазона
```

| Команда | Что реально происходит |
| --- | --- |
| `add dsh-chat-cost@<версия>` | ставит ровно эту версию, сразу — надёжный путь, когда релизу несколько минут |
| `add dsh-chat-cost@latest` | разрешает тег `latest`; версию, опубликованную минуты назад, может придержать политика цепочки поставок pnpm — тогда профиль либо запишет исключение в `pnpm-workspace.yaml`, либо поставится предыдущая версия |
| `update dsh-chat-cost` | двигает только внутри диапазона, уже объявленного в `package.json`; при точном закреплении не делает ничего |
| `add dsh-chat-cost` (без диапазона) | разрешает заново и ставит `latest`; так же плагин возвращается, если профиль его потерял |

После этого перезапустите хост: композиция профиля собирается при загрузке, поэтому работающий хост остаётся на той версии, с которой стартовал. Узнать, какой релиз работает, можно и без терминала — наведите на виджет, подсказка называет версию, и каждая запись лога несёт её в поле `plugin` (`"plugin":"dsh-chat-cost@0.6.8"`), поэтому запись можно проследить до выпустившего её релиза.

## Удаление и восстановление

```sh
dsh plugin --profile web remove dsh-chat-cost
```

После этого перезапустите хост. Удаление строки останавливает всё: виджет, отметки под ответами, инструменты, запись лога. То, что уже записано (лог, план, бюджет), — ваше и остаётся на месте.

Если плагин не даёт хосту запуститься, загрузка называет его (`plugin tree failed to load: …`), и лечится это той же командой с `remove`: композиция собирается при загрузке, поэтому выгрузить плагин из уже сломанного хоста нельзя.

## Совместимость

| Что нужно | Зачем |
| --- | --- |
| Профиль DSH | пакет — это бандл: `dsh.bundle.patch` указывает на `cordis.patch.yml`, а профиль сам подхватывает его в `dsh.profile.bundles` |
| Node ≥ 20 | объявлено в `engines`; плагин использует ESM и `AbortSignal.timeout` |
| Контракт загрузчика `apply(ctx, config)` | настройки приходят вторым аргументом; чтение их из `ctx` в этом Cordis бросает исключение — однажды это уронило хост при загрузке |
| Слоты веб-оболочки `conversation.composer.dock` и `conversation.chat.turnTail` | виджет в композере и отметки под ответами; без них хост-половина всё равно считает, пишет лог и отвечает на своём маршруте |
| npm CLI ≥ 11.5.1 | только чтобы выпускать сам пакет через доверенную публикацию npm, а не чтобы им пользоваться |

## Ограничения

- Токены рассуждений провайдеры тарифицируют как вывод — здесь они тоже считаются выводом.
- Опубликованные тарифы за длинный контекст у части моделей Gemini и Grok пока не применяются: берётся базовый тариф.
- Чат, которого нет в логе и который не открыт, показывает `—`: плагин считает то, что может доказать (живая сессия или лог), и говорит об этом, а не оценивает по проекции, которую не может прочитать.
- Живая сводка считает только хвост лога (`ledgerTailBytes`, по умолчанию 2 МиБ); более старый расход остаётся в файле, а сводка сообщает об усечении.
- Отметки под ответами есть для тех ходов, которые хост может посчитать. У чата, взятого из лога, есть итог и нет границ ходов, поэтому он не показывает отметки — вместо выдуманных.
- Очередь упорядочивает только собственные записи плагина. Правка `plan.json` человеком в момент записи моделью всё ещё может потеряться: файл небольшой и предназначен для просмотра, а не для одновременного редактирования.

## Лицензия

MIT
