@polderlabs/bizar 4.4.12 → 4.4.13
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/bizar-dash/dist/assets/main-BB5mJurD.js +352 -0
- package/bizar-dash/dist/assets/main-BB5mJurD.js.map +1 -0
- package/bizar-dash/dist/assets/{main-BKXEqU1w.css → main-BsnQLXdh.css} +1 -1
- package/bizar-dash/dist/index.html +2 -2
- package/bizar-dash/src/server/minimax.mjs +231 -105
- package/bizar-dash/src/server/providers-store.mjs +10 -6
- package/bizar-dash/src/server/routes/minimax.mjs +50 -57
- package/bizar-dash/src/web/styles/minimax-usage.css +136 -0
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +387 -22
- package/cli/bin.mjs +263 -3
- package/package.json +1 -1
- package/bizar-dash/dist/assets/main-EK_fzXn_.js +0 -352
- package/bizar-dash/dist/assets/main-EK_fzXn_.js.map +0 -1
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
}
|
|
30
30
|
})();
|
|
31
31
|
</script>
|
|
32
|
-
<script type="module" crossorigin src="/assets/main-
|
|
32
|
+
<script type="module" crossorigin src="/assets/main-BB5mJurD.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-
|
|
35
|
+
<link rel="stylesheet" crossorigin href="/assets/main-BsnQLXdh.css">
|
|
36
36
|
</head>
|
|
37
37
|
<body>
|
|
38
38
|
<div id="root"></div>
|
|
@@ -4,101 +4,175 @@
|
|
|
4
4
|
* v4.5.0 — MiniMax API client.
|
|
5
5
|
*
|
|
6
6
|
* Owns the network calls to platform.minimax.io for the Bizar
|
|
7
|
-
* dashboard.
|
|
7
|
+
* dashboard. The Subscription Key is read from opencode's canonical
|
|
8
|
+
* auth store (`~/.local/share/opencode/auth.json`) so the user only
|
|
9
|
+
* enters the key once (via opencode's `/connect` command) and the
|
|
10
|
+
* dashboard picks it up automatically.
|
|
8
11
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
12
|
+
* Key resolution chain (in order, first match wins):
|
|
13
|
+
* 1. process.env.MINIMAX_API_KEY
|
|
14
|
+
* 2. process.env.ANTHROPIC_API_KEY (MiniMax accepts Anthropic keys)
|
|
15
|
+
* 3. ~/.local/share/opencode/auth.json → "minimax" → "key"
|
|
16
|
+
* 4. ~/.config/opencode/opencode.json → provider.minimax.options.apiKey
|
|
17
|
+
* 5. ~/.config/opencode/opencode.json → provider.minimax.apiKey
|
|
13
18
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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.
|
|
19
|
+
* Two surfaces:
|
|
20
|
+
* - fetchRemains() — Token Plan quota (5h + weekly per model)
|
|
21
|
+
* - chatCompletion() — one-shot chat call for the "test prompt" UI
|
|
27
22
|
*
|
|
28
23
|
* All network calls are guarded by a 10s timeout so a hung API
|
|
29
|
-
* never wedges the dashboard.
|
|
24
|
+
* never wedges the dashboard. Responses are cached in memory (60s)
|
|
25
|
+
* and on disk (5min at ~/.config/bizar/minimax/remains-cache.json) so
|
|
26
|
+
* renders are fast and the API isn't hammered.
|
|
30
27
|
*/
|
|
31
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
28
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, statSync } from 'node:fs';
|
|
32
29
|
import { dirname, join } from 'node:path';
|
|
33
30
|
import { homedir } from 'node:os';
|
|
34
31
|
|
|
35
32
|
const HOME = homedir();
|
|
36
33
|
const BIZAR_HOME = join(HOME, '.config', 'bizar');
|
|
34
|
+
const OPENCODE_AUTH_FILE = join(HOME, '.local', 'share', 'opencode', 'auth.json');
|
|
35
|
+
const OPENCODE_CONFIG_FILE = join(HOME, '.config', 'opencode', 'opencode.json');
|
|
37
36
|
const CACHE_DIR = join(BIZAR_HOME, 'minimax');
|
|
38
37
|
const CACHE_FILE = join(CACHE_DIR, 'remains-cache.json');
|
|
39
38
|
|
|
40
|
-
// Default base
|
|
41
|
-
// (note: the `www` host, not `api`).
|
|
39
|
+
// Default base URLs. The docs say `https://www.minimax.io/v1/token_plan/remains`
|
|
40
|
+
// (note: the `www` host, not `api`). The chat completions / OpenAI-format
|
|
41
|
+
// surface lives on api.minimax.io.
|
|
42
42
|
export const DEFAULT_BASE_URL = 'https://www.minimax.io';
|
|
43
|
-
// Chat completions / OpenAI-style surface lives on api.minimax.io.
|
|
44
43
|
export const DEFAULT_CHAT_BASE_URL = 'https://api.minimax.io/v1';
|
|
45
44
|
|
|
46
|
-
//
|
|
45
|
+
// Full MiniMax model list. Pulled live from /v1/models on the user's key
|
|
46
|
+
// and cached in the dashboard. The dashboard's "Usage" view shows
|
|
47
|
+
// remaining quota per-model.
|
|
47
48
|
export const KNOWN_MODELS = [
|
|
48
49
|
'MiniMax-M3',
|
|
49
50
|
'MiniMax-M2.7',
|
|
51
|
+
'MiniMax-M2.7-highspeed',
|
|
50
52
|
'MiniMax-M2.5',
|
|
53
|
+
'MiniMax-M2.5-highspeed',
|
|
51
54
|
'MiniMax-M2.1',
|
|
55
|
+
'MiniMax-M2.1-highspeed',
|
|
52
56
|
'MiniMax-M2',
|
|
53
57
|
];
|
|
54
58
|
|
|
59
|
+
// Valid MiniMax API key prefixes (subscription + pay-as-you-go).
|
|
60
|
+
// The user's key starts with `sk-cp-` and is 125 chars long. Real
|
|
61
|
+
// keys have variable lengths and may include dashes, so the pattern
|
|
62
|
+
// only validates the prefix.
|
|
63
|
+
const MINIMAX_KEY_PATTERN = /^sk-(cp|ant|or)-[A-Za-z0-9_-]{20,}$/;
|
|
64
|
+
|
|
65
|
+
// ─── Key resolution ─────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
function safeReadJson(file, fallback = null) {
|
|
68
|
+
try {
|
|
69
|
+
if (!existsSync(file)) return fallback;
|
|
70
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
71
|
+
} catch {
|
|
72
|
+
return fallback;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
55
76
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
77
|
+
* Resolve the MiniMax Subscription Key from opencode's canonical store.
|
|
78
|
+
* Returns `{ key: string|null, source: string, groupId: string }`.
|
|
79
|
+
*
|
|
80
|
+
* `source` is one of:
|
|
81
|
+
* - 'env:MINIMAX_API_KEY'
|
|
82
|
+
* - 'env:ANTHROPIC_API_KEY'
|
|
83
|
+
* - 'auth.json'
|
|
84
|
+
* - 'opencode.json:options.apiKey'
|
|
85
|
+
* - 'opencode.json:apiKey'
|
|
86
|
+
* - 'none'
|
|
58
87
|
*/
|
|
59
|
-
export function
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
return
|
|
88
|
+
export function resolveApiKey() {
|
|
89
|
+
// 1. Env vars — opencode reads MINIMAX_API_KEY first
|
|
90
|
+
if (process.env.MINIMAX_API_KEY && process.env.MINIMAX_API_KEY.trim()) {
|
|
91
|
+
return {
|
|
92
|
+
key: process.env.MINIMAX_API_KEY.trim(),
|
|
93
|
+
source: 'env:MINIMAX_API_KEY',
|
|
94
|
+
groupId: 'default',
|
|
95
|
+
};
|
|
63
96
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const m = parsed.minimax;
|
|
67
|
-
if (!m || typeof m !== 'object') return defaultMinimaxSettings();
|
|
97
|
+
// 2. Anthropic key works against MiniMax's Anthropic-format surface
|
|
98
|
+
if (process.env.ANTHROPIC_API_KEY && process.env.ANTHROPIC_API_KEY.trim()) {
|
|
68
99
|
return {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
groupId:
|
|
72
|
-
|
|
73
|
-
|
|
100
|
+
key: process.env.ANTHROPIC_API_KEY.trim(),
|
|
101
|
+
source: 'env:ANTHROPIC_API_KEY',
|
|
102
|
+
groupId: 'default',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// 3. ~/.local/share/opencode/auth.json
|
|
106
|
+
const auth = safeReadJson(OPENCODE_AUTH_FILE, null);
|
|
107
|
+
if (auth && typeof auth === 'object' && auth.minimax && auth.minimax.key) {
|
|
108
|
+
return {
|
|
109
|
+
key: String(auth.minimax.key).trim(),
|
|
110
|
+
source: 'auth.json',
|
|
111
|
+
groupId: String(auth.minimax.group_id || 'default'),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
// 4. opencode.json → provider.minimax.options.apiKey
|
|
115
|
+
const cfg = safeReadJson(OPENCODE_CONFIG_FILE, null);
|
|
116
|
+
const minimax = cfg?.provider?.minimax;
|
|
117
|
+
if (minimax?.options?.apiKey) {
|
|
118
|
+
return {
|
|
119
|
+
key: String(minimax.options.apiKey).trim(),
|
|
120
|
+
source: 'opencode.json:options.apiKey',
|
|
121
|
+
groupId: String(cfg.provider.minimax.group_id || 'default'),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// 5. opencode.json → provider.minimax.apiKey
|
|
125
|
+
if (minimax?.apiKey) {
|
|
126
|
+
return {
|
|
127
|
+
key: String(minimax.apiKey).trim(),
|
|
128
|
+
source: 'opencode.json:apiKey',
|
|
129
|
+
groupId: String(cfg.provider.minimax.group_id || 'default'),
|
|
74
130
|
};
|
|
75
|
-
} catch {
|
|
76
|
-
return defaultMinimaxSettings();
|
|
77
131
|
}
|
|
132
|
+
return { key: null, source: 'none', groupId: 'default' };
|
|
78
133
|
}
|
|
79
134
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Base URL resolution. The Token Plan endpoint lives on www.minimax.io;
|
|
137
|
+
* the chat completions live on api.minimax.io/v1. If the user has
|
|
138
|
+
* set a custom base URL in opencode.json's `options.baseURL`, that
|
|
139
|
+
* wins (with path-aware logic — we extract the host).
|
|
140
|
+
*/
|
|
141
|
+
export function resolveBaseUrls() {
|
|
142
|
+
const cfg = safeReadJson(OPENCODE_CONFIG_FILE, null);
|
|
143
|
+
const minimaxOpts = cfg?.provider?.minimax?.options || {};
|
|
144
|
+
let tokenBase = DEFAULT_BASE_URL;
|
|
145
|
+
let chatBase = DEFAULT_CHAT_BASE_URL;
|
|
146
|
+
if (typeof minimaxOpts.baseURL === 'string' && minimaxOpts.baseURL.trim()) {
|
|
147
|
+
const u = minimaxOpts.baseURL.trim();
|
|
148
|
+
// If the user already points at the OpenAI-format surface, keep
|
|
149
|
+
// it as chatBase. Otherwise we use it for both, falling back to
|
|
150
|
+
// known hosts when the path looks like a generic base.
|
|
151
|
+
try {
|
|
152
|
+
const parsed = new URL(u);
|
|
153
|
+
const host = parsed.host;
|
|
154
|
+
if (host.startsWith('api.')) {
|
|
155
|
+
chatBase = u.replace(/\/$/, '');
|
|
156
|
+
tokenBase = chatBase.replace(/^https?:\/\/api\./, 'https://www.');
|
|
157
|
+
} else if (host.startsWith('www.')) {
|
|
158
|
+
tokenBase = u.replace(/\/$/, '');
|
|
159
|
+
chatBase = tokenBase.replace(/^https?:\/\/www\./, 'https://api.') + '/v1';
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
// ignore — fall through to defaults
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { tokenBase, chatBase };
|
|
88
166
|
}
|
|
89
167
|
|
|
90
|
-
// ─── Cache
|
|
168
|
+
// ─── Cache ────────────────────────────────────────────────────────────────
|
|
91
169
|
|
|
92
|
-
/**
|
|
93
|
-
* In-memory snapshot of the last remains response. Avoids re-hitting the
|
|
94
|
-
* API on every dashboard render.
|
|
95
|
-
*/
|
|
96
170
|
let memoryCache = null;
|
|
97
171
|
|
|
98
172
|
function readDiskCache() {
|
|
99
173
|
try {
|
|
100
174
|
if (!existsSync(CACHE_FILE)) return null;
|
|
101
|
-
const stat =
|
|
175
|
+
const stat = statSync(CACHE_FILE);
|
|
102
176
|
if (Date.now() - stat.mtimeMs > 5 * 60 * 1000) return null; // 5 min TTL
|
|
103
177
|
return JSON.parse(readFileSync(CACHE_FILE, 'utf8'));
|
|
104
178
|
} catch {
|
|
@@ -110,23 +184,16 @@ function writeDiskCache(snapshot) {
|
|
|
110
184
|
try {
|
|
111
185
|
mkdirSync(CACHE_DIR, { recursive: true });
|
|
112
186
|
writeFileSync(CACHE_FILE, JSON.stringify(snapshot, null, 2) + '\n', 'utf8');
|
|
113
|
-
} catch {
|
|
114
|
-
/* best-effort */
|
|
115
|
-
}
|
|
187
|
+
} catch { /* best-effort */ }
|
|
116
188
|
}
|
|
117
189
|
|
|
118
190
|
export function clearRemainsCache() {
|
|
119
191
|
memoryCache = null;
|
|
120
192
|
try {
|
|
121
|
-
if (existsSync(CACHE_FILE))
|
|
122
|
-
require('node:fs').unlinkSync(CACHE_FILE);
|
|
123
|
-
}
|
|
193
|
+
if (existsSync(CACHE_FILE)) unlinkSync(CACHE_FILE);
|
|
124
194
|
} catch { /* best-effort */ }
|
|
125
195
|
}
|
|
126
196
|
|
|
127
|
-
/**
|
|
128
|
-
* Try in-memory → disk cache. Returns the cached snapshot or null.
|
|
129
|
-
*/
|
|
130
197
|
export function readCachedRemains() {
|
|
131
198
|
if (memoryCache) return memoryCache;
|
|
132
199
|
const fromDisk = readDiskCache();
|
|
@@ -137,12 +204,8 @@ export function readCachedRemains() {
|
|
|
137
204
|
return null;
|
|
138
205
|
}
|
|
139
206
|
|
|
140
|
-
// ─── Network helpers
|
|
207
|
+
// ─── Network helpers ─────────────────────────────────────────────────────
|
|
141
208
|
|
|
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
209
|
async function fetchWithTimeout(url, opts = {}, timeoutMs = 10_000) {
|
|
147
210
|
const ctl = new AbortController();
|
|
148
211
|
const t = setTimeout(() => ctl.abort(), timeoutMs);
|
|
@@ -153,45 +216,109 @@ async function fetchWithTimeout(url, opts = {}, timeoutMs = 10_000) {
|
|
|
153
216
|
}
|
|
154
217
|
}
|
|
155
218
|
|
|
156
|
-
// ───
|
|
219
|
+
// ─── Auth-file writer (for onboarding) ─────────────────────────────────
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Write the MiniMax key to opencode's auth store at the canonical
|
|
223
|
+
* path. Returns the path that was written. Used by the dashboard's
|
|
224
|
+
* onboarding wizard so the user only enters the key once.
|
|
225
|
+
*/
|
|
226
|
+
export function writeAuthFile(key, groupId = 'default') {
|
|
227
|
+
mkdirSync(dirname(OPENCODE_AUTH_FILE), { recursive: true });
|
|
228
|
+
const cur = safeReadJson(OPENCODE_AUTH_FILE, {}) || {};
|
|
229
|
+
cur.minimax = {
|
|
230
|
+
type: 'api',
|
|
231
|
+
key: String(key).trim(),
|
|
232
|
+
};
|
|
233
|
+
if (groupId) cur.minimax.group_id = groupId;
|
|
234
|
+
writeFileSync(OPENCODE_AUTH_FILE, JSON.stringify(cur, null, 2) + '\n', 'utf8');
|
|
235
|
+
return OPENCODE_AUTH_FILE;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ─── Public API ─────────────────────────────────────────────────────────
|
|
157
239
|
|
|
158
240
|
/**
|
|
159
|
-
*
|
|
241
|
+
* Read the dashboard-side config that opencode doesn't own:
|
|
242
|
+
* - the onboarding "dismissed" flag (so we don't keep nagging the
|
|
243
|
+
* user with the first-run wizard after they've seen it once)
|
|
244
|
+
* - the per-model enabled flag (so the user can hide a model
|
|
245
|
+
* they don't want to track)
|
|
160
246
|
*
|
|
161
|
-
*
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
247
|
+
* Stored at ~/.config/bizar/minimax/onboarding.json.
|
|
248
|
+
*/
|
|
249
|
+
export function readOnboarding() {
|
|
250
|
+
return safeReadJson(join(BIZAR_HOME, 'minimax', 'onboarding.json'), {
|
|
251
|
+
dismissedAt: null,
|
|
252
|
+
hiddenModels: [],
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function writeOnboarding(patch) {
|
|
257
|
+
const cur = readOnboarding();
|
|
258
|
+
const next = { ...cur, ...patch };
|
|
259
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
260
|
+
writeFileSync(join(BIZAR_HOME, 'minimax', 'onboarding.json'), JSON.stringify(next, null, 2) + '\n', 'utf8');
|
|
261
|
+
return next;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Snapshot of the current MiniMax state for the dashboard. The
|
|
266
|
+
* `status` route returns this so the React view knows whether to
|
|
267
|
+
* show the wizard, the live dashboard, or a "needs configuration"
|
|
268
|
+
* banner.
|
|
269
|
+
*/
|
|
270
|
+
export function getStatus() {
|
|
271
|
+
const resolved = resolveApiKey();
|
|
272
|
+
const urls = resolveBaseUrls();
|
|
273
|
+
const cached = readCachedRemains();
|
|
274
|
+
return {
|
|
275
|
+
configured: !!resolved.key,
|
|
276
|
+
apiKeyHint: maskKey(resolved.key),
|
|
277
|
+
source: resolved.source,
|
|
278
|
+
groupId: resolved.groupId,
|
|
279
|
+
tokenBaseUrl: urls.tokenBase,
|
|
280
|
+
chatBaseUrl: urls.chatBase,
|
|
281
|
+
knownModels: KNOWN_MODELS,
|
|
282
|
+
keyPatternValid: resolved.key ? MINIMAX_KEY_PATTERN.test(resolved.key) : null,
|
|
283
|
+
cache: cached
|
|
284
|
+
? {
|
|
285
|
+
fetchedAt: cached.fetchedAt,
|
|
286
|
+
apiKeyHint: cached.apiKeyHint,
|
|
287
|
+
modelCount: (cached.models || []).length,
|
|
288
|
+
}
|
|
289
|
+
: null,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Hit /v1/token_plan/remains with the user's resolved key.
|
|
173
295
|
*
|
|
174
|
-
* Returns the raw response, augmented with `fetchedAt
|
|
175
|
-
*
|
|
176
|
-
*
|
|
296
|
+
* Returns the raw response, augmented with `fetchedAt`, ISO timestamps
|
|
297
|
+
* for the reset windows, and pre-computed consumed-percent fields so
|
|
298
|
+
* the UI doesn't have to do arithmetic on every cell.
|
|
177
299
|
*/
|
|
178
|
-
export async function fetchRemains({
|
|
179
|
-
|
|
180
|
-
|
|
300
|
+
export async function fetchRemains({ force = false } = {}) {
|
|
301
|
+
const resolved = resolveApiKey();
|
|
302
|
+
if (!resolved.key) {
|
|
303
|
+
return { ok: false, error: 'no_api_key', message: 'MiniMax Subscription Key is not configured. Run `bizar setup` or add it via the dashboard onboarding.' };
|
|
304
|
+
}
|
|
305
|
+
if (!MINIMAX_KEY_PATTERN.test(resolved.key)) {
|
|
306
|
+
return { ok: false, error: 'invalid_key_format', message: 'MiniMax key does not look right. Expected sk-cp-…, sk-ant-…, or sk-or-… prefix.' };
|
|
181
307
|
}
|
|
182
308
|
if (!force) {
|
|
183
309
|
const cached = readCachedRemains();
|
|
184
|
-
if (cached && cached.apiKeyHint === maskKey(
|
|
310
|
+
if (cached && cached.apiKeyHint === maskKey(resolved.key) && (Date.now() - cached.fetchedAt) < 60_000) {
|
|
185
311
|
return { ok: true, cached: true, ...cached };
|
|
186
312
|
}
|
|
187
313
|
}
|
|
188
|
-
const
|
|
314
|
+
const urls = resolveBaseUrls();
|
|
315
|
+
const url = `${urls.tokenBase.replace(/\/$/, '')}/v1/token_plan/remains?group_id=${encodeURIComponent(resolved.groupId)}`;
|
|
189
316
|
let resp;
|
|
190
317
|
try {
|
|
191
318
|
resp = await fetchWithTimeout(url, {
|
|
192
319
|
method: 'GET',
|
|
193
320
|
headers: {
|
|
194
|
-
Authorization: `Bearer ${
|
|
321
|
+
Authorization: `Bearer ${resolved.key}`,
|
|
195
322
|
'Content-Type': 'application/json',
|
|
196
323
|
Accept: 'application/json',
|
|
197
324
|
},
|
|
@@ -219,8 +346,7 @@ export async function fetchRemains({ apiKey, groupId = 'default', baseUrl = DEFA
|
|
|
219
346
|
};
|
|
220
347
|
}
|
|
221
348
|
|
|
222
|
-
// Augment with ISO timestamps
|
|
223
|
-
// arithmetic for every cell.
|
|
349
|
+
// Augment with ISO timestamps + human labels for the UI.
|
|
224
350
|
const models = (body.model_remains || []).map((m) => ({
|
|
225
351
|
...m,
|
|
226
352
|
endTimeISO: new Date(m.end_time).toISOString(),
|
|
@@ -240,9 +366,10 @@ export async function fetchRemains({ apiKey, groupId = 'default', baseUrl = DEFA
|
|
|
240
366
|
const snapshot = {
|
|
241
367
|
ok: true,
|
|
242
368
|
fetchedAt: Date.now(),
|
|
243
|
-
apiKeyHint: maskKey(
|
|
244
|
-
|
|
245
|
-
|
|
369
|
+
apiKeyHint: maskKey(resolved.key),
|
|
370
|
+
keySource: resolved.source,
|
|
371
|
+
groupId: resolved.groupId,
|
|
372
|
+
baseUrl: urls.tokenBase,
|
|
246
373
|
models,
|
|
247
374
|
baseResp: body.base_resp,
|
|
248
375
|
};
|
|
@@ -254,23 +381,22 @@ export async function fetchRemains({ apiKey, groupId = 'default', baseUrl = DEFA
|
|
|
254
381
|
/**
|
|
255
382
|
* Send a single chat-completion to /v1/chat/completions and return
|
|
256
383
|
* the parsed response + parsed `usage` block. Used by the dashboard's
|
|
257
|
-
* "Send a test prompt" button
|
|
258
|
-
* and see live token usage.
|
|
384
|
+
* "Send a test prompt" button.
|
|
259
385
|
*/
|
|
260
386
|
export async function chatCompletion({
|
|
261
|
-
apiKey,
|
|
262
387
|
prompt,
|
|
263
388
|
model = 'MiniMax-M3',
|
|
264
|
-
baseUrl = DEFAULT_CHAT_BASE_URL,
|
|
265
389
|
maxTokens = 256,
|
|
266
390
|
} = {}) {
|
|
267
|
-
|
|
268
|
-
|
|
391
|
+
const resolved = resolveApiKey();
|
|
392
|
+
if (!resolved.key) {
|
|
393
|
+
return { ok: false, error: 'no_api_key', message: 'MiniMax Subscription Key is not configured.' };
|
|
269
394
|
}
|
|
270
395
|
if (!prompt || !prompt.trim()) {
|
|
271
396
|
return { ok: false, error: 'no_prompt', message: 'prompt is empty' };
|
|
272
397
|
}
|
|
273
|
-
const
|
|
398
|
+
const urls = resolveBaseUrls();
|
|
399
|
+
const url = `${urls.chatBase.replace(/\/$/, '')}/chat/completions`;
|
|
274
400
|
const body = JSON.stringify({
|
|
275
401
|
model,
|
|
276
402
|
messages: [{ role: 'user', content: prompt }],
|
|
@@ -282,7 +408,7 @@ export async function chatCompletion({
|
|
|
282
408
|
resp = await fetchWithTimeout(url, {
|
|
283
409
|
method: 'POST',
|
|
284
410
|
headers: {
|
|
285
|
-
Authorization: `Bearer ${
|
|
411
|
+
Authorization: `Bearer ${resolved.key}`,
|
|
286
412
|
'Content-Type': 'application/json',
|
|
287
413
|
Accept: 'application/json',
|
|
288
414
|
},
|
|
@@ -545,16 +545,20 @@ export const providersStore = {
|
|
|
545
545
|
keyPattern: /^sk-[A-Za-z0-9]{20,}$/,
|
|
546
546
|
},
|
|
547
547
|
{
|
|
548
|
-
// MiniMax
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
//
|
|
548
|
+
// v4.5.0 — MiniMax provider, fully implemented. The dashboard
|
|
549
|
+
// onboarding wizard writes the Subscription Key to
|
|
550
|
+
// ~/.local/share/opencode/auth.json (the canonical store written
|
|
551
|
+
// by opencode's `/connect` command). The MiniMax key has a
|
|
552
|
+
// variable-length format; valid prefixes are `sk-cp-` (Token
|
|
553
|
+
// Plan + Coding Plan), `sk-ant-` (Anthropic-format compatible),
|
|
554
|
+
// and `sk-or-` (OpenRouter-style). The chat completions host
|
|
555
|
+
// is `api.minimax.io/v1` (NOT `api.minimax.chat` which 404s).
|
|
552
556
|
id: 'minimax',
|
|
553
557
|
name: 'MiniMax',
|
|
554
558
|
envKeys: ['MINIMAX_API_KEY', 'ANTHROPIC_API_KEY'],
|
|
555
559
|
backupEnvKeys: ['MINIMAX_API_KEY_BACKUP', 'MINIMAX_BACKUP_API_KEY', 'ANTHROPIC_API_KEY_BACKUP'],
|
|
556
|
-
baseURL: 'https://api.minimax.
|
|
557
|
-
keyPattern: /^[A-Za-z0-
|
|
560
|
+
baseURL: 'https://api.minimax.io/v1',
|
|
561
|
+
keyPattern: /^sk-(cp|ant|or)-[A-Za-z0-9_-]{20,}$/,
|
|
558
562
|
},
|
|
559
563
|
],
|
|
560
564
|
|