@jxgame2020/dsh-token-quota 0.1.0

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.
@@ -0,0 +1,232 @@
1
+ import { createTokenQuotaPanelStore } from "./store.js";
2
+ import { TokenQuotaPanel } from "./TokenQuotaPanel.js";
3
+ import { en, zh } from "./locales.js";
4
+ export { mergeModelRows } from "./store.js";
5
+ /** Dictionary namespace owning the panel copy. */
6
+ const NS = 'token-quota';
7
+ /** Settings namespace the Host quota package owns. */
8
+ const TOKEN_QUOTA_NAMESPACE = 'token-quota';
9
+ /** Snapshot pull cadence in milliseconds. */
10
+ const POLL_INTERVAL_MS = 3000;
11
+ /** Required services: slot registry, connection RPC, locale, settings scope. */
12
+ export const inject = ['slots', 'connection', 'locale', 'settingsScope'];
13
+ /**
14
+ * Client plugin body: poll the Host snapshot route, run the full-quota
15
+ * strategy, wire the injected face (directory load, limit write, model
16
+ * switch, preferences), and register the floating panel into `shell.overlay`.
17
+ * @param ctx - client root context.
18
+ */
19
+ export function apply(ctx) {
20
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'token-quota-ui: panel dictionaries');
21
+ const t = ctx.locale.bind(NS);
22
+ const scope = ctx.settingsScope.bind({ namespace: TOKEN_QUOTA_NAMESPACE });
23
+ const connection = ctx.get('connection');
24
+ // Store handle shared with the registration; the framework instantiates the
25
+ // live store per entry and hands its bound actions to the inject factory,
26
+ // so the apply-world only ever writes through `bound`.
27
+ const store = createTokenQuotaPanelStore();
28
+ let bound;
29
+ let lastGroups = [];
30
+ let lastCurrent = null;
31
+ let lastSessionId;
32
+ let lastMonitored = null;
33
+ let lastOnFull = 'stop';
34
+ let lastReset = null;
35
+ const keyOf = (selection) => {
36
+ if (selection === null)
37
+ return undefined;
38
+ return `${selection.provider}/${selection.model}`;
39
+ };
40
+ const isMonitoredKey = (key) => lastMonitored === null || lastMonitored.includes(key);
41
+ /** Pick an auto-switch target for the configured strategy. */
42
+ const pickSwitchTarget = (snapshot, currentKey) => {
43
+ const entryByKey = new Map(snapshot.entries.map(entry => [entry.key, entry]));
44
+ const candidates = [];
45
+ for (const group of lastGroups) {
46
+ for (const model of group.models) {
47
+ const key = `${group.id}/${model.id}`;
48
+ if (key === currentKey)
49
+ continue;
50
+ const entry = entryByKey.get(key);
51
+ candidates.push({
52
+ key,
53
+ provider: group.id,
54
+ model: model.id,
55
+ limit: entry?.limit ?? 0,
56
+ used: entry?.used ?? 0,
57
+ });
58
+ }
59
+ }
60
+ const available = (candidate) => isMonitoredKey(candidate.key) && (candidate.limit <= 0 || candidate.used < candidate.limit);
61
+ if (lastOnFull === 'switchQuota') {
62
+ return candidates
63
+ .filter(candidate => isMonitoredKey(candidate.key) && candidate.limit > 0 && candidate.used < candidate.limit)
64
+ .sort((a, b) => (a.used / a.limit) - (b.used / b.limit))[0];
65
+ }
66
+ if (lastOnFull === 'switchAll') {
67
+ return candidates.filter(available)[0];
68
+ }
69
+ // switchPriority: uncapped first, then unmonitored, then capped-but-free.
70
+ const uncapped = candidates.find(candidate => isMonitoredKey(candidate.key) && candidate.limit <= 0);
71
+ if (uncapped !== undefined)
72
+ return uncapped;
73
+ const unmonitored = candidates.find(candidate => !isMonitoredKey(candidate.key));
74
+ if (unmonitored !== undefined)
75
+ return unmonitored;
76
+ return candidates.find(available);
77
+ };
78
+ /** Run the configured full-quota strategy once per exhausted model. */
79
+ const actOnFull = (snapshot) => {
80
+ const currentKey = keyOf(lastCurrent);
81
+ if (currentKey === undefined)
82
+ return;
83
+ if (!isMonitoredKey(currentKey))
84
+ return;
85
+ const entry = snapshot.entries.find(candidate => candidate.key === currentKey);
86
+ if (entry === undefined || entry.limit <= 0 || entry.used < entry.limit)
87
+ return;
88
+ if (lastOnFull === 'stop' || lastSessionId === undefined) {
89
+ bound?.setFullNotice(t('fullNotice'));
90
+ return;
91
+ }
92
+ const target = pickSwitchTarget(snapshot, currentKey);
93
+ if (target === undefined) {
94
+ bound?.setFullNotice(t('fullSwitchFailed'));
95
+ return;
96
+ }
97
+ void connection.api.sessions.selectModel({
98
+ sessionId: lastSessionId,
99
+ provider: target.provider,
100
+ model: target.model,
101
+ }).then(({ result }) => {
102
+ if (result.ok) {
103
+ lastCurrent = result.value.selected;
104
+ bound?.setFullNotice(null);
105
+ pull();
106
+ }
107
+ else {
108
+ bound?.setFullNotice(t('fullSwitchFailed'));
109
+ }
110
+ }, () => { bound?.setFullNotice(t('fullSwitchFailed')); });
111
+ };
112
+ // Poll the Host's snapshot route and sync panel preferences. The Host is the
113
+ // single fact source for usage AND the resolved per-model caps; the panel
114
+ // converges within one poll interval (plus an immediate first pull). Every
115
+ // few pulls we re-read the model directory so a model switch made OUTSIDE
116
+ // this panel (the official selector, /model) moves the 「当前」 badge too.
117
+ let pullCount = 0;
118
+ const refreshCurrent = () => {
119
+ if (lastSessionId === undefined)
120
+ return;
121
+ void connection.api.sessions.models({ sessionId: lastSessionId }).then(({ result }) => {
122
+ if (result.ok) {
123
+ lastGroups = result.value.groups;
124
+ lastCurrent = result.value.current;
125
+ bound?.setDirectory(result.value.groups, result.value.current);
126
+ }
127
+ }, () => { });
128
+ };
129
+ const pull = () => {
130
+ pullCount += 1;
131
+ const doc = scope.getSnapshot().value;
132
+ lastMonitored = doc?.monitored !== undefined && doc.monitored.length > 0 ? doc.monitored : null;
133
+ lastOnFull = doc?.onFull ?? 'stop';
134
+ lastReset = doc?.reset !== undefined && doc.reset !== null
135
+ && typeof doc.reset === 'object'
136
+ && typeof doc.reset.offsetHours === 'number'
137
+ ? doc.reset
138
+ : null;
139
+ bound?.setSettings(lastMonitored, lastOnFull);
140
+ bound?.setReset(lastReset);
141
+ void fetch('/token-quota', { headers: { accept: 'application/json' } }).then((response) => {
142
+ if (!response.ok) {
143
+ bound?.setError(`quota route: ${String(response.status)}`);
144
+ return;
145
+ }
146
+ return response.json();
147
+ }, () => {
148
+ bound?.setError('quota snapshot pull failed');
149
+ }).then((snapshot) => {
150
+ if (snapshot === undefined)
151
+ return;
152
+ bound?.setSnapshot(snapshot);
153
+ actOnFull(snapshot);
154
+ });
155
+ if (pullCount % 4 === 0)
156
+ refreshCurrent();
157
+ };
158
+ ctx.effect(() => {
159
+ pull();
160
+ const timer = setInterval(pull, POLL_INTERVAL_MS);
161
+ return () => { clearInterval(timer); };
162
+ }, 'token-quota-ui: snapshot poll + full strategy');
163
+ const load = (sessionId) => {
164
+ lastSessionId = sessionId;
165
+ bound?.setLoading(true);
166
+ bound?.setError(null);
167
+ void connection.api.sessions.models({ sessionId }).then(({ result }) => {
168
+ if (result.ok) {
169
+ lastGroups = result.value.groups;
170
+ lastCurrent = result.value.current;
171
+ bound?.setDirectory(result.value.groups, result.value.current);
172
+ bound?.setLoading(false);
173
+ }
174
+ else {
175
+ bound?.setError(`${result.error.code}: ${result.error.message}`);
176
+ bound?.setLoading(false);
177
+ }
178
+ }, () => {
179
+ bound?.setLoading(false);
180
+ bound?.setError('directory load failed');
181
+ });
182
+ };
183
+ const setLimit = (key, limit) => {
184
+ const current = scope.getSnapshot().value?.limits ?? {};
185
+ void scope.set('limits', { ...current, [key]: limit });
186
+ };
187
+ const setMonitored = (monitored) => {
188
+ lastMonitored = monitored;
189
+ bound?.setSettings(lastMonitored, lastOnFull);
190
+ void scope.set('monitored', monitored ?? []);
191
+ };
192
+ const setOnFull = (action) => {
193
+ lastOnFull = action;
194
+ bound?.setSettings(lastMonitored, lastOnFull);
195
+ void scope.set('onFull', action);
196
+ };
197
+ const setReset = (reset) => {
198
+ lastReset = reset;
199
+ bound?.setReset(reset);
200
+ void scope.set('reset', reset);
201
+ };
202
+ const selectModel = (sessionId, provider, model) => {
203
+ void connection.api.sessions.selectModel({ sessionId, provider, model }).then(({ result }) => {
204
+ if (result.ok) {
205
+ lastCurrent = result.value.selected;
206
+ bound?.setDirectory(lastGroups, result.value.selected);
207
+ // Immediately re-pull the snapshot so the previous model's final
208
+ // usage (credited around the switch) shows without waiting 3 s.
209
+ pull();
210
+ }
211
+ else {
212
+ bound?.setError(`${result.error.code}: ${result.error.message}`);
213
+ }
214
+ }, () => {
215
+ bound?.setError('model switch failed');
216
+ });
217
+ };
218
+ const injected = (actions) => {
219
+ bound = actions;
220
+ return { load, setLimit, selectModel, setMonitored, setOnFull, setReset };
221
+ };
222
+ ctx.slots.inject('shell.overlay', () => ctx.slots.register({
223
+ name: 'shell.overlay',
224
+ id: 'token-quota',
225
+ order: 100,
226
+ label: () => t('title'),
227
+ store,
228
+ locale: NS,
229
+ inject: injected,
230
+ }, TokenQuotaPanel));
231
+ }
232
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Token-quota panel copy (zh + en). The panel is a product surface, so the
3
+ * primary copy is Chinese with an English pair, mirroring the shipped UI.
4
+ *
5
+ * @module @jxgame2020/dsh-token-quota/client/locales
6
+ */
7
+ /** Simplified Chinese copy (primary). */
8
+ export declare const zh: {
9
+ title: string;
10
+ subtitle: string;
11
+ current: string;
12
+ unlimited: string;
13
+ usedToday: string;
14
+ limitLabel: string;
15
+ limitPlaceholder: string;
16
+ save: string;
17
+ select: string;
18
+ setLimitHint: string;
19
+ loading: string;
20
+ waiting: string;
21
+ loadError: string;
22
+ empty: string;
23
+ collapse: string;
24
+ expand: string;
25
+ settings: string;
26
+ settingsTitle: string;
27
+ monitorLabel: string;
28
+ monitorHint: string;
29
+ monitorAll: string;
30
+ monitorNone: string;
31
+ noMonitored: string;
32
+ fullActionLabel: string;
33
+ fullStop: string;
34
+ fullSwitchQuota: string;
35
+ fullSwitchAll: string;
36
+ fullSwitchPriority: string;
37
+ fullNotice: string;
38
+ fullSwitchFailed: string;
39
+ close: string;
40
+ logs: string;
41
+ logsTitle: string;
42
+ logEmpty: string;
43
+ logDay: string;
44
+ logModel: string;
45
+ logUsed: string;
46
+ resetLabel: string;
47
+ resetHint: string;
48
+ resetOffset: string;
49
+ resetTime: string;
50
+ };
51
+ /** Dictionary keys owned by the token-quota panel (a string-literal union). */
52
+ export type TokenQuotaKey = keyof typeof zh;
53
+ /** English copy. */
54
+ export declare const en: {
55
+ title: string;
56
+ subtitle: string;
57
+ current: string;
58
+ unlimited: string;
59
+ usedToday: string;
60
+ limitLabel: string;
61
+ limitPlaceholder: string;
62
+ save: string;
63
+ select: string;
64
+ setLimitHint: string;
65
+ loading: string;
66
+ waiting: string;
67
+ loadError: string;
68
+ empty: string;
69
+ collapse: string;
70
+ expand: string;
71
+ settings: string;
72
+ settingsTitle: string;
73
+ monitorLabel: string;
74
+ monitorHint: string;
75
+ monitorAll: string;
76
+ monitorNone: string;
77
+ noMonitored: string;
78
+ fullActionLabel: string;
79
+ fullStop: string;
80
+ fullSwitchQuota: string;
81
+ fullSwitchAll: string;
82
+ fullSwitchPriority: string;
83
+ fullNotice: string;
84
+ fullSwitchFailed: string;
85
+ close: string;
86
+ logs: string;
87
+ logsTitle: string;
88
+ logEmpty: string;
89
+ logDay: string;
90
+ logModel: string;
91
+ logUsed: string;
92
+ resetLabel: string;
93
+ resetHint: string;
94
+ resetOffset: string;
95
+ resetTime: string;
96
+ };
97
+ //# sourceMappingURL=locales.d.ts.map
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Token-quota panel copy (zh + en). The panel is a product surface, so the
3
+ * primary copy is Chinese with an English pair, mirroring the shipped UI.
4
+ *
5
+ * @module @jxgame2020/dsh-token-quota/client/locales
6
+ */
7
+ /** Simplified Chinese copy (primary). */
8
+ export const zh = {
9
+ title: '每日 Token 限额',
10
+ subtitle: '按模型实时统计今天的 Token 消耗',
11
+ current: '当前',
12
+ unlimited: '不限',
13
+ usedToday: '今日已用',
14
+ limitLabel: '上限',
15
+ limitPlaceholder: '填数字,0=不限',
16
+ save: '保存',
17
+ select: '选择',
18
+ setLimitHint: '设置限额',
19
+ loading: '加载中…',
20
+ waiting: '等待数据…',
21
+ loadError: '加载失败',
22
+ empty: '暂无模型数据',
23
+ collapse: '收起',
24
+ expand: '展开',
25
+ settings: '设置',
26
+ settingsTitle: '限额设置',
27
+ monitorLabel: '监控模型',
28
+ monitorHint: '未勾选的模型不显示、不计入用量、不受限额限制',
29
+ monitorAll: '全部',
30
+ monitorNone: '无',
31
+ noMonitored: '未监控任何模型',
32
+ fullActionLabel: '满额后处理',
33
+ fullStop: '停止请求并提示',
34
+ fullSwitchQuota: '自动切换到其它限额模型',
35
+ fullSwitchAll: '自动切换到其它可用模型(含非限额)',
36
+ fullSwitchPriority: '自动切换(优先非限额,其次未监控)',
37
+ fullNotice: '当前模型今日额度已用尽,请求已停止。请选择其它模型或调整限额。',
38
+ fullSwitchFailed: '没有可切换的模型(所有限额模型均已满额)',
39
+ close: '关闭',
40
+ logs: '日志',
41
+ logsTitle: '用量日志',
42
+ logEmpty: '暂无记录',
43
+ logDay: '日期',
44
+ logModel: '模型',
45
+ logUsed: '用量',
46
+ resetLabel: '每日重置时间',
47
+ resetHint: '选择时区与时间,到点自动清零当日计数',
48
+ resetOffset: '时区',
49
+ resetTime: '时间',
50
+ };
51
+ /** English copy. */
52
+ export const en = {
53
+ title: 'Daily Token Quota',
54
+ subtitle: 'Real-time per-model token usage today',
55
+ current: 'current',
56
+ unlimited: 'unlimited',
57
+ usedToday: 'used today',
58
+ limitLabel: 'limit',
59
+ limitPlaceholder: 'number, 0 = unlimited',
60
+ save: 'Save',
61
+ select: 'Select',
62
+ setLimitHint: 'Set limit',
63
+ loading: 'Loading…',
64
+ waiting: 'Waiting…',
65
+ loadError: 'Load failed',
66
+ empty: 'No model data',
67
+ collapse: 'Collapse',
68
+ expand: 'Expand',
69
+ settings: 'Settings',
70
+ settingsTitle: 'Quota Settings',
71
+ monitorLabel: 'Monitored models',
72
+ monitorHint: 'Unchecked models are hidden, not metered, and never capped',
73
+ monitorAll: 'All',
74
+ monitorNone: 'None',
75
+ noMonitored: 'No model monitored',
76
+ fullActionLabel: 'When a model is full',
77
+ fullStop: 'Stop and prompt',
78
+ fullSwitchQuota: 'Switch to another quota model',
79
+ fullSwitchAll: 'Switch to any other model (incl. uncapped)',
80
+ fullSwitchPriority: 'Prefer uncapped, then unmonitored',
81
+ fullNotice: 'Today\u2019s quota for the current model is exhausted; the request was stopped. Pick another model or raise its limit.',
82
+ fullSwitchFailed: 'No switchable model (all quota models are full)',
83
+ close: 'Close',
84
+ logs: 'Logs',
85
+ logsTitle: 'Usage Log',
86
+ logEmpty: 'No records',
87
+ logDay: 'Day',
88
+ logModel: 'Model',
89
+ logUsed: 'Used',
90
+ resetLabel: 'Daily reset',
91
+ resetHint: 'Pick a timezone and time; counters reset there',
92
+ resetOffset: 'Timezone',
93
+ resetTime: 'Time',
94
+ };
95
+ //# sourceMappingURL=locales.js.map
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Token-quota panel store: the shared, remount-surviving view state. The
3
+ * apply-world is the only writer — forwarded `token-quota/updated` snapshots
4
+ * and the model-directory loader feed it — while the panel reads through
5
+ * `useStore`. Display rows are derived data (pure function over the two
6
+ * sources), so the component builds them with `useMemo`, never a store scan.
7
+ *
8
+ * @module @deepseek-ai/dsh-client-ui-token-quota/client/store
9
+ */
10
+ import { type EngineStoreHandle } from '@deepseek-ai/dsh-client-runtime/client';
11
+ import type { ModelProviderGroup, ModelSelection } from '@deepseek-ai/dsh-api-remotes/client';
12
+ import type { TokenQuotaFullAction, TokenQuotaLog, TokenQuotaReset, TokenQuotaSnapshot } from '@jxgame2020/dsh-token-quota/types';
13
+ /** One rendered model row: directory identity merged with the quota snapshot. */
14
+ export interface ModelQuotaRow {
15
+ /** Stable key `provider/model`. */
16
+ key: string;
17
+ /** Registered provider route. */
18
+ provider: string;
19
+ /** Provider-owned model id. */
20
+ model: string;
21
+ /** Display name from the directory, falling back to the model id. */
22
+ name: string;
23
+ /** Tokens used today (input + output + cache). */
24
+ used: number;
25
+ /** Daily cap; `0` means unlimited. */
26
+ limit: number;
27
+ /** Whether the current session uses this route. */
28
+ current: boolean;
29
+ }
30
+ /** Panel view state. */
31
+ export interface TokenQuotaPanelState {
32
+ /** Latest quota snapshot from the Host; null before the first event lands. */
33
+ snapshot: TokenQuotaSnapshot | null;
34
+ /** Advisory model directory of the current session (provider groups). */
35
+ groups: readonly ModelProviderGroup[];
36
+ /** Current model selection reported by the Host. */
37
+ current: ModelSelection | null;
38
+ /** Directory load in flight. */
39
+ loading: boolean;
40
+ /** Last load/selection failure text; null when none. */
41
+ error: string | null;
42
+ /**
43
+ * Model keys under active monitoring; `null` means every model (default).
44
+ * Unmonitored models are hidden, not metered, and never capped.
45
+ */
46
+ monitored: string[] | null;
47
+ /** Behavior when a monitored, capped model reaches its daily cap. */
48
+ onFull: TokenQuotaFullAction;
49
+ /** Whether the settings dialog is open. */
50
+ dialogOpen: boolean;
51
+ /** Full-quota notice shown for the `'stop'` strategy; null when none. */
52
+ fullNotice: string | null;
53
+ /** Daily reset moment; `null` = machine-local midnight. */
54
+ reset: TokenQuotaReset | null;
55
+ /** Whether the usage-log dialog is open. */
56
+ logOpen: boolean;
57
+ /** Latest usage history; null before the first fetch. */
58
+ log: TokenQuotaLog | null;
59
+ }
60
+ /** Declared write surface. */
61
+ export type TokenQuotaPanelActions = {
62
+ setSnapshot: (d: TokenQuotaPanelState, snapshot: TokenQuotaSnapshot) => void;
63
+ setDirectory: (d: TokenQuotaPanelState, groups: readonly ModelProviderGroup[], current: ModelSelection | null) => void;
64
+ setLoading: (d: TokenQuotaPanelState, loading: boolean) => void;
65
+ setError: (d: TokenQuotaPanelState, error: string | null) => void;
66
+ setSettings: (d: TokenQuotaPanelState, monitored: string[] | null, onFull: TokenQuotaFullAction) => void;
67
+ setDialogOpen: (d: TokenQuotaPanelState, open: boolean) => void;
68
+ setFullNotice: (d: TokenQuotaPanelState, notice: string | null) => void;
69
+ setReset: (d: TokenQuotaPanelState, reset: TokenQuotaReset | null) => void;
70
+ setLogOpen: (d: TokenQuotaPanelState, open: boolean) => void;
71
+ setLog: (d: TokenQuotaPanelState, log: TokenQuotaLog | null) => void;
72
+ };
73
+ /**
74
+ * Declares the panel state and write surface.
75
+ * @returns the store handle.
76
+ */
77
+ export declare function createTokenQuotaPanelStore(): EngineStoreHandle<TokenQuotaPanelState, TokenQuotaPanelActions>;
78
+ /**
79
+ * Merge the session's model directory with the quota snapshot into display
80
+ * rows. Every directory model gets a row (usage/limit fall back to
81
+ * `0`/`0`), and snapshot entries for routes missing from the directory are
82
+ * appended so no counter is ever hidden.
83
+ * @param groups - advisory provider groups of the current session.
84
+ * @param snapshot - latest quota snapshot, or null before the first one.
85
+ * @param current - current model selection reported by the Host.
86
+ * @returns rows sorted by key.
87
+ */
88
+ export declare function mergeModelRows(groups: readonly ModelProviderGroup[], snapshot: TokenQuotaSnapshot | null, current: ModelSelection | null): ModelQuotaRow[];
89
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Token-quota panel store: the shared, remount-surviving view state. The
3
+ * apply-world is the only writer — forwarded `token-quota/updated` snapshots
4
+ * and the model-directory loader feed it — while the panel reads through
5
+ * `useStore`. Display rows are derived data (pure function over the two
6
+ * sources), so the component builds them with `useMemo`, never a store scan.
7
+ *
8
+ * @module @deepseek-ai/dsh-client-ui-token-quota/client/store
9
+ */
10
+ import { defineStore } from '@deepseek-ai/dsh-client-runtime/client';
11
+ /** Build the stable per-model key shared by the counter, settings, and snapshot. */
12
+ function tokenQuotaKey(provider, model) {
13
+ return `${provider}/${model}`;
14
+ }
15
+ /**
16
+ * Declares the panel state and write surface.
17
+ * @returns the store handle.
18
+ */
19
+ export function createTokenQuotaPanelStore() {
20
+ return defineStore({
21
+ init: () => ({
22
+ snapshot: null,
23
+ groups: [],
24
+ current: null,
25
+ loading: false,
26
+ error: null,
27
+ monitored: null,
28
+ onFull: 'stop',
29
+ dialogOpen: false,
30
+ fullNotice: null,
31
+ reset: null,
32
+ logOpen: false,
33
+ log: null,
34
+ }),
35
+ actions: {
36
+ setSnapshot: (d, snapshot) => { d.snapshot = snapshot; },
37
+ setDirectory: (d, groups, current) => { d.groups = groups; d.current = current; },
38
+ setLoading: (d, loading) => { d.loading = loading; },
39
+ setError: (d, error) => { d.error = error; },
40
+ setSettings: (d, monitored, onFull) => { d.monitored = monitored; d.onFull = onFull; },
41
+ setDialogOpen: (d, open) => { d.dialogOpen = open; },
42
+ setFullNotice: (d, notice) => { d.fullNotice = notice; },
43
+ setReset: (d, reset) => { d.reset = reset; },
44
+ setLogOpen: (d, open) => { d.logOpen = open; },
45
+ setLog: (d, log) => { d.log = log; },
46
+ },
47
+ });
48
+ }
49
+ /**
50
+ * Merge the session's model directory with the quota snapshot into display
51
+ * rows. Every directory model gets a row (usage/limit fall back to
52
+ * `0`/`0`), and snapshot entries for routes missing from the directory are
53
+ * appended so no counter is ever hidden.
54
+ * @param groups - advisory provider groups of the current session.
55
+ * @param snapshot - latest quota snapshot, or null before the first one.
56
+ * @param current - current model selection reported by the Host.
57
+ * @returns rows sorted by key.
58
+ */
59
+ export function mergeModelRows(groups, snapshot, current) {
60
+ const entryByKey = new Map((snapshot?.entries ?? []).map(entry => [entry.key, entry]));
61
+ const rows = new Map();
62
+ for (const group of groups) {
63
+ for (const model of group.models) {
64
+ const key = tokenQuotaKey(group.id, model.id);
65
+ const entry = entryByKey.get(key);
66
+ rows.set(key, {
67
+ key,
68
+ provider: group.id,
69
+ model: model.id,
70
+ name: model.name,
71
+ used: entry?.used ?? 0,
72
+ limit: entry?.limit ?? 0,
73
+ current: current !== null && current.provider === group.id && current.model === model.id,
74
+ });
75
+ }
76
+ }
77
+ for (const entry of snapshot?.entries ?? []) {
78
+ if (rows.has(entry.key))
79
+ continue;
80
+ rows.set(entry.key, {
81
+ key: entry.key,
82
+ provider: entry.provider,
83
+ model: entry.model,
84
+ name: entry.model,
85
+ used: entry.used,
86
+ limit: entry.limit,
87
+ current: current !== null && current.provider === entry.provider && current.model === entry.model,
88
+ });
89
+ }
90
+ return [...rows.values()].sort((left, right) => left.key.localeCompare(right.key));
91
+ }
92
+ //# sourceMappingURL=store.js.map