@openchamber/web 1.9.2 → 1.9.3
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/dist/assets/ToolOutputDialog-iiUOHO3c.js +16 -0
- package/dist/assets/index-BZ8pfXBh.css +1 -0
- package/dist/assets/index-DEj7Q-1y.js +2 -0
- package/dist/assets/{main-BFP0Fw2a.js → main-Ba2uuSTQ.js} +119 -119
- package/dist/assets/{vendor-.bun-CjZZibdK.js → vendor-.bun-B34wtB0D.js} +39 -39
- package/dist/index.html +3 -3
- package/package.json +1 -1
- package/server/TERMINAL_WS_PROTOCOL.md +48 -0
- package/server/lib/fs/routes.js +48 -0
- package/server/lib/opencode/proxy.js +106 -2
- package/server/lib/quota/DOCUMENTATION.md +1 -0
- package/server/lib/quota/index.js +2 -1
- package/server/lib/quota/providers/copilot.js +1 -1
- package/server/lib/quota/providers/index.js +8 -0
- package/server/lib/quota/providers/minimax-cn-coding-plan.js +141 -15
- package/server/lib/quota/providers/minimax-coding-plan.js +139 -15
- package/server/lib/quota/providers/zhipuai.js +114 -0
- package/server/lib/terminal/DOCUMENTATION.md +41 -80
- package/server/lib/terminal/index.js +27 -8
- package/server/lib/terminal/output-replay-buffer.js +66 -0
- package/server/lib/terminal/output-replay-buffer.test.js +66 -0
- package/server/lib/terminal/runtime.js +107 -20
- package/server/lib/terminal/{input-ws-protocol.js → terminal-ws-protocol.js} +13 -11
- package/server/lib/terminal/{input-ws-protocol.test.js → terminal-ws-protocol.test.js} +39 -32
- package/server/opencode-proxy.test.js +83 -0
- package/server/proxy-headers.js +61 -0
- package/server/proxy-headers.test.js +58 -0
- package/dist/assets/ToolOutputDialog-DwlX_M_n.js +0 -16
- package/dist/assets/index-BQqVuvn2.js +0 -2
- package/dist/assets/index-CH1IFYgs.css +0 -1
- package/server/TERMINAL_INPUT_WS_PROTOCOL.md +0 -44
- package/server/lib/quota/providers/minimax-shared.js +0 -136
package/dist/index.html
CHANGED
|
@@ -442,10 +442,10 @@
|
|
|
442
442
|
pointer-events: none;
|
|
443
443
|
}
|
|
444
444
|
</style>
|
|
445
|
-
<script type="module" crossorigin src="/assets/index-
|
|
446
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-.bun-
|
|
445
|
+
<script type="module" crossorigin src="/assets/index-DEj7Q-1y.js"></script>
|
|
446
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-.bun-B34wtB0D.js">
|
|
447
447
|
<link rel="stylesheet" crossorigin href="/assets/vendor--DbVqbJpV.css">
|
|
448
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
448
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BZ8pfXBh.css">
|
|
449
449
|
</head>
|
|
450
450
|
<body class="h-full bg-background text-foreground">
|
|
451
451
|
<div id="root" class="h-full">
|
package/package.json
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Terminal WebSocket Transport Protocol
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
Use a single persistent WebSocket for terminal input and output, while keeping the legacy SSE output route and HTTP input route as compatibility fallbacks.
|
|
5
|
+
|
|
6
|
+
## Scope
|
|
7
|
+
- Primary full-duplex path: WebSocket (`/api/terminal/ws`)
|
|
8
|
+
- Legacy output fallback: SSE (`/api/terminal/:sessionId/stream`)
|
|
9
|
+
- HTTP input fallback remains: `POST /api/terminal/:sessionId/input`
|
|
10
|
+
|
|
11
|
+
## Framing
|
|
12
|
+
- Text frame:
|
|
13
|
+
- client -> server: terminal keystroke payload
|
|
14
|
+
- server -> client: raw PTY output chunk
|
|
15
|
+
- Binary frame: control envelope
|
|
16
|
+
- Byte 0: tag (`0x01` = JSON control)
|
|
17
|
+
- Bytes 1..N: UTF-8 JSON payload
|
|
18
|
+
|
|
19
|
+
## Control Messages
|
|
20
|
+
- Bind active socket to terminal session:
|
|
21
|
+
- client -> server: `{"t":"b","s":"<sessionId>","v":2}`
|
|
22
|
+
- Keepalive ping:
|
|
23
|
+
- client -> server: `{"t":"p","v":2}`
|
|
24
|
+
- server -> client: `{"t":"po","v":2}`
|
|
25
|
+
- Server control responses:
|
|
26
|
+
- ready: `{"t":"ok","v":2}`
|
|
27
|
+
- bind ok: `{"t":"bok","s":"<sessionId>","runtime":"node|bun","ptyBackend":"...","v":2}`
|
|
28
|
+
- exit: `{"t":"x","s":"<sessionId>","exitCode":0,"signal":null}`
|
|
29
|
+
- error: `{"t":"e","c":"<code>","f":true|false}`
|
|
30
|
+
|
|
31
|
+
## Multiplexing Model
|
|
32
|
+
- Single shared socket per client runtime.
|
|
33
|
+
- Socket has one mutable bound session.
|
|
34
|
+
- Client sends a bind control when the active terminal changes.
|
|
35
|
+
- Text frames always apply to the currently bound session.
|
|
36
|
+
- PTY output is pushed back over the same socket as text frames.
|
|
37
|
+
- Client keeps the socket primed so both stream subscription and input reuse the same transport.
|
|
38
|
+
|
|
39
|
+
## Security
|
|
40
|
+
- UI auth session required when UI password is enabled.
|
|
41
|
+
- Origin validation enforced for cookie-authenticated browser upgrades.
|
|
42
|
+
- Invalid or malformed frames are rate-limited and may close the socket.
|
|
43
|
+
|
|
44
|
+
## Fallback Behavior
|
|
45
|
+
- New clients prefer `capabilities.stream.ws` and reuse the same socket for input.
|
|
46
|
+
- If stream WebSocket capability is unavailable, clients fall back to SSE output.
|
|
47
|
+
- If terminal input cannot be sent over WebSocket, clients fall back to HTTP input.
|
|
48
|
+
- The removed `/api/terminal/input-ws` path should fail with `404 Not Found`.
|
package/server/lib/fs/routes.js
CHANGED
|
@@ -283,6 +283,54 @@ export const registerFsRoutes = (app, dependencies) => {
|
|
|
283
283
|
}
|
|
284
284
|
});
|
|
285
285
|
|
|
286
|
+
app.get('/api/fs/stat', async (req, res) => {
|
|
287
|
+
const filePath = typeof req.query.path === 'string' ? req.query.path.trim() : '';
|
|
288
|
+
if (!filePath) {
|
|
289
|
+
return res.status(400).json({ error: 'Path is required' });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
const resolved = await resolveWorkspacePathFromContext({
|
|
294
|
+
req,
|
|
295
|
+
targetPath: filePath,
|
|
296
|
+
resolveProjectDirectory,
|
|
297
|
+
path,
|
|
298
|
+
os,
|
|
299
|
+
normalizeDirectoryPath,
|
|
300
|
+
openchamberUserConfigRoot,
|
|
301
|
+
});
|
|
302
|
+
if (!resolved.ok) {
|
|
303
|
+
return res.status(400).json({ error: resolved.error });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const [canonicalPath, canonicalBase] = await Promise.all([
|
|
307
|
+
fsPromises.realpath(resolved.resolved),
|
|
308
|
+
fsPromises.realpath(resolved.base).catch(() => path.resolve(resolved.base)),
|
|
309
|
+
]);
|
|
310
|
+
|
|
311
|
+
if (!isPathWithinRoot(canonicalPath, canonicalBase, path, os)) {
|
|
312
|
+
return res.status(403).json({ error: 'Access to file denied' });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const stats = await fsPromises.stat(canonicalPath);
|
|
316
|
+
if (!stats.isFile()) {
|
|
317
|
+
return res.status(400).json({ error: 'Specified path is not a file' });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return res.json({ path: canonicalPath, isFile: true, size: stats.size });
|
|
321
|
+
} catch (error) {
|
|
322
|
+
const err = error;
|
|
323
|
+
if (err && typeof err === 'object' && err.code === 'ENOENT') {
|
|
324
|
+
return res.status(404).json({ error: 'File not found' });
|
|
325
|
+
}
|
|
326
|
+
if (err && typeof err === 'object' && err.code === 'EACCES') {
|
|
327
|
+
return res.status(403).json({ error: 'Access to file denied' });
|
|
328
|
+
}
|
|
329
|
+
console.error('Failed to stat file:', error);
|
|
330
|
+
return res.status(500).json({ error: (error && error.message) || 'Failed to stat file' });
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
286
334
|
app.get('/api/fs/read', async (req, res) => {
|
|
287
335
|
const filePath = typeof req.query.path === 'string' ? req.query.path.trim() : '';
|
|
288
336
|
if (!filePath) {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import express from 'express';
|
|
2
1
|
import { createProxyMiddleware } from 'http-proxy-middleware';
|
|
3
2
|
|
|
3
|
+
import {
|
|
4
|
+
applyForwardProxyResponseHeaders,
|
|
5
|
+
collectForwardProxyHeaders,
|
|
6
|
+
shouldForwardProxyResponseHeader,
|
|
7
|
+
} from '../../proxy-headers.js';
|
|
8
|
+
|
|
4
9
|
export const registerOpenCodeProxy = (app, deps) => {
|
|
5
10
|
const {
|
|
6
11
|
fs,
|
|
@@ -25,6 +30,91 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
25
30
|
}
|
|
26
31
|
app.set('opencodeProxyConfigured', true);
|
|
27
32
|
|
|
33
|
+
const isAbortError = (error) => error?.name === 'AbortError';
|
|
34
|
+
|
|
35
|
+
const forwardSseRequest = async (req, res) => {
|
|
36
|
+
const abortController = new AbortController();
|
|
37
|
+
const closeUpstream = () => abortController.abort();
|
|
38
|
+
let upstream = null;
|
|
39
|
+
let reader = null;
|
|
40
|
+
|
|
41
|
+
req.on('close', closeUpstream);
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const requestUrl = typeof req.originalUrl === 'string' && req.originalUrl.length > 0
|
|
45
|
+
? req.originalUrl
|
|
46
|
+
: (typeof req.url === 'string' ? req.url : '');
|
|
47
|
+
const upstreamPath = requestUrl.startsWith('/api') ? requestUrl.slice(4) || '/' : requestUrl;
|
|
48
|
+
const headers = collectForwardProxyHeaders(req.headers, getOpenCodeAuthHeaders());
|
|
49
|
+
headers.accept ??= 'text/event-stream';
|
|
50
|
+
headers['cache-control'] ??= 'no-cache';
|
|
51
|
+
|
|
52
|
+
upstream = await fetch(buildOpenCodeUrl(upstreamPath, ''), {
|
|
53
|
+
method: 'GET',
|
|
54
|
+
headers,
|
|
55
|
+
signal: abortController.signal,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
res.status(upstream.status);
|
|
59
|
+
applyForwardProxyResponseHeaders(upstream.headers, res);
|
|
60
|
+
|
|
61
|
+
const contentType = upstream.headers.get('content-type') || 'text/event-stream';
|
|
62
|
+
const isEventStream = contentType.toLowerCase().includes('text/event-stream');
|
|
63
|
+
|
|
64
|
+
if (!upstream.body) {
|
|
65
|
+
res.end(await upstream.text().catch(() => ''));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!isEventStream) {
|
|
70
|
+
res.end(await upstream.text());
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
res.setHeader('Content-Type', contentType);
|
|
75
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
76
|
+
res.setHeader('Connection', 'keep-alive');
|
|
77
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
78
|
+
if (typeof res.flushHeaders === 'function') {
|
|
79
|
+
res.flushHeaders();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
reader = upstream.body.getReader();
|
|
83
|
+
while (!abortController.signal.aborted) {
|
|
84
|
+
const { done, value } = await reader.read();
|
|
85
|
+
if (done) {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
if (value && value.length > 0) {
|
|
89
|
+
res.write(value);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
res.end();
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (isAbortError(error)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
console.error('[proxy] OpenCode SSE proxy error:', error?.message ?? error);
|
|
99
|
+
if (!res.headersSent) {
|
|
100
|
+
res.status(503).json({ error: 'OpenCode service unavailable' });
|
|
101
|
+
} else {
|
|
102
|
+
res.end();
|
|
103
|
+
}
|
|
104
|
+
} finally {
|
|
105
|
+
req.off('close', closeUpstream);
|
|
106
|
+
try {
|
|
107
|
+
if (reader) {
|
|
108
|
+
await reader.cancel();
|
|
109
|
+
reader.releaseLock();
|
|
110
|
+
} else if (upstream?.body && !upstream.body.locked) {
|
|
111
|
+
await upstream.body.cancel();
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
28
118
|
// Ensure API prefix is detected before proxying
|
|
29
119
|
app.use('/api', (_req, _res, next) => {
|
|
30
120
|
ensureOpenCodeApiPrefix();
|
|
@@ -138,7 +228,10 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
138
228
|
});
|
|
139
229
|
}
|
|
140
230
|
|
|
141
|
-
|
|
231
|
+
app.get('/api/global/event', forwardSseRequest);
|
|
232
|
+
app.get('/api/event', forwardSseRequest);
|
|
233
|
+
|
|
234
|
+
// Generic proxy for non-SSE OpenCode API routes.
|
|
142
235
|
const apiProxy = createProxyMiddleware({
|
|
143
236
|
target: `http://127.0.0.1:${runtime.openCodePort || 3902}`,
|
|
144
237
|
changeOrigin: true,
|
|
@@ -155,6 +248,17 @@ export const registerOpenCodeProxy = (app, deps) => {
|
|
|
155
248
|
if (authHeaders.Authorization) {
|
|
156
249
|
proxyReq.setHeader('Authorization', authHeaders.Authorization);
|
|
157
250
|
}
|
|
251
|
+
|
|
252
|
+
// Defensive: request identity encoding from upstream OpenCode.
|
|
253
|
+
// This avoids compressed-body/header mismatches in multi-proxy setups.
|
|
254
|
+
proxyReq.setHeader('accept-encoding', 'identity');
|
|
255
|
+
},
|
|
256
|
+
proxyRes: (proxyRes) => {
|
|
257
|
+
for (const key of Object.keys(proxyRes.headers || {})) {
|
|
258
|
+
if (!shouldForwardProxyResponseHeader(key)) {
|
|
259
|
+
delete proxyRes.headers[key];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
158
262
|
},
|
|
159
263
|
error: (err, _req, res) => {
|
|
160
264
|
console.error('[proxy] OpenCode proxy error:', err.message);
|
|
@@ -29,6 +29,7 @@ These provider IDs are currently dispatchable via `fetchQuotaForProvider(provide
|
|
|
29
29
|
| `minimax-coding-plan` | MiniMax Coding Plan (minimax.io) | `providers/minimax-coding-plan.js` | `minimax-coding-plan` |
|
|
30
30
|
| `minimax-cn-coding-plan` | MiniMax Coding Plan (minimaxi.com) | `providers/minimax-cn-coding-plan.js` | `minimax-cn-coding-plan` |
|
|
31
31
|
| `ollama-cloud` | Ollama Cloud | `providers/ollama-cloud.js` | Cookie file at `~/.config/ollama-quota/cookie` (raw session cookie string) |
|
|
32
|
+
| `zhipuai-coding-plan` | ZhipuAI | `providers/zhipuai.js` | `zhipuai-coding-plan`, `zhipuai`, `zhipu` |
|
|
32
33
|
|
|
33
34
|
## Internal-only provider module
|
|
34
35
|
- `providers/openai.js` exists for logic parity/reuse but is intentionally not registered for dispatcher ID routing.
|
|
@@ -18,7 +18,7 @@ const buildCopilotWindows = (payload) => {
|
|
|
18
18
|
const entitlement = toNumber(snapshot.entitlement);
|
|
19
19
|
const remaining = toNumber(snapshot.remaining);
|
|
20
20
|
const usedPercent = entitlement && remaining !== null
|
|
21
|
-
? Math.max(0,
|
|
21
|
+
? Math.max(0, 100 - (remaining / entitlement) * 100)
|
|
22
22
|
: null;
|
|
23
23
|
const valueLabel = entitlement !== null && remaining !== null
|
|
24
24
|
? `${remaining.toFixed(0)} / ${entitlement.toFixed(0)} left`
|
|
@@ -19,6 +19,7 @@ import * as zai from './zai.js';
|
|
|
19
19
|
import * as minimaxCodingPlan from './minimax-coding-plan.js';
|
|
20
20
|
import * as minimaxCnCodingPlan from './minimax-cn-coding-plan.js';
|
|
21
21
|
import * as ollamaCloud from './ollama-cloud.js';
|
|
22
|
+
import * as zhipuai from './zhipuai.js';
|
|
22
23
|
|
|
23
24
|
const registry = {
|
|
24
25
|
claude: {
|
|
@@ -92,6 +93,12 @@ const registry = {
|
|
|
92
93
|
providerName: ollamaCloud.providerName,
|
|
93
94
|
isConfigured: ollamaCloud.isConfigured,
|
|
94
95
|
fetchQuota: ollamaCloud.fetchQuota
|
|
96
|
+
},
|
|
97
|
+
'zhipuai-coding-plan': {
|
|
98
|
+
providerId: zhipuai.providerId,
|
|
99
|
+
providerName: zhipuai.providerName,
|
|
100
|
+
isConfigured: zhipuai.isConfigured,
|
|
101
|
+
fetchQuota: zhipuai.fetchQuota
|
|
95
102
|
}
|
|
96
103
|
};
|
|
97
104
|
|
|
@@ -150,3 +157,4 @@ export const fetchNanoGptQuota = nanogpt.fetchQuota;
|
|
|
150
157
|
export const fetchMinimaxCodingPlanQuota = minimaxCodingPlan.fetchQuota;
|
|
151
158
|
export const fetchMinimaxCnCodingPlanQuota = minimaxCnCodingPlan.fetchQuota;
|
|
152
159
|
export const fetchOllamaCloudQuota = ollamaCloud.fetchQuota;
|
|
160
|
+
export const fetchZhipuaiQuota = zhipuai.fetchQuota;
|
|
@@ -1,15 +1,141 @@
|
|
|
1
|
-
// MiniMax Coding Plan Provider
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export const
|
|
13
|
-
export const
|
|
14
|
-
export const
|
|
15
|
-
|
|
1
|
+
// MiniMax Coding Plan Provider (minimaxi.com)
|
|
2
|
+
import { readAuthFile } from '../../opencode/auth.js';
|
|
3
|
+
import {
|
|
4
|
+
getAuthEntry,
|
|
5
|
+
normalizeAuthEntry,
|
|
6
|
+
buildResult,
|
|
7
|
+
toUsageWindow,
|
|
8
|
+
toNumber,
|
|
9
|
+
toTimestamp,
|
|
10
|
+
} from '../utils/index.js';
|
|
11
|
+
|
|
12
|
+
export const providerId = 'minimax-cn-coding-plan';
|
|
13
|
+
export const providerName = 'MiniMax Coding Plan (minimaxi.com)';
|
|
14
|
+
export const aliases = ['minimax-cn-coding-plan'];
|
|
15
|
+
|
|
16
|
+
export const isConfigured = () => {
|
|
17
|
+
const auth = readAuthFile();
|
|
18
|
+
const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
|
|
19
|
+
return Boolean(entry?.key || entry?.token);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const fetchQuota = async () => {
|
|
23
|
+
const auth = readAuthFile();
|
|
24
|
+
const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
|
|
25
|
+
const apiKey = entry?.key ?? entry?.token;
|
|
26
|
+
|
|
27
|
+
if (!apiKey) {
|
|
28
|
+
return buildResult({
|
|
29
|
+
providerId,
|
|
30
|
+
providerName,
|
|
31
|
+
ok: false,
|
|
32
|
+
configured: false,
|
|
33
|
+
error: 'Not configured',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const response = await fetch(
|
|
39
|
+
'https://www.minimaxi.com/v1/api/openplatform/coding_plan/remains',
|
|
40
|
+
{
|
|
41
|
+
method: 'GET',
|
|
42
|
+
headers: {
|
|
43
|
+
Authorization: `Bearer ${apiKey}`,
|
|
44
|
+
'Content-Type': 'application/json',
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
return buildResult({
|
|
51
|
+
providerId,
|
|
52
|
+
providerName,
|
|
53
|
+
ok: false,
|
|
54
|
+
configured: true,
|
|
55
|
+
error: `API error: ${response.status}`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const payload = await response.json();
|
|
60
|
+
const baseResp = payload?.base_resp;
|
|
61
|
+
if (baseResp && baseResp.status_code !== 0) {
|
|
62
|
+
return buildResult({
|
|
63
|
+
providerId,
|
|
64
|
+
providerName,
|
|
65
|
+
ok: false,
|
|
66
|
+
configured: true,
|
|
67
|
+
error: baseResp.status_msg || `API error: ${baseResp.status_code}`,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const firstModel = payload?.model_remains?.[0];
|
|
72
|
+
if (!firstModel) {
|
|
73
|
+
return buildResult({
|
|
74
|
+
providerId,
|
|
75
|
+
providerName,
|
|
76
|
+
ok: false,
|
|
77
|
+
configured: true,
|
|
78
|
+
error: 'No model quota data available',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const intervalTotal = toNumber(firstModel.current_interval_total_count);
|
|
83
|
+
const intervalUsage = toNumber(firstModel.current_interval_usage_count);
|
|
84
|
+
const intervalStartAt = toTimestamp(firstModel.start_time);
|
|
85
|
+
const intervalResetAt = toTimestamp(firstModel.end_time);
|
|
86
|
+
const weeklyTotal = toNumber(firstModel.current_weekly_total_count);
|
|
87
|
+
const weeklyUsage = toNumber(firstModel.current_weekly_usage_count);
|
|
88
|
+
const weeklyStartAt = toTimestamp(firstModel.weekly_start_time);
|
|
89
|
+
const weeklyResetAt = toTimestamp(firstModel.weekly_end_time);
|
|
90
|
+
|
|
91
|
+
// For minimaxi.com: usage_count represents USED credits
|
|
92
|
+
const intervalUsed = intervalUsage;
|
|
93
|
+
const weeklyUsed = weeklyUsage;
|
|
94
|
+
|
|
95
|
+
const intervalUsedPercent =
|
|
96
|
+
intervalTotal > 0 && intervalUsed != null
|
|
97
|
+
? Math.max(0, Math.min(100, (intervalUsed / intervalTotal) * 100))
|
|
98
|
+
: null;
|
|
99
|
+
const intervalWindowSeconds =
|
|
100
|
+
intervalStartAt && intervalResetAt && intervalResetAt > intervalStartAt
|
|
101
|
+
? Math.floor((intervalResetAt - intervalStartAt) / 1000)
|
|
102
|
+
: null;
|
|
103
|
+
const weeklyUsedPercent =
|
|
104
|
+
weeklyTotal > 0 && weeklyUsed != null
|
|
105
|
+
? Math.max(0, Math.min(100, (weeklyUsed / weeklyTotal) * 100))
|
|
106
|
+
: null;
|
|
107
|
+
const weeklyWindowSeconds =
|
|
108
|
+
weeklyStartAt && weeklyResetAt && weeklyResetAt > weeklyStartAt
|
|
109
|
+
? Math.floor((weeklyResetAt - weeklyStartAt) / 1000)
|
|
110
|
+
: null;
|
|
111
|
+
|
|
112
|
+
const windows = {
|
|
113
|
+
'5h': toUsageWindow({
|
|
114
|
+
usedPercent: intervalUsedPercent,
|
|
115
|
+
windowSeconds: intervalWindowSeconds,
|
|
116
|
+
resetAt: intervalResetAt,
|
|
117
|
+
}),
|
|
118
|
+
weekly: toUsageWindow({
|
|
119
|
+
usedPercent: weeklyUsedPercent,
|
|
120
|
+
windowSeconds: weeklyWindowSeconds,
|
|
121
|
+
resetAt: weeklyResetAt,
|
|
122
|
+
}),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return buildResult({
|
|
126
|
+
providerId,
|
|
127
|
+
providerName,
|
|
128
|
+
ok: true,
|
|
129
|
+
configured: true,
|
|
130
|
+
usage: { windows },
|
|
131
|
+
});
|
|
132
|
+
} catch (error) {
|
|
133
|
+
return buildResult({
|
|
134
|
+
providerId,
|
|
135
|
+
providerName,
|
|
136
|
+
ok: false,
|
|
137
|
+
configured: true,
|
|
138
|
+
error: error instanceof Error ? error.message : 'Request failed',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
};
|
|
@@ -1,15 +1,139 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const providerId =
|
|
12
|
-
export const providerName =
|
|
13
|
-
export const aliases =
|
|
14
|
-
|
|
15
|
-
export const
|
|
1
|
+
import { readAuthFile } from '../../opencode/auth.js';
|
|
2
|
+
import {
|
|
3
|
+
getAuthEntry,
|
|
4
|
+
normalizeAuthEntry,
|
|
5
|
+
buildResult,
|
|
6
|
+
toUsageWindow,
|
|
7
|
+
toNumber,
|
|
8
|
+
toTimestamp,
|
|
9
|
+
} from '../utils/index.js';
|
|
10
|
+
|
|
11
|
+
export const providerId = 'minimax-coding-plan';
|
|
12
|
+
export const providerName = 'MiniMax Coding Plan (minimax.io)';
|
|
13
|
+
export const aliases = ['minimax-coding-plan'];
|
|
14
|
+
|
|
15
|
+
export const isConfigured = () => {
|
|
16
|
+
const auth = readAuthFile();
|
|
17
|
+
const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
|
|
18
|
+
return Boolean(entry?.key || entry?.token);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const fetchQuota = async () => {
|
|
22
|
+
const auth = readAuthFile();
|
|
23
|
+
const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
|
|
24
|
+
const apiKey = entry?.key ?? entry?.token;
|
|
25
|
+
|
|
26
|
+
if (!apiKey) {
|
|
27
|
+
return buildResult({
|
|
28
|
+
providerId,
|
|
29
|
+
providerName,
|
|
30
|
+
ok: false,
|
|
31
|
+
configured: false,
|
|
32
|
+
error: 'Not configured',
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetch(
|
|
38
|
+
'https://api.minimax.io/v1/api/openplatform/coding_plan/remains',
|
|
39
|
+
{
|
|
40
|
+
method: 'GET',
|
|
41
|
+
headers: {
|
|
42
|
+
Authorization: `Bearer ${apiKey}`,
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
return buildResult({
|
|
50
|
+
providerId,
|
|
51
|
+
providerName,
|
|
52
|
+
ok: false,
|
|
53
|
+
configured: true,
|
|
54
|
+
error: `API error: ${response.status}`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const payload = await response.json();
|
|
59
|
+
const baseResp = payload?.base_resp;
|
|
60
|
+
if (baseResp && baseResp.status_code !== 0) {
|
|
61
|
+
return buildResult({
|
|
62
|
+
providerId,
|
|
63
|
+
providerName,
|
|
64
|
+
ok: false,
|
|
65
|
+
configured: true,
|
|
66
|
+
error: baseResp.status_msg || `API error: ${baseResp.status_code}`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const firstModel = payload?.model_remains?.[0];
|
|
71
|
+
if (!firstModel) {
|
|
72
|
+
return buildResult({
|
|
73
|
+
providerId,
|
|
74
|
+
providerName,
|
|
75
|
+
ok: false,
|
|
76
|
+
configured: true,
|
|
77
|
+
error: 'No model quota data available',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const intervalTotal = toNumber(firstModel.current_interval_total_count);
|
|
82
|
+
const intervalUsage = toNumber(firstModel.current_interval_usage_count);
|
|
83
|
+
const intervalStartAt = toTimestamp(firstModel.start_time);
|
|
84
|
+
const intervalResetAt = toTimestamp(firstModel.end_time);
|
|
85
|
+
const weeklyTotal = toNumber(firstModel.current_weekly_total_count);
|
|
86
|
+
const weeklyUsage = toNumber(firstModel.current_weekly_usage_count);
|
|
87
|
+
const weeklyStartAt = toTimestamp(firstModel.weekly_start_time);
|
|
88
|
+
const weeklyResetAt = toTimestamp(firstModel.weekly_end_time);
|
|
89
|
+
|
|
90
|
+
const intervalUsed = intervalUsage;
|
|
91
|
+
const weeklyUsed = weeklyUsage;
|
|
92
|
+
|
|
93
|
+
const intervalUsedPercent =
|
|
94
|
+
intervalTotal > 0 && intervalUsed !== null
|
|
95
|
+
? Math.max(0, Math.min(100, (intervalUsed / intervalTotal) * 100))
|
|
96
|
+
: null;
|
|
97
|
+
const intervalWindowSeconds =
|
|
98
|
+
intervalStartAt && intervalResetAt && intervalResetAt > intervalStartAt
|
|
99
|
+
? Math.floor((intervalResetAt - intervalStartAt) / 1000)
|
|
100
|
+
: null;
|
|
101
|
+
const weeklyUsedPercent =
|
|
102
|
+
weeklyTotal > 0 && weeklyUsed !== null
|
|
103
|
+
? Math.max(0, Math.min(100, (weeklyUsed / weeklyTotal) * 100))
|
|
104
|
+
: null;
|
|
105
|
+
const weeklyWindowSeconds =
|
|
106
|
+
weeklyStartAt && weeklyResetAt && weeklyResetAt > weeklyStartAt
|
|
107
|
+
? Math.floor((weeklyResetAt - weeklyStartAt) / 1000)
|
|
108
|
+
: null;
|
|
109
|
+
|
|
110
|
+
const windows = {
|
|
111
|
+
'5h': toUsageWindow({
|
|
112
|
+
usedPercent: intervalUsedPercent,
|
|
113
|
+
windowSeconds: intervalWindowSeconds,
|
|
114
|
+
resetAt: intervalResetAt,
|
|
115
|
+
}),
|
|
116
|
+
weekly: toUsageWindow({
|
|
117
|
+
usedPercent: weeklyUsedPercent,
|
|
118
|
+
windowSeconds: weeklyWindowSeconds,
|
|
119
|
+
resetAt: weeklyResetAt,
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return buildResult({
|
|
124
|
+
providerId,
|
|
125
|
+
providerName,
|
|
126
|
+
ok: true,
|
|
127
|
+
configured: true,
|
|
128
|
+
usage: { windows },
|
|
129
|
+
});
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return buildResult({
|
|
132
|
+
providerId,
|
|
133
|
+
providerName,
|
|
134
|
+
ok: false,
|
|
135
|
+
configured: true,
|
|
136
|
+
error: error instanceof Error ? error.message : 'Request failed',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
};
|