@getmegabrain/cli 0.1.11 → 0.1.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.
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* `ModelClient` so the transport is swappable; in production it's the Gateway
|
|
5
5
|
* (`/api/gateway/v1/messages`) authenticated with the signed-in account token.
|
|
6
6
|
*/
|
|
7
|
+
import { DEFAULT_MODEL } from './models.js';
|
|
7
8
|
/** Transient HTTP statuses worth retrying: overload (429/529), gateway/upstream (500/502/503/504),
|
|
8
9
|
* and request timeout (408). A 503 "temporarily_unavailable" from the Gateway lands here. */
|
|
9
10
|
const RETRYABLE_STATUSES = new Set([408, 429, 500, 502, 503, 504, 529]);
|
|
@@ -21,7 +22,7 @@ export class GatewayModel {
|
|
|
21
22
|
process.env.MB_GATEWAY_URL ??
|
|
22
23
|
`${(process.env.MEGABRAIN_APP_URL ?? 'https://getmegabrain.com').replace(/\/+$/, '')}/api/gateway/v1/messages`;
|
|
23
24
|
this.apiKey = options.apiKey;
|
|
24
|
-
this.model = options.model ??
|
|
25
|
+
this.model = options.model ?? DEFAULT_MODEL;
|
|
25
26
|
this.maxTokens = options.maxTokens ?? 4096;
|
|
26
27
|
this.maxRetries = options.maxRetries ?? 4;
|
|
27
28
|
this.retryBaseMs = options.retryBaseMs ?? 800;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model picker source for MegaBrain Science. The list of selectable models is the owner-mode
|
|
3
|
+
* picker the Gateway serves from the `megabrain_model_map` table (curated `mb-auto/*` tiers +
|
|
4
|
+
* the concrete OmniRoute coding models) — fetched live from `/api/gateway/models` with the
|
|
5
|
+
* signed-in token. If that fetch fails we fall back to a small hardcoded mirror so the picker
|
|
6
|
+
* is never empty. The old default (`claude-opus-4-8`) is no longer served and returned 503s;
|
|
7
|
+
* the default is now a live Auto tier.
|
|
8
|
+
*/
|
|
9
|
+
/** Live default: the "frontier" Auto tier — the most capable routing for research work. */
|
|
10
|
+
export const DEFAULT_MODEL = process.env.MB_SCIENCE_MODEL ?? 'mb-auto/frontier';
|
|
11
|
+
// A minimal mirror of the live picker, used only when the Gateway list can't be fetched.
|
|
12
|
+
// The live Auto tiers are frontier + free (consolidated in #299); the concrete OmniRoute
|
|
13
|
+
// coding models stay pickable via megabrain_model_map.
|
|
14
|
+
export const FALLBACK_MODELS = [
|
|
15
|
+
{ id: 'mb-auto/frontier', label: 'Auto · Frontier' },
|
|
16
|
+
{ id: 'mb-auto/free', label: 'Auto · Free' },
|
|
17
|
+
{ id: 'moonshotai/kimi-k2.7-code', label: 'Kimi K2.7 Code' },
|
|
18
|
+
{ id: 'z-ai/glm-5.2', label: 'GLM-5.2' },
|
|
19
|
+
{ id: 'minimax/minimax-m3', label: 'MiniMax M3' },
|
|
20
|
+
];
|
|
21
|
+
function gatewayModelsUrl() {
|
|
22
|
+
if (process.env.MB_GATEWAY_MODELS_URL)
|
|
23
|
+
return process.env.MB_GATEWAY_MODELS_URL;
|
|
24
|
+
const base = (process.env.MEGABRAIN_APP_URL ?? 'https://getmegabrain.com').replace(/\/+$/, '');
|
|
25
|
+
return `${base}/api/gateway/models`;
|
|
26
|
+
}
|
|
27
|
+
/** Turn a model id into a readable label when the catalogue doesn't provide a name. */
|
|
28
|
+
export function prettyLabel(id) {
|
|
29
|
+
const autoTier = /^mb-auto\/(.+)$/.exec(id);
|
|
30
|
+
if (autoTier) {
|
|
31
|
+
const tier = autoTier[1].replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
32
|
+
return `Auto · ${tier}`;
|
|
33
|
+
}
|
|
34
|
+
const tail = id.includes('/') ? id.slice(id.indexOf('/') + 1) : id;
|
|
35
|
+
return tail.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
36
|
+
}
|
|
37
|
+
// Auto tiers surface first, most-capable first; unknown ids fall to the end of the Auto block.
|
|
38
|
+
const AUTO_TIER_ORDER = [
|
|
39
|
+
'mb-auto/frontier',
|
|
40
|
+
'mb-auto/best-coding',
|
|
41
|
+
'mb-auto/balanced',
|
|
42
|
+
'mb-auto/efficient',
|
|
43
|
+
'mb-auto/small',
|
|
44
|
+
'mb-auto/free',
|
|
45
|
+
];
|
|
46
|
+
function autoRank(id) {
|
|
47
|
+
const i = AUTO_TIER_ORDER.indexOf(id);
|
|
48
|
+
if (i >= 0)
|
|
49
|
+
return i;
|
|
50
|
+
return id.startsWith('mb-auto/') ? AUTO_TIER_ORDER.length : Infinity;
|
|
51
|
+
}
|
|
52
|
+
/** Sort so the Auto tiers surface first (most capable first), then the rest alphabetically. */
|
|
53
|
+
function orderModels(models) {
|
|
54
|
+
return [...models].sort((a, b) => {
|
|
55
|
+
const ra = autoRank(a.id);
|
|
56
|
+
const rb = autoRank(b.id);
|
|
57
|
+
if (ra !== rb)
|
|
58
|
+
return ra - rb;
|
|
59
|
+
return a.label.localeCompare(b.label);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Fetch the owner-mode picker models from the Gateway. Returns the curated
|
|
64
|
+
* `megabrain_model_map` set for the signed-in account; falls back to FALLBACK_MODELS
|
|
65
|
+
* on any error or an empty response.
|
|
66
|
+
*/
|
|
67
|
+
export async function fetchPickerModels(token) {
|
|
68
|
+
if (!token)
|
|
69
|
+
return [...FALLBACK_MODELS];
|
|
70
|
+
try {
|
|
71
|
+
const response = await fetch(gatewayModelsUrl(), {
|
|
72
|
+
headers: { authorization: `Bearer ${token}` },
|
|
73
|
+
});
|
|
74
|
+
if (!response.ok)
|
|
75
|
+
return [...FALLBACK_MODELS];
|
|
76
|
+
const data = (await response.json());
|
|
77
|
+
const models = (data.data ?? [])
|
|
78
|
+
.map(m => ({
|
|
79
|
+
id: String(m.id ?? ''),
|
|
80
|
+
label: m.name?.trim() || prettyLabel(String(m.id ?? '')),
|
|
81
|
+
}))
|
|
82
|
+
.filter(m => m.id);
|
|
83
|
+
return models.length ? orderModels(models) : [...FALLBACK_MODELS];
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return [...FALLBACK_MODELS];
|
|
87
|
+
}
|
|
88
|
+
}
|
package/dist/science/pages.js
CHANGED
|
@@ -316,7 +316,10 @@ export function sessionPage(projectId, sessionId) {
|
|
|
316
316
|
.composer-inner:focus-within { border-color:var(--accent); }
|
|
317
317
|
textarea#composer { flex:1; border:0; background:transparent; resize:none; color:inherit; font:inherit;
|
|
318
318
|
outline:none; max-height:160px; }
|
|
319
|
-
.model { font-size:12px; color:var(--muted); align-self:center; white-space:nowrap;
|
|
319
|
+
select.model { font-size:12px; color:var(--muted); align-self:center; white-space:nowrap;
|
|
320
|
+
max-width:180px; border:1px solid var(--border); border-radius:8px; background:var(--panel);
|
|
321
|
+
padding:5px 8px; font-family:inherit; cursor:pointer; outline:none; }
|
|
322
|
+
select.model:focus { border-color:var(--accent); }
|
|
320
323
|
#send { border:0; border-radius:10px; background:var(--accent); color:#fff; font:inherit; font-weight:600;
|
|
321
324
|
padding:8px 16px; cursor:pointer; }
|
|
322
325
|
#send:disabled { opacity:.5; cursor:default; }
|
|
@@ -336,7 +339,7 @@ export function sessionPage(projectId, sessionId) {
|
|
|
336
339
|
<div class="composer">
|
|
337
340
|
<div class="composer-inner">
|
|
338
341
|
<textarea id="composer" rows="1" placeholder="Ask anything — the agent runs code locally…"></textarea>
|
|
339
|
-
<
|
|
342
|
+
<select class="model" id="model" title="Model"></select>
|
|
340
343
|
<button id="send">Send</button>
|
|
341
344
|
</div>
|
|
342
345
|
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A per-key serial task queue. Tasks enqueued under the same key run strictly one after
|
|
3
|
+
* another (in enqueue order); tasks under different keys run independently.
|
|
4
|
+
*
|
|
5
|
+
* The Science daemon uses this to serialize agent turns per session: without it, sending
|
|
6
|
+
* several messages in quick succession spawns overlapping AgentSessions that share one
|
|
7
|
+
* kernel and race on the persisted transcript — so turns clobber each other and messages
|
|
8
|
+
* appear "unseen". Serializing means each turn starts only after the previous finished and
|
|
9
|
+
* therefore reads the prior turn's saved history.
|
|
10
|
+
*/
|
|
11
|
+
export function createKeyedQueue() {
|
|
12
|
+
const tails = new Map();
|
|
13
|
+
return function enqueue(key, task) {
|
|
14
|
+
const prev = tails.get(key) ?? Promise.resolve();
|
|
15
|
+
// Run `task` after the previous one settles — on both fulfill and reject, so a failed
|
|
16
|
+
// turn never blocks the queue.
|
|
17
|
+
const result = prev.then(task, task);
|
|
18
|
+
// The tail we chain the *next* task onto must never reject (that would skip the catch
|
|
19
|
+
// handler on subsequent tasks); swallow it here. Callers still see `result`'s outcome.
|
|
20
|
+
tails.set(key, result.catch(() => undefined));
|
|
21
|
+
return result;
|
|
22
|
+
};
|
|
23
|
+
}
|
package/dist/science/server.js
CHANGED
|
@@ -13,6 +13,8 @@ import { ArxivClient, literatureTool, MultiSourceClient } from './agent/literatu
|
|
|
13
13
|
import { OpenAlexClient } from './agent/openalex.js';
|
|
14
14
|
import { PubMedClient } from './agent/pubmed.js';
|
|
15
15
|
import { GatewayModel } from './agent/model.js';
|
|
16
|
+
import { DEFAULT_MODEL, fetchPickerModels } from './agent/models.js';
|
|
17
|
+
import { createKeyedQueue } from './queue.js';
|
|
16
18
|
/**
|
|
17
19
|
* The `megabrain science` local daemon's HTTP server. Serves the sign-in hand-off on
|
|
18
20
|
* localhost (docs/megabrain-science/reference/claude-science-flow.md §1.1) and, once signed
|
|
@@ -33,6 +35,9 @@ export async function startScienceServer(host = '127.0.0.1') {
|
|
|
33
35
|
// flow to every browser watching a session, tagged with a `channel`.
|
|
34
36
|
const sseClients = new Map();
|
|
35
37
|
const kernelHooked = new Set();
|
|
38
|
+
// Serialize agent turns per session so rapid messages are handled in order (each seeing the
|
|
39
|
+
// previous turn's history) instead of racing on the shared kernel and transcript.
|
|
40
|
+
const agentQueue = createKeyedQueue();
|
|
36
41
|
const fanout = (sid, channel, payload) => {
|
|
37
42
|
const set = sseClients.get(sid);
|
|
38
43
|
if (!set)
|
|
@@ -184,6 +189,14 @@ export async function startScienceServer(host = '127.0.0.1') {
|
|
|
184
189
|
clearCredentials();
|
|
185
190
|
return json(res, { ok: true });
|
|
186
191
|
}
|
|
192
|
+
// ── Model picker: the live owner-mode models from megabrain_model_map. ──
|
|
193
|
+
if (path === '/api/models' && method === 'GET') {
|
|
194
|
+
const creds = readCredentials();
|
|
195
|
+
if (!creds)
|
|
196
|
+
return json(res, { error: 'signed-out' }, 401);
|
|
197
|
+
const models = await fetchPickerModels(creds.token);
|
|
198
|
+
return json(res, { models, default: DEFAULT_MODEL });
|
|
199
|
+
}
|
|
187
200
|
// ── Onboarding API (#286). Signed-in only. ──────────────────────────
|
|
188
201
|
if (path === '/api/onboarding') {
|
|
189
202
|
if (!isSignedIn())
|
|
@@ -263,17 +276,29 @@ export async function startScienceServer(host = '127.0.0.1') {
|
|
|
263
276
|
const prompt = str(b.prompt)?.trim() ?? '';
|
|
264
277
|
if (!prompt)
|
|
265
278
|
return json(res, { error: 'prompt-required' }, 400);
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
|
|
279
|
+
const model = str(b.model)?.trim() || DEFAULT_MODEL;
|
|
280
|
+
const token = creds.token;
|
|
281
|
+
// Echo the user's message immediately so it shows up in order, then run the turn on the
|
|
282
|
+
// per-session queue. The turn reads history *at run time* so it includes any turn that
|
|
283
|
+
// was still in flight when this message arrived.
|
|
269
284
|
fanout(sid, 'agent', { type: 'user', text: prompt });
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
285
|
+
void agentQueue(sid, async () => {
|
|
286
|
+
const session = store.getSession(sid);
|
|
287
|
+
const project = session ? store.getProject(session.projectId) : null;
|
|
288
|
+
store.touchSession(sid, session && !session.summary ? { summary: prompt } : undefined);
|
|
289
|
+
const agent = new AgentSession(new GatewayModel({ apiKey: token, model }), [
|
|
290
|
+
pythonTool(kernels.get(sid)),
|
|
291
|
+
literatureTool(new MultiSourceClient([new ArxivClient(), new OpenAlexClient(), new PubMedClient()])),
|
|
292
|
+
], evt => fanout(sid, 'agent', evt), project?.agentContext ?? '', store.getMessages(sid));
|
|
293
|
+
try {
|
|
294
|
+
await agent.run(prompt);
|
|
295
|
+
}
|
|
296
|
+
finally {
|
|
297
|
+
// Persist the full history so the session remembers it and the transcript survives
|
|
298
|
+
// a refresh — and so the next queued turn reads it as its starting point.
|
|
299
|
+
store.setMessages(sid, agent.getMessages());
|
|
300
|
+
}
|
|
301
|
+
});
|
|
277
302
|
return json(res, { ok: true });
|
|
278
303
|
}
|
|
279
304
|
}
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
var composer = $('composer');
|
|
15
15
|
var sendBtn = $('send');
|
|
16
16
|
var kstate = $('kstate');
|
|
17
|
+
var modelSel = $('model');
|
|
17
18
|
var lastCard = null;
|
|
19
|
+
var MODEL_KEY = 'mb_sci_model';
|
|
18
20
|
|
|
19
21
|
function el(tag, cls, text) {
|
|
20
22
|
var e = document.createElement(tag);
|
|
@@ -77,9 +79,13 @@
|
|
|
77
79
|
scroll();
|
|
78
80
|
}
|
|
79
81
|
|
|
82
|
+
// Track how many turns are in flight. The send button stays enabled so you can queue
|
|
83
|
+
// follow-up messages while the agent works — the server runs them in order per session.
|
|
84
|
+
var pending = 0;
|
|
80
85
|
function setRunning(on) {
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
pending += on ? 1 : -1;
|
|
87
|
+
if (pending < 0) pending = 0;
|
|
88
|
+
sendBtn.textContent = pending > 0 ? 'Working…' : 'Send';
|
|
83
89
|
}
|
|
84
90
|
|
|
85
91
|
// Replay a persisted conversation (model messages) into the transcript on load.
|
|
@@ -116,6 +122,42 @@
|
|
|
116
122
|
});
|
|
117
123
|
}
|
|
118
124
|
|
|
125
|
+
// ── Model picker: the live owner-mode models (from megabrain_model_map) ──
|
|
126
|
+
function loadModels() {
|
|
127
|
+
if (!modelSel) return;
|
|
128
|
+
fetch('/api/models')
|
|
129
|
+
.then(function (r) {
|
|
130
|
+
return r.json();
|
|
131
|
+
})
|
|
132
|
+
.then(function (data) {
|
|
133
|
+
var models = (data && data.models) || [];
|
|
134
|
+
var saved = null;
|
|
135
|
+
try {
|
|
136
|
+
saved = localStorage.getItem(MODEL_KEY);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
saved = null;
|
|
139
|
+
}
|
|
140
|
+
var want = saved || (data && data.default);
|
|
141
|
+
modelSel.textContent = '';
|
|
142
|
+
models.forEach(function (m) {
|
|
143
|
+
var opt = el('option', null, m.label || m.id);
|
|
144
|
+
opt.value = m.id;
|
|
145
|
+
if (m.id === want) opt.selected = true;
|
|
146
|
+
modelSel.appendChild(opt);
|
|
147
|
+
});
|
|
148
|
+
// If the saved/default id wasn't in the list, fall back to the first option.
|
|
149
|
+
if (models.length && modelSel.selectedIndex < 0) modelSel.selectedIndex = 0;
|
|
150
|
+
modelSel.addEventListener('change', function () {
|
|
151
|
+
try {
|
|
152
|
+
localStorage.setItem(MODEL_KEY, modelSel.value);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
/* ignore */
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
})
|
|
158
|
+
.catch(function () {});
|
|
159
|
+
}
|
|
160
|
+
|
|
119
161
|
// ── Live event stream ───────────────────────────────────────────────
|
|
120
162
|
function connect() {
|
|
121
163
|
var es = new EventSource('/api/sessions/' + sid + '/events');
|
|
@@ -145,13 +187,13 @@
|
|
|
145
187
|
|
|
146
188
|
function send() {
|
|
147
189
|
var text = composer.value.trim();
|
|
148
|
-
if (!text
|
|
190
|
+
if (!text) return;
|
|
149
191
|
composer.value = '';
|
|
150
192
|
setRunning(true);
|
|
151
193
|
fetch('/api/sessions/' + sid + '/agent', {
|
|
152
194
|
method: 'POST',
|
|
153
195
|
headers: { 'content-type': 'application/json' },
|
|
154
|
-
body: JSON.stringify({ prompt: text }),
|
|
196
|
+
body: JSON.stringify({ prompt: text, model: modelSel ? modelSel.value : undefined }),
|
|
155
197
|
}).catch(function () {
|
|
156
198
|
setRunning(false);
|
|
157
199
|
});
|
|
@@ -199,6 +241,7 @@
|
|
|
199
241
|
.catch(function () {})
|
|
200
242
|
.then(function () {
|
|
201
243
|
loadSidebar();
|
|
244
|
+
loadModels();
|
|
202
245
|
connect();
|
|
203
246
|
});
|
|
204
247
|
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getmegabrain/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "MegaBrain CLI — sign in once, then code from your terminal via OpenCode wired to the MegaBrain Gateway, or run `megabrain science` for the local research workbench.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|