@goodandready/dsh-cost-meter 0.7.3 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -131
- package/docs/deployment/public-release.md +1 -1
- package/docs/design/DESIGN.md +25 -12
- package/docs/testing/projection-lifecycle.md +28 -0
- package/lib/client.js +299 -7
- package/lib/index.js +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,132 +1,78 @@
|
|
|
1
|
-
# dsh-cost-meter
|
|
2
|
-
|
|
3
|
-
Чип стоимости сессии в шапке диалога DeepSeek Harness: живой расход по токенам,
|
|
4
|
-
текущий тариф (пик / не-пик)
|
|
5
|
-
|
|
6
|
-
```
|
|
7
|
-
● ≈ ₽8.55 0:17 ← чип в conversation.session.header.utilities
|
|
8
|
-
```
|
|
9
|
-
|
|
10
|
-
По клику раскрывается панель: тарифная таблица за 1M токенов в двух колонках
|
|
11
|
-
(не-пик / пик, активная выделена), окна пика в UTC и итог за
|
|
12
|
-
|
|
13
|
-
##
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
**Тариф** вычисляется на клиенте по часам UTC: активная ставка = подходящее
|
|
53
|
-
окно, иначе базовая. Если окон два — таблица показывает обе колонки, активная
|
|
54
|
-
подсвечена, рядом обратный отсчёт до смены. Часы выводятся в зоне
|
|
55
|
-
`displayTimeZone` (по умолчанию московской) через `Intl`, так что переход на
|
|
56
|
-
летнее время учитывается. Если окон нет — одна колонка «ставка» без подсветки.
|
|
57
|
-
|
|
58
|
-
## Установка из приватного registry
|
|
59
|
-
|
|
60
|
-
```bash
|
|
1
|
+
# dsh-cost-meter
|
|
2
|
+
|
|
3
|
+
Чип стоимости сессии в шапке диалога DeepSeek Harness: живой расход по токенам,
|
|
4
|
+
текущий тариф (пик / не-пик), обратный отсчёт до его смены и карточка настроек.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
● ≈ ₽8.55 0:17 ← чип в conversation.session.header.utilities
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
По клику раскрывается панель: тарифная таблица за 1M токенов в двух колонках
|
|
11
|
+
(не-пик / пик, активная выделена), окна пика в UTC и итог за сессию с разбивкой по моделям.
|
|
12
|
+
|
|
13
|
+
## Возможности
|
|
14
|
+
|
|
15
|
+
- **Живой подсчёт расхода**: инкрементальный расчёт токенов и стоимости прямо во время стриминга ответа.
|
|
16
|
+
- **Двухуровневые тарифы**: поддержка официальной сетки DeepSeek (пик / не-пик) и скидочных окон OpenRouter.
|
|
17
|
+
- **Provider-aware маршрутизация**: раздельная тарификация для прямых моделей DeepSeek и хостинга моделей через OpenRouter.
|
|
18
|
+
- **Учёт по получасовым слотам UTC**: исторические траты фиксируются по ставке слота и не пересчитываются задним числом.
|
|
19
|
+
- **Карточка настроек в UI**: нативная карточка в `settings.plugin.item` для настройки валюты, курса USD и таймзоны.
|
|
20
|
+
- **Полная двуязычная локализация**: интерфейс чипа, панели и настроек на русском и английском языках (`ctx.locale`).
|
|
21
|
+
|
|
22
|
+
## Откуда берутся цифры
|
|
23
|
+
|
|
24
|
+
**Токены** — из собственной проекции сессии `costByModel`, которую плагин
|
|
25
|
+
регистрирует тем же механизмом, что и штатный `@deepseek-ai/dsh-token-meter`.
|
|
26
|
+
В журнале есть всё нужное: `request/header` несёт `config: {provider, model}`,
|
|
27
|
+
а usage приходит следом за тем же шагом, поэтому каждая выборка относится к
|
|
28
|
+
модели, которая её заработала.
|
|
29
|
+
|
|
30
|
+
Накопление идёт не одной кучей, а по **получасовым слотам суток UTC**: расход
|
|
31
|
+
фиксируется по ставке, действовавшей в момент трат, и переключение тарифа не
|
|
32
|
+
пересчитывает уже потраченное. Все реальные окна выровнены по получасу, так что
|
|
33
|
+
слот воспроизводит ставку точно.
|
|
34
|
+
|
|
35
|
+
Сессионный слот отдаёт компоненту `useProjection`, поэтому значения обновляются
|
|
36
|
+
прямо во время стрима, без опроса.
|
|
37
|
+
|
|
38
|
+
**Цены** берутся из трёх источников, в таком порядке:
|
|
39
|
+
|
|
40
|
+
1. **Ручные `prices`** из конфига — для подписок и локальных моделей.
|
|
41
|
+
2. **Provider-aware каталог**:
|
|
42
|
+
- Если указан провайдер `openrouter`, ставки берутся из каталога OpenRouter (даже для моделей `deepseek/*`).
|
|
43
|
+
- Для прямых моделей DeepSeek применяется официальная зашитая тарифная сетка: пик 01:00–04:00 и 06:00–10:00 UTC, вне окон — льготная ставка 50%.
|
|
44
|
+
3. **Каталог OpenRouter** (`https://openrouter.ai/api/v1/models`, без ключа) —
|
|
45
|
+
для всех остальных моделей. Каталог кэшируется на диск в runtime-директории профиля и обновляется раз в сутки.
|
|
46
|
+
|
|
47
|
+
Если модель не нашлась нигде, виджет пишет «тариф ?», а в разбивке сессии выводит `не указана стоимость`.
|
|
48
|
+
|
|
49
|
+
## Установка
|
|
50
|
+
|
|
51
|
+
```bash
|
|
61
52
|
dsh plugin --profile web add @goodandready/dsh-cost-meter
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
|
73
|
-
|
|
74
|
-
| `
|
|
75
|
-
| `
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
- id: dsh-cost-meter
|
|
89
|
-
config:
|
|
90
|
-
currency: '₽'
|
|
91
|
-
usdRate: 80
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
Модель сопоставляется с каталогом по точному id, затем по суффиксу:
|
|
95
|
-
`deepseek-official/deepseek-v4-flash` → `deepseek/deepseek-v4-flash`. Если
|
|
96
|
-
угадано неверно или модели в каталоге нет — либо `modelMap`, либо ручные ставки:
|
|
97
|
-
|
|
98
|
-
```yaml
|
|
99
|
-
modelMap:
|
|
100
|
-
'ollama/glm-5.2': 'z-ai/glm-5.2'
|
|
101
|
-
prices:
|
|
102
|
-
'opencode-go/minimax-m3': # подписка — считать бесплатным
|
|
103
|
-
cacheHit: 0
|
|
104
|
-
input: 0
|
|
105
|
-
cacheWrite: 0
|
|
106
|
-
output: 0
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
## Ограничения
|
|
110
|
-
|
|
111
|
-
- Модель DeepSeek, вызванная **через OpenRouter**, всё равно считается по
|
|
112
|
-
официальному прайсу DeepSeek, хотя счёт выставит OpenRouter. Помечено в коде.
|
|
113
|
-
- Данные приходят из usage, который присылает провайдер. Маршруты, не
|
|
114
|
-
сообщающие usage, дадут нули: в панели об этом сказано явно.
|
|
115
|
-
- Модели, для которых тариф не нашёлся, показываются отдельной строкой и в
|
|
116
|
-
сумму не входят — сумма честно неполная, а не занижена молча.
|
|
117
|
-
- Курс валюты — константа `usdRate`, живого курса плагин не тянет.
|
|
118
|
-
|
|
119
|
-
## Устройство
|
|
120
|
-
|
|
121
|
-
| Файл | Что делает |
|
|
122
|
-
|---|---|
|
|
123
|
-
| `lib/index.js` | конфиг, проекция `costByModel`, тарифы, маршрут `GET /dsh-cost-meter/state` |
|
|
124
|
-
| `lib/client.js` | чип и панель; читает проекцию, считает арифметику по слотам |
|
|
125
|
-
| `cordis.patch.yml` | строка плагина в дереве загрузчика профиля |
|
|
126
|
-
|
|
127
|
-
Хост не вешает ни одного хука на ход агента — виджет только читает. Плагин,
|
|
128
|
-
который ничего не инжектит в сессию, не может её испортить.
|
|
129
|
-
|
|
130
|
-
## Лицензия
|
|
131
|
-
|
|
132
|
-
MIT
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Настройки
|
|
56
|
+
|
|
57
|
+
Плагин регистрирует секцию конфигурации `dsh-cost-meter`:
|
|
58
|
+
|
|
59
|
+
| Параметр | По умолчанию | Описание |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `currency` | `₽` | Символ или код валюты для отображения |
|
|
62
|
+
| `usdRate` | `90` | Множитель пересчёта долларовых цен в валюту отображения |
|
|
63
|
+
| `displayTimeZone` | `UTC` | Таймзона для отображения окон в панели (например, `Europe/Moscow`) |
|
|
64
|
+
| `useOpenRouter` | `true` | Загружать ли каталог цен OpenRouter для сторонних моделей |
|
|
65
|
+
| `prices` | `{}` | Ручные ставки за 1M токенов `{ prompt, completion, cacheRead?, cacheWrite? }` |
|
|
66
|
+
| `modelMap` | `{}` | Алиасы моделей `{ "alias": "target-model" }` |
|
|
67
|
+
|
|
68
|
+
## Тестирование
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm test
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Включает проверку контрактных инвариантов публичного пакета и регрессионные тесты проекции и тарификации (`test/projection-lifecycle.test.mjs`).
|
|
75
|
+
|
|
76
|
+
## Лицензия
|
|
77
|
+
|
|
78
|
+
MIT
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Публичный релиз и deployment
|
|
2
2
|
|
|
3
|
-
Релизная версия повышается patch-изменением (v0.7.
|
|
3
|
+
Релизная версия повышается patch-изменением (v0.7.4). После merge в `main`
|
|
4
4
|
создаются annotated tag, пакет в npm (`@goodandready/dsh-cost-meter`) и
|
|
5
5
|
публичный GitHub release в `GooDAnDReaDY/dsh-cost-meter`.
|
|
6
6
|
|
package/docs/design/DESIGN.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# DESIGN.md — dsh-cost-meter
|
|
2
2
|
|
|
3
3
|
## Product / Purpose
|
|
4
|
-
- Назначение: Отображение стоимости активной сессии диалога в реальном времени, тарифов (пик / не-пик / единый) за 1M токенов, обратного отсчёта до смены
|
|
4
|
+
- Назначение: Отображение стоимости активной сессии диалога в реальном времени, тарифов (пик / не-пик / единый) за 1M токенов, обратного отсчёта до смены окна, прозрачной детализации расходов по каждой задействованной модели и нативного управления конфигурацией плагина.
|
|
5
5
|
- Аудитория: Пользователи и инженеры DeepSeek Harness, контролирующие потребление токенов и бюджет на LLM-провайдеров.
|
|
6
6
|
- Статус: Активная публичная поставка (@goodandready/dsh-cost-meter).
|
|
7
7
|
|
|
@@ -9,19 +9,22 @@
|
|
|
9
9
|
- Web/UI:
|
|
10
10
|
- Компактный чип в слоте `conversation.session.header.utilities` (order: 10).
|
|
11
11
|
- Выпадающая панель детализации (width: 320px, absolute positioning, click-outside dismissal).
|
|
12
|
+
- Карточка настроек плагина в `settings.plugin.item` (слот `settings.plugin.item` с fallback на `settings.section`).
|
|
12
13
|
- DSH UI / settings / slots:
|
|
13
|
-
-
|
|
14
|
+
- Слот чипа: `conversation.session.header.utilities`.
|
|
15
|
+
- Слот карточки настроек: `settings.plugin.item`.
|
|
14
16
|
- Настройки: реестр DSH `settings.register("dsh-cost-meter", Config)` (валюта `currency`, множитель курса `usdRate`, таймзона `displayTimeZone`, опрос OpenRouter `useOpenRouter`, ручные ставки `prices`, сопоставление `modelMap`).
|
|
17
|
+
- Локализация: регистрация словарей через `ctx.locale.register("dsh-cost-meter", { en, ru })` с поддержкой переключения языков через `useActiveLocale(ctx)`.
|
|
15
18
|
- API:
|
|
16
19
|
- HTTP `GET /dsh-cost-meter/state` (параметр `?models=...` для получения тарифов по задействованным моделям).
|
|
17
20
|
- CLI:
|
|
18
21
|
- Управление через CLI харнесса: `dsh plugin --profile web add @goodandready/dsh-cost-meter`.
|
|
19
22
|
- Документация:
|
|
20
|
-
- README.md, index.md, docs/testing/public-route.md, docs/deployment/public-release.md.
|
|
23
|
+
- README.md, index.md, docs/testing/public-route.md, docs/testing/projection-lifecycle.md, docs/deployment/public-release.md.
|
|
21
24
|
|
|
22
25
|
## Visual Direction
|
|
23
26
|
- Атмосфера: Строгий утилитарный нативный интерфейс DSH, высокая информационная плотность без визуального шума.
|
|
24
|
-
- Утверждённые референсы: Дизайн-система DeepSeek Harness (`--dsw-alias-*`), чипы в шапке сессии (`dsh-key-rotation`, `dsh-context-lens`).
|
|
27
|
+
- Утверждённые референсы: Дизайн-система DeepSeek Harness (`--dsw-alias-*`), чипы в шапке сессии (`dsh-key-rotation`, `dsh-context-lens`), стандарт карточек настроек DSH.
|
|
25
28
|
- Не копировать: Избыточные градиенты, внешние модальные окна, тяжелые тени, кастомные несогласованные шрифты.
|
|
26
29
|
|
|
27
30
|
## Foundations
|
|
@@ -29,7 +32,7 @@
|
|
|
29
32
|
- `--dsw-alias-label-primary`: основной текст, заголовки, суммы.
|
|
30
33
|
- `--dsw-alias-label-tertiary`: второстепенные пояснения, метки столбцов, чип без тарифа.
|
|
31
34
|
- `--dsw-alias-border-l2`: разделители секций и обводка панели.
|
|
32
|
-
- `--dsw-alias-bg-layer-2` / `--dsw-alias-bg-base`: подложка всплывающей
|
|
35
|
+
- `--dsw-alias-bg-layer-2` / `--dsw-alias-bg-base`: подложка всплывающей панели и карточки настроек.
|
|
33
36
|
- `--dsw-alias-state-success-primary` / `--dsw-alias-state-success-tertiary`: подсветка льготного окна (не-пик).
|
|
34
37
|
- `--dsw-alias-state-warn-primary` / `--dsw-alias-state-warn-tertiary`: подсветка полного тарифа (пик).
|
|
35
38
|
- Типографика:
|
|
@@ -38,13 +41,10 @@
|
|
|
38
41
|
- Сетка и отступы:
|
|
39
42
|
- Чип: высота 22px, padding: 0 8px, gap: 6px, border-radius: 6px.
|
|
40
43
|
- Панель: padding: 12px 14px, border-radius: 10px, ширина 320px.
|
|
41
|
-
-
|
|
42
|
-
- Accessibility:
|
|
43
|
-
- Семантические теги `button` для интерактивных элементов, нативные `title` подсказки при наведении, закрытие панели при клике вне компонента.
|
|
44
|
+
- Карточка настроек: нативный складной контейнер (`.dcm-settings-card`), отступы полей, бейдж статуса снимка (`ready`, `loading`, `unavailable`).
|
|
44
45
|
|
|
45
|
-
## Components
|
|
46
|
+
## Components & States
|
|
46
47
|
- Чип:
|
|
47
|
-
- `Loading / Init`: если данные ещё не загружены, чип скрыт (`null`).
|
|
48
48
|
- `Unknown Tariff`: отображает «тариф ?» в приглушённом цвете `label-tertiary`.
|
|
49
49
|
- `Flat Tariff`: фон `--dsw-alias-state-warn-tertiary`, текст `--dsw-alias-state-warn-primary`, заголовок «Единый тариф».
|
|
50
50
|
- `Off-Peak`: зелёная плашка успеха (льготный тариф).
|
|
@@ -55,28 +55,41 @@
|
|
|
55
55
|
- Сетка тарифов за 1M токенов (не-пик / пик / единая ставка).
|
|
56
56
|
- Блок «Из чего сложилась сумма»:
|
|
57
57
|
- Список всех моделей сессии с токенами и суммарной стоимостью.
|
|
58
|
-
- Для модели без тарифа отображается статус `не указана
|
|
58
|
+
- Для модели без тарифа отображается статус `не указана стоимость` (`rate_unspecified`).
|
|
59
59
|
- Итог сессии: `Сессия: ≈ <валюта><сумма>`.
|
|
60
60
|
- Отсутствие мусорных служебных плашек (строка источника OpenRouter исключена).
|
|
61
|
+
- Карточка настроек (`CostMeterCard`):
|
|
62
|
+
- Сворачиваемый заголовок с шевроном и иконкой счётчика.
|
|
63
|
+
- Статус снимка: `ready` (зелёный бейдж), `loading` (нейтральный), `unavailable` (серый).
|
|
64
|
+
- Поля: выбор символа валюты (`currency`), курс к USD (`usdRate`), таймзона отображения (`displayTimeZone`).
|
|
65
|
+
- Локализованные лейблы и плейсхолдеры на русском и английском языках.
|
|
61
66
|
|
|
62
67
|
## User Flows
|
|
63
68
|
1. Пользователь ведёт диалог с ассистентом: чип в шапке инкрементально обновляет потраченную сумму прямо во время стриминга ответа ассистента.
|
|
64
69
|
2. Пользователь нажимает на чип: открывается всплывающая панель с тарифной сеткой и расходами по каждой модели сессии.
|
|
65
70
|
3. Если модель не тарифицирована: в списке моделей выводится статус `не указана стоимость`.
|
|
71
|
+
4. Пользователь открывает настройки DSH (`Настройки` -> `Плагины`): карточка `dsh-cost-meter` позволяет изменить валюту, курс и таймзону с мгновенным сохранением в `settingsScope`.
|
|
66
72
|
|
|
67
73
|
## Do / Don't
|
|
68
74
|
- Do:
|
|
69
75
|
- Использовать CSS-переменные DSH для согласованности с тёмной и светлой темами.
|
|
70
76
|
- Всегда разделять расходы по задействованным моделям.
|
|
71
77
|
- Использовать табличные цифры (`tabular-nums`) для предотвращения дёргания вёрстки при стриме.
|
|
78
|
+
- Использовать `ctx.locale.register` и `useActiveLocale` для всех строк интерфейса.
|
|
72
79
|
- Don't:
|
|
73
80
|
- Не выводить техническую служебную информацию о каталоге OpenRouter в интерфейс пользователя.
|
|
74
81
|
- Не заменять отсутствие тарифа обманчивым нулём или невнятным прочерком — писать прямо `не указана стоимость`.
|
|
75
82
|
- Не допускать утечек локальных путей и инфраструктурных данных в коде или документации.
|
|
76
83
|
|
|
77
84
|
## History & Decisions
|
|
85
|
+
- 2026-09-05 (v0.7.4):
|
|
86
|
+
- Добавлена нативная карточка настроек в слот `settings.plugin.item` (`#12`).
|
|
87
|
+
- Реализована полная локализация клиентской части через `ctx.locale.register` (`#2`).
|
|
88
|
+
- Реализовано provider-aware разрешение тарифов OpenRouter vs DeepSeek (`#3`).
|
|
89
|
+
- Добавлены всеобъемлющие регрессионные тесты на проекцию и сопоставление (`test/projection-lifecycle.test.mjs`, `#1`).
|
|
90
|
+
- Закрыт issue о согласовании публичного scope пакета (`#13`).
|
|
78
91
|
- 2026-09-05 (v0.7.3):
|
|
79
92
|
- Удалена строка источника цен OpenRouter.
|
|
80
93
|
- Список моделей в блоке «Из чего сложилась сумма» унифицирован и отображается для всех задействованных моделей сессии.
|
|
81
94
|
- Для нетарифицированных моделей введён явный статус «не указана стоимость».
|
|
82
|
-
- Пакет переведён на публичный scope `@goodandready/dsh-cost-meter`.
|
|
95
|
+
- Пакет переведён на публичный scope `@goodandready/dsh-cost-meter`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Регрессионное тестирование проекции тарифов и жизненного цикла
|
|
2
|
+
|
|
3
|
+
Тесты проверяют логику тарификации, парсинг временных окон UTC, сопоставление моделей и provider-aware разрешение ставок.
|
|
4
|
+
|
|
5
|
+
## Запуск тестов
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm test
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Проверяемые сценарии (`test/projection-lifecycle.test.mjs`)
|
|
12
|
+
|
|
13
|
+
1. **`hhmmToMinutes` и `parseWindow`**:
|
|
14
|
+
- Корректный разбор диапазонов `HH:MM-HH:MM` в минуты от начала суток UTC.
|
|
15
|
+
- Граничные условия: переход через полночь (`23:00-02:00`), пустые или невалидные строки.
|
|
16
|
+
|
|
17
|
+
2. **`slotOf`**:
|
|
18
|
+
- Вычисление 30-минутного слота суток (`0..47`) на основе переданного timestamp.
|
|
19
|
+
- Независимость накопления расхода от текущего момента времени (сохранение исторической ставки слота).
|
|
20
|
+
|
|
21
|
+
3. **`indexCatalog` и `matchModel`**:
|
|
22
|
+
- Индексация моделей OpenRouter с поддержкой поиска без учета регистра.
|
|
23
|
+
- Нормализация суффиксов вендоров и версий (например, `vendor/model:variant`).
|
|
24
|
+
|
|
25
|
+
4. **`resolveTariff` (Provider-aware)**:
|
|
26
|
+
- Если указан `provider: "openrouter"`, плагин в первую очередь обращается к каталогу OpenRouter, даже если имя модели содержит `deepseek`.
|
|
27
|
+
- Если провайдер не `openrouter` (или `deepseek`), DeepSeek модели тарифицируются по официальной зашитой сетке (пик / не-пик).
|
|
28
|
+
- Ручные ставки `prices` из конфигурации имеют высший приоритет над внешними каталогами.
|
package/lib/client.js
CHANGED
|
@@ -19,6 +19,269 @@ window.__ModuleLoader__.load({
|
|
|
19
19
|
|
|
20
20
|
const STATE_PATH = '/dsh-cost-meter/state'
|
|
21
21
|
const DAY = 1440
|
|
22
|
+
const NS = 'dsh-cost-meter'
|
|
23
|
+
|
|
24
|
+
const en = {
|
|
25
|
+
title: 'Cost Meter',
|
|
26
|
+
subtitle: 'Session cost in header, token rates, and per-model breakdown',
|
|
27
|
+
chipUnknown: 'tariff ?',
|
|
28
|
+
chipRate: 'rate',
|
|
29
|
+
offPeak: 'off-peak',
|
|
30
|
+
peak: 'peak',
|
|
31
|
+
flat: 'flat',
|
|
32
|
+
modelsBreakdown: 'Model Breakdown',
|
|
33
|
+
sessionTokens: 'Session Tokens',
|
|
34
|
+
totalCost: 'Total Cost',
|
|
35
|
+
noPriceSpecified: 'rate not specified',
|
|
36
|
+
noUsageYet: 'Provider has not reported usage for this session yet.',
|
|
37
|
+
routeUnknown: 'route unknown',
|
|
38
|
+
catalogUnavailable: 'OpenRouter catalog unavailable: ',
|
|
39
|
+
modelNotFound: 'Model not found in OpenRouter catalog. Set manual prices in settings (prices) or map model via modelMap.',
|
|
40
|
+
'settings.loading': 'Loading settings…',
|
|
41
|
+
'settings.unavailable': 'Settings unavailable (namespace is not registered yet).',
|
|
42
|
+
'settings.save': 'Save',
|
|
43
|
+
'settings.saving': 'Saving…',
|
|
44
|
+
'settings.saved': 'Saved',
|
|
45
|
+
'settings.saveFailed': 'Failed to save settings: ',
|
|
46
|
+
'field.currency': 'Currency symbol',
|
|
47
|
+
'field.usdRate': 'USD exchange rate',
|
|
48
|
+
'field.displayTimeZone': 'Display timezone',
|
|
49
|
+
'hint.currency': 'Currency symbol shown in the widget (e.g. $, ₽, €).',
|
|
50
|
+
'hint.usdRate': 'Exchange rate: units of currency per 1 USD (e.g. 80 for ₽).',
|
|
51
|
+
'hint.displayTimeZone': 'IANA timezone used for tariff peak/off-peak schedule.',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ru = {
|
|
55
|
+
title: 'Счётчик расходов (Cost Meter)',
|
|
56
|
+
subtitle: 'Стоимость сессии в шапке, тарифы токенов и детализация по моделям',
|
|
57
|
+
chipUnknown: 'тариф ?',
|
|
58
|
+
chipRate: 'тариф',
|
|
59
|
+
offPeak: t('offPeak'),
|
|
60
|
+
peak: 'пик',
|
|
61
|
+
flat: 'единый',
|
|
62
|
+
modelsBreakdown: t('modelsBreakdown'),
|
|
63
|
+
sessionTokens: t('sessionTokens'),
|
|
64
|
+
totalCost: t('totalCost'),
|
|
65
|
+
noPriceSpecified: t('noPriceSpecified'),
|
|
66
|
+
noUsageYet: t('noUsageYet'),
|
|
67
|
+
routeUnknown: 'маршрут неизвестен',
|
|
68
|
+
catalogUnavailable: 'Каталог OpenRouter недоступен: ',
|
|
69
|
+
modelNotFound: t('modelNotFound'),
|
|
70
|
+
'settings.loading': 'Загрузка настроек…',
|
|
71
|
+
'settings.unavailable': 'Настройки недоступны (пространство ещё не зарегистрировано).',
|
|
72
|
+
'settings.save': 'Сохранить',
|
|
73
|
+
'settings.saving': 'Сохранение…',
|
|
74
|
+
'settings.saved': 'Сохранено',
|
|
75
|
+
'settings.saveFailed': 'Не удалось сохранить настройки: ',
|
|
76
|
+
'field.currency': 'Символ валюты',
|
|
77
|
+
'field.usdRate': 'Курс валюты к USD',
|
|
78
|
+
'field.displayTimeZone': 'Часовой пояс отображения',
|
|
79
|
+
'hint.currency': 'Символ валюты, отображаемый в виджете (например, ₽, $, €).',
|
|
80
|
+
'hint.usdRate': 'Сколько единиц валюты в 1 долларе США (например, 80 для ₽).',
|
|
81
|
+
'hint.displayTimeZone': 'IANA таймзона для расчёта часов пика и не-пика (например, Europe/Moscow).',
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let ChevronIcon = null
|
|
85
|
+
try {
|
|
86
|
+
const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
87
|
+
ChevronIcon = primitives && primitives.IconChevronDownOutline14
|
|
88
|
+
} catch {
|
|
89
|
+
ChevronIcon = null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function Chevron(props) {
|
|
93
|
+
if (ChevronIcon) {
|
|
94
|
+
return React.createElement(ChevronIcon, {
|
|
95
|
+
className: 'dcm-chev' + (props.open ? ' dcm-chev-open' : ''),
|
|
96
|
+
style: {
|
|
97
|
+
marginLeft: 'auto', flex: 'none', color: 'var(--dsw-alias-label-tertiary)',
|
|
98
|
+
transition: 'transform .16s', transform: props.open ? 'rotate(180deg)' : 'none',
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
return React.createElement('svg', {
|
|
103
|
+
className: 'dcm-chev' + (props.open ? ' dcm-chev-open' : ''),
|
|
104
|
+
style: {
|
|
105
|
+
marginLeft: 'auto', flex: 'none', color: 'var(--dsw-alias-label-tertiary)',
|
|
106
|
+
transition: 'transform .16s', transform: props.open ? 'rotate(180deg)' : 'none',
|
|
107
|
+
},
|
|
108
|
+
width: 14, height: 14, viewBox: '0 0 14 14', fill: 'none', 'aria-hidden': 'true',
|
|
109
|
+
}, React.createElement('path', {
|
|
110
|
+
d: 'M3.5 5.25L7 8.75L10.5 5.25', stroke: 'currentColor', strokeWidth: 1.5,
|
|
111
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
112
|
+
}))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const cardCss = '.dcm-card { border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-3); border-radius:12px; list-style:none } ' +
|
|
116
|
+
'.dcm-head { appearance:none; width:100%; font:inherit; color:inherit; text-align:left; cursor:pointer; background:0 0; border:0; border-radius:12px; display:flex; align-items:center; gap:12px; padding:14px 16px } ' +
|
|
117
|
+
'.dcm-title { color:var(--dsw-alias-label-primary); font-size:15px; font-weight:600; line-height:1.4 } ' +
|
|
118
|
+
'.dcm-sub { color:var(--dsw-alias-label-secondary); font-size:13px } ' +
|
|
119
|
+
'.dcm-body { border-top:1px solid var(--dsw-alias-border-l2); margin:0 16px; padding-bottom:8px } ' +
|
|
120
|
+
'.dcm-field { display:flex; flex-direction:column; gap:6px; padding:12px 0 } ' +
|
|
121
|
+
'.dcm-label { color:var(--dsw-alias-label-primary); font-size:13px; font-weight:500 } ' +
|
|
122
|
+
'.dcm-hint { color:var(--dsw-alias-label-secondary); font-size:12px } ' +
|
|
123
|
+
'.dcm-input { height:34px; border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-3); color:var(--dsw-alias-label-primary); border-radius:8px; padding:0 12px; font-size:13px } ' +
|
|
124
|
+
'.dcm-foot { border-top:1px solid var(--dsw-alias-border-l2); display:flex; justify-content:flex-end; align-items:center; gap:8px; padding:12px 0 4px } ' +
|
|
125
|
+
'.dcm-save { appearance:none; font:inherit; cursor:pointer; border:1px solid transparent; border-radius:8px; padding:5px 14px; font-size:13px; background:var(--dsw-alias-label-primary); color:var(--dsw-alias-bg-layer-3) } ' +
|
|
126
|
+
'.dcm-msg-ok { color:var(--dsw-alias-state-success-primary); font-size:12px } ' +
|
|
127
|
+
'.dcm-msg-err { color:var(--dsw-alias-state-danger-primary, #e53935); font-size:12px }';
|
|
128
|
+
|
|
129
|
+
function ensureCardStyles() {
|
|
130
|
+
if (typeof document === 'undefined') return
|
|
131
|
+
if (document.getElementById('dsh-cost-meter-card-styles')) return
|
|
132
|
+
const el = document.createElement('style')
|
|
133
|
+
el.id = 'dsh-cost-meter-card-styles'
|
|
134
|
+
el.textContent = cardCss
|
|
135
|
+
document.head.appendChild(el)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function makeT(dict, fallback) {
|
|
139
|
+
return function t(key, vars) {
|
|
140
|
+
let val = (dict && dict[key]) || (fallback && fallback[key]) || key
|
|
141
|
+
if (vars && typeof val === 'string') {
|
|
142
|
+
for (const k of Object.keys(vars)) {
|
|
143
|
+
val = val.replace(new RegExp('\\{' + k + '\\}', 'g'), String(vars[k]))
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return val
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function useActiveLocale(ctx) {
|
|
151
|
+
return React.useSyncExternalStore(
|
|
152
|
+
React.useMemo(() => (cb) => (ctx && ctx.locale ? ctx.locale.subscribe(cb) : () => {}), [ctx]),
|
|
153
|
+
React.useCallback(() => (ctx && ctx.locale && ctx.locale.getSnapshot ? ctx.locale.getSnapshot().active : 'ru'), [ctx]),
|
|
154
|
+
React.useCallback(() => 'ru', []),
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function CostMeterCard(props) {
|
|
159
|
+
const ctx = props.ctx
|
|
160
|
+
const locale = useActiveLocale(ctx)
|
|
161
|
+
const t = props.t || makeT(locale === 'ru' ? ru : en, en)
|
|
162
|
+
const [open, setOpen] = React.useState(false)
|
|
163
|
+
const [draft, setDraft] = React.useState(null)
|
|
164
|
+
const [saving, setSaving] = React.useState(false)
|
|
165
|
+
const [err, setErr] = React.useState('')
|
|
166
|
+
const [saved, setSaved] = React.useState(false)
|
|
167
|
+
|
|
168
|
+
const scope = React.useMemo(
|
|
169
|
+
() => (ctx && ctx.settingsScope ? ctx.settingsScope.bind({ namespace: NS }) : undefined),
|
|
170
|
+
[ctx],
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
const snapshot = React.useSyncExternalStore(
|
|
174
|
+
React.useMemo(() => (cb) => (scope ? scope.subscribe(cb) : () => {}), [scope]),
|
|
175
|
+
React.useCallback(() => (scope ? scope.getSnapshot() : { status: 'loading' }), [scope]),
|
|
176
|
+
React.useCallback(() => ({ status: 'loading' }), []),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
React.useEffect(() => { ensureCardStyles() }, [])
|
|
180
|
+
|
|
181
|
+
const status = (snapshot && snapshot.status) || 'loading'
|
|
182
|
+
const stored = (snapshot && snapshot.value) || {}
|
|
183
|
+
|
|
184
|
+
React.useEffect(() => {
|
|
185
|
+
if (status === 'ready' && draft === null) {
|
|
186
|
+
setDraft({
|
|
187
|
+
currency: stored.currency !== undefined ? String(stored.currency) : '$',
|
|
188
|
+
usdRate: stored.usdRate !== undefined ? String(stored.usdRate) : '1',
|
|
189
|
+
displayTimeZone: stored.displayTimeZone !== undefined ? String(stored.displayTimeZone) : 'Europe/Moscow',
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
}, [status, stored, draft])
|
|
193
|
+
|
|
194
|
+
const save = async () => {
|
|
195
|
+
if (!scope || !draft) return
|
|
196
|
+
setSaving(true); setErr(''); setSaved(false)
|
|
197
|
+
const broken = []
|
|
198
|
+
try {
|
|
199
|
+
await scope.set('currency', String(draft.currency || '$'))
|
|
200
|
+
} catch (e) { broken.push('currency: ' + (e && e.message || String(e))) }
|
|
201
|
+
|
|
202
|
+
const rateNum = Number(String(draft.usdRate).trim())
|
|
203
|
+
if (!Number.isFinite(rateNum) || rateNum <= 0) {
|
|
204
|
+
broken.push('usdRate: must be a positive number')
|
|
205
|
+
} else {
|
|
206
|
+
try {
|
|
207
|
+
await scope.set('usdRate', rateNum)
|
|
208
|
+
} catch (e) { broken.push('usdRate: ' + (e && e.message || String(e))) }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
await scope.set('displayTimeZone', String(draft.displayTimeZone || 'Europe/Moscow').trim())
|
|
213
|
+
} catch (e) { broken.push('displayTimeZone: ' + (e && e.message || String(e))) }
|
|
214
|
+
|
|
215
|
+
setSaving(false)
|
|
216
|
+
if (broken.length) {
|
|
217
|
+
setErr(t('settings.saveFailed') + broken.join('; '))
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
setSaved(true)
|
|
221
|
+
setTimeout(() => setSaved(false), 2500)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return React.createElement('li', { className: 'dcm-card' },
|
|
225
|
+
React.createElement('button', {
|
|
226
|
+
type: 'button', className: 'dcm-head', 'aria-expanded': open,
|
|
227
|
+
onClick: () => setOpen((v) => !v),
|
|
228
|
+
},
|
|
229
|
+
React.createElement('span', { style: { display: 'flex', flexDirection: 'column' } },
|
|
230
|
+
React.createElement('span', { className: 'dcm-title' }, t('title')),
|
|
231
|
+
React.createElement('span', { className: 'dcm-sub' }, t('subtitle')),
|
|
232
|
+
),
|
|
233
|
+
React.createElement(Chevron, { open }),
|
|
234
|
+
),
|
|
235
|
+
open ? React.createElement('div', { className: 'dcm-body' },
|
|
236
|
+
status === 'loading'
|
|
237
|
+
? React.createElement('p', { className: 'dcm-hint', style: { padding: '12px 0' } }, t('settings.loading'))
|
|
238
|
+
: status !== 'ready'
|
|
239
|
+
? React.createElement('p', { className: 'dcm-msg-err', style: { padding: '12px 0' } }, t('settings.unavailable'))
|
|
240
|
+
: React.createElement('div', null,
|
|
241
|
+
React.createElement('div', { className: 'dcm-field' },
|
|
242
|
+
React.createElement('label', { className: 'dcm-label' }, t('field.currency')),
|
|
243
|
+
React.createElement('input', {
|
|
244
|
+
className: 'dcm-input',
|
|
245
|
+
value: (draft && draft.currency) !== undefined ? draft.currency : '',
|
|
246
|
+
onChange: (e) => setDraft({ ...draft, currency: e.target.value }),
|
|
247
|
+
}),
|
|
248
|
+
React.createElement('span', { className: 'dcm-hint' }, t('hint.currency')),
|
|
249
|
+
),
|
|
250
|
+
React.createElement('div', { className: 'dcm-field' },
|
|
251
|
+
React.createElement('label', { className: 'dcm-label' }, t('field.usdRate')),
|
|
252
|
+
React.createElement('input', {
|
|
253
|
+
className: 'dcm-input',
|
|
254
|
+
type: 'number',
|
|
255
|
+
step: 'any',
|
|
256
|
+
value: (draft && draft.usdRate) !== undefined ? draft.usdRate : '',
|
|
257
|
+
onChange: (e) => setDraft({ ...draft, usdRate: e.target.value }),
|
|
258
|
+
}),
|
|
259
|
+
React.createElement('span', { className: 'dcm-hint' }, t('hint.usdRate')),
|
|
260
|
+
),
|
|
261
|
+
React.createElement('div', { className: 'dcm-field' },
|
|
262
|
+
React.createElement('label', { className: 'dcm-label' }, t('field.displayTimeZone')),
|
|
263
|
+
React.createElement('input', {
|
|
264
|
+
className: 'dcm-input',
|
|
265
|
+
value: (draft && draft.displayTimeZone) !== undefined ? draft.displayTimeZone : '',
|
|
266
|
+
onChange: (e) => setDraft({ ...draft, displayTimeZone: e.target.value }),
|
|
267
|
+
}),
|
|
268
|
+
React.createElement('span', { className: 'dcm-hint' }, t('hint.displayTimeZone')),
|
|
269
|
+
),
|
|
270
|
+
err ? React.createElement('div', { className: 'dcm-msg-err', style: { padding: '4px 0' } }, err) : null,
|
|
271
|
+
saved ? React.createElement('div', { className: 'dcm-msg-ok', style: { padding: '4px 0' } }, t('settings.saved')) : null,
|
|
272
|
+
React.createElement('div', { className: 'dcm-foot' },
|
|
273
|
+
React.createElement('button', {
|
|
274
|
+
type: 'button',
|
|
275
|
+
className: 'dcm-save',
|
|
276
|
+
disabled: saving,
|
|
277
|
+
onClick: save,
|
|
278
|
+
}, saving ? t('settings.saving') : t('settings.save')),
|
|
279
|
+
),
|
|
280
|
+
)
|
|
281
|
+
) : null,
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
22
285
|
|
|
23
286
|
// ---------------------------------------------------------------- tariff
|
|
24
287
|
|
|
@@ -198,6 +461,9 @@ window.__ModuleLoader__.load({
|
|
|
198
461
|
// ------------------------------------------------------------ component
|
|
199
462
|
|
|
200
463
|
function CostMeter(props) {
|
|
464
|
+
const ctx = props.ctx
|
|
465
|
+
const locale = useActiveLocale(ctx)
|
|
466
|
+
const t = props.t || makeT(locale === 'ru' ? ru : en, en)
|
|
201
467
|
const useProjection = props.useProjection
|
|
202
468
|
const usage = useProjection ? useProjection('tokenUsage') : undefined
|
|
203
469
|
const byModel = useProjection ? useProjection('costByModel') : undefined
|
|
@@ -253,14 +519,14 @@ window.__ModuleLoader__.load({
|
|
|
253
519
|
h('button', {
|
|
254
520
|
key: 'chip', type: 'button',
|
|
255
521
|
style: Object.assign({}, S.chip, { background: 'transparent', color: C.muted }),
|
|
256
|
-
onClick: () => setOpen((v) => !v), title: '
|
|
522
|
+
onClick: () => setOpen((v) => !v), title: t('chipUnknown'),
|
|
257
523
|
}, 'тариф ?'),
|
|
258
524
|
open ? h('div', { key: 'panel', style: S.panel }, [
|
|
259
525
|
h('div', { key: 't', style: S.title },
|
|
260
|
-
state.route ? state.route.provider + ' / ' + state.route.model : '
|
|
526
|
+
state.route ? state.route.provider + ' / ' + state.route.model : t('routeUnknown')),
|
|
261
527
|
h('div', { key: 'n', style: S.note },
|
|
262
528
|
state.catalogError
|
|
263
|
-
? '
|
|
529
|
+
? t('catalogUnavailable') + state.catalogError
|
|
264
530
|
: 'Модель не найдена в каталоге OpenRouter. Задайте ставки вручную в настройках (prices) или сопоставьте модель через modelMap.'),
|
|
265
531
|
]) : null,
|
|
266
532
|
])
|
|
@@ -353,7 +619,7 @@ window.__ModuleLoader__.load({
|
|
|
353
619
|
open ? h('div', { key: 'panel', style: S.panel }, [
|
|
354
620
|
h('div', { key: 'title', style: S.title },
|
|
355
621
|
(state.route ? state.route.model : 'модель') +
|
|
356
|
-
(flat ? '' : ' — сейчас ' + (cheapNow ? 'не-пик' : '
|
|
622
|
+
(flat ? '' : ' — сейчас ' + (cheapNow ? 'не-пик' : t('peak')))),
|
|
357
623
|
|
|
358
624
|
h('div', { key: 'note', style: S.note },
|
|
359
625
|
flat
|
|
@@ -457,15 +723,41 @@ window.__ModuleLoader__.load({
|
|
|
457
723
|
}
|
|
458
724
|
|
|
459
725
|
function apply(ctx) {
|
|
726
|
+
if (ctx.locale && ctx.locale.register) {
|
|
727
|
+
try { ctx.locale.register(NS, { en, ru }) } catch (_) {}
|
|
728
|
+
}
|
|
729
|
+
|
|
460
730
|
ctx.slots.inject('conversation.session.header.utilities', () =>
|
|
461
731
|
ctx.slots.register(
|
|
462
|
-
{ name: 'conversation.session.header.utilities', id: 'dsh-cost-meter', order: 10 },
|
|
463
|
-
CostMeter,
|
|
732
|
+
{ name: 'conversation.session.header.utilities', id: 'dsh-cost-meter', order: 10, inject: () => ({ ctx }) },
|
|
733
|
+
(p) => React.createElement(CostMeter, { ...p, ctx }),
|
|
464
734
|
),
|
|
465
735
|
)
|
|
736
|
+
|
|
737
|
+
let placed = false
|
|
738
|
+
ctx.slots.inject('settings.plugin.item', () => {
|
|
739
|
+
placed = true
|
|
740
|
+
return ctx.slots.register(
|
|
741
|
+
{ name: 'settings.plugin.item', key: NS, locale: NS, order: 20, inject: () => ({ ctx }) },
|
|
742
|
+
(p) => React.createElement(CostMeterCard, { ...p, ctx }),
|
|
743
|
+
)
|
|
744
|
+
})
|
|
745
|
+
|
|
746
|
+
// Fallback to settings.section if settings.plugin.item is not declared
|
|
747
|
+
setTimeout(() => {
|
|
748
|
+
if (placed) return
|
|
749
|
+
try {
|
|
750
|
+
ctx.slots.inject('settings.section', () =>
|
|
751
|
+
ctx.slots.register(
|
|
752
|
+
{ name: 'settings.section', id: '@goodandready/dsh-cost-meter', order: 25, locale: NS, label: () => makeT(useActiveLocale(ctx) === 'ru' ? ru : en, en)('title'), inject: () => ({ ctx }) },
|
|
753
|
+
(p) => React.createElement(CostMeterCard, { ...p, ctx }),
|
|
754
|
+
),
|
|
755
|
+
)
|
|
756
|
+
} catch (_) {}
|
|
757
|
+
}, 500)
|
|
466
758
|
}
|
|
467
759
|
|
|
468
|
-
module.exports = { apply, inject: ['slots'] }
|
|
760
|
+
module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'] }
|
|
469
761
|
return module.exports
|
|
470
762
|
},
|
|
471
763
|
})
|
package/lib/index.js
CHANGED
|
@@ -373,6 +373,14 @@ export function resolveTariff(cfg, index, route) {
|
|
|
373
373
|
// Keyed by MODEL, not provider: a DeepSeek model is billed at DeepSeek's
|
|
374
374
|
// published rate whoever routes it (deepseek-official, opencode-go, ollama).
|
|
375
375
|
// Date/variant suffixes (`deepseek-v4-flash:0731`) name the same product.
|
|
376
|
+
const providerLower = String(route.provider || "").toLowerCase();
|
|
377
|
+
// #3: Provider-aware routing: if routed explicitly through OpenRouter,
|
|
378
|
+
// consult OpenRouter catalog first to honor actual provider billing.
|
|
379
|
+
if (providerLower === "openrouter" && cfg.useOpenRouter !== false) {
|
|
380
|
+
const matchedId = matchModel(index, route, cfg.modelMap);
|
|
381
|
+
if (matchedId) return { schedule: index.byId[matchedId], source: "openrouter", matchedId };
|
|
382
|
+
}
|
|
383
|
+
|
|
376
384
|
const base = String(route.model).toLowerCase().split(':')[0]
|
|
377
385
|
const peak = (cfg.deepseekPeakPrices || {})[base] || DEEPSEEK_PEAK_RATES[base]
|
|
378
386
|
if (peak) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-cost-meter",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "DeepSeek Harness plugin: a session cost chip in the conversation header - live token usage by bucket, peak/off-peak tariff, and a countdown to the next tariff switch.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|