@schlessera/brain-ui-server 0.4.0 → 0.5.1
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 +15 -1
- package/dist/agent/backend.d.ts +17 -2
- package/dist/agent/backend.d.ts.map +1 -1
- package/dist/agent/backend.js +128 -30
- package/dist/agent/backend.js.map +1 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +2 -0
- package/dist/app.js.map +1 -1
- package/dist/db/settings.d.ts +7 -0
- package/dist/db/settings.d.ts.map +1 -0
- package/dist/db/settings.js +42 -0
- package/dist/db/settings.js.map +1 -0
- package/dist/routes/files.d.ts +3 -3
- package/dist/routes/models.d.ts +91 -0
- package/dist/routes/models.d.ts.map +1 -0
- package/dist/routes/models.js +61 -0
- package/dist/routes/models.js.map +1 -0
- package/dist/routes/providers.d.ts +2 -0
- package/dist/routes/providers.d.ts.map +1 -1
- package/dist/routes/providers.js +6 -1
- package/dist/routes/providers.js.map +1 -1
- package/migrations/006_settings.sql +10 -0
- package/package.json +4 -4
- package/src/agent/backend.ts +169 -34
- package/src/app.ts +2 -0
- package/src/db/settings.ts +46 -0
- package/src/routes/models.ts +85 -0
- package/src/routes/providers.ts +11 -1
package/README.md
CHANGED
|
@@ -50,16 +50,30 @@ different options reconfigures the first app rather than creating a second.
|
|
|
50
50
|
index.
|
|
51
51
|
- **Render seam** — `POST /api/render` answers 501 unless the deployment
|
|
52
52
|
injects a renderer (see `@schlessera/brain-render-puppeteer`).
|
|
53
|
+
- **Model catalog** — Anthropic model discovery (via
|
|
54
|
+
`@schlessera/brain-backend-claude`) behind `GET /api/models`, with a
|
|
55
|
+
server-side hidden set (`PUT /api/models/hidden`) and a manual
|
|
56
|
+
`POST /api/models/refresh`. `/api/providers` serves the same roster minus the
|
|
57
|
+
hidden entries; hidden profiles still resolve for sessions pinned to them.
|
|
53
58
|
|
|
54
59
|
## Environment
|
|
55
60
|
|
|
56
61
|
The package reads the same env contract the brain-ui deployment documents:
|
|
57
62
|
`AUTH_MODE`, `BRAIN_UI_PASSWORD_HASH`, `COOKIE_SECRET`, `TRUST_PROXY`,
|
|
58
63
|
`WEBAUTHN_*`, `BRAIN_PATH`, `DB_PATH`, `AGENT_BACKEND`,
|
|
59
|
-
`BRAIN_UI_CLAUDE_PROFILES`, `
|
|
64
|
+
`BRAIN_UI_CLAUDE_PROFILES`, `BRAIN_UI_MODEL_DISCOVERY`,
|
|
65
|
+
`BRAIN_UI_MODEL_TTL_HOURS`, `MAX_CONCURRENT_SESSIONS`, `DEEPGRAM_API_KEY`,
|
|
60
66
|
`VOICE_*`, `NOMINATIM_*`, `ALLOWED_ORIGINS`. `createApp()` options win over
|
|
61
67
|
env where both exist.
|
|
62
68
|
|
|
69
|
+
Model discovery is on by default and needs no configuration beyond the Claude
|
|
70
|
+
credential the agent already uses (`CLAUDE_CODE_OAUTH_TOKEN` or
|
|
71
|
+
`ANTHROPIC_API_KEY`). `BRAIN_UI_MODEL_DISCOVERY=0` turns it off — leaving the
|
|
72
|
+
picker to `BRAIN_UI_CLAUDE_PROFILES` alone — and `BRAIN_UI_MODEL_TTL_HOURS`
|
|
73
|
+
(default 24) sets how long a discovered roster is served before a background
|
|
74
|
+
refresh. It defaults to OFF under a test runner (`NODE_ENV=test`) so suites
|
|
75
|
+
don't depend on network access; set the var explicitly to opt in.
|
|
76
|
+
|
|
63
77
|
## Versioning
|
|
64
78
|
|
|
65
79
|
Versions in lockstep with all `@schlessera/brain-*` packages.
|
package/dist/agent/backend.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
import { type ModelSource } from "@schlessera/brain-backend-claude";
|
|
1
2
|
import type { ProviderInfo } from "@schlessera/brain-ui-sdk";
|
|
2
3
|
import type { AgentBackend, BackendCapabilities } from "@schlessera/brain-ui-sdk/server";
|
|
4
|
+
/** Drop the memo so the next read reflects a refresh or a settings change. */
|
|
5
|
+
export declare function invalidateProfiles(): void;
|
|
6
|
+
/**
|
|
7
|
+
* The Claude backend's discovery source, once the registry is built. Null when
|
|
8
|
+
* discovery is disabled or the deployment runs a different backend.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getModelSource(): Promise<ModelSource | null>;
|
|
3
11
|
/** All configured backends, with the default backend first. */
|
|
4
12
|
export declare function getBackends(): Promise<AgentBackend[]>;
|
|
5
13
|
/** Find a configured backend by its stable id. */
|
|
@@ -12,8 +20,15 @@ export declare function getDefaultBackendId(): Promise<string>;
|
|
|
12
20
|
export declare function getBackendForProfile(profileId: string): Promise<AgentBackend | undefined>;
|
|
13
21
|
/** Resolve a persisted backend id, falling back for legacy/unknown sessions. */
|
|
14
22
|
export declare function getBackendForSession(backendId: string | null | undefined): Promise<AgentBackend>;
|
|
15
|
-
/**
|
|
16
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Every available profile, tagged with its owning backend id.
|
|
25
|
+
*
|
|
26
|
+
* Hidden profiles are omitted by default (this feeds the picker); the settings
|
|
27
|
+
* screen passes `includeHidden` to render the full catalog.
|
|
28
|
+
*/
|
|
29
|
+
export declare function listAllProviders(options?: {
|
|
30
|
+
includeHidden?: boolean;
|
|
31
|
+
}): Promise<ProviderInfo[]>;
|
|
17
32
|
/** Per-backend capability metadata exposed by the providers route. */
|
|
18
33
|
export declare function getBackendsInfo(): Promise<Record<string, {
|
|
19
34
|
id: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../src/agent/backend.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../src/agent/backend.ts"],"names":[],"mappings":"AACA,OAAO,EAML,KAAK,WAAW,EACjB,MAAM,kCAAkC,CAAC;AAE1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EACV,YAAY,EACZ,mBAAmB,EACpB,MAAM,iCAAiC,CAAC;AA6FzC,8EAA8E;AAC9E,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED;;;GAGG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAGlE;AAwHD,+DAA+D;AAC/D,wBAAsB,WAAW,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC,CAE3D;AAED,kDAAkD;AAClD,wBAAsB,cAAc,CAClC,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAEnC;AAED,4DAA4D;AAC5D,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,YAAY,CAAC,CAG/D;AAED,4CAA4C;AAC5C,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,MAAM,CAAC,CAE3D;AAED,kEAAkE;AAClE,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAOnC;AAED,gFAAgF;AAChF,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACnC,OAAO,CAAC,YAAY,CAAC,CAMvB;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,GAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAA;CAAO,GACxC,OAAO,CAAC,YAAY,EAAE,CAAC,CAUzB;AAED,sEAAsE;AACtE,wBAAsB,eAAe,IAAI,OAAO,CAC9C,MAAM,CAAC,MAAM,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,mBAAmB,CAAA;CAAE,CAAC,CAClE,CAQA;AAED,0EAA0E;AAC1E,wBAAsB,UAAU,IAAI,OAAO,CAAC,YAAY,CAAC,CAExD;AAED,6EAA6E;AAC7E,wBAAgB,oBAAoB,IAAI,IAAI,CAI3C;AAED,oEAAoE;AACpE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAG9D;AAED,4EAA4E;AAC5E,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,YAAY,EAAE,EACxB,gBAAgB,SAAwB,GACvC,IAAI,CAGN"}
|
package/dist/agent/backend.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "module";
|
|
2
|
-
import { createClaudeBackend, defineProfiles, } from "@schlessera/brain-backend-claude";
|
|
2
|
+
import { createClaudeBackend, createModelSource, defineProfiles, } from "@schlessera/brain-backend-claude";
|
|
3
|
+
import { getHiddenModelIds } from "../db/settings.js";
|
|
3
4
|
/**
|
|
4
5
|
* Backend registry for the deployment. Claude is always available.
|
|
5
6
|
* `AGENT_BACKEND=pi` retains the legacy single-backend deployment mode when the
|
|
@@ -16,7 +17,10 @@ const BRAIN_PATH = process.env.BRAIN_PATH || `${process.env.HOME || "/root"}/bra
|
|
|
16
17
|
// /usr/local/bin/claude in the container). The Agent SDK can't always
|
|
17
18
|
// auto-discover it, so hand it the explicit path; override with CLAUDE_CODE_PATH.
|
|
18
19
|
const CLAUDE_CODE_PATH = process.env.CLAUDE_CODE_PATH || "/usr/local/bin/claude";
|
|
20
|
+
const PROFILE_MEMO_MS = 5_000;
|
|
19
21
|
let cachedRegistry = null;
|
|
22
|
+
let profileSnapshot = null;
|
|
23
|
+
let modelSource = null;
|
|
20
24
|
async function createRegistry(backends, defaultBackendId) {
|
|
21
25
|
if (backends.length === 0) {
|
|
22
26
|
throw new Error("Backend registry requires at least one backend.");
|
|
@@ -29,29 +33,95 @@ async function createRegistry(backends, defaultBackendId) {
|
|
|
29
33
|
byId.get(resolvedDefaultId),
|
|
30
34
|
...backends.filter((backend) => backend.id !== resolvedDefaultId),
|
|
31
35
|
];
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
return { backends: ordered, byId, defaultBackendId: resolvedDefaultId };
|
|
37
|
+
}
|
|
38
|
+
/** Recompute the profile roster (memoized), asserting cross-backend id uniqueness. */
|
|
39
|
+
async function getProfileSnapshot() {
|
|
40
|
+
if (profileSnapshot && Date.now() - profileSnapshot.at < PROFILE_MEMO_MS) {
|
|
41
|
+
return profileSnapshot;
|
|
42
|
+
}
|
|
43
|
+
const registry = await getRegistry();
|
|
44
|
+
const byBackend = new Map();
|
|
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}".`);
|
|
44
53
|
}
|
|
45
|
-
|
|
54
|
+
owners.set(profile.id, backend.id);
|
|
46
55
|
}
|
|
47
56
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Hidden profile ids, from the settings table. Presentation only — a hidden
|
|
74
|
+
* profile still resolves for sessions pinned to it. A db read failure must not
|
|
75
|
+
* take the picker down, so it degrades to "nothing hidden".
|
|
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();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Model discovery is on unless explicitly disabled — except under a test
|
|
88
|
+
* runner, where the default flips to off: discovery is a live call to the
|
|
89
|
+
* Anthropic API, and a suite that silently depends on network access (and on
|
|
90
|
+
* whatever credentials the developer happens to have exported) is flaky by
|
|
91
|
+
* construction. Tests that want it set BRAIN_UI_MODEL_DISCOVERY=1 explicitly.
|
|
92
|
+
*/
|
|
93
|
+
function modelDiscoveryEnabled() {
|
|
94
|
+
const raw = process.env.BRAIN_UI_MODEL_DISCOVERY?.trim().toLowerCase();
|
|
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.
|
|
109
|
+
*
|
|
110
|
+
* Memoized on the discovered array's identity — `createModelSource` swaps the
|
|
111
|
+
* array only on a successful refresh, so steady state is one comparison.
|
|
112
|
+
*/
|
|
113
|
+
let mergeCache = null;
|
|
114
|
+
function mergeDiscovered(declared, discovered) {
|
|
115
|
+
if (mergeCache &&
|
|
116
|
+
mergeCache.declared === declared &&
|
|
117
|
+
mergeCache.discovered === discovered) {
|
|
118
|
+
return mergeCache.result;
|
|
119
|
+
}
|
|
120
|
+
const declaredIds = new Set(declared.map((profile) => profile.id));
|
|
121
|
+
const extra = discovered.filter((input) => !declaredIds.has(input.id));
|
|
122
|
+
const result = [...declared, ...defineProfiles(extra)];
|
|
123
|
+
mergeCache = { declared, discovered, result };
|
|
124
|
+
return result;
|
|
55
125
|
}
|
|
56
126
|
async function buildRegistry() {
|
|
57
127
|
const primary = (process.env.AGENT_BACKEND || "claude")
|
|
@@ -61,10 +131,20 @@ async function buildRegistry() {
|
|
|
61
131
|
const pi = buildPiBackend();
|
|
62
132
|
return createRegistry([pi], pi.id);
|
|
63
133
|
}
|
|
134
|
+
// Declared profiles are resolved EAGERLY so a malformed
|
|
135
|
+
// BRAIN_UI_CLAUDE_PROFILES still fails at boot rather than on first request.
|
|
136
|
+
const declared = loadClaudeProfiles();
|
|
137
|
+
modelSource = createModelSource({
|
|
138
|
+
brainPath: BRAIN_PATH,
|
|
139
|
+
enabled: modelDiscoveryEnabled(),
|
|
140
|
+
ttlMs: modelTtlMs(),
|
|
141
|
+
});
|
|
64
142
|
const claude = createClaudeBackend({
|
|
65
143
|
brainPath: BRAIN_PATH,
|
|
66
144
|
claudeCodePath: CLAUDE_CODE_PATH,
|
|
67
|
-
|
|
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() ?? []),
|
|
68
148
|
});
|
|
69
149
|
const backends = [claude];
|
|
70
150
|
// AGENT_BACKEND must name a configured backend. The in-process options are
|
|
@@ -102,7 +182,10 @@ export async function getDefaultBackendId() {
|
|
|
102
182
|
/** Resolve the backend that owns a globally unique profile id. */
|
|
103
183
|
export async function getBackendForProfile(profileId) {
|
|
104
184
|
const registry = await getRegistry();
|
|
105
|
-
|
|
185
|
+
// Deliberately resolved against the UNFILTERED roster: a session pinned to a
|
|
186
|
+
// profile the user later hid must keep running.
|
|
187
|
+
const snapshot = await getProfileSnapshot();
|
|
188
|
+
const backendId = snapshot.owners.get(profileId);
|
|
106
189
|
return backendId ? registry.byId.get(backendId) : undefined;
|
|
107
190
|
}
|
|
108
191
|
/** Resolve a persisted backend id, falling back for legacy/unknown sessions. */
|
|
@@ -114,13 +197,19 @@ export async function getBackendForSession(backendId) {
|
|
|
114
197
|
}
|
|
115
198
|
return getDefaultBackend();
|
|
116
199
|
}
|
|
117
|
-
/**
|
|
118
|
-
|
|
200
|
+
/**
|
|
201
|
+
* Every available profile, tagged with its owning backend id.
|
|
202
|
+
*
|
|
203
|
+
* Hidden profiles are omitted by default (this feeds the picker); the settings
|
|
204
|
+
* screen passes `includeHidden` to render the full catalog.
|
|
205
|
+
*/
|
|
206
|
+
export async function listAllProviders(options = {}) {
|
|
119
207
|
const registry = await getRegistry();
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
208
|
+
const snapshot = await getProfileSnapshot();
|
|
209
|
+
const hidden = options.includeHidden ? new Set() : hiddenIds();
|
|
210
|
+
return registry.backends.flatMap((backend) => (snapshot.byBackend.get(backend.id) ?? [])
|
|
211
|
+
.filter((profile) => !hidden.has(profile.id))
|
|
212
|
+
.map((profile) => ({ ...profile, backendId: backend.id })));
|
|
124
213
|
}
|
|
125
214
|
/** Per-backend capability metadata exposed by the providers route. */
|
|
126
215
|
export async function getBackendsInfo() {
|
|
@@ -137,14 +226,18 @@ export async function getBackend() {
|
|
|
137
226
|
/** Test seam: drop the cached registry so later access rebuilds from env. */
|
|
138
227
|
export function resetBackendForTests() {
|
|
139
228
|
cachedRegistry = null;
|
|
229
|
+
profileSnapshot = null;
|
|
230
|
+
modelSource = null;
|
|
140
231
|
}
|
|
141
232
|
/** Test seam: install one fake backend as the complete registry. */
|
|
142
233
|
export function setBackendForTests(backend) {
|
|
143
234
|
cachedRegistry = createRegistry([backend], backend.id);
|
|
235
|
+
profileSnapshot = null;
|
|
144
236
|
}
|
|
145
237
|
/** Test seam: install a complete fake registry with an explicit default. */
|
|
146
238
|
export function setBackendsForTests(backends, defaultBackendId = backends[0]?.id ?? "") {
|
|
147
239
|
cachedRegistry = createRegistry(backends, defaultBackendId);
|
|
240
|
+
profileSnapshot = null;
|
|
148
241
|
}
|
|
149
242
|
// The built-in default profile's model. The Claude backend historically pinned
|
|
150
243
|
// the default to a specific model; after the SDK extraction the pin was lost and
|
|
@@ -161,6 +254,7 @@ function builtinDefaultProfiles() {
|
|
|
161
254
|
vendor: "anthropic",
|
|
162
255
|
model: BUILTIN_DEFAULT_MODEL,
|
|
163
256
|
modelAliases: true,
|
|
257
|
+
source: "builtin",
|
|
164
258
|
},
|
|
165
259
|
]);
|
|
166
260
|
}
|
|
@@ -200,7 +294,11 @@ function loadClaudeProfiles() {
|
|
|
200
294
|
}
|
|
201
295
|
seen.add(input.id);
|
|
202
296
|
}
|
|
203
|
-
|
|
297
|
+
const declared = inputs.map((input) => ({
|
|
298
|
+
source: "declared",
|
|
299
|
+
...input,
|
|
300
|
+
}));
|
|
301
|
+
return [...base, ...defineProfiles(declared)];
|
|
204
302
|
}
|
|
205
303
|
/**
|
|
206
304
|
* Optional pi backend. @schlessera/brain-backend-pi is NOT a hard dependency — a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../../src/agent/backend.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EACL,mBAAmB,EACnB,cAAc,
|
|
1
|
+
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../../src/agent/backend.ts"],"names":[],"mappings":"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"}
|
package/dist/app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAWA,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAWA,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAmB1E,YAAY,EAAE,WAAW,EAAE,CAAC;AAE5B,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,WAAW,CAAC;IACvB,mDAAmD;IACnD,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAqBD,wBAAgB,SAAS,CAAC,OAAO,GAAE,gBAAqB;;;EAwFvD"}
|
package/dist/app.js
CHANGED
|
@@ -10,6 +10,7 @@ import { voiceRoutes } from "./routes/voice.js";
|
|
|
10
10
|
import { filesRoutes } from "./routes/files.js";
|
|
11
11
|
import { createRenderRoutes } from "./routes/render.js";
|
|
12
12
|
import { providerRoutes } from "./routes/providers.js";
|
|
13
|
+
import { modelRoutes } from "./routes/models.js";
|
|
13
14
|
import { resolveAuthMode, assertAuthConfig, authGuard, authRoutes, isWsAuthorized, } from "./middleware/auth.js";
|
|
14
15
|
import { passkeyPublicRoutes, passkeyManagementRoutes, passwordLoginDisabled, assertPasskeyConfig, } from "./middleware/passkeys.js";
|
|
15
16
|
import { configureDb } from "./db/client.js";
|
|
@@ -89,6 +90,7 @@ export function createApp(options = {}) {
|
|
|
89
90
|
app.route("/api", filesRoutes);
|
|
90
91
|
app.route("/api", createRenderRoutes(options.renderer));
|
|
91
92
|
app.route("/api", providerRoutes);
|
|
93
|
+
app.route("/api", modelRoutes);
|
|
92
94
|
// WebSocket endpoint. Browsers can't set headers on the WS handshake, so the
|
|
93
95
|
// upgrade authenticates via the session cookie (or IP/proxy header) INSIDE
|
|
94
96
|
// the handler — no header-modifying middleware may sit on this route.
|
package/dist/app.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function getSetting<T>(key: string, fallback: T): T;
|
|
2
|
+
export declare function setSetting(key: string, value: unknown): void;
|
|
3
|
+
/** Profile ids the user keeps out of the model picker. */
|
|
4
|
+
export declare function getHiddenModelIds(): string[];
|
|
5
|
+
/** Replace the hidden set (the client always sends the full list, not a delta). */
|
|
6
|
+
export declare function setHiddenModelIds(ids: string[]): void;
|
|
7
|
+
//# sourceMappingURL=settings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/db/settings.ts"],"names":[],"mappings":"AAYA,wBAAgB,UAAU,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,CAAC,CAWzD;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAO5D;AAED,0DAA0D;AAC1D,wBAAgB,iBAAiB,IAAI,MAAM,EAAE,CAI5C;AAED,mFAAmF;AACnF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,CAGrD"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Server-side settings: a JSON-per-key KV table (migration 006).
|
|
2
|
+
//
|
|
3
|
+
// These are deployment-wide preferences that must follow the user across
|
|
4
|
+
// devices, so they live in SQLite on the persisted volume rather than in the
|
|
5
|
+
// browser. Values are stored as JSON text; readers are total — a missing or
|
|
6
|
+
// corrupt row degrades to the caller's fallback rather than throwing, because
|
|
7
|
+
// a bad preference must never take a route down.
|
|
8
|
+
import { getDb } from "./client.js";
|
|
9
|
+
const HIDDEN_MODELS_KEY = "models.hidden";
|
|
10
|
+
export function getSetting(key, fallback) {
|
|
11
|
+
const row = getDb()
|
|
12
|
+
.query("SELECT value FROM settings WHERE key = ?")
|
|
13
|
+
.get(key);
|
|
14
|
+
if (!row)
|
|
15
|
+
return fallback;
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(row.value);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
console.warn(`[settings] Corrupt JSON for "${key}"; using fallback`);
|
|
21
|
+
return fallback;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function setSetting(key, value) {
|
|
25
|
+
getDb()
|
|
26
|
+
.prepare(`INSERT INTO settings (key, value, updated_at) VALUES (?, ?, ?)
|
|
27
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`)
|
|
28
|
+
.run(key, JSON.stringify(value), Date.now());
|
|
29
|
+
}
|
|
30
|
+
/** Profile ids the user keeps out of the model picker. */
|
|
31
|
+
export function getHiddenModelIds() {
|
|
32
|
+
const value = getSetting(HIDDEN_MODELS_KEY, []);
|
|
33
|
+
if (!Array.isArray(value))
|
|
34
|
+
return [];
|
|
35
|
+
return value.filter((id) => typeof id === "string");
|
|
36
|
+
}
|
|
37
|
+
/** Replace the hidden set (the client always sends the full list, not a delta). */
|
|
38
|
+
export function setHiddenModelIds(ids) {
|
|
39
|
+
const unique = [...new Set(ids.filter((id) => typeof id === "string" && id))];
|
|
40
|
+
setSetting(HIDDEN_MODELS_KEY, unique);
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=settings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/db/settings.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,EAAE;AACF,yEAAyE;AACzE,6EAA6E;AAC7E,4EAA4E;AAC5E,8EAA8E;AAC9E,iDAAiD;AAEjD,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEpC,MAAM,iBAAiB,GAAG,eAAe,CAAC;AAE1C,MAAM,UAAU,UAAU,CAAI,GAAW,EAAE,QAAW;IACpD,MAAM,GAAG,GAAG,KAAK,EAAE;SAChB,KAAK,CAAC,0CAA0C,CAAC;SACjD,GAAG,CAAC,GAAG,CAA6B,CAAC;IACxC,IAAI,CAAC,GAAG;QAAE,OAAO,QAAQ,CAAC;IAC1B,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAM,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,IAAI,CAAC,gCAAgC,GAAG,mBAAmB,CAAC,CAAC;QACrE,OAAO,QAAQ,CAAC;IAClB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,GAAW,EAAE,KAAc;IACpD,KAAK,EAAE;SACJ,OAAO,CACN;+FACyF,CAC1F;SACA,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;AACjD,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,iBAAiB;IAC/B,MAAM,KAAK,GAAG,UAAU,CAAU,iBAAiB,EAAE,EAAE,CAAC,CAAC;IACzD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACrC,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,EAAgB,EAAE,CAAC,OAAO,EAAE,KAAK,QAAQ,CAAC,CAAC;AACpE,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,iBAAiB,CAAC,GAAa;IAC7C,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,KAAK,QAAQ,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAC9E,UAAU,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC;AACxC,CAAC"}
|
package/dist/routes/files.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export declare const filesRoutes: import("hono/hono-base").HonoBase<import("hono
|
|
|
21
21
|
size?: number | undefined;
|
|
22
22
|
};
|
|
23
23
|
outputFormat: "json";
|
|
24
|
-
status: 400 | 404 | 413
|
|
24
|
+
status: 500 | 400 | 404 | 413;
|
|
25
25
|
};
|
|
26
26
|
};
|
|
27
27
|
} & {
|
|
@@ -42,7 +42,7 @@ export declare const filesRoutes: import("hono/hono-base").HonoBase<import("hono
|
|
|
42
42
|
size?: number | undefined;
|
|
43
43
|
};
|
|
44
44
|
outputFormat: "json";
|
|
45
|
-
status: 400 | 404 | 413
|
|
45
|
+
status: 500 | 400 | 404 | 413;
|
|
46
46
|
} | {
|
|
47
47
|
input: {};
|
|
48
48
|
output: {
|
|
@@ -71,7 +71,7 @@ export declare const filesRoutes: import("hono/hono-base").HonoBase<import("hono
|
|
|
71
71
|
size?: number | undefined;
|
|
72
72
|
};
|
|
73
73
|
outputFormat: "json";
|
|
74
|
-
status: 400 | 404 | 413
|
|
74
|
+
status: 500 | 400 | 404 | 413;
|
|
75
75
|
} | {
|
|
76
76
|
input: {};
|
|
77
77
|
output: {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export declare const modelRoutes: import("hono/hono-base").HonoBase<import("hono/types").BlankEnv, {
|
|
2
|
+
"/models": {
|
|
3
|
+
$get: {
|
|
4
|
+
input: {};
|
|
5
|
+
output: {
|
|
6
|
+
models: {
|
|
7
|
+
hidden: boolean;
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
vendor?: string | undefined;
|
|
11
|
+
backendId?: string | undefined;
|
|
12
|
+
contextWindow?: number | undefined;
|
|
13
|
+
source?: "builtin" | "declared" | "discovered" | undefined;
|
|
14
|
+
}[];
|
|
15
|
+
refreshedAt: number | null;
|
|
16
|
+
stale: boolean;
|
|
17
|
+
discovery: {
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
error?: string | undefined;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
outputFormat: "json";
|
|
23
|
+
status: import("hono/utils/http-status").ContentfulStatusCode;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
} & {
|
|
27
|
+
"/models/hidden": {
|
|
28
|
+
$put: {
|
|
29
|
+
input: {};
|
|
30
|
+
output: {
|
|
31
|
+
models: {
|
|
32
|
+
hidden: boolean;
|
|
33
|
+
id: string;
|
|
34
|
+
label: string;
|
|
35
|
+
vendor?: string | undefined;
|
|
36
|
+
backendId?: string | undefined;
|
|
37
|
+
contextWindow?: number | undefined;
|
|
38
|
+
source?: "builtin" | "declared" | "discovered" | undefined;
|
|
39
|
+
}[];
|
|
40
|
+
refreshedAt: number | null;
|
|
41
|
+
stale: boolean;
|
|
42
|
+
discovery: {
|
|
43
|
+
enabled: boolean;
|
|
44
|
+
error?: string | undefined;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
outputFormat: "json";
|
|
48
|
+
status: import("hono/utils/http-status").ContentfulStatusCode;
|
|
49
|
+
} | {
|
|
50
|
+
input: {};
|
|
51
|
+
output: {
|
|
52
|
+
error: string;
|
|
53
|
+
};
|
|
54
|
+
outputFormat: "json";
|
|
55
|
+
status: 400;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
} & {
|
|
59
|
+
"/models/refresh": {
|
|
60
|
+
$post: {
|
|
61
|
+
input: {};
|
|
62
|
+
output: {
|
|
63
|
+
models: {
|
|
64
|
+
hidden: boolean;
|
|
65
|
+
id: string;
|
|
66
|
+
label: string;
|
|
67
|
+
vendor?: string | undefined;
|
|
68
|
+
backendId?: string | undefined;
|
|
69
|
+
contextWindow?: number | undefined;
|
|
70
|
+
source?: "builtin" | "declared" | "discovered" | undefined;
|
|
71
|
+
}[];
|
|
72
|
+
refreshedAt: number | null;
|
|
73
|
+
stale: boolean;
|
|
74
|
+
discovery: {
|
|
75
|
+
enabled: boolean;
|
|
76
|
+
error?: string | undefined;
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
outputFormat: "json";
|
|
80
|
+
status: import("hono/utils/http-status").ContentfulStatusCode;
|
|
81
|
+
} | {
|
|
82
|
+
input: {};
|
|
83
|
+
output: {
|
|
84
|
+
error: string;
|
|
85
|
+
};
|
|
86
|
+
outputFormat: "json";
|
|
87
|
+
status: 409;
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
}, "/", "/models/refresh">;
|
|
91
|
+
//# sourceMappingURL=models.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/routes/models.ts"],"names":[],"mappings":"AAsCA,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0BA8CpB,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Model catalog routes — the settings screen's view of the picker.
|
|
2
|
+
//
|
|
3
|
+
// `/api/providers` answers the picker and omits hidden profiles. These routes
|
|
4
|
+
// answer the settings screen and therefore show everything, each row tagged
|
|
5
|
+
// with its visibility, plus discovery freshness so the UI can say when the list
|
|
6
|
+
// was last refreshed and why it might be stale.
|
|
7
|
+
import { Hono } from "hono";
|
|
8
|
+
import { getModelSource, invalidateProfiles, listAllProviders, } from "../agent/backend.js";
|
|
9
|
+
import { getHiddenModelIds, setHiddenModelIds } from "../db/settings.js";
|
|
10
|
+
async function buildCatalog() {
|
|
11
|
+
const source = await getModelSource();
|
|
12
|
+
const hidden = new Set(getHiddenModelIds());
|
|
13
|
+
const models = (await listAllProviders({ includeHidden: true })).map((profile) => ({ ...profile, hidden: hidden.has(profile.id) }));
|
|
14
|
+
const state = source?.state();
|
|
15
|
+
return {
|
|
16
|
+
models,
|
|
17
|
+
refreshedAt: state?.refreshedAt ?? null,
|
|
18
|
+
stale: state?.stale ?? false,
|
|
19
|
+
discovery: {
|
|
20
|
+
enabled: state?.enabled ?? false,
|
|
21
|
+
...(state?.error !== undefined ? { error: state.error } : {}),
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export const modelRoutes = new Hono()
|
|
26
|
+
.get("/models", async (c) => {
|
|
27
|
+
// Serves the cached roster immediately and refreshes behind the response
|
|
28
|
+
// when stale; only a cold start (nothing cached) waits on the network.
|
|
29
|
+
const source = await getModelSource();
|
|
30
|
+
await source?.ensureFresh();
|
|
31
|
+
return c.json(await buildCatalog());
|
|
32
|
+
})
|
|
33
|
+
.put("/models/hidden", async (c) => {
|
|
34
|
+
const body = (await c.req.json().catch(() => null));
|
|
35
|
+
const hidden = body?.hidden;
|
|
36
|
+
if (!Array.isArray(hidden) ||
|
|
37
|
+
hidden.some((id) => typeof id !== "string")) {
|
|
38
|
+
return c.json({ error: "hidden must be an array of profile ids" }, 400);
|
|
39
|
+
}
|
|
40
|
+
setHiddenModelIds(hidden);
|
|
41
|
+
// The picker reads through a memo — drop it so the change is immediate.
|
|
42
|
+
invalidateProfiles();
|
|
43
|
+
return c.json(await buildCatalog());
|
|
44
|
+
})
|
|
45
|
+
.post("/models/refresh", async (c) => {
|
|
46
|
+
const source = await getModelSource();
|
|
47
|
+
if (!source) {
|
|
48
|
+
return c.json({ error: "Model discovery is not available on this backend" }, 409);
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
await source.refresh();
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
// The previous roster is still served; report the failure in the payload
|
|
55
|
+
// rather than 500ing, so the settings screen can show it inline.
|
|
56
|
+
console.warn(`[models] Refresh failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
57
|
+
}
|
|
58
|
+
invalidateProfiles();
|
|
59
|
+
return c.json(await buildCatalog());
|
|
60
|
+
});
|
|
61
|
+
//# sourceMappingURL=models.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"models.js","sourceRoot":"","sources":["../../src/routes/models.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,EAAE;AACF,8EAA8E;AAC9E,4EAA4E;AAC5E,gFAAgF;AAChF,gDAAgD;AAEhD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAK5B,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAEzE,KAAK,UAAU,YAAY;IACzB,MAAM,MAAM,GAAG,MAAM,cAAc,EAAE,CAAC;IACtC,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,iBAAiB,EAAE,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAwB,CAClC,MAAM,gBAAgB,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAChD,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAErE,MAAM,KAAK,GAAG,MAAM,EAAE,KAAK,EAAE,CAAC;IAC9B,OAAO;QACL,MAAM;QACN,WAAW,EAAE,KAAK,EAAE,WAAW,IAAI,IAAI;QACvC,KAAK,EAAE,KAAK,EAAE,KAAK,IAAI,KAAK;QAC5B,SAAS,EAAE;YACT,OAAO,EAAE,KAAK,EAAE,OAAO,IAAI,KAAK;YAChC,GAAG,CAAC,KAAK,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC9D;KACF,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,IAAI,EAAE;KAClC,GAAG,CAAC,SAAS,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;IAC1B,yEAAyE;IACzE,uEAAuE;IACvE,MAAM,MAAM,GAAG,MAAM,cAAc,EAAE,CAAC;IACtC,MAAM,MAAM,EAAE,WAAW,EAAE,CAAC;IAC5B,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,YAAY,EAAE,CAAC,CAAC;AACtC,CAAC,CAAC;KAED,GAAG,CAAC,gBAAgB,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;IACjC,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAY,CAAC;IAC/D,MAAM,MAAM,GAAI,IAAoC,EAAE,MAAM,CAAC;IAC7D,IACE,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QACtB,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,KAAK,QAAQ,CAAC,EAC3C,CAAC;QACD,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,wCAAwC,EAAE,EAAE,GAAG,CAAC,CAAC;IAC1E,CAAC;IAED,iBAAiB,CAAC,MAAkB,CAAC,CAAC;IACtC,wEAAwE;IACxE,kBAAkB,EAAE,CAAC;IACrB,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,YAAY,EAAE,CAAC,CAAC;AACtC,CAAC,CAAC;KAED,IAAI,CAAC,iBAAiB,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;IACnC,MAAM,MAAM,GAAG,MAAM,cAAc,EAAE,CAAC;IACtC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,CAAC,CAAC,IAAI,CACX,EAAE,KAAK,EAAE,kDAAkD,EAAE,EAC7D,GAAG,CACJ,CAAC;IACJ,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,OAAO,EAAE,CAAC;IACzB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,yEAAyE;QACzE,iEAAiE;QACjE,OAAO,CAAC,IAAI,CACV,4BACE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CACjD,EAAE,CACH,CAAC;IACJ,CAAC;IACD,kBAAkB,EAAE,CAAC;IACrB,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,YAAY,EAAE,CAAC,CAAC;AACtC,CAAC,CAAC,CAAC"}
|
|
@@ -8,6 +8,8 @@ export declare const providerRoutes: import("hono/hono-base").HonoBase<import("h
|
|
|
8
8
|
label: string;
|
|
9
9
|
vendor?: string | undefined;
|
|
10
10
|
backendId?: string | undefined;
|
|
11
|
+
contextWindow?: number | undefined;
|
|
12
|
+
source?: "builtin" | "declared" | "discovered" | undefined;
|
|
11
13
|
}[];
|
|
12
14
|
backends: {
|
|
13
15
|
[x: string]: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../../src/routes/providers.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../../src/routes/providers.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAWzB,CAAC"}
|
package/dist/routes/providers.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import { getBackendsInfo, listAllProviders } from "../agent/backend.js";
|
|
2
|
+
import { getBackendsInfo, getModelSource, listAllProviders, } from "../agent/backend.js";
|
|
3
3
|
export const providerRoutes = new Hono().get("/providers", async (c) => {
|
|
4
|
+
// Keeps the roster current without a restart: serves the cached list and
|
|
5
|
+
// refreshes behind the response when it has gone stale. Only a cold start
|
|
6
|
+
// (nothing cached yet) waits on the Models API.
|
|
7
|
+
const source = await getModelSource();
|
|
8
|
+
await source?.ensureFresh();
|
|
4
9
|
return c.json({
|
|
5
10
|
providers: await listAllProviders(),
|
|
6
11
|
backends: await getBackendsInfo(),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"providers.js","sourceRoot":"","sources":["../../src/routes/providers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,
|
|
1
|
+
{"version":3,"file":"providers.js","sourceRoot":"","sources":["../../src/routes/providers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EACL,eAAe,EACf,cAAc,EACd,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAE7B,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,IAAI,EAAE,CAAC,GAAG,CAAC,YAAY,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;IACrE,yEAAyE;IACzE,0EAA0E;IAC1E,gDAAgD;IAChD,MAAM,MAAM,GAAG,MAAM,cAAc,EAAE,CAAC;IACtC,MAAM,MAAM,EAAE,WAAW,EAAE,CAAC;IAE5B,OAAO,CAAC,CAAC,IAAI,CAAC;QACZ,SAAS,EAAE,MAAM,gBAAgB,EAAE;QACnC,QAAQ,EAAE,MAAM,eAAe,EAAE;KAClC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
-- Generic server-side settings, stored as a JSON-per-key KV table so a new
|
|
2
|
+
-- preference does not need its own migration.
|
|
3
|
+
--
|
|
4
|
+
-- Keys in use:
|
|
5
|
+
-- models.hidden -> string[] of profile ids kept out of the model picker
|
|
6
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
7
|
+
key TEXT PRIMARY KEY,
|
|
8
|
+
value TEXT NOT NULL,
|
|
9
|
+
updated_at INTEGER NOT NULL
|
|
10
|
+
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schlessera/brain-ui-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "brain-kit chat-UI server: Hono app factory, WebSocket turn coordinator, auth (password/passkeys/tailscale/proxy), session catalog, and brain/files/voice routes",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"prepublishOnly": "bun ../../scripts/check-dist.ts"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@schlessera/brain-backend-claude": "0.
|
|
49
|
-
"@schlessera/brain-ui-sdk": "0.
|
|
48
|
+
"@schlessera/brain-backend-claude": "0.5.1",
|
|
49
|
+
"@schlessera/brain-ui-sdk": "0.5.1",
|
|
50
50
|
"@simplewebauthn/server": "^13.3.2",
|
|
51
51
|
"hono": "^4",
|
|
52
52
|
"ignore": "^7.0.5",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"zod": "^4.4.3"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"@schlessera/brain-backend-pi": "
|
|
57
|
+
"@schlessera/brain-backend-pi": "*",
|
|
58
58
|
"@types/bun": ">=1.3.0"
|
|
59
59
|
},
|
|
60
60
|
"peerDependenciesMeta": {
|
package/src/agent/backend.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { createRequire } from "module";
|
|
2
2
|
import {
|
|
3
3
|
createClaudeBackend,
|
|
4
|
+
createModelSource,
|
|
4
5
|
defineProfiles,
|
|
5
6
|
type InferenceProfile,
|
|
6
7
|
type InferenceProfileInput,
|
|
8
|
+
type ModelSource,
|
|
7
9
|
} from "@schlessera/brain-backend-claude";
|
|
10
|
+
import { getHiddenModelIds } from "../db/settings.js";
|
|
8
11
|
import type { ProviderInfo } from "@schlessera/brain-ui-sdk";
|
|
9
12
|
import type {
|
|
10
13
|
AgentBackend,
|
|
@@ -35,11 +38,24 @@ interface BackendRegistry {
|
|
|
35
38
|
backends: AgentBackend[];
|
|
36
39
|
byId: Map<string, AgentBackend>;
|
|
37
40
|
defaultBackendId: string;
|
|
38
|
-
profileOwners: Map<string, string>;
|
|
39
|
-
profiles: Map<string, ProviderInfo[]>;
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Profiles are NOT part of the registry snapshot: a backend's roster can grow
|
|
45
|
+
* while the process runs (model discovery refreshing behind a request), so they
|
|
46
|
+
* are recomputed behind a short memo and can be invalidated explicitly.
|
|
47
|
+
*/
|
|
48
|
+
interface ProfileSnapshot {
|
|
49
|
+
at: number;
|
|
50
|
+
byBackend: Map<string, ProviderInfo[]>;
|
|
51
|
+
owners: Map<string, string>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const PROFILE_MEMO_MS = 5_000;
|
|
55
|
+
|
|
42
56
|
let cachedRegistry: Promise<BackendRegistry> | null = null;
|
|
57
|
+
let profileSnapshot: ProfileSnapshot | null = null;
|
|
58
|
+
let modelSource: ModelSource | null = null;
|
|
43
59
|
|
|
44
60
|
async function createRegistry(
|
|
45
61
|
backends: AgentBackend[],
|
|
@@ -57,35 +73,121 @@ async function createRegistry(
|
|
|
57
73
|
byId.get(resolvedDefaultId)!,
|
|
58
74
|
...backends.filter((backend) => backend.id !== resolvedDefaultId),
|
|
59
75
|
];
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
|
|
77
|
+
return { backends: ordered, byId, defaultBackendId: resolvedDefaultId };
|
|
78
|
+
}
|
|
79
|
+
|
|
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
|
+
|
|
86
|
+
const registry = await getRegistry();
|
|
87
|
+
const byBackend = new Map<string, ProviderInfo[]>();
|
|
88
|
+
const owners = new Map<string, string>();
|
|
89
|
+
|
|
90
|
+
for (const backend of registry.backends) {
|
|
91
|
+
const profiles = await backend.listProfiles();
|
|
92
|
+
byBackend.set(backend.id, profiles);
|
|
93
|
+
for (const profile of profiles) {
|
|
94
|
+
const owner = owners.get(profile.id);
|
|
95
|
+
if (owner && owner !== backend.id) {
|
|
74
96
|
throw new Error(
|
|
75
|
-
`Profile id collision across backends: "${profile.id}" is exposed by "${owner}" and "${
|
|
97
|
+
`Profile id collision across backends: "${profile.id}" is exposed by "${owner}" and "${backend.id}".`
|
|
76
98
|
);
|
|
77
99
|
}
|
|
78
|
-
|
|
100
|
+
owners.set(profile.id, backend.id);
|
|
79
101
|
}
|
|
80
102
|
}
|
|
81
103
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The Claude backend's discovery source, once the registry is built. Null when
|
|
115
|
+
* discovery is disabled or the deployment runs a different backend.
|
|
116
|
+
*/
|
|
117
|
+
export async function getModelSource(): Promise<ModelSource | null> {
|
|
118
|
+
await getRegistry();
|
|
119
|
+
return modelSource;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Hidden profile ids, from the settings table. Presentation only — a hidden
|
|
124
|
+
* profile still resolves for sessions pinned to it. A db read failure must not
|
|
125
|
+
* take the picker down, so it degrades to "nothing hidden".
|
|
126
|
+
*/
|
|
127
|
+
function hiddenIds(): Set<string> {
|
|
128
|
+
try {
|
|
129
|
+
return new Set(getHiddenModelIds());
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.warn(
|
|
132
|
+
`[models] Could not read hidden models: ${
|
|
133
|
+
err instanceof Error ? err.message : String(err)
|
|
134
|
+
}`
|
|
135
|
+
);
|
|
136
|
+
return new Set();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Model discovery is on unless explicitly disabled — except under a test
|
|
142
|
+
* runner, where the default flips to off: discovery is a live call to the
|
|
143
|
+
* Anthropic API, and a suite that silently depends on network access (and on
|
|
144
|
+
* whatever credentials the developer happens to have exported) is flaky by
|
|
145
|
+
* construction. Tests that want it set BRAIN_UI_MODEL_DISCOVERY=1 explicitly.
|
|
146
|
+
*/
|
|
147
|
+
function modelDiscoveryEnabled(): boolean {
|
|
148
|
+
const raw = process.env.BRAIN_UI_MODEL_DISCOVERY?.trim().toLowerCase();
|
|
149
|
+
if (raw) return !(raw === "0" || raw === "off" || raw === "false");
|
|
150
|
+
return process.env.NODE_ENV !== "test";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** How long a discovery result stays fresh. Default 24h. */
|
|
154
|
+
function modelTtlMs(): number {
|
|
155
|
+
const raw = Number(process.env.BRAIN_UI_MODEL_TTL_HOURS);
|
|
156
|
+
const hours = Number.isFinite(raw) && raw > 0 ? raw : 24;
|
|
157
|
+
return hours * 60 * 60 * 1000;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Declared profiles (built-in default + BRAIN_UI_CLAUDE_PROFILES) plus every
|
|
162
|
+
* discovered model whose id isn't already declared. Declared wins: the env stays
|
|
163
|
+
* an override mechanism, and a hand-pinned entry keeps its label and endpoint.
|
|
164
|
+
*
|
|
165
|
+
* Memoized on the discovered array's identity — `createModelSource` swaps the
|
|
166
|
+
* array only on a successful refresh, so steady state is one comparison.
|
|
167
|
+
*/
|
|
168
|
+
let mergeCache: {
|
|
169
|
+
declared: InferenceProfile[];
|
|
170
|
+
discovered: InferenceProfileInput[];
|
|
171
|
+
result: InferenceProfile[];
|
|
172
|
+
} | null = null;
|
|
173
|
+
|
|
174
|
+
function mergeDiscovered(
|
|
175
|
+
declared: InferenceProfile[],
|
|
176
|
+
discovered: InferenceProfileInput[]
|
|
177
|
+
): InferenceProfile[] {
|
|
178
|
+
if (
|
|
179
|
+
mergeCache &&
|
|
180
|
+
mergeCache.declared === declared &&
|
|
181
|
+
mergeCache.discovered === discovered
|
|
182
|
+
) {
|
|
183
|
+
return mergeCache.result;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const declaredIds = new Set(declared.map((profile) => profile.id));
|
|
187
|
+
const extra = discovered.filter((input) => !declaredIds.has(input.id));
|
|
188
|
+
const result = [...declared, ...defineProfiles(extra)];
|
|
189
|
+
mergeCache = { declared, discovered, result };
|
|
190
|
+
return result;
|
|
89
191
|
}
|
|
90
192
|
|
|
91
193
|
async function buildRegistry(): Promise<BackendRegistry> {
|
|
@@ -98,10 +200,22 @@ async function buildRegistry(): Promise<BackendRegistry> {
|
|
|
98
200
|
return createRegistry([pi], pi.id);
|
|
99
201
|
}
|
|
100
202
|
|
|
203
|
+
// Declared profiles are resolved EAGERLY so a malformed
|
|
204
|
+
// BRAIN_UI_CLAUDE_PROFILES still fails at boot rather than on first request.
|
|
205
|
+
const declared = loadClaudeProfiles();
|
|
206
|
+
|
|
207
|
+
modelSource = createModelSource({
|
|
208
|
+
brainPath: BRAIN_PATH,
|
|
209
|
+
enabled: modelDiscoveryEnabled(),
|
|
210
|
+
ttlMs: modelTtlMs(),
|
|
211
|
+
});
|
|
212
|
+
|
|
101
213
|
const claude = createClaudeBackend({
|
|
102
214
|
brainPath: BRAIN_PATH,
|
|
103
215
|
claudeCodePath: CLAUDE_CODE_PATH,
|
|
104
|
-
|
|
216
|
+
// A function, not an array: discovery refreshes in the background and the
|
|
217
|
+
// new roster has to be visible without restarting the process.
|
|
218
|
+
profiles: () => mergeDiscovered(declared, modelSource?.list() ?? []),
|
|
105
219
|
});
|
|
106
220
|
const backends = [claude];
|
|
107
221
|
|
|
@@ -151,7 +265,10 @@ export async function getBackendForProfile(
|
|
|
151
265
|
profileId: string
|
|
152
266
|
): Promise<AgentBackend | undefined> {
|
|
153
267
|
const registry = await getRegistry();
|
|
154
|
-
|
|
268
|
+
// Deliberately resolved against the UNFILTERED roster: a session pinned to a
|
|
269
|
+
// profile the user later hid must keep running.
|
|
270
|
+
const snapshot = await getProfileSnapshot();
|
|
271
|
+
const backendId = snapshot.owners.get(profileId);
|
|
155
272
|
return backendId ? registry.byId.get(backendId) : undefined;
|
|
156
273
|
}
|
|
157
274
|
|
|
@@ -166,14 +283,23 @@ export async function getBackendForSession(
|
|
|
166
283
|
return getDefaultBackend();
|
|
167
284
|
}
|
|
168
285
|
|
|
169
|
-
/**
|
|
170
|
-
|
|
286
|
+
/**
|
|
287
|
+
* Every available profile, tagged with its owning backend id.
|
|
288
|
+
*
|
|
289
|
+
* Hidden profiles are omitted by default (this feeds the picker); the settings
|
|
290
|
+
* screen passes `includeHidden` to render the full catalog.
|
|
291
|
+
*/
|
|
292
|
+
export async function listAllProviders(
|
|
293
|
+
options: { includeHidden?: boolean } = {}
|
|
294
|
+
): Promise<ProviderInfo[]> {
|
|
171
295
|
const registry = await getRegistry();
|
|
296
|
+
const snapshot = await getProfileSnapshot();
|
|
297
|
+
const hidden = options.includeHidden ? new Set<string>() : hiddenIds();
|
|
298
|
+
|
|
172
299
|
return registry.backends.flatMap((backend) =>
|
|
173
|
-
(
|
|
174
|
-
|
|
175
|
-
backendId: backend.id
|
|
176
|
-
}))
|
|
300
|
+
(snapshot.byBackend.get(backend.id) ?? [])
|
|
301
|
+
.filter((profile) => !hidden.has(profile.id))
|
|
302
|
+
.map((profile) => ({ ...profile, backendId: backend.id }))
|
|
177
303
|
);
|
|
178
304
|
}
|
|
179
305
|
|
|
@@ -198,11 +324,14 @@ export async function getBackend(): Promise<AgentBackend> {
|
|
|
198
324
|
/** Test seam: drop the cached registry so later access rebuilds from env. */
|
|
199
325
|
export function resetBackendForTests(): void {
|
|
200
326
|
cachedRegistry = null;
|
|
327
|
+
profileSnapshot = null;
|
|
328
|
+
modelSource = null;
|
|
201
329
|
}
|
|
202
330
|
|
|
203
331
|
/** Test seam: install one fake backend as the complete registry. */
|
|
204
332
|
export function setBackendForTests(backend: AgentBackend): void {
|
|
205
333
|
cachedRegistry = createRegistry([backend], backend.id);
|
|
334
|
+
profileSnapshot = null;
|
|
206
335
|
}
|
|
207
336
|
|
|
208
337
|
/** Test seam: install a complete fake registry with an explicit default. */
|
|
@@ -211,6 +340,7 @@ export function setBackendsForTests(
|
|
|
211
340
|
defaultBackendId = backends[0]?.id ?? ""
|
|
212
341
|
): void {
|
|
213
342
|
cachedRegistry = createRegistry(backends, defaultBackendId);
|
|
343
|
+
profileSnapshot = null;
|
|
214
344
|
}
|
|
215
345
|
|
|
216
346
|
// The built-in default profile's model. The Claude backend historically pinned
|
|
@@ -230,6 +360,7 @@ function builtinDefaultProfiles(): InferenceProfile[] {
|
|
|
230
360
|
vendor: "anthropic",
|
|
231
361
|
model: BUILTIN_DEFAULT_MODEL,
|
|
232
362
|
modelAliases: true,
|
|
363
|
+
source: "builtin",
|
|
233
364
|
},
|
|
234
365
|
]);
|
|
235
366
|
}
|
|
@@ -279,7 +410,11 @@ function loadClaudeProfiles(): InferenceProfile[] {
|
|
|
279
410
|
seen.add(input.id);
|
|
280
411
|
}
|
|
281
412
|
|
|
282
|
-
|
|
413
|
+
const declared = (inputs as InferenceProfileInput[]).map((input) => ({
|
|
414
|
+
source: "declared" as const,
|
|
415
|
+
...input,
|
|
416
|
+
}));
|
|
417
|
+
return [...base, ...defineProfiles(declared)];
|
|
283
418
|
}
|
|
284
419
|
|
|
285
420
|
/**
|
package/src/app.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { voiceRoutes } from "./routes/voice.js";
|
|
|
11
11
|
import { filesRoutes } from "./routes/files.js";
|
|
12
12
|
import { createRenderRoutes, type AppRenderer } from "./routes/render.js";
|
|
13
13
|
import { providerRoutes } from "./routes/providers.js";
|
|
14
|
+
import { modelRoutes } from "./routes/models.js";
|
|
14
15
|
import {
|
|
15
16
|
resolveAuthMode,
|
|
16
17
|
assertAuthConfig,
|
|
@@ -130,6 +131,7 @@ export function createApp(options: CreateAppOptions = {}) {
|
|
|
130
131
|
app.route("/api", filesRoutes);
|
|
131
132
|
app.route("/api", createRenderRoutes(options.renderer));
|
|
132
133
|
app.route("/api", providerRoutes);
|
|
134
|
+
app.route("/api", modelRoutes);
|
|
133
135
|
|
|
134
136
|
// WebSocket endpoint. Browsers can't set headers on the WS handshake, so the
|
|
135
137
|
// upgrade authenticates via the session cookie (or IP/proxy header) INSIDE
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Server-side settings: a JSON-per-key KV table (migration 006).
|
|
2
|
+
//
|
|
3
|
+
// These are deployment-wide preferences that must follow the user across
|
|
4
|
+
// devices, so they live in SQLite on the persisted volume rather than in the
|
|
5
|
+
// browser. Values are stored as JSON text; readers are total — a missing or
|
|
6
|
+
// corrupt row degrades to the caller's fallback rather than throwing, because
|
|
7
|
+
// a bad preference must never take a route down.
|
|
8
|
+
|
|
9
|
+
import { getDb } from "./client.js";
|
|
10
|
+
|
|
11
|
+
const HIDDEN_MODELS_KEY = "models.hidden";
|
|
12
|
+
|
|
13
|
+
export function getSetting<T>(key: string, fallback: T): T {
|
|
14
|
+
const row = getDb()
|
|
15
|
+
.query("SELECT value FROM settings WHERE key = ?")
|
|
16
|
+
.get(key) as { value: string } | null;
|
|
17
|
+
if (!row) return fallback;
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(row.value) as T;
|
|
20
|
+
} catch {
|
|
21
|
+
console.warn(`[settings] Corrupt JSON for "${key}"; using fallback`);
|
|
22
|
+
return fallback;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function setSetting(key: string, value: unknown): void {
|
|
27
|
+
getDb()
|
|
28
|
+
.prepare(
|
|
29
|
+
`INSERT INTO settings (key, value, updated_at) VALUES (?, ?, ?)
|
|
30
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
|
|
31
|
+
)
|
|
32
|
+
.run(key, JSON.stringify(value), Date.now());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Profile ids the user keeps out of the model picker. */
|
|
36
|
+
export function getHiddenModelIds(): string[] {
|
|
37
|
+
const value = getSetting<unknown>(HIDDEN_MODELS_KEY, []);
|
|
38
|
+
if (!Array.isArray(value)) return [];
|
|
39
|
+
return value.filter((id): id is string => typeof id === "string");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Replace the hidden set (the client always sends the full list, not a delta). */
|
|
43
|
+
export function setHiddenModelIds(ids: string[]): void {
|
|
44
|
+
const unique = [...new Set(ids.filter((id) => typeof id === "string" && id))];
|
|
45
|
+
setSetting(HIDDEN_MODELS_KEY, unique);
|
|
46
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Model catalog routes — the settings screen's view of the picker.
|
|
2
|
+
//
|
|
3
|
+
// `/api/providers` answers the picker and omits hidden profiles. These routes
|
|
4
|
+
// answer the settings screen and therefore show everything, each row tagged
|
|
5
|
+
// with its visibility, plus discovery freshness so the UI can say when the list
|
|
6
|
+
// was last refreshed and why it might be stale.
|
|
7
|
+
|
|
8
|
+
import { Hono } from "hono";
|
|
9
|
+
import type {
|
|
10
|
+
ModelCatalogEntry,
|
|
11
|
+
ModelCatalogResponse,
|
|
12
|
+
} from "@schlessera/brain-ui-sdk";
|
|
13
|
+
import {
|
|
14
|
+
getModelSource,
|
|
15
|
+
invalidateProfiles,
|
|
16
|
+
listAllProviders,
|
|
17
|
+
} from "../agent/backend.js";
|
|
18
|
+
import { getHiddenModelIds, setHiddenModelIds } from "../db/settings.js";
|
|
19
|
+
|
|
20
|
+
async function buildCatalog(): Promise<ModelCatalogResponse> {
|
|
21
|
+
const source = await getModelSource();
|
|
22
|
+
const hidden = new Set(getHiddenModelIds());
|
|
23
|
+
const models: ModelCatalogEntry[] = (
|
|
24
|
+
await listAllProviders({ includeHidden: true })
|
|
25
|
+
).map((profile) => ({ ...profile, hidden: hidden.has(profile.id) }));
|
|
26
|
+
|
|
27
|
+
const state = source?.state();
|
|
28
|
+
return {
|
|
29
|
+
models,
|
|
30
|
+
refreshedAt: state?.refreshedAt ?? null,
|
|
31
|
+
stale: state?.stale ?? false,
|
|
32
|
+
discovery: {
|
|
33
|
+
enabled: state?.enabled ?? false,
|
|
34
|
+
...(state?.error !== undefined ? { error: state.error } : {}),
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const modelRoutes = new Hono()
|
|
40
|
+
.get("/models", async (c) => {
|
|
41
|
+
// Serves the cached roster immediately and refreshes behind the response
|
|
42
|
+
// when stale; only a cold start (nothing cached) waits on the network.
|
|
43
|
+
const source = await getModelSource();
|
|
44
|
+
await source?.ensureFresh();
|
|
45
|
+
return c.json(await buildCatalog());
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
.put("/models/hidden", async (c) => {
|
|
49
|
+
const body = (await c.req.json().catch(() => null)) as unknown;
|
|
50
|
+
const hidden = (body as { hidden?: unknown } | null)?.hidden;
|
|
51
|
+
if (
|
|
52
|
+
!Array.isArray(hidden) ||
|
|
53
|
+
hidden.some((id) => typeof id !== "string")
|
|
54
|
+
) {
|
|
55
|
+
return c.json({ error: "hidden must be an array of profile ids" }, 400);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setHiddenModelIds(hidden as string[]);
|
|
59
|
+
// The picker reads through a memo — drop it so the change is immediate.
|
|
60
|
+
invalidateProfiles();
|
|
61
|
+
return c.json(await buildCatalog());
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
.post("/models/refresh", async (c) => {
|
|
65
|
+
const source = await getModelSource();
|
|
66
|
+
if (!source) {
|
|
67
|
+
return c.json(
|
|
68
|
+
{ error: "Model discovery is not available on this backend" },
|
|
69
|
+
409
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
await source.refresh();
|
|
74
|
+
} catch (err) {
|
|
75
|
+
// The previous roster is still served; report the failure in the payload
|
|
76
|
+
// rather than 500ing, so the settings screen can show it inline.
|
|
77
|
+
console.warn(
|
|
78
|
+
`[models] Refresh failed: ${
|
|
79
|
+
err instanceof Error ? err.message : String(err)
|
|
80
|
+
}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
invalidateProfiles();
|
|
84
|
+
return c.json(await buildCatalog());
|
|
85
|
+
});
|
package/src/routes/providers.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getBackendsInfo,
|
|
4
|
+
getModelSource,
|
|
5
|
+
listAllProviders,
|
|
6
|
+
} from "../agent/backend.js";
|
|
3
7
|
|
|
4
8
|
export const providerRoutes = new Hono().get("/providers", async (c) => {
|
|
9
|
+
// Keeps the roster current without a restart: serves the cached list and
|
|
10
|
+
// refreshes behind the response when it has gone stale. Only a cold start
|
|
11
|
+
// (nothing cached yet) waits on the Models API.
|
|
12
|
+
const source = await getModelSource();
|
|
13
|
+
await source?.ensureFresh();
|
|
14
|
+
|
|
5
15
|
return c.json({
|
|
6
16
|
providers: await listAllProviders(),
|
|
7
17
|
backends: await getBackendsInfo(),
|