@polderlabs/bizar 4.4.11 → 4.4.12

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.
@@ -29,10 +29,10 @@
29
29
  }
30
30
  })();
31
31
  </script>
32
- <script type="module" crossorigin src="/assets/main-CEazNxxy.js"></script>
32
+ <script type="module" crossorigin src="/assets/main-EK_fzXn_.js"></script>
33
33
  <link rel="modulepreload" crossorigin href="/assets/mobile-Dl1q7Cyq.js">
34
34
  <link rel="stylesheet" crossorigin href="/assets/mobile-CsZQAswA.css">
35
- <link rel="stylesheet" crossorigin href="/assets/main-Nq8Dq3VR.css">
35
+ <link rel="stylesheet" crossorigin href="/assets/main-BKXEqU1w.css">
36
36
  </head>
37
37
  <body>
38
38
  <div id="root"></div>
@@ -48,6 +48,7 @@ import { createDiagnosticsRouter } from './routes/diagnostics.mjs';
48
48
  import { createPairRouter } from './routes/pair.mjs';
49
49
  import { createThemesRouter } from './routes/themes.mjs';
50
50
  import { createNotificationsRouter } from './routes/notifications.mjs';
51
+ import { createMinimaxRouter } from './routes/minimax.mjs';
51
52
  import { createMiscRouter } from './routes/misc.mjs';
52
53
 
