@getmegabrain/cli 0.1.10 → 0.1.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.
|
@@ -4,43 +4,89 @@
|
|
|
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';
|
|
8
|
+
/** Transient HTTP statuses worth retrying: overload (429/529), gateway/upstream (500/502/503/504),
|
|
9
|
+
* and request timeout (408). A 503 "temporarily_unavailable" from the Gateway lands here. */
|
|
10
|
+
const RETRYABLE_STATUSES = new Set([408, 429, 500, 502, 503, 504, 529]);
|
|
7
11
|
export class GatewayModel {
|
|
8
12
|
url;
|
|
9
13
|
apiKey;
|
|
10
14
|
model;
|
|
11
15
|
maxTokens;
|
|
16
|
+
maxRetries;
|
|
17
|
+
retryBaseMs;
|
|
18
|
+
sleep;
|
|
12
19
|
constructor(options) {
|
|
13
20
|
this.url =
|
|
14
21
|
options.url ??
|
|
15
22
|
process.env.MB_GATEWAY_URL ??
|
|
16
23
|
`${(process.env.MEGABRAIN_APP_URL ?? 'https://getmegabrain.com').replace(/\/+$/, '')}/api/gateway/v1/messages`;
|
|
17
24
|
this.apiKey = options.apiKey;
|
|
18
|
-
this.model = options.model ??
|
|
25
|
+
this.model = options.model ?? DEFAULT_MODEL;
|
|
19
26
|
this.maxTokens = options.maxTokens ?? 4096;
|
|
27
|
+
this.maxRetries = options.maxRetries ?? 4;
|
|
28
|
+
this.retryBaseMs = options.retryBaseMs ?? 800;
|
|
29
|
+
this.sleep = options.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms)));
|
|
30
|
+
}
|
|
31
|
+
/** Backoff for a given attempt (0-indexed): exponential + jitter, honoring Retry-After. */
|
|
32
|
+
backoffMs(attempt, retryAfter) {
|
|
33
|
+
const headerSeconds = retryAfter ? Number(retryAfter) : NaN;
|
|
34
|
+
if (Number.isFinite(headerSeconds) && headerSeconds >= 0) {
|
|
35
|
+
return Math.min(headerSeconds * 1000, 30_000);
|
|
36
|
+
}
|
|
37
|
+
const base = this.retryBaseMs * 2 ** attempt;
|
|
38
|
+
return Math.min(base, 15_000) + Math.floor(Math.random() * 250);
|
|
20
39
|
}
|
|
21
40
|
async complete(request) {
|
|
22
41
|
if (!this.apiKey)
|
|
23
42
|
throw new Error('Sign in to MegaBrain to use the agent.');
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
},
|
|
31
|
-
body: JSON.stringify({
|
|
32
|
-
model: this.model,
|
|
33
|
-
max_tokens: this.maxTokens,
|
|
34
|
-
system: request.system,
|
|
35
|
-
messages: request.messages,
|
|
36
|
-
tools: request.tools,
|
|
37
|
-
}),
|
|
43
|
+
const body = JSON.stringify({
|
|
44
|
+
model: this.model,
|
|
45
|
+
max_tokens: this.maxTokens,
|
|
46
|
+
system: request.system,
|
|
47
|
+
messages: request.messages,
|
|
48
|
+
tools: request.tools,
|
|
38
49
|
});
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
50
|
+
let lastError = '';
|
|
51
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt += 1) {
|
|
52
|
+
let response;
|
|
53
|
+
try {
|
|
54
|
+
response = await fetch(this.url, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: {
|
|
57
|
+
'content-type': 'application/json',
|
|
58
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
59
|
+
'anthropic-version': '2023-06-01',
|
|
60
|
+
},
|
|
61
|
+
body,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
// Network-level failure (DNS, connection reset) — treat as transient.
|
|
66
|
+
lastError = `network error: ${error instanceof Error ? error.message : String(error)}`;
|
|
67
|
+
if (attempt < this.maxRetries) {
|
|
68
|
+
await this.sleep(this.backoffMs(attempt));
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
throw new Error(`Gateway request failed (${lastError}) after ${attempt + 1} attempts`);
|
|
72
|
+
}
|
|
73
|
+
if (response.ok) {
|
|
74
|
+
const data = (await response.json());
|
|
75
|
+
return { content: data.content ?? [], stopReason: data.stop_reason ?? 'end_turn' };
|
|
76
|
+
}
|
|
77
|
+
const detail = (await response.text()).slice(0, 500);
|
|
78
|
+
if (RETRYABLE_STATUSES.has(response.status)) {
|
|
79
|
+
lastError = `${response.status}: ${detail}`;
|
|
80
|
+
if (attempt < this.maxRetries) {
|
|
81
|
+
await this.sleep(this.backoffMs(attempt, response.headers.get('retry-after')));
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
// Transient, but out of retries.
|
|
85
|
+
throw new Error(`Gateway request failed (${lastError}) after ${attempt + 1} attempts`);
|
|
86
|
+
}
|
|
87
|
+
throw new Error(`Gateway request failed (${response.status}): ${detail}`);
|
|
42
88
|
}
|
|
43
|
-
|
|
44
|
-
|
|
89
|
+
// Unreachable: the loop always returns or throws. Present for exhaustiveness.
|
|
90
|
+
throw new Error(`Gateway request failed (${lastError})`);
|
|
45
91
|
}
|
|
46
92
|
}
|
|
@@ -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>
|
package/dist/science/server.js
CHANGED
|
@@ -13,6 +13,7 @@ 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';
|
|
16
17
|
/**
|
|
17
18
|
* The `megabrain science` local daemon's HTTP server. Serves the sign-in hand-off on
|
|
18
19
|
* localhost (docs/megabrain-science/reference/claude-science-flow.md §1.1) and, once signed
|
|
@@ -184,6 +185,14 @@ export async function startScienceServer(host = '127.0.0.1') {
|
|
|
184
185
|
clearCredentials();
|
|
185
186
|
return json(res, { ok: true });
|
|
186
187
|
}
|
|
188
|
+
// ── Model picker: the live owner-mode models from megabrain_model_map. ──
|
|
189
|
+
if (path === '/api/models' && method === 'GET') {
|
|
190
|
+
const creds = readCredentials();
|
|
191
|
+
if (!creds)
|
|
192
|
+
return json(res, { error: 'signed-out' }, 401);
|
|
193
|
+
const models = await fetchPickerModels(creds.token);
|
|
194
|
+
return json(res, { models, default: DEFAULT_MODEL });
|
|
195
|
+
}
|
|
187
196
|
// ── Onboarding API (#286). Signed-in only. ──────────────────────────
|
|
188
197
|
if (path === '/api/onboarding') {
|
|
189
198
|
if (!isSignedIn())
|
|
@@ -263,11 +272,12 @@ export async function startScienceServer(host = '127.0.0.1') {
|
|
|
263
272
|
const prompt = str(b.prompt)?.trim() ?? '';
|
|
264
273
|
if (!prompt)
|
|
265
274
|
return json(res, { error: 'prompt-required' }, 400);
|
|
275
|
+
const model = str(b.model)?.trim() || DEFAULT_MODEL;
|
|
266
276
|
const session = store.getSession(sid);
|
|
267
277
|
const project = session ? store.getProject(session.projectId) : null;
|
|
268
278
|
store.touchSession(sid, session && !session.summary ? { summary: prompt } : undefined);
|
|
269
279
|
fanout(sid, 'agent', { type: 'user', text: prompt });
|
|
270
|
-
const agent = new AgentSession(new GatewayModel({ apiKey: creds.token }), [
|
|
280
|
+
const agent = new AgentSession(new GatewayModel({ apiKey: creds.token, model }), [
|
|
271
281
|
pythonTool(kernels.get(sid)),
|
|
272
282
|
literatureTool(new MultiSourceClient([new ArxivClient(), new OpenAlexClient(), new PubMedClient()])),
|
|
273
283
|
], evt => fanout(sid, 'agent', evt), project?.agentContext ?? '', store.getMessages(sid));
|
|
@@ -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);
|
|
@@ -116,6 +118,42 @@
|
|
|
116
118
|
});
|
|
117
119
|
}
|
|
118
120
|
|
|
121
|
+
// ── Model picker: the live owner-mode models (from megabrain_model_map) ──
|
|
122
|
+
function loadModels() {
|
|
123
|
+
if (!modelSel) return;
|
|
124
|
+
fetch('/api/models')
|
|
125
|
+
.then(function (r) {
|
|
126
|
+
return r.json();
|
|
127
|
+
})
|
|
128
|
+
.then(function (data) {
|
|
129
|
+
var models = (data && data.models) || [];
|
|
130
|
+
var saved = null;
|
|
131
|
+
try {
|
|
132
|
+
saved = localStorage.getItem(MODEL_KEY);
|
|
133
|
+
} catch (e) {
|
|
134
|
+
saved = null;
|
|
135
|
+
}
|
|
136
|
+
var want = saved || (data && data.default);
|
|
137
|
+
modelSel.textContent = '';
|
|
138
|
+
models.forEach(function (m) {
|
|
139
|
+
var opt = el('option', null, m.label || m.id);
|
|
140
|
+
opt.value = m.id;
|
|
141
|
+
if (m.id === want) opt.selected = true;
|
|
142
|
+
modelSel.appendChild(opt);
|
|
143
|
+
});
|
|
144
|
+
// If the saved/default id wasn't in the list, fall back to the first option.
|
|
145
|
+
if (models.length && modelSel.selectedIndex < 0) modelSel.selectedIndex = 0;
|
|
146
|
+
modelSel.addEventListener('change', function () {
|
|
147
|
+
try {
|
|
148
|
+
localStorage.setItem(MODEL_KEY, modelSel.value);
|
|
149
|
+
} catch (e) {
|
|
150
|
+
/* ignore */
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
})
|
|
154
|
+
.catch(function () {});
|
|
155
|
+
}
|
|
156
|
+
|
|
119
157
|
// ── Live event stream ───────────────────────────────────────────────
|
|
120
158
|
function connect() {
|
|
121
159
|
var es = new EventSource('/api/sessions/' + sid + '/events');
|
|
@@ -151,7 +189,7 @@
|
|
|
151
189
|
fetch('/api/sessions/' + sid + '/agent', {
|
|
152
190
|
method: 'POST',
|
|
153
191
|
headers: { 'content-type': 'application/json' },
|
|
154
|
-
body: JSON.stringify({ prompt: text }),
|
|
192
|
+
body: JSON.stringify({ prompt: text, model: modelSel ? modelSel.value : undefined }),
|
|
155
193
|
}).catch(function () {
|
|
156
194
|
setRunning(false);
|
|
157
195
|
});
|
|
@@ -199,6 +237,7 @@
|
|
|
199
237
|
.catch(function () {})
|
|
200
238
|
.then(function () {
|
|
201
239
|
loadSidebar();
|
|
240
|
+
loadModels();
|
|
202
241
|
connect();
|
|
203
242
|
});
|
|
204
243
|
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getmegabrain/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
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",
|