@schlessera/brain-ui-server 0.23.0 → 0.25.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/dist/agent/backend.d.ts +29 -0
- package/dist/agent/backend.d.ts.map +1 -1
- package/dist/agent/backend.js +115 -8
- package/dist/agent/backend.js.map +1 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +4 -1
- package/dist/app.js.map +1 -1
- package/dist/db/settings.d.ts +21 -1
- package/dist/db/settings.d.ts.map +1 -1
- package/dist/db/settings.js +51 -1
- package/dist/db/settings.js.map +1 -1
- package/dist/routes/models.d.ts.map +1 -1
- package/dist/routes/models.js +91 -2
- package/dist/routes/models.js.map +1 -1
- package/dist/ws/routing.d.ts.map +1 -1
- package/dist/ws/routing.js +9 -0
- package/dist/ws/routing.js.map +1 -1
- package/package.json +3 -3
- package/src/agent/backend.ts +156 -8
- package/src/app.ts +10 -1
- package/src/db/settings.ts +70 -1
- package/src/routes/models.ts +122 -0
- package/src/ws/routing.ts +9 -0
package/dist/routes/models.js
CHANGED
|
@@ -5,14 +5,15 @@
|
|
|
5
5
|
// with its visibility, plus discovery freshness so the UI can say when the list
|
|
6
6
|
// was last refreshed and why it might be stale.
|
|
7
7
|
import { Hono } from "hono";
|
|
8
|
-
import { isBillingMode, } from "@schlessera/brain-ui-sdk";
|
|
9
|
-
import { getBillingOverrides, getHiddenModelIds, setBillingOverrides, setHiddenModelIds, } from "../db/settings.js";
|
|
8
|
+
import { isBillingMode, isThinkingLevel, } from "@schlessera/brain-ui-sdk";
|
|
9
|
+
import { getBillingOverrides, getCustomOpenRouterModels, getDefaultModelId, getHiddenModelIds, getThinkingOverrides, setBillingOverrides, setCustomOpenRouterModels, setDefaultModelId, setHiddenModelIds, setThinkingOverrides, } from "../db/settings.js";
|
|
10
10
|
export function createModelRoutes(deps) {
|
|
11
11
|
const { registry, db, pricing } = deps;
|
|
12
12
|
async function buildCatalog() {
|
|
13
13
|
const source = await registry.getModelSource();
|
|
14
14
|
const hidden = new Set(getHiddenModelIds(db));
|
|
15
15
|
const overrides = getBillingOverrides(db);
|
|
16
|
+
const thinking = getThinkingOverrides(db);
|
|
16
17
|
// `billingMode` already rides each provider entry (the registry applies the
|
|
17
18
|
// override last); the catalog additionally tags WHICH rows carry an explicit
|
|
18
19
|
// override, so the settings screen can render auto vs forced.
|
|
@@ -20,10 +21,18 @@ export function createModelRoutes(deps) {
|
|
|
20
21
|
...profile,
|
|
21
22
|
hidden: hidden.has(profile.id),
|
|
22
23
|
...(overrides[profile.id] ? { billingOverride: overrides[profile.id] } : {}),
|
|
24
|
+
// `thinkingLevel` on the profile is already the EFFECTIVE level (the
|
|
25
|
+
// registry applies overrides at read time); this tags which rows carry
|
|
26
|
+
// an explicit user choice, so the UI can render default vs forced.
|
|
27
|
+
...(thinking[profile.id] ? { thinkingOverride: thinking[profile.id] } : {}),
|
|
23
28
|
}));
|
|
24
29
|
const state = source?.state();
|
|
30
|
+
const resolvedDefaultId = await registry.getPreferredProfileId();
|
|
25
31
|
return {
|
|
26
32
|
models,
|
|
33
|
+
defaultModelId: getDefaultModelId(db),
|
|
34
|
+
...(resolvedDefaultId ? { resolvedDefaultId } : {}),
|
|
35
|
+
customModels: getCustomOpenRouterModels(db),
|
|
27
36
|
refreshedAt: state?.refreshedAt ?? null,
|
|
28
37
|
stale: state?.stale ?? false,
|
|
29
38
|
discovery: {
|
|
@@ -62,6 +71,86 @@ export function createModelRoutes(deps) {
|
|
|
62
71
|
// The picker reads through a memo — drop it so the change is immediate.
|
|
63
72
|
registry.invalidateProfiles();
|
|
64
73
|
return c.json(await buildCatalog());
|
|
74
|
+
})
|
|
75
|
+
.put("/models/default", async (c) => {
|
|
76
|
+
const body = (await c.req.json().catch(() => null));
|
|
77
|
+
const defaultId = body?.defaultId;
|
|
78
|
+
if (defaultId !== null && typeof defaultId !== "string") {
|
|
79
|
+
return c.json({ error: "defaultId must be a profile id or null" }, 400);
|
|
80
|
+
}
|
|
81
|
+
if (typeof defaultId === "string") {
|
|
82
|
+
const known = await registry.listAllProviders({ includeHidden: true });
|
|
83
|
+
if (!known.some((profile) => profile.id === defaultId)) {
|
|
84
|
+
return c.json({ error: `Unknown profile id: ${defaultId}` }, 400);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
setDefaultModelId(db, defaultId);
|
|
88
|
+
registry.invalidateProfiles();
|
|
89
|
+
return c.json(await buildCatalog());
|
|
90
|
+
})
|
|
91
|
+
.put("/models/custom", async (c) => {
|
|
92
|
+
const body = (await c.req.json().catch(() => null));
|
|
93
|
+
const models = body?.models;
|
|
94
|
+
if (!Array.isArray(models) || models.some((id) => typeof id !== "string")) {
|
|
95
|
+
return c.json({ error: "models must be an array of OpenRouter model ids" }, 400);
|
|
96
|
+
}
|
|
97
|
+
// OpenRouter ids are "<org>/<model>", org and model from a small safe
|
|
98
|
+
// charset (e.g. "z.ai/glm-5.3-flash", "openai/gpt-oss-120b:nitro").
|
|
99
|
+
const ID_SHAPE = /^[A-Za-z0-9][\w.-]*\/[\w.:-]+$/;
|
|
100
|
+
const bad = models.find((id) => !ID_SHAPE.test(id));
|
|
101
|
+
if (bad !== undefined) {
|
|
102
|
+
return c.json({ error: `Not an OpenRouter model id: "${bad}"` }, 400);
|
|
103
|
+
}
|
|
104
|
+
// A generated id ("openrouter:<model>") that collides with a profile
|
|
105
|
+
// another source owns (a pi profile, a discovered model) must be refused
|
|
106
|
+
// BEFORE persisting: stored, it would 500 every roster read until the
|
|
107
|
+
// setting is dug out of the database. An id the CLAUDE env already
|
|
108
|
+
// declares is fine — the merge dedupes it in the declared entry's favor.
|
|
109
|
+
const currentCustomIds = new Set(getCustomOpenRouterModels(db).map((model) => `openrouter:${model}`));
|
|
110
|
+
const nonCustomIds = new Set((await registry.listAllProviders({ includeHidden: true }))
|
|
111
|
+
.filter((profile) => !currentCustomIds.has(profile.id))
|
|
112
|
+
.map((profile) => profile.id));
|
|
113
|
+
const collision = models
|
|
114
|
+
.map((model) => `openrouter:${model}`)
|
|
115
|
+
.find((id) => nonCustomIds.has(id));
|
|
116
|
+
if (collision !== undefined) {
|
|
117
|
+
return c.json({ error: `"${collision}" collides with an existing profile id.` }, 400);
|
|
118
|
+
}
|
|
119
|
+
setCustomOpenRouterModels(db, models);
|
|
120
|
+
registry.invalidateProfiles();
|
|
121
|
+
// A removal that takes the stored default's profile off the roster would
|
|
122
|
+
// strand the default on nothing; reset it to auto — but only when the
|
|
123
|
+
// profile is actually gone (an id the env also declares survives the
|
|
124
|
+
// removal of its custom duplicate).
|
|
125
|
+
const currentDefault = getDefaultModelId(db);
|
|
126
|
+
if (currentDefault) {
|
|
127
|
+
const roster = await registry.listAllProviders({ includeHidden: true });
|
|
128
|
+
if (!roster.some((profile) => profile.id === currentDefault)) {
|
|
129
|
+
setDefaultModelId(db, null);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return c.json(await buildCatalog());
|
|
133
|
+
})
|
|
134
|
+
.put("/models/thinking", async (c) => {
|
|
135
|
+
const body = (await c.req.json().catch(() => null));
|
|
136
|
+
const thinking = body?.thinking;
|
|
137
|
+
if (typeof thinking !== "object" ||
|
|
138
|
+
thinking === null ||
|
|
139
|
+
Array.isArray(thinking) ||
|
|
140
|
+
Object.values(thinking).some((level) => !isThinkingLevel(level))) {
|
|
141
|
+
return c.json({ error: "thinking must map profile ids to a reasoning-effort level" }, 400);
|
|
142
|
+
}
|
|
143
|
+
// Only rows that actually take an effort level accept an override — a
|
|
144
|
+
// stray id would be stored dead weight and mislead the settings UI.
|
|
145
|
+
const known = await registry.listAllProviders({ includeHidden: true });
|
|
146
|
+
const supported = new Set(known.filter((profile) => profile.thinkingLevel).map((profile) => profile.id));
|
|
147
|
+
const stray = Object.keys(thinking).find((id) => !supported.has(id));
|
|
148
|
+
if (stray !== undefined) {
|
|
149
|
+
return c.json({ error: `Profile "${stray}" does not take a reasoning-effort level.` }, 400);
|
|
150
|
+
}
|
|
151
|
+
setThinkingOverrides(db, thinking);
|
|
152
|
+
registry.invalidateProfiles();
|
|
153
|
+
return c.json(await buildCatalog());
|
|
65
154
|
})
|
|
66
155
|
.put("/models/billing", async (c) => {
|
|
67
156
|
const body = (await c.req.json().catch(() => null));
|
|
@@ -1 +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;AAGhD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EACL,aAAa,
|
|
1
|
+
{"version":3,"file":"models.js","sourceRoot":"","sources":["../../src/routes/models.ts"],"names":[],"mappings":"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"}
|
package/dist/ws/routing.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/ws/routing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,eAAe,EACzB,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,OAAO,CAAC;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,YAAY,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/ws/routing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,eAAe,EACzB,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,OAAO,CAAC;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,YAAY,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAkC7E"}
|
package/dist/ws/routing.js
CHANGED
|
@@ -23,6 +23,15 @@ export async function resolveTurnTarget(registry, catalog, sessionId, requested)
|
|
|
23
23
|
return { backend, profileId: requested };
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
// No explicit choice (fresh client, share, host-initiated action): the
|
|
27
|
+
// preferred default — the Settings-stored default model, else a connected
|
|
28
|
+
// subscription-auth profile — before the default backend's own first profile.
|
|
29
|
+
const preferred = await registry.getPreferredProfileId();
|
|
30
|
+
if (preferred) {
|
|
31
|
+
const backend = await registry.getBackendForProfile(preferred);
|
|
32
|
+
if (backend)
|
|
33
|
+
return { backend, profileId: preferred };
|
|
34
|
+
}
|
|
26
35
|
const backend = await registry.getDefaultBackend();
|
|
27
36
|
const profileId = (await backend.listProfiles())[0]?.id;
|
|
28
37
|
return { backend, ...(profileId ? { profileId } : {}) };
|
package/dist/ws/routing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routing.js","sourceRoot":"","sources":["../../src/ws/routing.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,QAAyB,EACzB,OAAuB,EACvB,SAA6B,EAC7B,SAA6B;IAE7B,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,oBAAoB,CAAC,OAAO,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC,CAAC;QAC3F,MAAM,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,SAAS,CAAC,CAAC;QACtD,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,YAAY,EAAE,CAAC;QAC/C,IAAI,MAAM,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,KAAK,MAAM,CAAC,EAAE,CAAC;YACjE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;QACxC,CAAC;QACD,yEAAyE;QACzE,0EAA0E;QAC1E,wEAAwE;QACxE,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;IAChE,CAAC;IAED,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,gBAAgB,EAAE,CAAC;QACpD,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,KAAK,SAAS,CAAC,EAAE,CAAC;YAC1D,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,oBAAoB,CAAC,SAAS,CAAC,CAAC;YAC/D,IAAI,OAAO;gBAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC;QACxD,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,iBAAiB,EAAE,CAAC;IACnD,MAAM,SAAS,GAAG,CAAC,MAAM,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;IACxD,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AAC1D,CAAC"}
|
|
1
|
+
{"version":3,"file":"routing.js","sourceRoot":"","sources":["../../src/ws/routing.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,QAAyB,EACzB,OAAuB,EACvB,SAA6B,EAC7B,SAA6B;IAE7B,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,oBAAoB,CAAC,OAAO,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC,CAAC;QAC3F,MAAM,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,SAAS,CAAC,CAAC;QACtD,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,YAAY,EAAE,CAAC;QAC/C,IAAI,MAAM,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,KAAK,MAAM,CAAC,EAAE,CAAC;YACjE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;QACxC,CAAC;QACD,yEAAyE;QACzE,0EAA0E;QAC1E,wEAAwE;QACxE,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;IAChE,CAAC;IAED,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,gBAAgB,EAAE,CAAC;QACpD,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,KAAK,SAAS,CAAC,EAAE,CAAC;YAC1D,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,oBAAoB,CAAC,SAAS,CAAC,CAAC;YAC/D,IAAI,OAAO;gBAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC;QACxD,CAAC;IACH,CAAC;IAED,uEAAuE;IACvE,0EAA0E;IAC1E,8EAA8E;IAC9E,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,qBAAqB,EAAE,CAAC;IACzD,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,oBAAoB,CAAC,SAAS,CAAC,CAAC;QAC/D,IAAI,OAAO;YAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC;IACxD,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,iBAAiB,EAAE,CAAC;IACnD,MAAM,SAAS,GAAG,CAAC,MAAM,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;IACxD,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AAC1D,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schlessera/brain-ui-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
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": [
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@opentelemetry/api": "^1.9.1",
|
|
53
53
|
"@opentelemetry/api-logs": "0.221.0",
|
|
54
|
-
"@schlessera/brain-render-template": "0.
|
|
55
|
-
"@schlessera/brain-ui-sdk": "0.
|
|
54
|
+
"@schlessera/brain-render-template": "0.25.0",
|
|
55
|
+
"@schlessera/brain-ui-sdk": "0.25.0",
|
|
56
56
|
"@simplewebauthn/server": "^13.3.2",
|
|
57
57
|
"hono": "^4",
|
|
58
58
|
"ignore": "^7.0.5",
|
package/src/agent/backend.ts
CHANGED
|
@@ -199,6 +199,23 @@ export interface BackendRegistryOptions {
|
|
|
199
199
|
* sessions pinned to it. Defaults to "nothing hidden".
|
|
200
200
|
*/
|
|
201
201
|
getHiddenModelIds?: () => string[];
|
|
202
|
+
/**
|
|
203
|
+
* Settings-stored default profile id (Settings → Models). Null/absent =
|
|
204
|
+
* auto (a connected subscription-auth profile, else the default backend's
|
|
205
|
+
* own default).
|
|
206
|
+
*/
|
|
207
|
+
getDefaultModelId?: () => string | null;
|
|
208
|
+
/**
|
|
209
|
+
* User-managed OpenRouter model ids (from the app's settings table), added
|
|
210
|
+
* to the Claude roster as declared OpenRouter profiles at runtime.
|
|
211
|
+
*/
|
|
212
|
+
getCustomOpenRouterModels?: () => string[];
|
|
213
|
+
/**
|
|
214
|
+
* Per-profile reasoning-effort overrides (from the app's settings table),
|
|
215
|
+
* applied over the pi roster's configured levels at read time — no rebuild
|
|
216
|
+
* or redeploy needed.
|
|
217
|
+
*/
|
|
218
|
+
getThinkingOverrides?: () => Record<string, PiThinkingLevel>;
|
|
202
219
|
/**
|
|
203
220
|
* Per-profile billing-mode overrides (from the app's settings table).
|
|
204
221
|
* Consulted LAST: an override wins over both the declared-credential rule
|
|
@@ -234,6 +251,17 @@ export interface BackendRegistry {
|
|
|
234
251
|
* catalog.
|
|
235
252
|
*/
|
|
236
253
|
listAllProviders(options?: { includeHidden?: boolean }): Promise<ProviderInfo[]>;
|
|
254
|
+
/**
|
|
255
|
+
* The profile new sessions and host-initiated turns (shares, actions)
|
|
256
|
+
* default to when the client named none, or null for "the default
|
|
257
|
+
* backend's own default". Resolution: the Settings-stored default model
|
|
258
|
+
* (when it still exists on the roster) wins; otherwise AUTO — a
|
|
259
|
+
* subscription-auth profile (pi vendor "openai-codex") that is on the
|
|
260
|
+
* roster, not hidden, and whose account is actually connected. The
|
|
261
|
+
* resolved profile is also listed FIRST by `listAllProviders`, so a fresh
|
|
262
|
+
* client's picker lands on it.
|
|
263
|
+
*/
|
|
264
|
+
getPreferredProfileId(): Promise<string | null>;
|
|
237
265
|
/** Per-backend capability metadata exposed by the providers route. */
|
|
238
266
|
getBackendsInfo(): Promise<
|
|
239
267
|
Record<string, { id: string; capabilities: BackendCapabilities }>
|
|
@@ -552,26 +580,54 @@ export function createBackendRegistry(
|
|
|
552
580
|
let mergeCache: {
|
|
553
581
|
declared: ClaudeProfile[];
|
|
554
582
|
discovered: ClaudeProfileInput[];
|
|
583
|
+
customKey: string;
|
|
555
584
|
result: ClaudeProfile[];
|
|
556
585
|
} | null = null;
|
|
557
586
|
|
|
587
|
+
/**
|
|
588
|
+
* User-managed OpenRouter models (Settings → Models) as declared Claude
|
|
589
|
+
* profiles — same shape a BRAIN_UI_CLAUDE_PROFILES OpenRouter entry has,
|
|
590
|
+
* but editable at runtime without an env change or redeploy.
|
|
591
|
+
*/
|
|
592
|
+
function customOpenRouterInputs(): ClaudeProfileInput[] {
|
|
593
|
+
const models = options.getCustomOpenRouterModels?.() ?? [];
|
|
594
|
+
return models.map((model) => ({
|
|
595
|
+
id: `openrouter:${model}`,
|
|
596
|
+
label: `${model} (OpenRouter)`,
|
|
597
|
+
vendor: "openrouter",
|
|
598
|
+
model,
|
|
599
|
+
baseUrl: "https://openrouter.ai/api",
|
|
600
|
+
authTokenEnv: "OPENROUTER_API_KEY",
|
|
601
|
+
modelAliases: true,
|
|
602
|
+
source: "declared" as const,
|
|
603
|
+
}));
|
|
604
|
+
}
|
|
605
|
+
|
|
558
606
|
function mergeDiscovered(
|
|
559
607
|
claude: ClaudeBackendModule,
|
|
560
608
|
declared: ClaudeProfile[],
|
|
561
609
|
discovered: ClaudeProfileInput[]
|
|
562
610
|
): ClaudeProfile[] {
|
|
611
|
+
const custom = customOpenRouterInputs();
|
|
612
|
+
const customKey = custom.map((profile) => profile.id).join("\n");
|
|
563
613
|
if (
|
|
564
614
|
mergeCache &&
|
|
565
615
|
mergeCache.declared === declared &&
|
|
566
|
-
mergeCache.discovered === discovered
|
|
616
|
+
mergeCache.discovered === discovered &&
|
|
617
|
+
mergeCache.customKey === customKey
|
|
567
618
|
) {
|
|
568
619
|
return mergeCache.result;
|
|
569
620
|
}
|
|
570
621
|
|
|
571
622
|
const declaredIds = new Set(declared.map((profile) => profile.id));
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
623
|
+
const customExtra = custom.filter((input) => !declaredIds.has(input.id));
|
|
624
|
+
// OpenRouter runs on its own key: always api-billed, like an env-declared
|
|
625
|
+
// profile with explicit credentials.
|
|
626
|
+
for (const input of customExtra) declaredApiProfileIds.add(input.id);
|
|
627
|
+
const knownIds = new Set([...declaredIds, ...customExtra.map((input) => input.id)]);
|
|
628
|
+
const extra = discovered.filter((input) => !knownIds.has(input.id));
|
|
629
|
+
const result = [...declared, ...claude.defineProfiles([...customExtra, ...extra])];
|
|
630
|
+
mergeCache = { declared, discovered, customKey, result };
|
|
575
631
|
return result;
|
|
576
632
|
}
|
|
577
633
|
|
|
@@ -697,7 +753,7 @@ export function createBackendRegistry(
|
|
|
697
753
|
const mod = (await loadBackendModule("pi")) as {
|
|
698
754
|
createPiBackend?: (opts: {
|
|
699
755
|
brainPath: string;
|
|
700
|
-
profiles?: PiProfileInput[];
|
|
756
|
+
profiles?: PiProfileInput[] | (() => PiProfileInput[]);
|
|
701
757
|
log?: BackendLogFn;
|
|
702
758
|
}) => AgentBackend;
|
|
703
759
|
};
|
|
@@ -709,9 +765,20 @@ export function createBackendRegistry(
|
|
|
709
765
|
// Re-parsed here (assertBackendResolvable already validated at boot) so an
|
|
710
766
|
// injected-registry path without the boot assert still fails loudly.
|
|
711
767
|
const profiles = parsePiProfiles(agent.piProfilesJson, agent.profilesJson);
|
|
768
|
+
// A FUNCTION, so per-profile thinking overrides from settings are read on
|
|
769
|
+
// every use (roster listing AND new-session model resolution) — a change
|
|
770
|
+
// in Settings applies to the next turn without a rebuild.
|
|
771
|
+
const withOverrides = (): PiProfileInput[] => {
|
|
772
|
+
const overrides = options.getThinkingOverrides?.() ?? {};
|
|
773
|
+
return profiles.map((profile) =>
|
|
774
|
+
overrides[profile.id]
|
|
775
|
+
? { ...profile, thinkingLevel: overrides[profile.id] }
|
|
776
|
+
: profile
|
|
777
|
+
);
|
|
778
|
+
};
|
|
712
779
|
return mod.createPiBackend({
|
|
713
780
|
brainPath,
|
|
714
|
-
...(profiles.length > 0 ? { profiles } : {}),
|
|
781
|
+
...(profiles.length > 0 ? { profiles: withOverrides } : {}),
|
|
715
782
|
...(backendLog ? { log: backendLog } : {}),
|
|
716
783
|
});
|
|
717
784
|
}
|
|
@@ -749,6 +816,33 @@ export function createBackendRegistry(
|
|
|
749
816
|
return cachedRegistry;
|
|
750
817
|
}
|
|
751
818
|
|
|
819
|
+
/**
|
|
820
|
+
* Whether the openai-codex (ChatGPT subscription) account is connected —
|
|
821
|
+
* a cheap file probe through the pi package, memoized briefly so provider
|
|
822
|
+
* listings and turn routing don't re-read the auth store on every call.
|
|
823
|
+
*/
|
|
824
|
+
let codexCredentialCache: { at: number; value: boolean } | null = null;
|
|
825
|
+
async function hasCodexCredential(): Promise<boolean> {
|
|
826
|
+
const piInPlay = Boolean(agent.piProfilesJson) || (agent.backend || "claude") === "pi";
|
|
827
|
+
if (!piInPlay) return false;
|
|
828
|
+
if (codexCredentialCache && Date.now() - codexCredentialCache.at < PROFILE_MEMO_MS) {
|
|
829
|
+
return codexCredentialCache.value;
|
|
830
|
+
}
|
|
831
|
+
let value = false;
|
|
832
|
+
try {
|
|
833
|
+
const mod = (await loadBackendModule("pi")) as {
|
|
834
|
+
hasStoredCredential?: (providerId: string) => boolean;
|
|
835
|
+
};
|
|
836
|
+
value =
|
|
837
|
+
typeof mod.hasStoredCredential === "function" &&
|
|
838
|
+
mod.hasStoredCredential("openai-codex");
|
|
839
|
+
} catch {
|
|
840
|
+
value = false;
|
|
841
|
+
}
|
|
842
|
+
codexCredentialCache = { at: Date.now(), value };
|
|
843
|
+
return value;
|
|
844
|
+
}
|
|
845
|
+
|
|
752
846
|
return makeRegistry(
|
|
753
847
|
getRegistry,
|
|
754
848
|
getHidden,
|
|
@@ -762,6 +856,10 @@ export function createBackendRegistry(
|
|
|
762
856
|
...(options.getBillingOverrides
|
|
763
857
|
? { getOverrides: options.getBillingOverrides }
|
|
764
858
|
: {}),
|
|
859
|
+
},
|
|
860
|
+
{
|
|
861
|
+
...(options.getDefaultModelId ? { getOverride: options.getDefaultModelId } : {}),
|
|
862
|
+
auto: { vendor: "openai-codex", hasCredential: hasCodexCredential },
|
|
765
863
|
}
|
|
766
864
|
);
|
|
767
865
|
}
|
|
@@ -777,6 +875,7 @@ export function createStaticBackendRegistry(
|
|
|
777
875
|
defaultBackendId = backends[0]?.id ?? "",
|
|
778
876
|
options: {
|
|
779
877
|
getHiddenModelIds?: () => string[];
|
|
878
|
+
getDefaultModelId?: () => string | null;
|
|
780
879
|
getBillingOverrides?: () => Record<string, BillingMode>;
|
|
781
880
|
log?: Logger;
|
|
782
881
|
} = {}
|
|
@@ -791,7 +890,9 @@ export function createStaticBackendRegistry(
|
|
|
791
890
|
// registry could reason about, so only an explicit override sets a mode.
|
|
792
891
|
options.getBillingOverrides
|
|
793
892
|
? { getOverrides: options.getBillingOverrides }
|
|
794
|
-
: {}
|
|
893
|
+
: {},
|
|
894
|
+
// No auto rule either — only the stored default applies here.
|
|
895
|
+
options.getDefaultModelId ? { getOverride: options.getDefaultModelId } : {}
|
|
795
896
|
);
|
|
796
897
|
}
|
|
797
898
|
|
|
@@ -806,6 +907,12 @@ function makeRegistry(
|
|
|
806
907
|
classify?: (profile: ProviderInfo & { backendId: string }) => BillingMode;
|
|
807
908
|
/** Settings overrides, consulted LAST — an override wins over `classify`. */
|
|
808
909
|
getOverrides?: () => Record<string, BillingMode>;
|
|
910
|
+
} = {},
|
|
911
|
+
defaults: {
|
|
912
|
+
/** Settings-stored default profile id; null/absent = auto. */
|
|
913
|
+
getOverride?: () => string | null;
|
|
914
|
+
/** Auto rule: prefer this vendor's profile while its account is connected. */
|
|
915
|
+
auto?: { vendor: string; hasCredential: () => Promise<boolean> };
|
|
809
916
|
} = {}
|
|
810
917
|
): BackendRegistry {
|
|
811
918
|
let profileSnapshot: ProfileSnapshot | null = null;
|
|
@@ -866,6 +973,36 @@ function makeRegistry(
|
|
|
866
973
|
}
|
|
867
974
|
}
|
|
868
975
|
|
|
976
|
+
/**
|
|
977
|
+
* The preferred-default profile. The stored Settings override wins while it
|
|
978
|
+
* still names a roster profile (a vanished profile falls through to auto
|
|
979
|
+
* rather than pinning turns to nothing); auto is the first non-hidden
|
|
980
|
+
* roster entry of the auto vendor, but only while its credential exists.
|
|
981
|
+
* Any failure degrades to "no preference" — this must never take routing
|
|
982
|
+
* down.
|
|
983
|
+
*/
|
|
984
|
+
async function getPreferredProfileId(): Promise<string | null> {
|
|
985
|
+
try {
|
|
986
|
+
const snapshot = await getProfileSnapshot();
|
|
987
|
+
const override = defaults.getOverride?.() ?? null;
|
|
988
|
+
if (override && snapshot.owners.has(override)) return override;
|
|
989
|
+
|
|
990
|
+
const auto = defaults.auto;
|
|
991
|
+
if (!auto) return null;
|
|
992
|
+
const registry = await getRegistry();
|
|
993
|
+
const hidden = hiddenIds();
|
|
994
|
+
for (const backend of registry.backends) {
|
|
995
|
+
const match = (snapshot.byBackend.get(backend.id) ?? []).find(
|
|
996
|
+
(profile) => profile.vendor === auto.vendor && !hidden.has(profile.id)
|
|
997
|
+
);
|
|
998
|
+
if (match) return (await auto.hasCredential()) ? match.id : null;
|
|
999
|
+
}
|
|
1000
|
+
return null;
|
|
1001
|
+
} catch {
|
|
1002
|
+
return null;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
869
1006
|
return {
|
|
870
1007
|
async getBackends() {
|
|
871
1008
|
return (await getRegistry()).backends;
|
|
@@ -910,7 +1047,7 @@ function makeRegistry(
|
|
|
910
1047
|
// saved override is live for the very next run without invalidation.
|
|
911
1048
|
const overrides = billingOverrides();
|
|
912
1049
|
|
|
913
|
-
|
|
1050
|
+
const providers = registry.backends.flatMap((backend) =>
|
|
914
1051
|
(snapshot.byBackend.get(backend.id) ?? [])
|
|
915
1052
|
.filter((profile) => !hidden.has(profile.id))
|
|
916
1053
|
.map((profile) => {
|
|
@@ -919,8 +1056,19 @@ function makeRegistry(
|
|
|
919
1056
|
return billingMode ? { ...entry, billingMode } : entry;
|
|
920
1057
|
})
|
|
921
1058
|
);
|
|
1059
|
+
|
|
1060
|
+
// A connected subscription-auth profile leads the list: a fresh client
|
|
1061
|
+
// with no stored selection defaults to providers[0].
|
|
1062
|
+
const preferredId = await getPreferredProfileId();
|
|
1063
|
+
if (preferredId) {
|
|
1064
|
+
const index = providers.findIndex((provider) => provider.id === preferredId);
|
|
1065
|
+
if (index > 0) providers.unshift(...providers.splice(index, 1));
|
|
1066
|
+
}
|
|
1067
|
+
return providers;
|
|
922
1068
|
},
|
|
923
1069
|
|
|
1070
|
+
getPreferredProfileId,
|
|
1071
|
+
|
|
924
1072
|
async getBackendsInfo() {
|
|
925
1073
|
const backends = (await getRegistry()).backends;
|
|
926
1074
|
return Object.fromEntries(
|
package/src/app.ts
CHANGED
|
@@ -34,7 +34,13 @@ import {
|
|
|
34
34
|
type PasskeyContext,
|
|
35
35
|
} from "./middleware/passkeys.js";
|
|
36
36
|
import { createUiDb } from "./db/client.js";
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
getBillingOverrides,
|
|
39
|
+
getCustomOpenRouterModels,
|
|
40
|
+
getDefaultModelId,
|
|
41
|
+
getHiddenModelIds,
|
|
42
|
+
getThinkingOverrides,
|
|
43
|
+
} from "./db/settings.js";
|
|
38
44
|
import { createActivityRuntime } from "./activity/runtime.js";
|
|
39
45
|
import { createModelPricing } from "./pricing/model-pricing.js";
|
|
40
46
|
import { createPushRoutes } from "./routes/push.js";
|
|
@@ -200,6 +206,9 @@ export function createApp(options: CreateAppOptions = {}): BrainUiApp {
|
|
|
200
206
|
brainPath: config.brainPath,
|
|
201
207
|
agent: config.agent,
|
|
202
208
|
getHiddenModelIds: () => getHiddenModelIds(db, dbLog),
|
|
209
|
+
getDefaultModelId: () => getDefaultModelId(db, dbLog),
|
|
210
|
+
getCustomOpenRouterModels: () => getCustomOpenRouterModels(db, dbLog),
|
|
211
|
+
getThinkingOverrides: () => getThinkingOverrides(db, dbLog),
|
|
203
212
|
getBillingOverrides: () => getBillingOverrides(db, dbLog),
|
|
204
213
|
log: observability.logger("agent"),
|
|
205
214
|
});
|
package/src/db/settings.ts
CHANGED
|
@@ -8,10 +8,18 @@
|
|
|
8
8
|
|
|
9
9
|
import type { Logger } from "@opentelemetry/api-logs";
|
|
10
10
|
import type { Database } from "bun:sqlite";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
isBillingMode,
|
|
13
|
+
isThinkingLevel,
|
|
14
|
+
type BillingMode,
|
|
15
|
+
type ThinkingLevel,
|
|
16
|
+
} from "@schlessera/brain-ui-sdk/protocol";
|
|
12
17
|
|
|
13
18
|
const HIDDEN_MODELS_KEY = "models.hidden";
|
|
14
19
|
const BILLING_OVERRIDES_KEY = "models.billing";
|
|
20
|
+
const DEFAULT_MODEL_KEY = "models.default";
|
|
21
|
+
const CUSTOM_OPENROUTER_KEY = "models.customOpenRouter";
|
|
22
|
+
const THINKING_OVERRIDES_KEY = "models.thinking";
|
|
15
23
|
const DETAIL_RETENTION_KEY = "activity.retention.detailDays";
|
|
16
24
|
const DETAIL_RETENTION_DEFAULT_DAYS = 7;
|
|
17
25
|
|
|
@@ -89,6 +97,67 @@ export function setBillingOverrides(
|
|
|
89
97
|
setSetting(db, BILLING_OVERRIDES_KEY, clean);
|
|
90
98
|
}
|
|
91
99
|
|
|
100
|
+
/**
|
|
101
|
+
* The user's chosen default model profile, used when a turn names none (new
|
|
102
|
+
* conversations from a fresh client, shares, host-initiated actions). Null =
|
|
103
|
+
* auto (a connected subscription-auth profile, else the default backend's
|
|
104
|
+
* own default).
|
|
105
|
+
*/
|
|
106
|
+
export function getDefaultModelId(db: Database, log?: Logger): string | null {
|
|
107
|
+
const value = getSetting<unknown>(db, DEFAULT_MODEL_KEY, null, log);
|
|
108
|
+
return typeof value === "string" && value ? value : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function setDefaultModelId(db: Database, id: string | null): void {
|
|
112
|
+
setSetting(db, DEFAULT_MODEL_KEY, id);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** User-managed OpenRouter model ids (e.g. "z.ai/glm-5.3-flash"). */
|
|
116
|
+
export function getCustomOpenRouterModels(db: Database, log?: Logger): string[] {
|
|
117
|
+
const value = getSetting<unknown>(db, CUSTOM_OPENROUTER_KEY, [], log);
|
|
118
|
+
if (!Array.isArray(value)) return [];
|
|
119
|
+
return value.filter((id): id is string => typeof id === "string" && id.length > 0);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Replace the custom OpenRouter list (full list, not a delta). */
|
|
123
|
+
export function setCustomOpenRouterModels(db: Database, models: string[]): void {
|
|
124
|
+
const unique = [...new Set(models.filter((id) => typeof id === "string" && id))];
|
|
125
|
+
setSetting(db, CUSTOM_OPENROUTER_KEY, unique);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Per-profile reasoning-effort overrides: profile id → forced level. A
|
|
130
|
+
* profile absent from the record keeps its configured default. Invalid
|
|
131
|
+
* levels are dropped on read (same degradation discipline as billing).
|
|
132
|
+
*/
|
|
133
|
+
export function getThinkingOverrides(
|
|
134
|
+
db: Database,
|
|
135
|
+
log?: Logger
|
|
136
|
+
): Record<string, ThinkingLevel> {
|
|
137
|
+
const value = getSetting<unknown>(db, THINKING_OVERRIDES_KEY, {}, log);
|
|
138
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
139
|
+
return Object.create(null);
|
|
140
|
+
}
|
|
141
|
+
const overrides: Record<string, ThinkingLevel> = Object.create(null);
|
|
142
|
+
for (const [profileId, level] of Object.entries(value)) {
|
|
143
|
+
if (isThinkingLevel(level)) overrides[profileId] = level;
|
|
144
|
+
}
|
|
145
|
+
return overrides;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Replace the override record (the client always sends the full record, not a delta). */
|
|
149
|
+
export function setThinkingOverrides(
|
|
150
|
+
db: Database,
|
|
151
|
+
overrides: Record<string, ThinkingLevel>
|
|
152
|
+
): void {
|
|
153
|
+
const clean = Object.fromEntries(
|
|
154
|
+
Object.entries(overrides).filter(
|
|
155
|
+
([profileId, level]) => profileId && isThinkingLevel(level)
|
|
156
|
+
)
|
|
157
|
+
);
|
|
158
|
+
setSetting(db, THINKING_OVERRIDES_KEY, clean);
|
|
159
|
+
}
|
|
160
|
+
|
|
92
161
|
/** Minimum days a finished run keeps its detail (spans/events) before the
|
|
93
162
|
* digest-covered prune may take it. 0 is valid (prune as soon as covered);
|
|
94
163
|
* anything non-numeric or negative degrades to the default. */
|