@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.
@@ -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":"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"}
1
+ {"version":3,"file":"models.js","sourceRoot":"","sources":["../../src/routes/models.ts"],"names":[],"mappings":"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"}
@@ -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,CAyB7E"}
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"}
@@ -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 } : {}) };
@@ -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.23.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.23.0",
55
- "@schlessera/brain-ui-sdk": "0.23.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",
@@ -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 extra = discovered.filter((input) => !declaredIds.has(input.id));
573
- const result = [...declared, ...claude.defineProfiles(extra)];
574
- mergeCache = { declared, discovered, result };
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
- return registry.backends.flatMap((backend) =>
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 { getBillingOverrides, getHiddenModelIds } from "./db/settings.js";
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
  });
@@ -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 { isBillingMode, type BillingMode } from "@schlessera/brain-ui-sdk/protocol";
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. */