53
54
  /**
@@ -118,6 +119,7 @@ export async function createApiRouter({
118
119
  router.use(createThemesRouter({ state }));
119
120
  router.use(createNotificationsRouter({ broadcast }));
120
121
  router.use(createArtifactsRouter({ state, broadcast, projectRoot }));
122
+ router.use(createMinimaxRouter({ state, broadcast }));
121
123
  router.use(createMiscRouter({ state, broadcast }));
122
124
 
123
125
  // /api/auth/* must be reachable WITHOUT the bearer token so a fresh
@@ -0,0 +1,352 @@
1
+ /**
2
+ * src/server/minimax.mjs
3
+ *
4
+ * v4.5.0 — MiniMax API client.
5
+ *
6
+ * Owns the network calls to platform.minimax.io for the Bizar
7
+ * dashboard. Two surfaces:
8
+ *
9
+ * 1. fetchRemains() — the public `/v1/token_plan/remains` endpoint.
10
+ * Returns the user's remaining Token Plan quota per model — both
11
+ * 5-hour rolling window and weekly window — including the reset
12
+ * times for each. This is the headline data the user wants to see.
13
+ *
14
+ * 2. chatCompletion() — a thin wrapper around `/v1/chat/completions`
15
+ * (OpenAI-compatible) for the dashboard's "Send a test prompt"
16
+ * button. Also captures the `usage` block from the response so the
17
+ * dashboard can show per-call consumption.
18
+ *
19
+ * The client always reads the API key from the user's settings
20
+ * (settings.minimax.apiKey) and never logs it. The base URL is
21
+ * configurable but defaults to `https://www.minimax.io` per the docs.
22
+ *
23
+ * Token Plan endpoints live on `www.minimax.io` (NOT `api.minimax.io`
24
+ * — the chat completions endpoint uses that one). The default
25
+ * `baseUrl` in the settings uses the Token Plan host so `fetchRemains`
26
+ * works out of the box.
27
+ *
28
+ * All network calls are guarded by a 10s timeout so a hung API
29
+ * never wedges the dashboard.
30
+ */
31
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
32
+ import { dirname, join } from 'node:path';
33
+ import { homedir } from 'node:os';
34
+
35
+ const HOME = homedir();
36
+ const BIZAR_HOME = join(HOME, '.config', 'bizar');
37
+ const CACHE_DIR = join(BIZAR_HOME, 'minimax');
38
+ const CACHE_FILE = join(CACHE_DIR, 'remains-cache.json');
39
+
40
+ // Default base URL. The docs say `https://www.minimax.io/v1/token_plan/remains`
41
+ // (note: the `www` host, not `api`). Allow override per-user in settings.
42
+ export const DEFAULT_BASE_URL = 'https://www.minimax.io';
43
+ // Chat completions / OpenAI-style surface lives on api.minimax.io.
44
+ export const DEFAULT_CHAT_BASE_URL = 'https://api.minimax.io/v1';
45
+
46
+ // Models Bizar exposes by default in the dashboard.
47
+ export const KNOWN_MODELS = [
48
+ 'MiniMax-M3',
49
+ 'MiniMax-M2.7',
50
+ 'MiniMax-M2.5',
51
+ 'MiniMax-M2.1',
52
+ 'MiniMax-M2',
53
+ ];
54
+
55
+ /**
56
+ * Read the merged settings.json and return the MiniMax config block.
57
+ * Never throws — returns sane defaults on parse failure.
58
+ */
59
+ export function readMinimaxSettings() {
60
+ const file = join(BIZAR_HOME, 'settings.json');
61
+ if (!existsSync(file)) {
62
+ return defaultMinimaxSettings();
63
+ }
64
+ try {
65
+ const parsed = JSON.parse(readFileSync(file, 'utf8'));
66
+ const m = parsed.minimax;
67
+ if (!m || typeof m !== 'object') return defaultMinimaxSettings();
68
+ return {
69
+ enabled: m.enabled !== false,
70
+ apiKey: typeof m.apiKey === 'string' ? m.apiKey : '',
71
+ groupId: typeof m.groupId === 'string' ? m.groupId : 'default',
72
+ baseUrl: typeof m.baseUrl === 'string' ? m.baseUrl : DEFAULT_BASE_URL,
73
+ chatBaseUrl: typeof m.chatBaseUrl === 'string' ? m.chatBaseUrl : DEFAULT_CHAT_BASE_URL,
74
+ };
75
+ } catch {
76
+ return defaultMinimaxSettings();
77
+ }
78
+ }
79
+
80
+ export function defaultMinimaxSettings() {
81
+ return {
82
+ enabled: true,
83
+ apiKey: '',
84
+ groupId: 'default',
85
+ baseUrl: DEFAULT_BASE_URL,
86
+ chatBaseUrl: DEFAULT_CHAT_BASE_URL,
87
+ };
88
+ }
89
+
90
+ // ─── Cache (in-memory + disk) ──────────────────────────────────────────────
91
+
92
+ /**
93
+ * In-memory snapshot of the last remains response. Avoids re-hitting the
94
+ * API on every dashboard render.
95
+ */
96
+ let memoryCache = null;
97
+
98
+ function readDiskCache() {
99
+ try {
100
+ if (!existsSync(CACHE_FILE)) return null;
101
+ const stat = require('node:fs').statSync(CACHE_FILE);
102
+ if (Date.now() - stat.mtimeMs > 5 * 60 * 1000) return null; // 5 min TTL
103
+ return JSON.parse(readFileSync(CACHE_FILE, 'utf8'));
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ function writeDiskCache(snapshot) {
110
+ try {
111
+ mkdirSync(CACHE_DIR, { recursive: true });
112
+ writeFileSync(CACHE_FILE, JSON.stringify(snapshot, null, 2) + '\n', 'utf8');
113
+ } catch {
114
+ /* best-effort */
115
+ }
116
+ }
117
+
118
+ export function clearRemainsCache() {
119
+ memoryCache = null;
120
+ try {
121
+ if (existsSync(CACHE_FILE)) {
122
+ require('node:fs').unlinkSync(CACHE_FILE);
123
+ }
124
+ } catch { /* best-effort */ }
125
+ }
126
+
127
+ /**
128
+ * Try in-memory → disk cache. Returns the cached snapshot or null.
129
+ */
130
+ export function readCachedRemains() {
131
+ if (memoryCache) return memoryCache;
132
+ const fromDisk = readDiskCache();
133
+ if (fromDisk) {
134
+ memoryCache = fromDisk;
135
+ return fromDisk;
136
+ }
137
+ return null;
138
+ }
139
+
140
+ // ─── Network helpers ──────────────────────────────────────────────────────
141
+
142
+ /**
143
+ * Fetch with a hard timeout. Uses undici (Node 18+ global fetch is
144
+ * fine but AbortSignal.timeout is simpler with node:fetch).
145
+ */
146
+ async function fetchWithTimeout(url, opts = {}, timeoutMs = 10_000) {
147
+ const ctl = new AbortController();
148
+ const t = setTimeout(() => ctl.abort(), timeoutMs);
149
+ try {
150
+ return await fetch(url, { ...opts, signal: ctl.signal });
151
+ } finally {
152
+ clearTimeout(t);
153
+ }
154
+ }
155
+
156
+ // ─── Public API ────────────────────────────────────────────────────────────
157
+
158
+ /**
159
+ * Hit /v1/token_plan/remains with the user's API key + group_id.
160
+ *
161
+ * Response shape:
162
+ * {
163
+ * model_remains: [
164
+ * { model_name, current_interval_remaining_percent, current_weekly_remaining_percent,
165
+ * start_time, end_time, remains_time, weekly_start_time, weekly_end_time,
166
+ * weekly_remains_time, current_interval_total_count, current_interval_usage_count,
167
+ * current_weekly_total_count, current_weekly_usage_count,
168
+ * current_interval_status, current_weekly_status },
169
+ * ...
170
+ * ],
171
+ * base_resp: { status_code, status_msg }
172
+ * }
173
+ *
174
+ * Returns the raw response, augmented with `fetchedAt` and (if we got
175
+ * data) the per-model `endTimeISO` / `weeklyEndTimeISO` strings for
176
+ * convenient rendering in the UI.
177
+ */
178
+ export async function fetchRemains({ apiKey, groupId = 'default', baseUrl = DEFAULT_BASE_URL, force = false } = {}) {
179
+ if (!apiKey || !apiKey.trim()) {
180
+ return { ok: false, error: 'no_api_key', message: 'MiniMax API key is not configured' };
181
+ }
182
+ if (!force) {
183
+ const cached = readCachedRemains();
184
+ if (cached && cached.apiKeyHint === maskKey(apiKey) && (Date.now() - cached.fetchedAt) < 60_000) {
185
+ return { ok: true, cached: true, ...cached };
186
+ }
187
+ }
188
+ const url = `${baseUrl.replace(/\/$/, '')}/v1/token_plan/remains?group_id=${encodeURIComponent(groupId)}`;
189
+ let resp;
190
+ try {
191
+ resp = await fetchWithTimeout(url, {
192
+ method: 'GET',
193
+ headers: {
194
+ Authorization: `Bearer ${apiKey}`,
195
+ 'Content-Type': 'application/json',
196
+ Accept: 'application/json',
197
+ },
198
+ });
199
+ } catch (err) {
200
+ return { ok: false, error: 'network_error', message: err.message || 'fetch failed' };
201
+ }
202
+ const text = await resp.text();
203
+ let body = null;
204
+ try { body = text ? JSON.parse(text) : null; } catch { /* keep as null */ }
205
+ if (!resp.ok) {
206
+ return {
207
+ ok: false,
208
+ error: `http_${resp.status}`,
209
+ message: body?.base_resp?.status_msg || resp.statusText || 'request failed',
210
+ status: resp.status,
211
+ };
212
+ }
213
+ if (!body || body.base_resp?.status_code !== 0) {
214
+ return {
215
+ ok: false,
216
+ error: 'api_error',
217
+ message: body?.base_resp?.status_msg || 'unknown error',
218
+ raw: body,
219
+ };
220
+ }
221
+
222
+ // Augment with ISO timestamps so the UI doesn't have to do ms → Date
223
+ // arithmetic for every cell.
224
+ const models = (body.model_remains || []).map((m) => ({
225
+ ...m,
226
+ endTimeISO: new Date(m.end_time).toISOString(),
227
+ weeklyEndTimeISO: new Date(m.weekly_end_time).toISOString(),
228
+ intervalResetInMs: m.remains_time,
229
+ weeklyResetInMs: m.weekly_remains_time,
230
+ intervalResetInHuman: humanizeDuration(m.remains_time),
231
+ weeklyResetInHuman: humanizeDuration(m.weekly_remains_time),
232
+ intervalConsumedPercent: clamp(100 - (m.current_interval_remaining_percent || 0), 0, 100),
233
+ weeklyConsumedPercent: clamp(100 - (m.current_weekly_remaining_percent || 0), 0, 100),
234
+ intervalUsed: m.current_interval_usage_count || 0,
235
+ intervalTotal: m.current_interval_total_count || 0,
236
+ weeklyUsed: m.current_weekly_usage_count || 0,
237
+ weeklyTotal: m.current_weekly_total_count || 0,
238
+ }));
239
+
240
+ const snapshot = {
241
+ ok: true,
242
+ fetchedAt: Date.now(),
243
+ apiKeyHint: maskKey(apiKey),
244
+ groupId,
245
+ baseUrl,
246
+ models,
247
+ baseResp: body.base_resp,
248
+ };
249
+ memoryCache = snapshot;
250
+ writeDiskCache(snapshot);
251
+ return snapshot;
252
+ }
253
+
254
+ /**
255
+ * Send a single chat-completion to /v1/chat/completions and return
256
+ * the parsed response + parsed `usage` block. Used by the dashboard's
257
+ * "Send a test prompt" button so the user can verify their key works
258
+ * and see live token usage.
259
+ */
260
+ export async function chatCompletion({
261
+ apiKey,
262
+ prompt,
263
+ model = 'MiniMax-M3',
264
+ baseUrl = DEFAULT_CHAT_BASE_URL,
265
+ maxTokens = 256,
266
+ } = {}) {
267
+ if (!apiKey || !apiKey.trim()) {
268
+ return { ok: false, error: 'no_api_key', message: 'MiniMax API key is not configured' };
269
+ }
270
+ if (!prompt || !prompt.trim()) {
271
+ return { ok: false, error: 'no_prompt', message: 'prompt is empty' };
272
+ }
273
+ const url = `${baseUrl.replace(/\/$/, '')}/chat/completions`;
274
+ const body = JSON.stringify({
275
+ model,
276
+ messages: [{ role: 'user', content: prompt }],
277
+ max_completion_tokens: maxTokens,
278
+ stream: false,
279
+ });
280
+ let resp;
281
+ try {
282
+ resp = await fetchWithTimeout(url, {
283
+ method: 'POST',
284
+ headers: {
285
+ Authorization: `Bearer ${apiKey}`,
286
+ 'Content-Type': 'application/json',
287
+ Accept: 'application/json',
288
+ },
289
+ body,
290
+ });
291
+ } catch (err) {
292
+ return { ok: false, error: 'network_error', message: err.message || 'fetch failed' };
293
+ }
294
+ const text = await resp.text();
295
+ let data = null;
296
+ try { data = text ? JSON.parse(text) : null; } catch { /* ignore */ }
297
+ if (!resp.ok) {
298
+ return {
299
+ ok: false,
300
+ error: `http_${resp.status}`,
301
+ message: data?.base_resp?.status_msg || resp.statusText || 'request failed',
302
+ status: resp.status,
303
+ raw: data,
304
+ };
305
+ }
306
+ if (!data || data.base_resp?.status_code !== 0) {
307
+ return {
308
+ ok: false,
309
+ error: 'api_error',
310
+ message: data?.base_resp?.status_msg || 'unknown error',
311
+ raw: data,
312
+ };
313
+ }
314
+ const choice = (data.choices || [])[0] || {};
315
+ const content = choice?.message?.content || '';
316
+ return {
317
+ ok: true,
318
+ model: data.model || model,
319
+ content,
320
+ reasoning: choice?.message?.reasoning_content || null,
321
+ finishReason: choice?.finish_reason || null,
322
+ usage: data.usage || null,
323
+ baseResp: data.base_resp,
324
+ };
325
+ }
326
+
327
+ // ─── Helpers ────────────────────────────────────────────────────────────────
328
+
329
+ /** Mask the API key for safe logging / display. Shows last 4 chars. */
330
+ export function maskKey(key) {
331
+ if (!key) return '';
332
+ const trimmed = String(key).trim();
333
+ if (trimmed.length <= 6) return '****';
334
+ return `****${trimmed.slice(-4)}`;
335
+ }
336
+
337
+ /** Convert a millisecond duration into a compact human label. */
338
+ export function humanizeDuration(ms) {
339
+ if (typeof ms !== 'number' || !Number.isFinite(ms) || ms <= 0) return 'now';
340
+ const sec = Math.floor(ms / 1000);
341
+ const min = Math.floor(sec / 60);
342
+ const hr = Math.floor(min / 60);
343
+ const day = Math.floor(hr / 24);
344
+ if (day >= 1) return `${day}d ${hr % 24}h`;
345
+ if (hr >= 1) return `${hr}h ${min % 60}m`;
346
+ if (min >= 1) return `${min}m`;
347
+ return `${sec}s`;
348
+ }
349
+
350
+ function clamp(n, lo, hi) {
351
+ return Math.max(lo, Math.min(hi, n));
352
+ }
@@ -134,6 +134,20 @@ export const DEFAULT_SETTINGS = {
134
134
  provider: 'opencode',
135
135
  model: 'opencode/deepseek-v4-flash-free',
136
136
  },
137
+ // v4.5.0 — MiniMax Token Plan integration. `apiKey` is the
138
+ // user's Subscription Key from
139
+ // https://platform.minimax.io/user-center/payment/token-plan
140
+ // `groupId` is usually 'default' for an individual team. `baseUrl`
141
+ // is the Token Plan host (the remains endpoint lives on www.*, NOT
142
+ // api.*). `chatBaseUrl` is the chat-completions host (api.*).
143
+ // No key is shipped by default — the user enters their own.
144
+ minimax: {
145
+ enabled: true,
146
+ apiKey: '',
147
+ groupId: 'default',
148
+ baseUrl: 'https://www.minimax.io',
149
+ chatBaseUrl: 'https://api.minimax.io/v1',
150
+ },
137
151
  };
138
152
 
139
153
  /**
@@ -159,6 +173,7 @@ export function mergeSettings(existing) {
159
173
  merged.about = { ...DEFAULT_SETTINGS.about, ...(existing.about || {}) };
160
174
  merged.agents = { ...DEFAULT_SETTINGS.agents, ...(existing.agents || {}) };
161
175
  merged.systemLlm = { ...DEFAULT_SETTINGS.systemLlm, ...(existing.systemLlm || {}) };
176
+ merged.minimax = { ...DEFAULT_SETTINGS.minimax, ...(existing.minimax || {}) };
162
177
  // Always use the package version — never let user settings override it
163
178
  merged.about.version = DEFAULT_SETTINGS.about.version;
164
179
  return merged;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * src/server/routes/minimax.mjs
3
+ *
4
+ * v4.5.0 — REST surface for the MiniMax Token Plan integration.
5
+ *
6
+ * Endpoints:
7
+ * GET /api/minimax/status — current key + cached remains summary
8
+ * GET /api/minimax/remains — full remains snapshot (5h + weekly per model)
9
+ * POST /api/minimax/remains/refresh — force re-fetch from the API
10
+ * POST /api/minimax/test — smoke-test the key with a one-shot chat call
11
+ * GET /api/minimax/cache — read the on-disk cache file (for debugging)
12
+ *
13
+ * All endpoints read the API key from settings.json (no key is ever
14
+ * accepted as a URL param or body field — the user puts it in the
15
+ * settings UI). Responses never echo the key back; they include a
16
+ * masked `apiKeyHint` (last 4 chars only).
17
+ */
18
+ import { Router } from 'express';
19
+ import {
20
+ fetchRemains,
21
+ chatCompletion,
22
+ readCachedRemains,
23
+ clearRemainsCache,
24
+ readMinimaxSettings,
25
+ maskKey,
26
+ KNOWN_MODELS,
27
+ } from '../minimax.mjs';
28
+ import { wrap } from './_shared.mjs';
29
+
30
+ /**
31
+ * @param {object} deps
32
+ * @param {object} deps.state
33
+ * @param {Function} deps.broadcast
34
+ * @returns {import('express').Router}
35
+ */
36
+ export function createMinimaxRouter({ state, broadcast }) {
37
+ const router = Router();
38
+
39
+ // GET / — health + config summary
40
+ router.get('/minimax/status', wrap(async (_req, res) => {
41
+ const settings = readMinimaxSettings();
42
+ const cached = readCachedRemains();
43
+ res.json({
44
+ enabled: settings.enabled,
45
+ configured: !!settings.apiKey,
46
+ apiKeyHint: maskKey(settings.apiKey),
47
+ groupId: settings.groupId,
48
+ baseUrl: settings.baseUrl,
49
+ chatBaseUrl: settings.chatBaseUrl,
50
+ knownModels: KNOWN_MODELS,
51
+ cache: cached
52
+ ? {
53
+ fetchedAt: cached.fetchedAt,
54
+ apiKeyHint: cached.apiKeyHint,
55
+ groupId: cached.groupId,
56
+ modelCount: (cached.models || []).length,
57
+ }
58
+ : null,
59
+ });
60
+ }));
61
+
62
+ // GET /remains — current snapshot (cache-or-fetch)
63
+ router.get('/minimax/remains', wrap(async (_req, res) => {
64
+ const settings = readMinimaxSettings();
65
+ if (!settings.enabled) {
66
+ res.json({ ok: false, error: 'disabled', message: 'MiniMax integration is disabled in settings' });
67
+ return;
68
+ }
69
+ const result = await fetchRemains({
70
+ apiKey: settings.apiKey,
71
+ groupId: settings.groupId,
72
+ baseUrl: settings.baseUrl,
73
+ });
74
+ if (broadcast && result.ok) {
75
+ broadcast({ type: 'minimax:remains', remains: result });
76
+ }
77
+ res.json(result);
78
+ }));
79
+
80
+ // POST /remains/refresh — force re-fetch (bypass cache TTL)
81
+ router.post('/minimax/remains/refresh', wrap(async (_req, res) => {
82
+ const settings = readMinimaxSettings();
83
+ if (!settings.enabled) {
84
+ res.json({ ok: false, error: 'disabled', message: 'MiniMax integration is disabled in settings' });
85
+ return;
86
+ }
87
+ const result = await fetchRemains({
88
+ apiKey: settings.apiKey,
89
+ groupId: settings.groupId,
90
+ baseUrl: settings.baseUrl,
91
+ force: true,
92
+ });
93
+ if (broadcast && result.ok) {
94
+ broadcast({ type: 'minimax:remains:refreshed', remains: result });
95
+ }
96
+ res.json(result);
97
+ }));
98
+
99
+ // POST /test — smoke-test the key with a chat completion
100
+ router.post('/minimax/test', wrap(async (req, res) => {
101
+ const settings = readMinimaxSettings();
102
+ if (!settings.enabled) {
103
+ res.json({ ok: false, error: 'disabled', message: 'MiniMax integration is disabled in settings' });
104
+ return;
105
+ }
106
+ if (!settings.apiKey) {
107
+ res.json({ ok: false, error: 'no_api_key', message: 'MiniMax API key is not configured' });
108
+ return;
109
+ }
110
+ const body = req.body || {};
111
+ const prompt = typeof body.prompt === 'string' && body.prompt.trim()
112
+ ? body.prompt
113
+ : 'Reply with the single word: ok';
114
+ const model = typeof body.model === 'string' && body.model.trim()
115
+ ? body.model.trim()
116
+ : 'MiniMax-M3';
117
+ const result = await chatCompletion({
118
+ apiKey: settings.apiKey,
119
+ prompt,
120
+ model,
121
+ baseUrl: settings.chatBaseUrl,
122
+ maxTokens: typeof body.maxTokens === 'number' ? body.maxTokens : 32,
123
+ });
124
+ res.json(result);
125
+ }));
126
+
127
+ // GET /cache — read the on-disk cache (debugging)
128
+ router.get('/minimax/cache', wrap(async (_req, res) => {
129
+ res.json(readCachedRemains() || null);
130
+ }));
131
+
132
+ // DELETE /cache — clear cache (admin endpoint)
133
+ router.delete('/minimax/cache', wrap(async (_req, res) => {
134
+ clearRemainsCache();
135
+ if (broadcast) broadcast({ type: 'minimax:cache:cleared' });
136
+ res.json({ ok: true });
137
+ }));
138
+
139
+ return router;
140
+ }
@@ -35,6 +35,7 @@ import { ModView, type ModView as ModViewType } from './views/ModView';
35
35
  import { Schedules } from './views/Schedules';
36
36
  import { Skills } from './views/Skills';
37
37
  import { History } from './views/History';
38
+ import { MiniMaxUsage } from './views/MiniMaxUsage';
38
39
  import { BackgroundAgents } from './views/BackgroundAgents';
39
40
  import { Spinner } from './components/Spinner';
40
41
  import { Button } from './components/Button';
@@ -75,6 +76,7 @@ const VIEW_MAP: Record<string, (p: ViewProps) => React.ReactNode> = {
75
76
  schedules: Schedules,
76
77
  skills: Skills,
77
78
  history: History,
79
+ minimax: MiniMaxUsage,
78
80
  };
79
81
 
80
82
  const VERSION = 'v3.21.0';
@@ -21,6 +21,7 @@ import {
21
21
  Sparkles,
22
22
  Activity,
23
23
  Radio,
24
+ Coins,
24
25
  type LucideIcon,
25
26
  } from 'lucide-react';
26
27
  import { cn } from '../lib/utils';
@@ -48,6 +49,7 @@ export const TABS: TabDef[] = [
48
49
  { id: 'mods', label: 'Mods', icon: Puzzle },
49
50
  { id: 'schedules', label: 'Schedules', icon: Clock },
50
51
  { id: 'history', label: 'History', icon: HistoryIcon },
52
+ { id: 'minimax', label: 'Usage', icon: Coins },
51
53
  { id: 'config', label: 'Config', icon: Settings2 },
52
54
  { id: 'settings', label: 'Settings', icon: Sliders },
53
55
  ];
@@ -265,6 +265,18 @@ export type Settings = {
265
265
  };
266
266
  // v3.21.0 — System LLM calls (auto-title, enhance-prompt, summarization).
267
267
  systemLlm?: SystemLlmConfig;
268
+ // v4.5.0 — MiniMax Token Plan integration. `apiKey` is the user's
269
+ // Subscription Key (NOT a pay-as-you-go API key). `groupId` is usually
270
+ // 'default' for an individual team. `baseUrl` is the Token Plan host
271
+ // (the remains endpoint lives on www.*, not api.*). `chatBaseUrl` is
272
+ // the chat-completions host (api.*/v1).
273
+ minimax?: {
274
+ enabled: boolean;
275
+ apiKey: string;
276
+ groupId: string;
277
+ baseUrl: string;
278
+ chatBaseUrl: string;
279
+ };
268
280
  };
269
281
 
270
282
  export type SettingsResponse = {
@@ -8,6 +8,7 @@ import { MobileApp } from './MobileApp';
8
8
  import './styles/main.css';
9
9
  import './styles/mobile.css';
10
10
  import './styles/glyphs.css';
11
+ import './styles/minimax-usage.css';
11
12
 
12
13
  function Root() {
13
14
  const [isMobile, setIsMobile] = useState(() => {