@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/src/agent/backend.ts
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
import { createRequire } from "module";
|
|
2
|
-
import {
|
|
3
|
-
createClaudeBackend,
|
|
4
|
-
createModelSource,
|
|
5
|
-
defineProfiles,
|
|
6
|
-
type InferenceProfile,
|
|
7
|
-
type InferenceProfileInput,
|
|
8
|
-
type ModelSource,
|
|
9
|
-
} from "@schlessera/brain-backend-claude";
|
|
10
|
-
import { getHiddenModelIds } from "../db/settings.js";
|
|
2
|
+
import type { AgentConfig } from "../config/env.js";
|
|
11
3
|
import type { ProviderInfo } from "@schlessera/brain-ui-sdk";
|
|
12
4
|
import type {
|
|
13
5
|
AgentBackend,
|
|
@@ -15,9 +7,15 @@ import type {
|
|
|
15
7
|
} from "@schlessera/brain-ui-sdk/server";
|
|
16
8
|
|
|
17
9
|
/**
|
|
18
|
-
* Backend registry for
|
|
19
|
-
*
|
|
20
|
-
*
|
|
10
|
+
* Backend registry for one app instance. Built by `createApp()` from the
|
|
11
|
+
* resolved configuration — no ambient environment and no module-level state,
|
|
12
|
+
* so two apps with different backend configuration coexist in one process and
|
|
13
|
+
* tests install fakes by constructing a registry, not by mutating a module.
|
|
14
|
+
*
|
|
15
|
+
* BOTH backend packages are optional peers loaded lazily (the same
|
|
16
|
+
* `createRequire` path): a deployment installs the one its AGENT_BACKEND
|
|
17
|
+
* names, and the other never has to be present — at runtime AND at
|
|
18
|
+
* type-check time (see the structural mirrors below).
|
|
21
19
|
*
|
|
22
20
|
* The old five hardcoded personal inference profiles are gone: the Claude
|
|
23
21
|
* backend ships the single "claude" default, and additional Anthropic-compatible
|
|
@@ -25,16 +23,163 @@ import type {
|
|
|
25
23
|
* .env.example) rather than in code.
|
|
26
24
|
*/
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
/*
|
|
27
|
+
* Structural mirrors of @schlessera/brain-backend-claude.
|
|
28
|
+
*
|
|
29
|
+
* Both backend packages are optional peers, and NOTHING here may reference
|
|
30
|
+
* their specifiers — not even in a type position. A type import (or a
|
|
31
|
+
* `typeof import(...)`) is resolved by TypeScript, so it would either survive
|
|
32
|
+
* into the emitted `.d.ts` (breaking the `types`-condition consumer) or, once
|
|
33
|
+
* scrubbed there, still break the `bun`-condition consumer whose tsconfig
|
|
34
|
+
* follows `src/index.ts` (this repo's own root tsconfig does exactly that).
|
|
35
|
+
* Either way a pi-only TypeScript deployment would need the Claude package —
|
|
36
|
+
* and its Anthropic Agent SDK — installed just to typecheck. Half of F1
|
|
37
|
+
* undone.
|
|
38
|
+
*
|
|
39
|
+
* So the slice of the Claude module this registry drives is mirrored here by
|
|
40
|
+
* hand, in exchange for two mechanical guards in
|
|
41
|
+
* tests/declaration-surface.test.ts: type-level assertions that the REAL
|
|
42
|
+
* module (tests may import it; only src must stay clean) is assignable to
|
|
43
|
+
* these mirrors — so drift fails the build — and a scan proving no backend
|
|
44
|
+
* specifier appears anywhere under src/ or in any emitted declaration.
|
|
45
|
+
* Function members are property-style on purpose: strictFunctionTypes checks
|
|
46
|
+
* them contravariantly, where method syntax would be bivariant and let an
|
|
47
|
+
* incompatible drift slide.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/** Mirror of the Claude package's `InferenceProfileInput` (declarative shape). */
|
|
51
|
+
export interface ClaudeProfileInput {
|
|
52
|
+
id: string;
|
|
53
|
+
label: string;
|
|
54
|
+
vendor?: string;
|
|
55
|
+
/** Model id. Undefined = the SDK/CLI default model. */
|
|
56
|
+
model?: string;
|
|
57
|
+
/** Anthropic-compatible endpoint. */
|
|
58
|
+
baseUrl?: string;
|
|
59
|
+
/** Name of the env var holding a bearer token. */
|
|
60
|
+
authTokenEnv?: string;
|
|
61
|
+
/** Name of the env var holding an x-api-key. */
|
|
62
|
+
apiKeyEnv?: string;
|
|
63
|
+
/** Remap the CLI's model-alias envs to `model`. Requires `model`. */
|
|
64
|
+
modelAliases?: boolean;
|
|
65
|
+
allowedTools?: string[];
|
|
66
|
+
/** Context window in tokens, when known. Presentation only. */
|
|
67
|
+
contextWindow?: number;
|
|
68
|
+
/** Where this profile came from. Presentation only. */
|
|
69
|
+
source?: "builtin" | "declared" | "discovered";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Mirror of the Claude package's resolved `InferenceProfile`. */
|
|
73
|
+
export interface ClaudeProfile {
|
|
74
|
+
id: string;
|
|
75
|
+
label: string;
|
|
76
|
+
vendor?: string;
|
|
77
|
+
model?: string;
|
|
78
|
+
allowedTools?: string[];
|
|
79
|
+
contextWindow?: number;
|
|
80
|
+
source?: "builtin" | "declared" | "discovered";
|
|
81
|
+
/** Env vars that must be present (non-empty) for this profile to be usable. */
|
|
82
|
+
requiredEnvKeys: string[];
|
|
83
|
+
/** Environment overrides merged over the host environment. */
|
|
84
|
+
buildEnv: () => Record<string, string>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The slice of the lazily-required Claude module this registry drives. The
|
|
89
|
+
* real module carries more (and looser-optional) options; assignability is
|
|
90
|
+
* asserted in tests/declaration-surface.test.ts.
|
|
91
|
+
*/
|
|
92
|
+
export interface ClaudeBackendModule {
|
|
93
|
+
createClaudeBackend: (options: {
|
|
94
|
+
brainPath: string;
|
|
95
|
+
claudeCodePath?: string;
|
|
96
|
+
profiles?: ClaudeProfile[] | (() => ClaudeProfile[]);
|
|
97
|
+
}) => AgentBackend;
|
|
98
|
+
createModelSource: (options: {
|
|
99
|
+
brainPath: string;
|
|
100
|
+
ttlMs?: number;
|
|
101
|
+
enabled?: boolean;
|
|
102
|
+
}) => ClaudeModelSource;
|
|
103
|
+
defineProfiles: (inputs: ClaudeProfileInput[]) => ClaudeProfile[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Discovery source as the registry internals see it: the public slice + list(). */
|
|
107
|
+
export interface ClaudeModelSource extends ModelDiscoverySource {
|
|
108
|
+
list: () => ClaudeProfileInput[];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Discovery freshness, as exposed on the public registry surface.
|
|
113
|
+
*
|
|
114
|
+
* `list()` is omitted on purpose — it returns Claude profile inputs, only the
|
|
115
|
+
* registry internals consume it (see {@link ClaudeModelSource}), and exposing
|
|
116
|
+
* it would put those types on every consumer's plate for no reader.
|
|
117
|
+
*/
|
|
118
|
+
export interface ModelDiscoveryState {
|
|
119
|
+
enabled: boolean;
|
|
120
|
+
/** When discovery last succeeded; null when it never has. */
|
|
121
|
+
refreshedAt: number | null;
|
|
122
|
+
/** The cached result is older than the TTL (or absent). */
|
|
123
|
+
stale: boolean;
|
|
124
|
+
/** Last discovery failure, if the current list is served despite one. */
|
|
125
|
+
error?: string;
|
|
126
|
+
}
|
|
30
127
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
128
|
+
/** The slice of the Claude backend's discovery source the routes consume. */
|
|
129
|
+
export interface ModelDiscoverySource {
|
|
130
|
+
state(): ModelDiscoveryState;
|
|
131
|
+
/** Refresh if stale. Awaits only when there is nothing cached to serve. */
|
|
132
|
+
ensureFresh(): Promise<void>;
|
|
133
|
+
/** Force a refresh regardless of TTL. Rejects on failure. */
|
|
134
|
+
refresh(): Promise<void>;
|
|
135
|
+
}
|
|
36
136
|
|
|
37
|
-
interface
|
|
137
|
+
export interface BackendRegistryOptions {
|
|
138
|
+
/** Where the brain repo lives (handed to every backend). */
|
|
139
|
+
brainPath: string;
|
|
140
|
+
/** Resolved agent configuration (AGENT_BACKEND, profiles, discovery...). */
|
|
141
|
+
agent: AgentConfig;
|
|
142
|
+
/**
|
|
143
|
+
* Profile ids the user keeps out of the model picker (from the app's
|
|
144
|
+
* settings table). Presentation only — a hidden profile still resolves for
|
|
145
|
+
* sessions pinned to it. Defaults to "nothing hidden".
|
|
146
|
+
*/
|
|
147
|
+
getHiddenModelIds?: () => string[];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface BackendRegistry {
|
|
151
|
+
/** All configured backends, with the default backend first. */
|
|
152
|
+
getBackends(): Promise<AgentBackend[]>;
|
|
153
|
+
/** Find a configured backend by its stable id. */
|
|
154
|
+
getBackendById(id: string): Promise<AgentBackend | undefined>;
|
|
155
|
+
/** The default backend used for new and legacy sessions. */
|
|
156
|
+
getDefaultBackend(): Promise<AgentBackend>;
|
|
157
|
+
/** The stable id of the default backend. */
|
|
158
|
+
getDefaultBackendId(): Promise<string>;
|
|
159
|
+
/** Resolve the backend that owns a globally unique profile id. */
|
|
160
|
+
getBackendForProfile(profileId: string): Promise<AgentBackend | undefined>;
|
|
161
|
+
/** Resolve a persisted backend id, falling back for legacy/unknown sessions. */
|
|
162
|
+
getBackendForSession(backendId: string | null | undefined): Promise<AgentBackend>;
|
|
163
|
+
/**
|
|
164
|
+
* Every available profile, tagged with its owning backend id. Hidden
|
|
165
|
+
* profiles are omitted by default (this feeds the picker); the settings
|
|
166
|
+
* screen passes `includeHidden` to render the full catalog.
|
|
167
|
+
*/
|
|
168
|
+
listAllProviders(options?: { includeHidden?: boolean }): Promise<ProviderInfo[]>;
|
|
169
|
+
/** Per-backend capability metadata exposed by the providers route. */
|
|
170
|
+
getBackendsInfo(): Promise<
|
|
171
|
+
Record<string, { id: string; capabilities: BackendCapabilities }>
|
|
172
|
+
>;
|
|
173
|
+
/**
|
|
174
|
+
* The Claude backend's discovery source, once the registry is built. Null
|
|
175
|
+
* when discovery is disabled or the deployment runs a different backend.
|
|
176
|
+
*/
|
|
177
|
+
getModelSource(): Promise<ModelDiscoverySource | null>;
|
|
178
|
+
/** Drop the profile memo so the next read reflects a refresh or settings change. */
|
|
179
|
+
invalidateProfiles(): void;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
interface RegistrySnapshot {
|
|
38
183
|
backends: AgentBackend[];
|
|
39
184
|
byId: Map<string, AgentBackend>;
|
|
40
185
|
defaultBackendId: string;
|
|
@@ -53,14 +198,10 @@ interface ProfileSnapshot {
|
|
|
53
198
|
|
|
54
199
|
const PROFILE_MEMO_MS = 5_000;
|
|
55
200
|
|
|
56
|
-
|
|
57
|
-
let profileSnapshot: ProfileSnapshot | null = null;
|
|
58
|
-
let modelSource: ModelSource | null = null;
|
|
59
|
-
|
|
60
|
-
async function createRegistry(
|
|
201
|
+
function buildSnapshot(
|
|
61
202
|
backends: AgentBackend[],
|
|
62
203
|
defaultBackendId: string
|
|
63
|
-
):
|
|
204
|
+
): RegistrySnapshot {
|
|
64
205
|
if (backends.length === 0) {
|
|
65
206
|
throw new Error("Backend registry requires at least one backend.");
|
|
66
207
|
}
|
|
@@ -77,367 +218,395 @@ async function createRegistry(
|
|
|
77
218
|
return { backends: ordered, byId, defaultBackendId: resolvedDefaultId };
|
|
78
219
|
}
|
|
79
220
|
|
|
80
|
-
/** Recompute the profile roster (memoized), asserting cross-backend id uniqueness. */
|
|
81
|
-
async function getProfileSnapshot(): Promise<ProfileSnapshot> {
|
|
82
|
-
if (profileSnapshot && Date.now() - profileSnapshot.at < PROFILE_MEMO_MS) {
|
|
83
|
-
return profileSnapshot;
|
|
84
|
-
}
|
|
85
221
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
222
|
+
const BACKEND_SPECIFIERS = {
|
|
223
|
+
claude: "@schlessera/brain-backend-claude",
|
|
224
|
+
pi: "@schlessera/brain-backend-pi",
|
|
225
|
+
} as const;
|
|
89
226
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (owner && owner !== backend.id) {
|
|
96
|
-
throw new Error(
|
|
97
|
-
`Profile id collision across backends: "${profile.id}" is exposed by "${owner}" and "${backend.id}".`
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
owners.set(profile.id, backend.id);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
profileSnapshot = { at: Date.now(), byBackend, owners };
|
|
105
|
-
return profileSnapshot;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Drop the memo so the next read reflects a refresh or a settings change. */
|
|
109
|
-
export function invalidateProfiles(): void {
|
|
110
|
-
profileSnapshot = null;
|
|
227
|
+
function unknownBackendError(primary: string): Error {
|
|
228
|
+
return new Error(
|
|
229
|
+
`AGENT_BACKEND="${primary}" does not match any configured backend ` +
|
|
230
|
+
`(expected "claude" or "pi").`
|
|
231
|
+
);
|
|
111
232
|
}
|
|
112
233
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
234
|
+
function missingBackendError(primary: "claude" | "pi"): Error {
|
|
235
|
+
if (primary === "pi") {
|
|
236
|
+
return new Error(
|
|
237
|
+
'AGENT_BACKEND=pi but "@schlessera/brain-backend-pi" is not installed. ' +
|
|
238
|
+
"Add it (with its pi SDK dependencies) or set AGENT_BACKEND=claude."
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
return new Error(
|
|
242
|
+
`AGENT_BACKEND=${primary} but "@schlessera/brain-backend-claude" is not installed. ` +
|
|
243
|
+
"Add it (it carries the Claude Agent SDK) or set AGENT_BACKEND=pi."
|
|
244
|
+
);
|
|
120
245
|
}
|
|
121
246
|
|
|
122
247
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
248
|
+
* Boot-time guard: the SELECTED backend's package must at least RESOLVE, so a
|
|
249
|
+
* broken install refuses to start instead of reporting healthy and failing
|
|
250
|
+
* every agent turn (the guarantee the old static import gave, restored without
|
|
251
|
+
* giving up the lazy load — `require.resolve` never executes the module, so
|
|
252
|
+
* the Agent SDK still loads on first use only). Called by `createApp()` next
|
|
253
|
+
* to the auth assertions; skipped when an explicit registry is injected.
|
|
254
|
+
*
|
|
255
|
+
* An unrecognized AGENT_BACKEND fails here too, for the same reason.
|
|
256
|
+
*
|
|
257
|
+
* The `resolve` parameter exists for tests (simulating an absent package);
|
|
258
|
+
* production callers pass nothing.
|
|
126
259
|
*/
|
|
127
|
-
function
|
|
260
|
+
export function assertBackendResolvable(
|
|
261
|
+
agent: AgentConfig,
|
|
262
|
+
resolve: (specifier: string) => void = (specifier) => {
|
|
263
|
+
createRequire(import.meta.url).resolve(specifier);
|
|
264
|
+
}
|
|
265
|
+
): void {
|
|
266
|
+
const primary = agent.backend || "claude";
|
|
267
|
+
if (!(primary in BACKEND_SPECIFIERS)) throw unknownBackendError(primary);
|
|
268
|
+
const key = primary as keyof typeof BACKEND_SPECIFIERS;
|
|
128
269
|
try {
|
|
129
|
-
|
|
130
|
-
} catch
|
|
131
|
-
|
|
132
|
-
`[models] Could not read hidden models: ${
|
|
133
|
-
err instanceof Error ? err.message : String(err)
|
|
134
|
-
}`
|
|
135
|
-
);
|
|
136
|
-
return new Set();
|
|
270
|
+
resolve(BACKEND_SPECIFIERS[key]);
|
|
271
|
+
} catch {
|
|
272
|
+
throw missingBackendError(key);
|
|
137
273
|
}
|
|
138
274
|
}
|
|
139
275
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
276
|
+
export function createBackendRegistry(
|
|
277
|
+
options: BackendRegistryOptions
|
|
278
|
+
): BackendRegistry {
|
|
279
|
+
const { brainPath, agent } = options;
|
|
280
|
+
const getHidden = options.getHiddenModelIds ?? (() => []);
|
|
281
|
+
|
|
282
|
+
let cachedRegistry: Promise<RegistrySnapshot> | null = null;
|
|
283
|
+
let modelSource: ClaudeModelSource | null = null;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Declared profiles (built-in default + BRAIN_UI_CLAUDE_PROFILES) plus every
|
|
287
|
+
* discovered model whose id isn't already declared. Declared wins: the env
|
|
288
|
+
* stays an override mechanism, and a hand-pinned entry keeps its label and
|
|
289
|
+
* endpoint.
|
|
290
|
+
*
|
|
291
|
+
* Memoized on the discovered array's identity — `createModelSource` swaps the
|
|
292
|
+
* array only on a successful refresh, so steady state is one comparison.
|
|
293
|
+
*/
|
|
294
|
+
let mergeCache: {
|
|
295
|
+
declared: ClaudeProfile[];
|
|
296
|
+
discovered: ClaudeProfileInput[];
|
|
297
|
+
result: ClaudeProfile[];
|
|
298
|
+
} | null = null;
|
|
299
|
+
|
|
300
|
+
function mergeDiscovered(
|
|
301
|
+
claude: ClaudeBackendModule,
|
|
302
|
+
declared: ClaudeProfile[],
|
|
303
|
+
discovered: ClaudeProfileInput[]
|
|
304
|
+
): ClaudeProfile[] {
|
|
305
|
+
if (
|
|
306
|
+
mergeCache &&
|
|
307
|
+
mergeCache.declared === declared &&
|
|
308
|
+
mergeCache.discovered === discovered
|
|
309
|
+
) {
|
|
310
|
+
return mergeCache.result;
|
|
311
|
+
}
|
|
152
312
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
313
|
+
const declaredIds = new Set(declared.map((profile) => profile.id));
|
|
314
|
+
const extra = discovered.filter((input) => !declaredIds.has(input.id));
|
|
315
|
+
const result = [...declared, ...claude.defineProfiles(extra)];
|
|
316
|
+
mergeCache = { declared, discovered, result };
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
159
319
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
): InferenceProfile[] {
|
|
178
|
-
if (
|
|
179
|
-
mergeCache &&
|
|
180
|
-
mergeCache.declared === declared &&
|
|
181
|
-
mergeCache.discovered === discovered
|
|
182
|
-
) {
|
|
183
|
-
return mergeCache.result;
|
|
320
|
+
// The built-in default profile's model. The Claude backend historically
|
|
321
|
+
// pinned the default to a specific model; after the SDK extraction the pin
|
|
322
|
+
// was lost and resumed default-profile sessions drifted to "whatever the CLI
|
|
323
|
+
// defaults to", silently changing model/behaviour/billing on every CLI bump.
|
|
324
|
+
// Re-pin it (a committed default, overridable per deploy via
|
|
325
|
+
// BRAIN_UI_CLAUDE_DEFAULT_MODEL) so the default stays on a known model.
|
|
326
|
+
function builtinDefaultProfiles(claude: ClaudeBackendModule): ClaudeProfile[] {
|
|
327
|
+
return claude.defineProfiles([
|
|
328
|
+
{
|
|
329
|
+
id: "claude",
|
|
330
|
+
label: "Claude",
|
|
331
|
+
vendor: "anthropic",
|
|
332
|
+
model: agent.defaultModel,
|
|
333
|
+
modelAliases: true,
|
|
334
|
+
source: "builtin",
|
|
335
|
+
},
|
|
336
|
+
]);
|
|
184
337
|
}
|
|
185
338
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
339
|
+
/**
|
|
340
|
+
* The Claude roster: the pinned built-in "claude" default first, plus any
|
|
341
|
+
* extra profiles from BRAIN_UI_CLAUDE_PROFILES (a JSON array of profile
|
|
342
|
+
* inputs: {id,label,model?,baseUrl?,authTokenEnv?,apiKeyEnv?,modelAliases?}).
|
|
343
|
+
*
|
|
344
|
+
* A malformed or duplicate-id roster THROWS — caught at boot by the registry
|
|
345
|
+
* fail-fast — rather than silently degrading to default-only and rebilling
|
|
346
|
+
* every pinned session to the subscription with nothing in the logs.
|
|
347
|
+
*/
|
|
348
|
+
function loadClaudeProfiles(claude: ClaudeBackendModule): ClaudeProfile[] {
|
|
349
|
+
const base = builtinDefaultProfiles(claude);
|
|
350
|
+
const raw = agent.profilesJson;
|
|
351
|
+
if (!raw) return base;
|
|
352
|
+
|
|
353
|
+
let inputs: unknown;
|
|
354
|
+
try {
|
|
355
|
+
inputs = JSON.parse(raw);
|
|
356
|
+
} catch (err) {
|
|
357
|
+
throw new Error(
|
|
358
|
+
`BRAIN_UI_CLAUDE_PROFILES is not valid JSON: ${
|
|
359
|
+
err instanceof Error ? err.message : String(err)
|
|
360
|
+
}`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
if (!Array.isArray(inputs)) {
|
|
364
|
+
throw new Error("BRAIN_UI_CLAUDE_PROFILES must be a JSON array.");
|
|
365
|
+
}
|
|
192
366
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
.
|
|
196
|
-
|
|
367
|
+
// Reject duplicate ids (including collisions with the built-in "claude"): a
|
|
368
|
+
// duplicate silently shadows and can resolve to the wrong credentials.
|
|
369
|
+
const seen = new Set(base.map((profile) => profile.id));
|
|
370
|
+
for (const input of inputs as ClaudeProfileInput[]) {
|
|
371
|
+
if (!input || typeof input.id !== "string" || input.id.length === 0) {
|
|
372
|
+
throw new Error(
|
|
373
|
+
"Each BRAIN_UI_CLAUDE_PROFILES entry needs a non-empty string id."
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
if (seen.has(input.id)) {
|
|
377
|
+
throw new Error(
|
|
378
|
+
`Duplicate profile id in BRAIN_UI_CLAUDE_PROFILES: "${input.id}".`
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
seen.add(input.id);
|
|
382
|
+
}
|
|
197
383
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
384
|
+
const declared = (inputs as ClaudeProfileInput[]).map((input) => ({
|
|
385
|
+
source: "declared" as const,
|
|
386
|
+
...input,
|
|
387
|
+
}));
|
|
388
|
+
return [...base, ...claude.defineProfiles(declared)];
|
|
201
389
|
}
|
|
202
390
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
391
|
+
/**
|
|
392
|
+
* NEITHER backend package is a hard dependency — a deployment installs the
|
|
393
|
+
* one its AGENT_BACKEND names (both, if it switches). Loaded lazily through
|
|
394
|
+
* the same `createRequire` path so the server still boots without the unused
|
|
395
|
+
* one; when the configured backend's package is absent, fail with an
|
|
396
|
+
* actionable message.
|
|
397
|
+
*/
|
|
398
|
+
function buildClaudeBackend(): AgentBackend {
|
|
399
|
+
const require = createRequire(import.meta.url);
|
|
400
|
+
let claude: ClaudeBackendModule;
|
|
401
|
+
try {
|
|
402
|
+
claude = require("@schlessera/brain-backend-claude");
|
|
403
|
+
} catch {
|
|
404
|
+
throw missingBackendError("claude");
|
|
405
|
+
}
|
|
406
|
+
if (typeof claude.createClaudeBackend !== "function") {
|
|
407
|
+
throw new Error(
|
|
408
|
+
'"@schlessera/brain-backend-claude" does not export createClaudeBackend.'
|
|
409
|
+
);
|
|
410
|
+
}
|
|
212
411
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
);
|
|
412
|
+
// Declared profiles are resolved EAGERLY so a malformed
|
|
413
|
+
// BRAIN_UI_CLAUDE_PROFILES still fails at boot rather than on first request.
|
|
414
|
+
const declared = loadClaudeProfiles(claude);
|
|
415
|
+
|
|
416
|
+
modelSource = claude.createModelSource({
|
|
417
|
+
brainPath,
|
|
418
|
+
enabled: agent.modelDiscovery,
|
|
419
|
+
ttlMs: agent.modelTtlMs,
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
return claude.createClaudeBackend({
|
|
423
|
+
brainPath,
|
|
424
|
+
claudeCodePath: agent.claudeCodePath,
|
|
425
|
+
// A function, not an array: discovery refreshes in the background and the
|
|
426
|
+
// new roster has to be visible without restarting the process.
|
|
427
|
+
profiles: () =>
|
|
428
|
+
mergeDiscovered(claude, declared, modelSource?.list() ?? []),
|
|
429
|
+
});
|
|
231
430
|
}
|
|
232
|
-
return createRegistry(backends, primary);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
async function getRegistry(): Promise<BackendRegistry> {
|
|
236
|
-
if (!cachedRegistry) cachedRegistry = buildRegistry();
|
|
237
|
-
return cachedRegistry;
|
|
238
|
-
}
|
|
239
431
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
432
|
+
function buildPiBackend(): AgentBackend {
|
|
433
|
+
const require = createRequire(import.meta.url);
|
|
434
|
+
let mod: { createPiBackend?: (opts: { brainPath: string }) => AgentBackend };
|
|
435
|
+
try {
|
|
436
|
+
mod = require("@schlessera/brain-backend-pi");
|
|
437
|
+
} catch {
|
|
438
|
+
throw missingBackendError("pi");
|
|
439
|
+
}
|
|
440
|
+
if (typeof mod.createPiBackend !== "function") {
|
|
441
|
+
throw new Error(
|
|
442
|
+
'"@schlessera/brain-backend-pi" does not export createPiBackend.'
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
return mod.createPiBackend({ brainPath });
|
|
446
|
+
}
|
|
244
447
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
id: string
|
|
248
|
-
): Promise<AgentBackend | undefined> {
|
|
249
|
-
return (await getRegistry()).byId.get(id);
|
|
250
|
-
}
|
|
448
|
+
async function buildRegistry(): Promise<RegistrySnapshot> {
|
|
449
|
+
const primary = agent.backend || "claude";
|
|
251
450
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
451
|
+
if (primary === "pi") {
|
|
452
|
+
const pi = buildPiBackend();
|
|
453
|
+
return buildSnapshot([pi], pi.id);
|
|
454
|
+
}
|
|
257
455
|
|
|
258
|
-
|
|
259
|
-
export async function getDefaultBackendId(): Promise<string> {
|
|
260
|
-
return (await getRegistry()).defaultBackendId;
|
|
261
|
-
}
|
|
456
|
+
const backends = [buildClaudeBackend()];
|
|
262
457
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
return backendId ? registry.byId.get(backendId) : undefined;
|
|
273
|
-
}
|
|
458
|
+
// AGENT_BACKEND must name a configured backend. The in-process options are
|
|
459
|
+
// "claude" (default) and "pi" (handled above). An unrecognized value is a
|
|
460
|
+
// misconfiguration — fail loudly instead of silently coercing to claude and
|
|
461
|
+
// running on the wrong backend with nothing in the logs.
|
|
462
|
+
if (agent.backend && !backends.some((b) => b.id === primary)) {
|
|
463
|
+
throw unknownBackendError(primary);
|
|
464
|
+
}
|
|
465
|
+
return buildSnapshot(backends, primary);
|
|
466
|
+
}
|
|
274
467
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
): Promise<AgentBackend> {
|
|
279
|
-
if (backendId) {
|
|
280
|
-
const backend = await getBackendById(backendId);
|
|
281
|
-
if (backend) return backend;
|
|
468
|
+
async function getRegistry(): Promise<RegistrySnapshot> {
|
|
469
|
+
if (!cachedRegistry) cachedRegistry = buildRegistry();
|
|
470
|
+
return cachedRegistry;
|
|
282
471
|
}
|
|
283
|
-
|
|
472
|
+
|
|
473
|
+
return makeRegistry(getRegistry, getHidden, async () => {
|
|
474
|
+
await getRegistry();
|
|
475
|
+
return modelSource;
|
|
476
|
+
});
|
|
284
477
|
}
|
|
285
478
|
|
|
286
479
|
/**
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
480
|
+
* A registry over an explicit backend list — the seam tests (and embedders
|
|
481
|
+
* with their own backend wiring) use instead of mutating module state. Takes
|
|
482
|
+
* the same optional hidden-ids reader so visibility behavior matches
|
|
483
|
+
* production.
|
|
291
484
|
*/
|
|
292
|
-
export
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
.map((profile) => ({ ...profile, backendId: backend.id }))
|
|
485
|
+
export function createStaticBackendRegistry(
|
|
486
|
+
backends: AgentBackend[],
|
|
487
|
+
defaultBackendId = backends[0]?.id ?? "",
|
|
488
|
+
options: { getHiddenModelIds?: () => string[] } = {}
|
|
489
|
+
): BackendRegistry {
|
|
490
|
+
const snapshot = buildSnapshot(backends, defaultBackendId);
|
|
491
|
+
return makeRegistry(
|
|
492
|
+
async () => snapshot,
|
|
493
|
+
options.getHiddenModelIds ?? (() => []),
|
|
494
|
+
async () => null
|
|
303
495
|
);
|
|
304
496
|
}
|
|
305
497
|
|
|
306
|
-
/**
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
498
|
+
/** The accessor surface, shared by the config-driven and static registries. */
|
|
499
|
+
function makeRegistry(
|
|
500
|
+
getRegistry: () => Promise<RegistrySnapshot>,
|
|
501
|
+
getHidden: () => string[],
|
|
502
|
+
getModelSource: () => Promise<ModelDiscoverySource | null>
|
|
503
|
+
): BackendRegistry {
|
|
504
|
+
let profileSnapshot: ProfileSnapshot | null = null;
|
|
505
|
+
|
|
506
|
+
/** Recompute the profile roster (memoized), asserting cross-backend id uniqueness. */
|
|
507
|
+
async function getProfileSnapshot(): Promise<ProfileSnapshot> {
|
|
508
|
+
if (profileSnapshot && Date.now() - profileSnapshot.at < PROFILE_MEMO_MS) {
|
|
509
|
+
return profileSnapshot;
|
|
510
|
+
}
|
|
318
511
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
512
|
+
const registry = await getRegistry();
|
|
513
|
+
const byBackend = new Map<string, ProviderInfo[]>();
|
|
514
|
+
const owners = new Map<string, string>();
|
|
515
|
+
|
|
516
|
+
for (const backend of registry.backends) {
|
|
517
|
+
const profiles = await backend.listProfiles();
|
|
518
|
+
byBackend.set(backend.id, profiles);
|
|
519
|
+
for (const profile of profiles) {
|
|
520
|
+
const owner = owners.get(profile.id);
|
|
521
|
+
if (owner && owner !== backend.id) {
|
|
522
|
+
throw new Error(
|
|
523
|
+
`Profile id collision across backends: "${profile.id}" is exposed by "${owner}" and "${backend.id}".`
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
owners.set(profile.id, backend.id);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
323
529
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
profileSnapshot = null;
|
|
328
|
-
modelSource = null;
|
|
329
|
-
}
|
|
530
|
+
profileSnapshot = { at: Date.now(), byBackend, owners };
|
|
531
|
+
return profileSnapshot;
|
|
532
|
+
}
|
|
330
533
|
|
|
331
|
-
/**
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
534
|
+
/** A settings read failure must not take the picker down — degrade to "nothing hidden". */
|
|
535
|
+
function hiddenIds(): Set<string> {
|
|
536
|
+
try {
|
|
537
|
+
return new Set(getHidden());
|
|
538
|
+
} catch (err) {
|
|
539
|
+
console.warn(
|
|
540
|
+
`[models] Could not read hidden models: ${
|
|
541
|
+
err instanceof Error ? err.message : String(err)
|
|
542
|
+
}`
|
|
543
|
+
);
|
|
544
|
+
return new Set();
|
|
545
|
+
}
|
|
546
|
+
}
|
|
336
547
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
): void {
|
|
342
|
-
cachedRegistry = createRegistry(backends, defaultBackendId);
|
|
343
|
-
profileSnapshot = null;
|
|
344
|
-
}
|
|
548
|
+
return {
|
|
549
|
+
async getBackends() {
|
|
550
|
+
return (await getRegistry()).backends;
|
|
551
|
+
},
|
|
345
552
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
// resumed default-profile sessions drifted to "whatever the CLI defaults to",
|
|
349
|
-
// silently changing model/behaviour/billing on every CLI bump. Re-pin it here
|
|
350
|
-
// (a committed default, overridable per deploy via BRAIN_UI_CLAUDE_DEFAULT_MODEL)
|
|
351
|
-
// so the default stays on a known model.
|
|
352
|
-
const BUILTIN_DEFAULT_MODEL =
|
|
353
|
-
process.env.BRAIN_UI_CLAUDE_DEFAULT_MODEL?.trim() || "claude-sonnet-4-6";
|
|
354
|
-
|
|
355
|
-
function builtinDefaultProfiles(): InferenceProfile[] {
|
|
356
|
-
return defineProfiles([
|
|
357
|
-
{
|
|
358
|
-
id: "claude",
|
|
359
|
-
label: "Claude",
|
|
360
|
-
vendor: "anthropic",
|
|
361
|
-
model: BUILTIN_DEFAULT_MODEL,
|
|
362
|
-
modelAliases: true,
|
|
363
|
-
source: "builtin",
|
|
553
|
+
async getBackendById(id) {
|
|
554
|
+
return (await getRegistry()).byId.get(id);
|
|
364
555
|
},
|
|
365
|
-
]);
|
|
366
|
-
}
|
|
367
556
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
*
|
|
373
|
-
* A malformed or duplicate-id roster THROWS — caught at boot by the registry
|
|
374
|
-
* fail-fast — rather than silently degrading to default-only and rebilling
|
|
375
|
-
* every pinned session to the subscription with nothing in the logs.
|
|
376
|
-
*/
|
|
377
|
-
function loadClaudeProfiles(): InferenceProfile[] {
|
|
378
|
-
const base = builtinDefaultProfiles();
|
|
379
|
-
const raw = process.env.BRAIN_UI_CLAUDE_PROFILES?.trim();
|
|
380
|
-
if (!raw) return base;
|
|
557
|
+
async getDefaultBackend() {
|
|
558
|
+
const registry = await getRegistry();
|
|
559
|
+
return registry.byId.get(registry.defaultBackendId)!;
|
|
560
|
+
},
|
|
381
561
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
} catch (err) {
|
|
386
|
-
throw new Error(
|
|
387
|
-
`BRAIN_UI_CLAUDE_PROFILES is not valid JSON: ${
|
|
388
|
-
err instanceof Error ? err.message : String(err)
|
|
389
|
-
}`
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
if (!Array.isArray(inputs)) {
|
|
393
|
-
throw new Error("BRAIN_UI_CLAUDE_PROFILES must be a JSON array.");
|
|
394
|
-
}
|
|
562
|
+
async getDefaultBackendId() {
|
|
563
|
+
return (await getRegistry()).defaultBackendId;
|
|
564
|
+
},
|
|
395
565
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
566
|
+
async getBackendForProfile(profileId) {
|
|
567
|
+
const registry = await getRegistry();
|
|
568
|
+
// Deliberately resolved against the UNFILTERED roster: a session pinned
|
|
569
|
+
// to a profile the user later hid must keep running.
|
|
570
|
+
const snapshot = await getProfileSnapshot();
|
|
571
|
+
const backendId = snapshot.owners.get(profileId);
|
|
572
|
+
return backendId ? registry.byId.get(backendId) : undefined;
|
|
573
|
+
},
|
|
574
|
+
|
|
575
|
+
async getBackendForSession(backendId) {
|
|
576
|
+
const registry = await getRegistry();
|
|
577
|
+
if (backendId) {
|
|
578
|
+
const backend = registry.byId.get(backendId);
|
|
579
|
+
if (backend) return backend;
|
|
580
|
+
}
|
|
581
|
+
return registry.byId.get(registry.defaultBackendId)!;
|
|
582
|
+
},
|
|
583
|
+
|
|
584
|
+
async listAllProviders(options = {}) {
|
|
585
|
+
const registry = await getRegistry();
|
|
586
|
+
const snapshot = await getProfileSnapshot();
|
|
587
|
+
const hidden = options.includeHidden ? new Set<string>() : hiddenIds();
|
|
588
|
+
|
|
589
|
+
return registry.backends.flatMap((backend) =>
|
|
590
|
+
(snapshot.byBackend.get(backend.id) ?? [])
|
|
591
|
+
.filter((profile) => !hidden.has(profile.id))
|
|
592
|
+
.map((profile) => ({ ...profile, backendId: backend.id }))
|
|
403
593
|
);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
594
|
+
},
|
|
595
|
+
|
|
596
|
+
async getBackendsInfo() {
|
|
597
|
+
const backends = (await getRegistry()).backends;
|
|
598
|
+
return Object.fromEntries(
|
|
599
|
+
backends.map((backend) => [
|
|
600
|
+
backend.id,
|
|
601
|
+
{ id: backend.id, capabilities: backend.capabilities },
|
|
602
|
+
])
|
|
408
603
|
);
|
|
409
|
-
}
|
|
410
|
-
seen.add(input.id);
|
|
411
|
-
}
|
|
604
|
+
},
|
|
412
605
|
|
|
413
|
-
|
|
414
|
-
source: "declared" as const,
|
|
415
|
-
...input,
|
|
416
|
-
}));
|
|
417
|
-
return [...base, ...defineProfiles(declared)];
|
|
418
|
-
}
|
|
606
|
+
getModelSource,
|
|
419
607
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
* but absent, fail with an actionable message.
|
|
425
|
-
*/
|
|
426
|
-
function buildPiBackend(): AgentBackend {
|
|
427
|
-
const require = createRequire(import.meta.url);
|
|
428
|
-
let mod: { createPiBackend?: (opts: { brainPath: string }) => AgentBackend };
|
|
429
|
-
try {
|
|
430
|
-
mod = require("@schlessera/brain-backend-pi");
|
|
431
|
-
} catch {
|
|
432
|
-
throw new Error(
|
|
433
|
-
'AGENT_BACKEND=pi but "@schlessera/brain-backend-pi" is not installed. ' +
|
|
434
|
-
"Add it (with its pi SDK dependencies) or set AGENT_BACKEND=claude."
|
|
435
|
-
);
|
|
436
|
-
}
|
|
437
|
-
if (typeof mod.createPiBackend !== "function") {
|
|
438
|
-
throw new Error(
|
|
439
|
-
'"@schlessera/brain-backend-pi" does not export createPiBackend.'
|
|
440
|
-
);
|
|
441
|
-
}
|
|
442
|
-
return mod.createPiBackend({ brainPath: BRAIN_PATH });
|
|
608
|
+
invalidateProfiles() {
|
|
609
|
+
profileSnapshot = null;
|
|
610
|
+
},
|
|
611
|
+
};
|
|
443
612
|
}
|