@schlessera/brain-ui-server 0.13.1 → 0.15.0
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/README.md +51 -12
- package/dist/agent/backend.d.ts +163 -37
- package/dist/agent/backend.d.ts.map +1 -1
- package/dist/agent/backend.js +281 -286
- package/dist/agent/backend.js.map +1 -1
- package/dist/app.d.ts +46 -5
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +94 -35
- package/dist/app.js.map +1 -1
- package/dist/brain/client.d.ts +41 -30
- package/dist/brain/client.d.ts.map +1 -1
- package/dist/brain/client.js +138 -132
- package/dist/brain/client.js.map +1 -1
- package/dist/config/env.d.ts +104 -0
- package/dist/config/env.d.ts.map +1 -0
- package/dist/config/env.js +296 -0
- package/dist/config/env.js.map +1 -0
- package/dist/cron/scheduler.d.ts +16 -8
- package/dist/cron/scheduler.d.ts.map +1 -1
- package/dist/cron/scheduler.js +54 -53
- package/dist/cron/scheduler.js.map +1 -1
- package/dist/db/brain-db.d.ts +50 -0
- package/dist/db/brain-db.d.ts.map +1 -0
- package/dist/db/brain-db.js +75 -0
- package/dist/db/brain-db.js.map +1 -0
- package/dist/db/client.d.ts +8 -6
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +12 -25
- package/dist/db/client.js.map +1 -1
- package/dist/db/settings.d.ts +5 -4
- package/dist/db/settings.d.ts.map +1 -1
- package/dist/db/settings.js +9 -12
- package/dist/db/settings.js.map +1 -1
- package/dist/files/walker.d.ts +6 -7
- package/dist/files/walker.d.ts.map +1 -1
- package/dist/files/walker.js +6 -9
- package/dist/files/walker.js.map +1 -1
- package/dist/graph/reader.d.ts +6 -6
- package/dist/graph/reader.d.ts.map +1 -1
- package/dist/graph/reader.js +22 -12
- package/dist/graph/reader.js.map +1 -1
- package/dist/index.d.ts +15 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -10
- package/dist/index.js.map +1 -1
- package/dist/middleware/auth.d.ts +17 -6
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +34 -43
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/origin.d.ts +1 -2
- package/dist/middleware/origin.d.ts.map +1 -1
- package/dist/middleware/origin.js +1 -7
- package/dist/middleware/origin.js.map +1 -1
- package/dist/middleware/passkeys.d.ts +29 -5
- package/dist/middleware/passkeys.d.ts.map +1 -1
- package/dist/middleware/passkeys.js +47 -76
- package/dist/middleware/passkeys.js.map +1 -1
- package/dist/middleware/tailscale.d.ts +14 -11
- package/dist/middleware/tailscale.d.ts.map +1 -1
- package/dist/middleware/tailscale.js +11 -10
- package/dist/middleware/tailscale.js.map +1 -1
- package/dist/routes/brain.d.ts +9 -154
- package/dist/routes/brain.d.ts.map +1 -1
- package/dist/routes/brain.js +187 -188
- package/dist/routes/brain.js.map +1 -1
- package/dist/routes/files.d.ts +4 -88
- package/dist/routes/files.d.ts.map +1 -1
- package/dist/routes/files.js +74 -71
- package/dist/routes/files.js.map +1 -1
- package/dist/routes/graph.d.ts +4 -255
- package/dist/routes/graph.d.ts.map +1 -1
- package/dist/routes/graph.js +67 -64
- package/dist/routes/graph.js.map +1 -1
- package/dist/routes/health.d.ts +8 -22
- package/dist/routes/health.d.ts.map +1 -1
- package/dist/routes/health.js +10 -10
- package/dist/routes/health.js.map +1 -1
- package/dist/routes/models.d.ts +7 -90
- package/dist/routes/models.d.ts.map +1 -1
- package/dist/routes/models.js +53 -51
- package/dist/routes/models.js.map +1 -1
- package/dist/routes/providers.d.ts +5 -34
- package/dist/routes/providers.d.ts.map +1 -1
- package/dist/routes/providers.js +13 -11
- package/dist/routes/providers.js.map +1 -1
- package/dist/routes/sessions.d.ts +7 -78
- package/dist/routes/sessions.d.ts.map +1 -1
- package/dist/routes/sessions.js +35 -34
- package/dist/routes/sessions.js.map +1 -1
- package/dist/routes/share.d.ts +7 -60
- package/dist/routes/share.d.ts.map +1 -1
- package/dist/routes/share.js +80 -77
- package/dist/routes/share.js.map +1 -1
- package/dist/routes/voice.d.ts +8 -91
- package/dist/routes/voice.d.ts.map +1 -1
- package/dist/routes/voice.js +62 -59
- package/dist/routes/voice.js.map +1 -1
- package/dist/share/staging.d.ts +4 -4
- package/dist/share/staging.d.ts.map +1 -1
- package/dist/share/staging.js +7 -8
- package/dist/share/staging.js.map +1 -1
- package/dist/voice/deepgram-token.d.ts +1 -1
- package/dist/voice/deepgram-token.d.ts.map +1 -1
- package/dist/voice/deepgram-token.js +1 -2
- package/dist/voice/deepgram-token.js.map +1 -1
- package/dist/voice/keyterm-builder.d.ts +23 -8
- package/dist/voice/keyterm-builder.d.ts.map +1 -1
- package/dist/voice/keyterm-builder.js +67 -43
- package/dist/voice/keyterm-builder.js.map +1 -1
- package/dist/voice/speech-providers.d.ts +6 -4
- package/dist/voice/speech-providers.d.ts.map +1 -1
- package/dist/voice/speech-providers.js +28 -25
- package/dist/voice/speech-providers.js.map +1 -1
- package/dist/ws/bridge.d.ts.map +1 -1
- package/dist/ws/bridge.js +2 -3
- package/dist/ws/bridge.js.map +1 -1
- package/dist/ws/clients.d.ts +18 -10
- package/dist/ws/clients.d.ts.map +1 -1
- package/dist/ws/clients.js +35 -37
- package/dist/ws/clients.js.map +1 -1
- package/dist/ws/connection.d.ts.map +1 -1
- package/dist/ws/connection.js +5 -7
- package/dist/ws/connection.js.map +1 -1
- package/dist/ws/dispatch.d.ts.map +1 -1
- package/dist/ws/dispatch.js +1 -2
- package/dist/ws/dispatch.js.map +1 -1
- package/dist/ws/host.d.ts +16 -12
- package/dist/ws/host.d.ts.map +1 -1
- package/dist/ws/host.js +14 -23
- package/dist/ws/host.js.map +1 -1
- package/dist/ws/routing.d.ts +2 -1
- package/dist/ws/routing.d.ts.map +1 -1
- package/dist/ws/routing.js +5 -6
- package/dist/ws/routing.js.map +1 -1
- package/dist/ws/run-session.js +1 -1
- package/dist/ws/run-session.js.map +1 -1
- package/dist/ws/session-catalog.d.ts +2 -2
- package/dist/ws/session-catalog.d.ts.map +1 -1
- package/dist/ws/session-catalog.js +2 -3
- package/dist/ws/session-catalog.js.map +1 -1
- package/package.json +8 -5
- package/src/agent/backend.ts +509 -340
- package/src/app.ts +157 -36
- package/src/brain/client.ts +166 -143
- package/src/config/env.ts +401 -0
- package/src/cron/scheduler.ts +77 -59
- package/src/db/brain-db.ts +104 -0
- package/src/db/client.ts +12 -31
- package/src/db/settings.ts +12 -14
- package/src/files/walker.ts +6 -10
- package/src/graph/reader.ts +26 -15
- package/src/index.ts +56 -17
- package/src/middleware/auth.ts +51 -45
- package/src/middleware/origin.ts +1 -8
- package/src/middleware/passkeys.ts +72 -68
- package/src/middleware/tailscale.ts +22 -17
- package/src/routes/brain.ts +31 -30
- package/src/routes/files.ts +10 -7
- package/src/routes/graph.ts +9 -6
- package/src/routes/health.ts +17 -10
- package/src/routes/models.ts +19 -15
- package/src/routes/providers.ts +14 -15
- package/src/routes/sessions.ts +18 -11
- package/src/routes/share.ts +35 -24
- package/src/routes/voice.ts +78 -68
- package/src/share/staging.ts +10 -8
- package/src/voice/deepgram-token.ts +4 -2
- package/src/voice/keyterm-builder.ts +90 -56
- package/src/voice/speech-providers.ts +28 -24
- package/src/ws/bridge.ts +2 -3
- package/src/ws/clients.ts +38 -41
- package/src/ws/connection.ts +5 -7
- package/src/ws/dispatch.ts +1 -2
- package/src/ws/host.ts +24 -27
- package/src/ws/routing.ts +6 -10
- package/src/ws/run-session.ts +1 -1
- package/src/ws/session-catalog.ts +2 -3
- package/dist/ws/handler.d.ts +0 -34
- package/dist/ws/handler.d.ts.map +0 -1
- package/dist/ws/handler.js +0 -33
- package/dist/ws/handler.js.map +0 -1
- package/src/ws/handler.ts +0 -50
package/dist/agent/backend.js
CHANGED
|
@@ -1,27 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "module";
|
|
2
|
-
import { createClaudeBackend, createModelSource, defineProfiles, } from "@schlessera/brain-backend-claude";
|
|
3
|
-
import { getHiddenModelIds } from "../db/settings.js";
|
|
4
|
-
/**
|
|
5
|
-
* Backend registry for the deployment. Claude is always available.
|
|
6
|
-
* `AGENT_BACKEND=pi` retains the legacy single-backend deployment mode when the
|
|
7
|
-
* optional pi package is installed.
|
|
8
|
-
*
|
|
9
|
-
* The old five hardcoded personal inference profiles are gone: the Claude
|
|
10
|
-
* backend ships the single "claude" default, and additional Anthropic-compatible
|
|
11
|
-
* endpoints are declared via the BRAIN_UI_CLAUDE_PROFILES env var (documented in
|
|
12
|
-
* .env.example) rather than in code.
|
|
13
|
-
*/
|
|
14
|
-
const BRAIN_PATH = process.env.BRAIN_PATH || `${process.env.HOME || "/root"}/brain`;
|
|
15
|
-
// The Claude Code native binary is not in node_modules — it is installed via
|
|
16
|
-
// the official curl-bash installer (~/.local/bin/claude, symlinked to
|
|
17
|
-
// /usr/local/bin/claude in the container). The Agent SDK can't always
|
|
18
|
-
// auto-discover it, so hand it the explicit path; override with CLAUDE_CODE_PATH.
|
|
19
|
-
const CLAUDE_CODE_PATH = process.env.CLAUDE_CODE_PATH || "/usr/local/bin/claude";
|
|
20
2
|
const PROFILE_MEMO_MS = 5_000;
|
|
21
|
-
|
|
22
|
-
let profileSnapshot = null;
|
|
23
|
-
let modelSource = null;
|
|
24
|
-
async function createRegistry(backends, defaultBackendId) {
|
|
3
|
+
function buildSnapshot(backends, defaultBackendId) {
|
|
25
4
|
if (backends.length === 0) {
|
|
26
5
|
throw new Error("Backend registry requires at least one backend.");
|
|
27
6
|
}
|
|
@@ -35,290 +14,306 @@ async function createRegistry(backends, defaultBackendId) {
|
|
|
35
14
|
];
|
|
36
15
|
return { backends: ordered, byId, defaultBackendId: resolvedDefaultId };
|
|
37
16
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const owners = new Map();
|
|
46
|
-
for (const backend of registry.backends) {
|
|
47
|
-
const profiles = await backend.listProfiles();
|
|
48
|
-
byBackend.set(backend.id, profiles);
|
|
49
|
-
for (const profile of profiles) {
|
|
50
|
-
const owner = owners.get(profile.id);
|
|
51
|
-
if (owner && owner !== backend.id) {
|
|
52
|
-
throw new Error(`Profile id collision across backends: "${profile.id}" is exposed by "${owner}" and "${backend.id}".`);
|
|
53
|
-
}
|
|
54
|
-
owners.set(profile.id, backend.id);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
profileSnapshot = { at: Date.now(), byBackend, owners };
|
|
58
|
-
return profileSnapshot;
|
|
59
|
-
}
|
|
60
|
-
/** Drop the memo so the next read reflects a refresh or a settings change. */
|
|
61
|
-
export function invalidateProfiles() {
|
|
62
|
-
profileSnapshot = null;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* The Claude backend's discovery source, once the registry is built. Null when
|
|
66
|
-
* discovery is disabled or the deployment runs a different backend.
|
|
67
|
-
*/
|
|
68
|
-
export async function getModelSource() {
|
|
69
|
-
await getRegistry();
|
|
70
|
-
return modelSource;
|
|
17
|
+
const BACKEND_SPECIFIERS = {
|
|
18
|
+
claude: "@schlessera/brain-backend-claude",
|
|
19
|
+
pi: "@schlessera/brain-backend-pi",
|
|
20
|
+
};
|
|
21
|
+
function unknownBackendError(primary) {
|
|
22
|
+
return new Error(`AGENT_BACKEND="${primary}" does not match any configured backend ` +
|
|
23
|
+
`(expected "claude" or "pi").`);
|
|
71
24
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
*/
|
|
77
|
-
function hiddenIds() {
|
|
78
|
-
try {
|
|
79
|
-
return new Set(getHiddenModelIds());
|
|
80
|
-
}
|
|
81
|
-
catch (err) {
|
|
82
|
-
console.warn(`[models] Could not read hidden models: ${err instanceof Error ? err.message : String(err)}`);
|
|
83
|
-
return new Set();
|
|
25
|
+
function missingBackendError(primary) {
|
|
26
|
+
if (primary === "pi") {
|
|
27
|
+
return new Error('AGENT_BACKEND=pi but "@schlessera/brain-backend-pi" is not installed. ' +
|
|
28
|
+
"Add it (with its pi SDK dependencies) or set AGENT_BACKEND=claude.");
|
|
84
29
|
}
|
|
30
|
+
return new Error(`AGENT_BACKEND=${primary} but "@schlessera/brain-backend-claude" is not installed. ` +
|
|
31
|
+
"Add it (it carries the Claude Agent SDK) or set AGENT_BACKEND=pi.");
|
|
85
32
|
}
|
|
86
33
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (raw)
|
|
96
|
-
return !(raw === "0" || raw === "off" || raw === "false");
|
|
97
|
-
return process.env.NODE_ENV !== "test";
|
|
98
|
-
}
|
|
99
|
-
/** How long a discovery result stays fresh. Default 24h. */
|
|
100
|
-
function modelTtlMs() {
|
|
101
|
-
const raw = Number(process.env.BRAIN_UI_MODEL_TTL_HOURS);
|
|
102
|
-
const hours = Number.isFinite(raw) && raw > 0 ? raw : 24;
|
|
103
|
-
return hours * 60 * 60 * 1000;
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Declared profiles (built-in default + BRAIN_UI_CLAUDE_PROFILES) plus every
|
|
107
|
-
* discovered model whose id isn't already declared. Declared wins: the env stays
|
|
108
|
-
* an override mechanism, and a hand-pinned entry keeps its label and endpoint.
|
|
34
|
+
* Boot-time guard: the SELECTED backend's package must at least RESOLVE, so a
|
|
35
|
+
* broken install refuses to start instead of reporting healthy and failing
|
|
36
|
+
* every agent turn (the guarantee the old static import gave, restored without
|
|
37
|
+
* giving up the lazy load — `require.resolve` never executes the module, so
|
|
38
|
+
* the Agent SDK still loads on first use only). Called by `createApp()` next
|
|
39
|
+
* to the auth assertions; skipped when an explicit registry is injected.
|
|
40
|
+
*
|
|
41
|
+
* An unrecognized AGENT_BACKEND fails here too, for the same reason.
|
|
109
42
|
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
43
|
+
* The `resolve` parameter exists for tests (simulating an absent package);
|
|
44
|
+
* production callers pass nothing.
|
|
112
45
|
*/
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const result = [...declared, ...defineProfiles(extra)];
|
|
123
|
-
mergeCache = { declared, discovered, result };
|
|
124
|
-
return result;
|
|
125
|
-
}
|
|
126
|
-
async function buildRegistry() {
|
|
127
|
-
const primary = (process.env.AGENT_BACKEND || "claude")
|
|
128
|
-
.trim()
|
|
129
|
-
.toLowerCase();
|
|
130
|
-
if (primary === "pi") {
|
|
131
|
-
const pi = buildPiBackend();
|
|
132
|
-
return createRegistry([pi], pi.id);
|
|
46
|
+
export function assertBackendResolvable(agent, resolve = (specifier) => {
|
|
47
|
+
createRequire(import.meta.url).resolve(specifier);
|
|
48
|
+
}) {
|
|
49
|
+
const primary = agent.backend || "claude";
|
|
50
|
+
if (!(primary in BACKEND_SPECIFIERS))
|
|
51
|
+
throw unknownBackendError(primary);
|
|
52
|
+
const key = primary;
|
|
53
|
+
try {
|
|
54
|
+
resolve(BACKEND_SPECIFIERS[key]);
|
|
133
55
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const declared = loadClaudeProfiles();
|
|
137
|
-
modelSource = createModelSource({
|
|
138
|
-
brainPath: BRAIN_PATH,
|
|
139
|
-
enabled: modelDiscoveryEnabled(),
|
|
140
|
-
ttlMs: modelTtlMs(),
|
|
141
|
-
});
|
|
142
|
-
const claude = createClaudeBackend({
|
|
143
|
-
brainPath: BRAIN_PATH,
|
|
144
|
-
claudeCodePath: CLAUDE_CODE_PATH,
|
|
145
|
-
// A function, not an array: discovery refreshes in the background and the
|
|
146
|
-
// new roster has to be visible without restarting the process.
|
|
147
|
-
profiles: () => mergeDiscovered(declared, modelSource?.list() ?? []),
|
|
148
|
-
});
|
|
149
|
-
const backends = [claude];
|
|
150
|
-
// AGENT_BACKEND must name a configured backend. The in-process options are
|
|
151
|
-
// "claude" (default) and "pi" (handled above). An unrecognized value is a
|
|
152
|
-
// misconfiguration — fail loudly instead of silently coercing to claude and
|
|
153
|
-
// running on the wrong backend with nothing in the logs.
|
|
154
|
-
if (process.env.AGENT_BACKEND && !backends.some((b) => b.id === primary)) {
|
|
155
|
-
throw new Error(`AGENT_BACKEND="${primary}" does not match any configured backend ` +
|
|
156
|
-
`(expected "claude" or "pi").`);
|
|
56
|
+
catch {
|
|
57
|
+
throw missingBackendError(key);
|
|
157
58
|
}
|
|
158
|
-
return createRegistry(backends, primary);
|
|
159
|
-
}
|
|
160
|
-
async function getRegistry() {
|
|
161
|
-
if (!cachedRegistry)
|
|
162
|
-
cachedRegistry = buildRegistry();
|
|
163
|
-
return cachedRegistry;
|
|
164
|
-
}
|
|
165
|
-
/** All configured backends, with the default backend first. */
|
|
166
|
-
export async function getBackends() {
|
|
167
|
-
return (await getRegistry()).backends;
|
|
168
|
-
}
|
|
169
|
-
/** Find a configured backend by its stable id. */
|
|
170
|
-
export async function getBackendById(id) {
|
|
171
|
-
return (await getRegistry()).byId.get(id);
|
|
172
|
-
}
|
|
173
|
-
/** The default backend used for new and legacy sessions. */
|
|
174
|
-
export async function getDefaultBackend() {
|
|
175
|
-
const registry = await getRegistry();
|
|
176
|
-
return registry.byId.get(registry.defaultBackendId);
|
|
177
59
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
60
|
+
export function createBackendRegistry(options) {
|
|
61
|
+
const { brainPath, agent } = options;
|
|
62
|
+
const getHidden = options.getHiddenModelIds ?? (() => []);
|
|
63
|
+
let cachedRegistry = null;
|
|
64
|
+
let modelSource = null;
|
|
65
|
+
/**
|
|
66
|
+
* Declared profiles (built-in default + BRAIN_UI_CLAUDE_PROFILES) plus every
|
|
67
|
+
* discovered model whose id isn't already declared. Declared wins: the env
|
|
68
|
+
* stays an override mechanism, and a hand-pinned entry keeps its label and
|
|
69
|
+
* endpoint.
|
|
70
|
+
*
|
|
71
|
+
* Memoized on the discovered array's identity — `createModelSource` swaps the
|
|
72
|
+
* array only on a successful refresh, so steady state is one comparison.
|
|
73
|
+
*/
|
|
74
|
+
let mergeCache = null;
|
|
75
|
+
function mergeDiscovered(claude, declared, discovered) {
|
|
76
|
+
if (mergeCache &&
|
|
77
|
+
mergeCache.declared === declared &&
|
|
78
|
+
mergeCache.discovered === discovered) {
|
|
79
|
+
return mergeCache.result;
|
|
80
|
+
}
|
|
81
|
+
const declaredIds = new Set(declared.map((profile) => profile.id));
|
|
82
|
+
const extra = discovered.filter((input) => !declaredIds.has(input.id));
|
|
83
|
+
const result = [...declared, ...claude.defineProfiles(extra)];
|
|
84
|
+
mergeCache = { declared, discovered, result };
|
|
85
|
+
return result;
|
|
197
86
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
source: "builtin",
|
|
258
|
-
},
|
|
259
|
-
]);
|
|
260
|
-
}
|
|
261
|
-
/**
|
|
262
|
-
* The Claude roster: the pinned built-in "claude" default first, plus any extra
|
|
263
|
-
* profiles from BRAIN_UI_CLAUDE_PROFILES (a JSON array of InferenceProfileInput
|
|
264
|
-
* {id,label,model?,baseUrl?,authTokenEnv?,apiKeyEnv?,modelAliases?}).
|
|
265
|
-
*
|
|
266
|
-
* A malformed or duplicate-id roster THROWS — caught at boot by the registry
|
|
267
|
-
* fail-fast — rather than silently degrading to default-only and rebilling
|
|
268
|
-
* every pinned session to the subscription with nothing in the logs.
|
|
269
|
-
*/
|
|
270
|
-
function loadClaudeProfiles() {
|
|
271
|
-
const base = builtinDefaultProfiles();
|
|
272
|
-
const raw = process.env.BRAIN_UI_CLAUDE_PROFILES?.trim();
|
|
273
|
-
if (!raw)
|
|
274
|
-
return base;
|
|
275
|
-
let inputs;
|
|
276
|
-
try {
|
|
277
|
-
inputs = JSON.parse(raw);
|
|
87
|
+
// The built-in default profile's model. The Claude backend historically
|
|
88
|
+
// pinned the default to a specific model; after the SDK extraction the pin
|
|
89
|
+
// was lost and resumed default-profile sessions drifted to "whatever the CLI
|
|
90
|
+
// defaults to", silently changing model/behaviour/billing on every CLI bump.
|
|
91
|
+
// Re-pin it (a committed default, overridable per deploy via
|
|
92
|
+
// BRAIN_UI_CLAUDE_DEFAULT_MODEL) so the default stays on a known model.
|
|
93
|
+
function builtinDefaultProfiles(claude) {
|
|
94
|
+
return claude.defineProfiles([
|
|
95
|
+
{
|
|
96
|
+
id: "claude",
|
|
97
|
+
label: "Claude",
|
|
98
|
+
vendor: "anthropic",
|
|
99
|
+
model: agent.defaultModel,
|
|
100
|
+
modelAliases: true,
|
|
101
|
+
source: "builtin",
|
|
102
|
+
},
|
|
103
|
+
]);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The Claude roster: the pinned built-in "claude" default first, plus any
|
|
107
|
+
* extra profiles from BRAIN_UI_CLAUDE_PROFILES (a JSON array of profile
|
|
108
|
+
* inputs: {id,label,model?,baseUrl?,authTokenEnv?,apiKeyEnv?,modelAliases?}).
|
|
109
|
+
*
|
|
110
|
+
* A malformed or duplicate-id roster THROWS — caught at boot by the registry
|
|
111
|
+
* fail-fast — rather than silently degrading to default-only and rebilling
|
|
112
|
+
* every pinned session to the subscription with nothing in the logs.
|
|
113
|
+
*/
|
|
114
|
+
function loadClaudeProfiles(claude) {
|
|
115
|
+
const base = builtinDefaultProfiles(claude);
|
|
116
|
+
const raw = agent.profilesJson;
|
|
117
|
+
if (!raw)
|
|
118
|
+
return base;
|
|
119
|
+
let inputs;
|
|
120
|
+
try {
|
|
121
|
+
inputs = JSON.parse(raw);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
throw new Error(`BRAIN_UI_CLAUDE_PROFILES is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
125
|
+
}
|
|
126
|
+
if (!Array.isArray(inputs)) {
|
|
127
|
+
throw new Error("BRAIN_UI_CLAUDE_PROFILES must be a JSON array.");
|
|
128
|
+
}
|
|
129
|
+
// Reject duplicate ids (including collisions with the built-in "claude"): a
|
|
130
|
+
// duplicate silently shadows and can resolve to the wrong credentials.
|
|
131
|
+
const seen = new Set(base.map((profile) => profile.id));
|
|
132
|
+
for (const input of inputs) {
|
|
133
|
+
if (!input || typeof input.id !== "string" || input.id.length === 0) {
|
|
134
|
+
throw new Error("Each BRAIN_UI_CLAUDE_PROFILES entry needs a non-empty string id.");
|
|
135
|
+
}
|
|
136
|
+
if (seen.has(input.id)) {
|
|
137
|
+
throw new Error(`Duplicate profile id in BRAIN_UI_CLAUDE_PROFILES: "${input.id}".`);
|
|
138
|
+
}
|
|
139
|
+
seen.add(input.id);
|
|
140
|
+
}
|
|
141
|
+
const declared = inputs.map((input) => ({
|
|
142
|
+
source: "declared",
|
|
143
|
+
...input,
|
|
144
|
+
}));
|
|
145
|
+
return [...base, ...claude.defineProfiles(declared)];
|
|
278
146
|
}
|
|
279
|
-
|
|
280
|
-
|
|
147
|
+
/**
|
|
148
|
+
* NEITHER backend package is a hard dependency — a deployment installs the
|
|
149
|
+
* one its AGENT_BACKEND names (both, if it switches). Loaded lazily through
|
|
150
|
+
* the same `createRequire` path so the server still boots without the unused
|
|
151
|
+
* one; when the configured backend's package is absent, fail with an
|
|
152
|
+
* actionable message.
|
|
153
|
+
*/
|
|
154
|
+
function buildClaudeBackend() {
|
|
155
|
+
const require = createRequire(import.meta.url);
|
|
156
|
+
let claude;
|
|
157
|
+
try {
|
|
158
|
+
claude = require("@schlessera/brain-backend-claude");
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
throw missingBackendError("claude");
|
|
162
|
+
}
|
|
163
|
+
if (typeof claude.createClaudeBackend !== "function") {
|
|
164
|
+
throw new Error('"@schlessera/brain-backend-claude" does not export createClaudeBackend.');
|
|
165
|
+
}
|
|
166
|
+
// Declared profiles are resolved EAGERLY so a malformed
|
|
167
|
+
// BRAIN_UI_CLAUDE_PROFILES still fails at boot rather than on first request.
|
|
168
|
+
const declared = loadClaudeProfiles(claude);
|
|
169
|
+
modelSource = claude.createModelSource({
|
|
170
|
+
brainPath,
|
|
171
|
+
enabled: agent.modelDiscovery,
|
|
172
|
+
ttlMs: agent.modelTtlMs,
|
|
173
|
+
});
|
|
174
|
+
return claude.createClaudeBackend({
|
|
175
|
+
brainPath,
|
|
176
|
+
claudeCodePath: agent.claudeCodePath,
|
|
177
|
+
// A function, not an array: discovery refreshes in the background and the
|
|
178
|
+
// new roster has to be visible without restarting the process.
|
|
179
|
+
profiles: () => mergeDiscovered(claude, declared, modelSource?.list() ?? []),
|
|
180
|
+
});
|
|
281
181
|
}
|
|
282
|
-
|
|
283
|
-
|
|
182
|
+
function buildPiBackend() {
|
|
183
|
+
const require = createRequire(import.meta.url);
|
|
184
|
+
let mod;
|
|
185
|
+
try {
|
|
186
|
+
mod = require("@schlessera/brain-backend-pi");
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
throw missingBackendError("pi");
|
|
190
|
+
}
|
|
191
|
+
if (typeof mod.createPiBackend !== "function") {
|
|
192
|
+
throw new Error('"@schlessera/brain-backend-pi" does not export createPiBackend.');
|
|
193
|
+
}
|
|
194
|
+
return mod.createPiBackend({ brainPath });
|
|
284
195
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
throw new Error("Each BRAIN_UI_CLAUDE_PROFILES entry needs a non-empty string id.");
|
|
196
|
+
async function buildRegistry() {
|
|
197
|
+
const primary = agent.backend || "claude";
|
|
198
|
+
if (primary === "pi") {
|
|
199
|
+
const pi = buildPiBackend();
|
|
200
|
+
return buildSnapshot([pi], pi.id);
|
|
291
201
|
}
|
|
292
|
-
|
|
293
|
-
|
|
202
|
+
const backends = [buildClaudeBackend()];
|
|
203
|
+
// AGENT_BACKEND must name a configured backend. The in-process options are
|
|
204
|
+
// "claude" (default) and "pi" (handled above). An unrecognized value is a
|
|
205
|
+
// misconfiguration — fail loudly instead of silently coercing to claude and
|
|
206
|
+
// running on the wrong backend with nothing in the logs.
|
|
207
|
+
if (agent.backend && !backends.some((b) => b.id === primary)) {
|
|
208
|
+
throw unknownBackendError(primary);
|
|
294
209
|
}
|
|
295
|
-
|
|
210
|
+
return buildSnapshot(backends, primary);
|
|
211
|
+
}
|
|
212
|
+
async function getRegistry() {
|
|
213
|
+
if (!cachedRegistry)
|
|
214
|
+
cachedRegistry = buildRegistry();
|
|
215
|
+
return cachedRegistry;
|
|
296
216
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
})
|
|
301
|
-
return [...base, ...defineProfiles(declared)];
|
|
217
|
+
return makeRegistry(getRegistry, getHidden, async () => {
|
|
218
|
+
await getRegistry();
|
|
219
|
+
return modelSource;
|
|
220
|
+
});
|
|
302
221
|
}
|
|
303
222
|
/**
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
223
|
+
* A registry over an explicit backend list — the seam tests (and embedders
|
|
224
|
+
* with their own backend wiring) use instead of mutating module state. Takes
|
|
225
|
+
* the same optional hidden-ids reader so visibility behavior matches
|
|
226
|
+
* production.
|
|
308
227
|
*/
|
|
309
|
-
function
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
228
|
+
export function createStaticBackendRegistry(backends, defaultBackendId = backends[0]?.id ?? "", options = {}) {
|
|
229
|
+
const snapshot = buildSnapshot(backends, defaultBackendId);
|
|
230
|
+
return makeRegistry(async () => snapshot, options.getHiddenModelIds ?? (() => []), async () => null);
|
|
231
|
+
}
|
|
232
|
+
/** The accessor surface, shared by the config-driven and static registries. */
|
|
233
|
+
function makeRegistry(getRegistry, getHidden, getModelSource) {
|
|
234
|
+
let profileSnapshot = null;
|
|
235
|
+
/** Recompute the profile roster (memoized), asserting cross-backend id uniqueness. */
|
|
236
|
+
async function getProfileSnapshot() {
|
|
237
|
+
if (profileSnapshot && Date.now() - profileSnapshot.at < PROFILE_MEMO_MS) {
|
|
238
|
+
return profileSnapshot;
|
|
239
|
+
}
|
|
240
|
+
const registry = await getRegistry();
|
|
241
|
+
const byBackend = new Map();
|
|
242
|
+
const owners = new Map();
|
|
243
|
+
for (const backend of registry.backends) {
|
|
244
|
+
const profiles = await backend.listProfiles();
|
|
245
|
+
byBackend.set(backend.id, profiles);
|
|
246
|
+
for (const profile of profiles) {
|
|
247
|
+
const owner = owners.get(profile.id);
|
|
248
|
+
if (owner && owner !== backend.id) {
|
|
249
|
+
throw new Error(`Profile id collision across backends: "${profile.id}" is exposed by "${owner}" and "${backend.id}".`);
|
|
250
|
+
}
|
|
251
|
+
owners.set(profile.id, backend.id);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
profileSnapshot = { at: Date.now(), byBackend, owners };
|
|
255
|
+
return profileSnapshot;
|
|
318
256
|
}
|
|
319
|
-
|
|
320
|
-
|
|
257
|
+
/** A settings read failure must not take the picker down — degrade to "nothing hidden". */
|
|
258
|
+
function hiddenIds() {
|
|
259
|
+
try {
|
|
260
|
+
return new Set(getHidden());
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
console.warn(`[models] Could not read hidden models: ${err instanceof Error ? err.message : String(err)}`);
|
|
264
|
+
return new Set();
|
|
265
|
+
}
|
|
321
266
|
}
|
|
322
|
-
return
|
|
267
|
+
return {
|
|
268
|
+
async getBackends() {
|
|
269
|
+
return (await getRegistry()).backends;
|
|
270
|
+
},
|
|
271
|
+
async getBackendById(id) {
|
|
272
|
+
return (await getRegistry()).byId.get(id);
|
|
273
|
+
},
|
|
274
|
+
async getDefaultBackend() {
|
|
275
|
+
const registry = await getRegistry();
|
|
276
|
+
return registry.byId.get(registry.defaultBackendId);
|
|
277
|
+
},
|
|
278
|
+
async getDefaultBackendId() {
|
|
279
|
+
return (await getRegistry()).defaultBackendId;
|
|
280
|
+
},
|
|
281
|
+
async getBackendForProfile(profileId) {
|
|
282
|
+
const registry = await getRegistry();
|
|
283
|
+
// Deliberately resolved against the UNFILTERED roster: a session pinned
|
|
284
|
+
// to a profile the user later hid must keep running.
|
|
285
|
+
const snapshot = await getProfileSnapshot();
|
|
286
|
+
const backendId = snapshot.owners.get(profileId);
|
|
287
|
+
return backendId ? registry.byId.get(backendId) : undefined;
|
|
288
|
+
},
|
|
289
|
+
async getBackendForSession(backendId) {
|
|
290
|
+
const registry = await getRegistry();
|
|
291
|
+
if (backendId) {
|
|
292
|
+
const backend = registry.byId.get(backendId);
|
|
293
|
+
if (backend)
|
|
294
|
+
return backend;
|
|
295
|
+
}
|
|
296
|
+
return registry.byId.get(registry.defaultBackendId);
|
|
297
|
+
},
|
|
298
|
+
async listAllProviders(options = {}) {
|
|
299
|
+
const registry = await getRegistry();
|
|
300
|
+
const snapshot = await getProfileSnapshot();
|
|
301
|
+
const hidden = options.includeHidden ? new Set() : hiddenIds();
|
|
302
|
+
return registry.backends.flatMap((backend) => (snapshot.byBackend.get(backend.id) ?? [])
|
|
303
|
+
.filter((profile) => !hidden.has(profile.id))
|
|
304
|
+
.map((profile) => ({ ...profile, backendId: backend.id })));
|
|
305
|
+
},
|
|
306
|
+
async getBackendsInfo() {
|
|
307
|
+
const backends = (await getRegistry()).backends;
|
|
308
|
+
return Object.fromEntries(backends.map((backend) => [
|
|
309
|
+
backend.id,
|
|
310
|
+
{ id: backend.id, capabilities: backend.capabilities },
|
|
311
|
+
]));
|
|
312
|
+
},
|
|
313
|
+
getModelSource,
|
|
314
|
+
invalidateProfiles() {
|
|
315
|
+
profileSnapshot = null;
|
|
316
|
+
},
|
|
317
|
+
};
|
|
323
318
|
}
|
|
324
319
|
//# sourceMappingURL=backend.js.map
|