@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/src/routes/models.ts
CHANGED
|
@@ -10,17 +10,25 @@ import { Hono } from "hono";
|
|
|
10
10
|
import type { Database } from "bun:sqlite";
|
|
11
11
|
import {
|
|
12
12
|
isBillingMode,
|
|
13
|
+
isThinkingLevel,
|
|
13
14
|
type BillingMode,
|
|
14
15
|
type ModelCatalogEntry,
|
|
15
16
|
type ModelCatalogResponse,
|
|
17
|
+
type ThinkingLevel,
|
|
16
18
|
} from "@schlessera/brain-ui-sdk";
|
|
17
19
|
import type { BackendRegistry } from "../agent/backend.js";
|
|
18
20
|
import type { ModelPricingState } from "../pricing/model-pricing.js";
|
|
19
21
|
import {
|
|
20
22
|
getBillingOverrides,
|
|
23
|
+
getCustomOpenRouterModels,
|
|
24
|
+
getDefaultModelId,
|
|
21
25
|
getHiddenModelIds,
|
|
26
|
+
getThinkingOverrides,
|
|
22
27
|
setBillingOverrides,
|
|
28
|
+
setCustomOpenRouterModels,
|
|
29
|
+
setDefaultModelId,
|
|
23
30
|
setHiddenModelIds,
|
|
31
|
+
setThinkingOverrides,
|
|
24
32
|
} from "../db/settings.js";
|
|
25
33
|
|
|
26
34
|
export function createModelRoutes(deps: {
|
|
@@ -37,6 +45,7 @@ export function createModelRoutes(deps: {
|
|
|
37
45
|
const source = await registry.getModelSource();
|
|
38
46
|
const hidden = new Set(getHiddenModelIds(db));
|
|
39
47
|
const overrides = getBillingOverrides(db);
|
|
48
|
+
const thinking = getThinkingOverrides(db);
|
|
40
49
|
// `billingMode` already rides each provider entry (the registry applies the
|
|
41
50
|
// override last); the catalog additionally tags WHICH rows carry an explicit
|
|
42
51
|
// override, so the settings screen can render auto vs forced.
|
|
@@ -46,11 +55,19 @@ export function createModelRoutes(deps: {
|
|
|
46
55
|
...profile,
|
|
47
56
|
hidden: hidden.has(profile.id),
|
|
48
57
|
...(overrides[profile.id] ? { billingOverride: overrides[profile.id] } : {}),
|
|
58
|
+
// `thinkingLevel` on the profile is already the EFFECTIVE level (the
|
|
59
|
+
// registry applies overrides at read time); this tags which rows carry
|
|
60
|
+
// an explicit user choice, so the UI can render default vs forced.
|
|
61
|
+
...(thinking[profile.id] ? { thinkingOverride: thinking[profile.id] } : {}),
|
|
49
62
|
}));
|
|
50
63
|
|
|
51
64
|
const state = source?.state();
|
|
65
|
+
const resolvedDefaultId = await registry.getPreferredProfileId();
|
|
52
66
|
return {
|
|
53
67
|
models,
|
|
68
|
+
defaultModelId: getDefaultModelId(db),
|
|
69
|
+
...(resolvedDefaultId ? { resolvedDefaultId } : {}),
|
|
70
|
+
customModels: getCustomOpenRouterModels(db),
|
|
54
71
|
refreshedAt: state?.refreshedAt ?? null,
|
|
55
72
|
stale: state?.stale ?? false,
|
|
56
73
|
discovery: {
|
|
@@ -96,6 +113,111 @@ export function createModelRoutes(deps: {
|
|
|
96
113
|
return c.json(await buildCatalog());
|
|
97
114
|
})
|
|
98
115
|
|
|
116
|
+
.put("/models/default", async (c) => {
|
|
117
|
+
const body = (await c.req.json().catch(() => null)) as unknown;
|
|
118
|
+
const defaultId = (body as { defaultId?: unknown } | null)?.defaultId;
|
|
119
|
+
if (defaultId !== null && typeof defaultId !== "string") {
|
|
120
|
+
return c.json({ error: "defaultId must be a profile id or null" }, 400);
|
|
121
|
+
}
|
|
122
|
+
if (typeof defaultId === "string") {
|
|
123
|
+
const known = await registry.listAllProviders({ includeHidden: true });
|
|
124
|
+
if (!known.some((profile) => profile.id === defaultId)) {
|
|
125
|
+
return c.json({ error: `Unknown profile id: ${defaultId}` }, 400);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
setDefaultModelId(db, defaultId);
|
|
130
|
+
registry.invalidateProfiles();
|
|
131
|
+
return c.json(await buildCatalog());
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
.put("/models/custom", async (c) => {
|
|
135
|
+
const body = (await c.req.json().catch(() => null)) as unknown;
|
|
136
|
+
const models = (body as { models?: unknown } | null)?.models;
|
|
137
|
+
if (!Array.isArray(models) || models.some((id) => typeof id !== "string")) {
|
|
138
|
+
return c.json({ error: "models must be an array of OpenRouter model ids" }, 400);
|
|
139
|
+
}
|
|
140
|
+
// OpenRouter ids are "<org>/<model>", org and model from a small safe
|
|
141
|
+
// charset (e.g. "z.ai/glm-5.3-flash", "openai/gpt-oss-120b:nitro").
|
|
142
|
+
const ID_SHAPE = /^[A-Za-z0-9][\w.-]*\/[\w.:-]+$/;
|
|
143
|
+
const bad = (models as string[]).find((id) => !ID_SHAPE.test(id));
|
|
144
|
+
if (bad !== undefined) {
|
|
145
|
+
return c.json({ error: `Not an OpenRouter model id: "${bad}"` }, 400);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// A generated id ("openrouter:<model>") that collides with a profile
|
|
149
|
+
// another source owns (a pi profile, a discovered model) must be refused
|
|
150
|
+
// BEFORE persisting: stored, it would 500 every roster read until the
|
|
151
|
+
// setting is dug out of the database. An id the CLAUDE env already
|
|
152
|
+
// declares is fine — the merge dedupes it in the declared entry's favor.
|
|
153
|
+
const currentCustomIds = new Set(
|
|
154
|
+
getCustomOpenRouterModels(db).map((model) => `openrouter:${model}`)
|
|
155
|
+
);
|
|
156
|
+
const nonCustomIds = new Set(
|
|
157
|
+
(await registry.listAllProviders({ includeHidden: true }))
|
|
158
|
+
.filter((profile) => !currentCustomIds.has(profile.id))
|
|
159
|
+
.map((profile) => profile.id)
|
|
160
|
+
);
|
|
161
|
+
const collision = (models as string[])
|
|
162
|
+
.map((model) => `openrouter:${model}`)
|
|
163
|
+
.find((id) => nonCustomIds.has(id));
|
|
164
|
+
if (collision !== undefined) {
|
|
165
|
+
return c.json(
|
|
166
|
+
{ error: `"${collision}" collides with an existing profile id.` },
|
|
167
|
+
400
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
setCustomOpenRouterModels(db, models as string[]);
|
|
172
|
+
registry.invalidateProfiles();
|
|
173
|
+
|
|
174
|
+
// A removal that takes the stored default's profile off the roster would
|
|
175
|
+
// strand the default on nothing; reset it to auto — but only when the
|
|
176
|
+
// profile is actually gone (an id the env also declares survives the
|
|
177
|
+
// removal of its custom duplicate).
|
|
178
|
+
const currentDefault = getDefaultModelId(db);
|
|
179
|
+
if (currentDefault) {
|
|
180
|
+
const roster = await registry.listAllProviders({ includeHidden: true });
|
|
181
|
+
if (!roster.some((profile) => profile.id === currentDefault)) {
|
|
182
|
+
setDefaultModelId(db, null);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return c.json(await buildCatalog());
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
.put("/models/thinking", async (c) => {
|
|
189
|
+
const body = (await c.req.json().catch(() => null)) as unknown;
|
|
190
|
+
const thinking = (body as { thinking?: unknown } | null)?.thinking;
|
|
191
|
+
if (
|
|
192
|
+
typeof thinking !== "object" ||
|
|
193
|
+
thinking === null ||
|
|
194
|
+
Array.isArray(thinking) ||
|
|
195
|
+
Object.values(thinking).some((level) => !isThinkingLevel(level))
|
|
196
|
+
) {
|
|
197
|
+
return c.json(
|
|
198
|
+
{ error: "thinking must map profile ids to a reasoning-effort level" },
|
|
199
|
+
400
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
// Only rows that actually take an effort level accept an override — a
|
|
203
|
+
// stray id would be stored dead weight and mislead the settings UI.
|
|
204
|
+
const known = await registry.listAllProviders({ includeHidden: true });
|
|
205
|
+
const supported = new Set(
|
|
206
|
+
known.filter((profile) => profile.thinkingLevel).map((profile) => profile.id)
|
|
207
|
+
);
|
|
208
|
+
const stray = Object.keys(thinking).find((id) => !supported.has(id));
|
|
209
|
+
if (stray !== undefined) {
|
|
210
|
+
return c.json(
|
|
211
|
+
{ error: `Profile "${stray}" does not take a reasoning-effort level.` },
|
|
212
|
+
400
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
setThinkingOverrides(db, thinking as Record<string, ThinkingLevel>);
|
|
217
|
+
registry.invalidateProfiles();
|
|
218
|
+
return c.json(await buildCatalog());
|
|
219
|
+
})
|
|
220
|
+
|
|
99
221
|
.put("/models/billing", async (c) => {
|
|
100
222
|
const body = (await c.req.json().catch(() => null)) as unknown;
|
|
101
223
|
const billing = (body as { billing?: unknown } | null)?.billing;
|
package/src/ws/routing.ts
CHANGED
|
@@ -33,6 +33,15 @@ export async function resolveTurnTarget(
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// No explicit choice (fresh client, share, host-initiated action): the
|
|
37
|
+
// preferred default — the Settings-stored default model, else a connected
|
|
38
|
+
// subscription-auth profile — before the default backend's own first profile.
|
|
39
|
+
const preferred = await registry.getPreferredProfileId();
|
|
40
|
+
if (preferred) {
|
|
41
|
+
const backend = await registry.getBackendForProfile(preferred);
|
|
42
|
+
if (backend) return { backend, profileId: preferred };
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
const backend = await registry.getDefaultBackend();
|
|
37
46
|
const profileId = (await backend.listProfiles())[0]?.id;
|
|
38
47
|
return { backend, ...(profileId ? { profileId } : {}) };
|