@schlessera/brain-ui-server 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/agent/backend.d.ts +6 -0
- package/dist/agent/backend.d.ts.map +1 -1
- package/dist/agent/backend.js +15 -1
- 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/config/env.d.ts +7 -0
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +28 -1
- package/dist/config/env.js.map +1 -1
- package/dist/db/settings.d.ts +9 -1
- package/dist/db/settings.d.ts.map +1 -1
- package/dist/db/settings.js +24 -1
- package/dist/db/settings.js.map +1 -1
- package/dist/routes/models.d.ts.map +1 -1
- package/dist/routes/models.js +28 -2
- package/dist/routes/models.js.map +1 -1
- package/dist/routes/web-search.d.ts +47 -0
- package/dist/routes/web-search.d.ts.map +1 -0
- package/dist/routes/web-search.js +167 -0
- package/dist/routes/web-search.js.map +1 -0
- package/package.json +3 -3
- package/src/agent/backend.ts +25 -2
- package/src/app.ts +4 -0
- package/src/config/env.ts +29 -1
- package/src/db/settings.ts +40 -1
- package/src/routes/models.ts +42 -0
- package/src/routes/web-search.ts +220 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web-search configuration for the pi backend's `pi-web-access` extension —
|
|
3
|
+
* the Settings UI's way to pick a search provider and store its API key
|
|
4
|
+
* without a shell on the host.
|
|
5
|
+
*
|
|
6
|
+
* The extension reads `web-search.json` from the pi config dir; these routes
|
|
7
|
+
* read-modify-write that same file, touching ONLY the managed fields (the
|
|
8
|
+
* `provider` selector and the per-provider `<name>ApiKey` entries) so any
|
|
9
|
+
* hand-edited config beside them survives.
|
|
10
|
+
*
|
|
11
|
+
* Defaults are deliberate: no file (or `provider: "auto"`) means the
|
|
12
|
+
* extension's automatic chain, which starts with Exa's keyless MCP endpoint —
|
|
13
|
+
* free, throttled. Adding an `exaApiKey` lifts the throttle; picking another
|
|
14
|
+
* provider routes searches there instead.
|
|
15
|
+
*
|
|
16
|
+
* Key VALUES never leave the server: GET reports only which providers have a
|
|
17
|
+
* key configured. Mount BEHIND the /api auth guard.
|
|
18
|
+
*/
|
|
19
|
+
import { Hono } from "hono";
|
|
20
|
+
import type { AgentConfig } from "../config/env.js";
|
|
21
|
+
/** One selectable provider, as the Settings UI renders it. */
|
|
22
|
+
export interface WebSearchProviderView {
|
|
23
|
+
id: string;
|
|
24
|
+
label: string;
|
|
25
|
+
/** Whether this provider accepts an API key ("auto" does not). */
|
|
26
|
+
hasKeyField: boolean;
|
|
27
|
+
/** True when a key for it is present in the config file. */
|
|
28
|
+
keyConfigured: boolean;
|
|
29
|
+
/** True when the provider works without any key (Exa's free MCP tier). */
|
|
30
|
+
keyless: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface WebSearchConfigView {
|
|
33
|
+
/** False when the pi backend is not configured — the client hides the card. */
|
|
34
|
+
configured: boolean;
|
|
35
|
+
/** The active provider selection ("auto" when unset). */
|
|
36
|
+
provider: string;
|
|
37
|
+
providers: WebSearchProviderView[];
|
|
38
|
+
}
|
|
39
|
+
export interface WebSearchRoutesDeps {
|
|
40
|
+
agent: AgentConfig;
|
|
41
|
+
/** Test seam — overrides the config file location. */
|
|
42
|
+
configPath?: string;
|
|
43
|
+
/** Test seam — forwarded to loadBackendModule. */
|
|
44
|
+
importer?: (specifier: string) => Promise<unknown>;
|
|
45
|
+
}
|
|
46
|
+
export declare function createWebSearchRoutes(deps: WebSearchRoutesDeps): Hono;
|
|
47
|
+
//# sourceMappingURL=web-search.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-search.d.ts","sourceRoot":"","sources":["../../src/routes/web-search.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAIpD,8DAA8D;AAC9D,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,WAAW,EAAE,OAAO,CAAC;IACrB,4DAA4D;IAC5D,aAAa,EAAE,OAAO,CAAC;IACvB,0EAA0E;IAC1E,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,+EAA+E;IAC/E,UAAU,EAAE,OAAO,CAAC;IACpB,yDAAyD;IACzD,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,qBAAqB,EAAE,CAAC;CACpC;AA4BD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,WAAW,CAAC;IACnB,sDAAsD;IACtD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACpD;AA0BD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI,CAmHrE"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web-search configuration for the pi backend's `pi-web-access` extension —
|
|
3
|
+
* the Settings UI's way to pick a search provider and store its API key
|
|
4
|
+
* without a shell on the host.
|
|
5
|
+
*
|
|
6
|
+
* The extension reads `web-search.json` from the pi config dir; these routes
|
|
7
|
+
* read-modify-write that same file, touching ONLY the managed fields (the
|
|
8
|
+
* `provider` selector and the per-provider `<name>ApiKey` entries) so any
|
|
9
|
+
* hand-edited config beside them survives.
|
|
10
|
+
*
|
|
11
|
+
* Defaults are deliberate: no file (or `provider: "auto"`) means the
|
|
12
|
+
* extension's automatic chain, which starts with Exa's keyless MCP endpoint —
|
|
13
|
+
* free, throttled. Adding an `exaApiKey` lifts the throttle; picking another
|
|
14
|
+
* provider routes searches there instead.
|
|
15
|
+
*
|
|
16
|
+
* Key VALUES never leave the server: GET reports only which providers have a
|
|
17
|
+
* key configured. Mount BEHIND the /api auth guard.
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
20
|
+
import { dirname, join } from "path";
|
|
21
|
+
import { Hono } from "hono";
|
|
22
|
+
import { resolvePiConfigDir } from "../config/env.js";
|
|
23
|
+
import { loadBackendModule } from "../agent/backend.js";
|
|
24
|
+
/**
|
|
25
|
+
* The providers this surface manages. A curated subset of pi-web-access's
|
|
26
|
+
* catalog: the ones configurable by a single `<id>ApiKey` field. The
|
|
27
|
+
* extension itself accepts more (SearXNG endpoints, Ollama, …) — those stay
|
|
28
|
+
* hand-edited config, deliberately outside this UI.
|
|
29
|
+
*/
|
|
30
|
+
const MANAGED_PROVIDERS = [
|
|
31
|
+
{ id: "auto", label: "Auto — Exa (free) with fallbacks", keyField: null, keyless: true },
|
|
32
|
+
{ id: "exa", label: "Exa", keyField: "exaApiKey", keyless: true },
|
|
33
|
+
{ id: "openai", label: "OpenAI", keyField: "openaiApiKey", keyless: false },
|
|
34
|
+
{ id: "brave", label: "Brave", keyField: "braveApiKey", keyless: false },
|
|
35
|
+
{ id: "tavily", label: "Tavily", keyField: "tavilyApiKey", keyless: false },
|
|
36
|
+
{ id: "perplexity", label: "Perplexity", keyField: "perplexityApiKey", keyless: false },
|
|
37
|
+
{ id: "firecrawl", label: "Firecrawl", keyField: "firecrawlApiKey", keyless: false },
|
|
38
|
+
{ id: "jina", label: "Jina", keyField: "jinaApiKey", keyless: false },
|
|
39
|
+
{ id: "kagi", label: "Kagi", keyField: "kagiApiKey", keyless: false },
|
|
40
|
+
{ id: "gemini", label: "Gemini", keyField: "geminiApiKey", keyless: false },
|
|
41
|
+
];
|
|
42
|
+
const MANAGED_IDS = new Set(MANAGED_PROVIDERS.map((p) => p.id));
|
|
43
|
+
function readConfig(path) {
|
|
44
|
+
if (!existsSync(path))
|
|
45
|
+
return {};
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
48
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
49
|
+
? parsed
|
|
50
|
+
: {};
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Unreadable config: treat as empty for GET; PUT refuses instead of
|
|
54
|
+
// silently clobbering whatever the user had there.
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function writeConfig(path, config) {
|
|
59
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
60
|
+
// Atomic-enough: temp file + rename, so the extension never reads a torn write.
|
|
61
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
62
|
+
writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", { encoding: "utf-8", mode: 0o600 });
|
|
63
|
+
renameSync(tmp, path);
|
|
64
|
+
}
|
|
65
|
+
export function createWebSearchRoutes(deps) {
|
|
66
|
+
const { agent } = deps;
|
|
67
|
+
const configPath = () => deps.configPath ?? join(resolvePiConfigDir(), "web-search.json");
|
|
68
|
+
const piConfigured = () => Boolean(agent.piProfilesJson) || (agent.backend || "claude") === "pi";
|
|
69
|
+
function view() {
|
|
70
|
+
const config = readConfig(configPath());
|
|
71
|
+
const provider = typeof config.provider === "string" && MANAGED_IDS.has(config.provider)
|
|
72
|
+
? config.provider
|
|
73
|
+
: "auto";
|
|
74
|
+
return {
|
|
75
|
+
configured: true,
|
|
76
|
+
provider,
|
|
77
|
+
providers: MANAGED_PROVIDERS.map((p) => ({
|
|
78
|
+
id: p.id,
|
|
79
|
+
label: p.label,
|
|
80
|
+
hasKeyField: p.keyField !== null,
|
|
81
|
+
keyConfigured: p.keyField !== null &&
|
|
82
|
+
typeof config[p.keyField] === "string" &&
|
|
83
|
+
config[p.keyField].trim() !== "",
|
|
84
|
+
keyless: p.keyless,
|
|
85
|
+
})),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return new Hono()
|
|
89
|
+
.get("/web-search", (c) => {
|
|
90
|
+
// Not-configured is a normal state, not an error: the client hides the
|
|
91
|
+
// whole card (same convention as /pi-auth/providers).
|
|
92
|
+
if (!piConfigured()) {
|
|
93
|
+
return c.json({ configured: false, provider: "auto", providers: [] });
|
|
94
|
+
}
|
|
95
|
+
return c.json(view());
|
|
96
|
+
})
|
|
97
|
+
.put("/web-search", async (c) => {
|
|
98
|
+
if (!piConfigured()) {
|
|
99
|
+
return c.json({ error: "The pi backend is not configured." }, 409);
|
|
100
|
+
}
|
|
101
|
+
const body = (await c.req.json().catch(() => null));
|
|
102
|
+
if (!body || typeof body !== "object") {
|
|
103
|
+
return c.json({ error: "Invalid body." }, 400);
|
|
104
|
+
}
|
|
105
|
+
const path = configPath();
|
|
106
|
+
if (existsSync(path)) {
|
|
107
|
+
// A config that exists but does not parse must not be clobbered by a
|
|
108
|
+
// settings save — that would eat hand-written provider config.
|
|
109
|
+
try {
|
|
110
|
+
JSON.parse(readFileSync(path, "utf-8"));
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return c.json({ error: `${path} exists but is not valid JSON — fix or remove it first.` }, 409);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const config = readConfig(path);
|
|
117
|
+
if (body.provider !== undefined) {
|
|
118
|
+
if (typeof body.provider !== "string" || !MANAGED_IDS.has(body.provider)) {
|
|
119
|
+
return c.json({ error: "Unknown provider." }, 400);
|
|
120
|
+
}
|
|
121
|
+
// "auto" is the extension's default — store it as absence, so an
|
|
122
|
+
// untouched deployment and a reset-to-default one look identical.
|
|
123
|
+
if (body.provider === "auto")
|
|
124
|
+
delete config.provider;
|
|
125
|
+
else
|
|
126
|
+
config.provider = body.provider;
|
|
127
|
+
}
|
|
128
|
+
if (body.apiKeys !== undefined) {
|
|
129
|
+
if (!body.apiKeys || typeof body.apiKeys !== "object" || Array.isArray(body.apiKeys)) {
|
|
130
|
+
return c.json({ error: "apiKeys must be an object." }, 400);
|
|
131
|
+
}
|
|
132
|
+
for (const [id, value] of Object.entries(body.apiKeys)) {
|
|
133
|
+
const spec = MANAGED_PROVIDERS.find((p) => p.id === id);
|
|
134
|
+
if (!spec || spec.keyField === null) {
|
|
135
|
+
return c.json({ error: `Provider "${id}" does not take an API key here.` }, 400);
|
|
136
|
+
}
|
|
137
|
+
if (value === null || value === "") {
|
|
138
|
+
delete config[spec.keyField];
|
|
139
|
+
}
|
|
140
|
+
else if (typeof value === "string") {
|
|
141
|
+
const trimmed = value.trim();
|
|
142
|
+
if (!trimmed || trimmed.length > 512 || /[\r\n]/.test(trimmed)) {
|
|
143
|
+
return c.json({ error: `Invalid API key for "${id}".` }, 400);
|
|
144
|
+
}
|
|
145
|
+
config[spec.keyField] = trimmed;
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
return c.json({ error: `Invalid API key for "${id}".` }, 400);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
writeConfig(path, config);
|
|
153
|
+
// Extension modules cache their config per process; clearing pi's
|
|
154
|
+
// extension cache makes NEW sessions re-read web-search.json. Best
|
|
155
|
+
// effort: on failure the change still lands, it just needs a restart
|
|
156
|
+
// (or a naturally recycled session) to apply.
|
|
157
|
+
try {
|
|
158
|
+
const mod = (await loadBackendModule("pi", deps.importer));
|
|
159
|
+
await mod.invalidateExtensionCache?.();
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
/* config written; cache clearing is an optimization */
|
|
163
|
+
}
|
|
164
|
+
return c.json(view());
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=web-search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-search.js","sourceRoot":"","sources":["../../src/routes/web-search.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schlessera/brain-ui-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.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.26.0",
|
|
55
|
+
"@schlessera/brain-ui-sdk": "0.26.0",
|
|
56
56
|
"@simplewebauthn/server": "^13.3.2",
|
|
57
57
|
"hono": "^4",
|
|
58
58
|
"ignore": "^7.0.5",
|
package/src/agent/backend.ts
CHANGED
|
@@ -210,6 +210,12 @@ export interface BackendRegistryOptions {
|
|
|
210
210
|
* to the Claude roster as declared OpenRouter profiles at runtime.
|
|
211
211
|
*/
|
|
212
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>;
|
|
213
219
|
/**
|
|
214
220
|
* Per-profile billing-mode overrides (from the app's settings table).
|
|
215
221
|
* Consulted LAST: an override wins over both the declared-credential rule
|
|
@@ -747,8 +753,9 @@ export function createBackendRegistry(
|
|
|
747
753
|
const mod = (await loadBackendModule("pi")) as {
|
|
748
754
|
createPiBackend?: (opts: {
|
|
749
755
|
brainPath: string;
|
|
750
|
-
profiles?: PiProfileInput[];
|
|
756
|
+
profiles?: PiProfileInput[] | (() => PiProfileInput[]);
|
|
751
757
|
log?: BackendLogFn;
|
|
758
|
+
confirmBashPatterns?: readonly string[];
|
|
752
759
|
}) => AgentBackend;
|
|
753
760
|
};
|
|
754
761
|
if (typeof mod.createPiBackend !== "function") {
|
|
@@ -759,10 +766,26 @@ export function createBackendRegistry(
|
|
|
759
766
|
// Re-parsed here (assertBackendResolvable already validated at boot) so an
|
|
760
767
|
// injected-registry path without the boot assert still fails loudly.
|
|
761
768
|
const profiles = parsePiProfiles(agent.piProfilesJson, agent.profilesJson);
|
|
769
|
+
// A FUNCTION, so per-profile thinking overrides from settings are read on
|
|
770
|
+
// every use (roster listing AND new-session model resolution) — a change
|
|
771
|
+
// in Settings applies to the next turn without a rebuild.
|
|
772
|
+
const withOverrides = (): PiProfileInput[] => {
|
|
773
|
+
const overrides = options.getThinkingOverrides?.() ?? {};
|
|
774
|
+
return profiles.map((profile) =>
|
|
775
|
+
overrides[profile.id]
|
|
776
|
+
? { ...profile, thinkingLevel: overrides[profile.id] }
|
|
777
|
+
: profile
|
|
778
|
+
);
|
|
779
|
+
};
|
|
762
780
|
return mod.createPiBackend({
|
|
763
781
|
brainPath,
|
|
764
|
-
...(profiles.length > 0 ? { profiles } : {}),
|
|
782
|
+
...(profiles.length > 0 ? { profiles: withOverrides } : {}),
|
|
765
783
|
...(backendLog ? { log: backendLog } : {}),
|
|
784
|
+
// Same shared confirm-pattern config as the Claude backend, so both
|
|
785
|
+
// backends stop on the same destructive bash shapes.
|
|
786
|
+
...(agent.confirmBashPatterns !== null
|
|
787
|
+
? { confirmBashPatterns: agent.confirmBashPatterns }
|
|
788
|
+
: {}),
|
|
766
789
|
});
|
|
767
790
|
}
|
|
768
791
|
|
package/src/app.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { createRenderRoutes, type AppRenderer } from "./routes/render.js";
|
|
|
16
16
|
import { createProviderRoutes } from "./routes/providers.js";
|
|
17
17
|
import { createModelRoutes } from "./routes/models.js";
|
|
18
18
|
import { createPiAuthRoutes } from "./routes/pi-auth.js";
|
|
19
|
+
import { createWebSearchRoutes } from "./routes/web-search.js";
|
|
19
20
|
import { createGraphRoutes } from "./routes/graph.js";
|
|
20
21
|
import {
|
|
21
22
|
resolveAuthMode,
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
getCustomOpenRouterModels,
|
|
40
41
|
getDefaultModelId,
|
|
41
42
|
getHiddenModelIds,
|
|
43
|
+
getThinkingOverrides,
|
|
42
44
|
} from "./db/settings.js";
|
|
43
45
|
import { createActivityRuntime } from "./activity/runtime.js";
|
|
44
46
|
import { createModelPricing } from "./pricing/model-pricing.js";
|
|
@@ -207,6 +209,7 @@ export function createApp(options: CreateAppOptions = {}): BrainUiApp {
|
|
|
207
209
|
getHiddenModelIds: () => getHiddenModelIds(db, dbLog),
|
|
208
210
|
getDefaultModelId: () => getDefaultModelId(db, dbLog),
|
|
209
211
|
getCustomOpenRouterModels: () => getCustomOpenRouterModels(db, dbLog),
|
|
212
|
+
getThinkingOverrides: () => getThinkingOverrides(db, dbLog),
|
|
210
213
|
getBillingOverrides: () => getBillingOverrides(db, dbLog),
|
|
211
214
|
log: observability.logger("agent"),
|
|
212
215
|
});
|
|
@@ -353,6 +356,7 @@ export function createApp(options: CreateAppOptions = {}): BrainUiApp {
|
|
|
353
356
|
createModelRoutes({ registry, db, pricing, log: observability.logger("models") })
|
|
354
357
|
);
|
|
355
358
|
app.route("/api", createPiAuthRoutes({ agent: config.agent }));
|
|
359
|
+
app.route("/api", createWebSearchRoutes({ agent: config.agent }));
|
|
356
360
|
app.route(
|
|
357
361
|
"/api",
|
|
358
362
|
createGraphRoutes({ brainRoot: config.brainPath, log: observability.logger("graph") })
|
package/src/config/env.ts
CHANGED
|
@@ -60,10 +60,26 @@ export const ENV_VARS: readonly EnvVarDescriptor[] = [
|
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
62
|
name: "HOME",
|
|
63
|
-
description:
|
|
63
|
+
description:
|
|
64
|
+
"Fallback anchor for the BRAIN_PATH default and the pi config dir (~/.pi).",
|
|
64
65
|
default: "/root",
|
|
65
66
|
required: false,
|
|
66
67
|
},
|
|
68
|
+
{
|
|
69
|
+
name: "PI_CODING_AGENT_DIR",
|
|
70
|
+
description:
|
|
71
|
+
"pi config dir override — where the web-search settings write the " +
|
|
72
|
+
"pi-web-access extension's web-search.json (same precedence the " +
|
|
73
|
+
"extension itself uses).",
|
|
74
|
+
default: "$XDG_CONFIG_HOME/pi, else $HOME/.pi",
|
|
75
|
+
required: false,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "XDG_CONFIG_HOME",
|
|
79
|
+
description: "Second-precedence anchor for the pi config dir ($XDG_CONFIG_HOME/pi).",
|
|
80
|
+
default: null,
|
|
81
|
+
required: false,
|
|
82
|
+
},
|
|
67
83
|
{
|
|
68
84
|
name: "DB_PATH",
|
|
69
85
|
description: "SQLite file for the UI's own database (sessions, passkeys, settings).",
|
|
@@ -624,3 +640,15 @@ export function resolveServerConfig(env: EnvRecord = process.env): ServerConfig
|
|
|
624
640
|
export function subprocessEnv(extra: Record<string, string> = {}): EnvRecord {
|
|
625
641
|
return { ...process.env, ...extra };
|
|
626
642
|
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* The pi config directory, resolved with EXACTLY the precedence pi-web-access
|
|
646
|
+
* uses for its `web-search.json` (PI_CODING_AGENT_DIR, then XDG_CONFIG_HOME/pi,
|
|
647
|
+
* then ~/.pi) — the settings routes write the file the extension reads, so
|
|
648
|
+
* the two resolutions must never diverge.
|
|
649
|
+
*/
|
|
650
|
+
export function resolvePiConfigDir(env: EnvRecord = process.env): string {
|
|
651
|
+
if (env.PI_CODING_AGENT_DIR) return env.PI_CODING_AGENT_DIR;
|
|
652
|
+
if (env.XDG_CONFIG_HOME) return join(env.XDG_CONFIG_HOME, "pi");
|
|
653
|
+
return join(env.HOME || "/root", ".pi");
|
|
654
|
+
}
|
package/src/db/settings.ts
CHANGED
|
@@ -8,12 +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";
|
|
15
20
|
const DEFAULT_MODEL_KEY = "models.default";
|
|
16
21
|
const CUSTOM_OPENROUTER_KEY = "models.customOpenRouter";
|
|
22
|
+
const THINKING_OVERRIDES_KEY = "models.thinking";
|
|
17
23
|
const DETAIL_RETENTION_KEY = "activity.retention.detailDays";
|
|
18
24
|
const DETAIL_RETENTION_DEFAULT_DAYS = 7;
|
|
19
25
|
|
|
@@ -119,6 +125,39 @@ export function setCustomOpenRouterModels(db: Database, models: string[]): void
|
|
|
119
125
|
setSetting(db, CUSTOM_OPENROUTER_KEY, unique);
|
|
120
126
|
}
|
|
121
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
|
+
|
|
122
161
|
/** Minimum days a finished run keeps its detail (spans/events) before the
|
|
123
162
|
* digest-covered prune may take it. 0 is valid (prune as soon as covered);
|
|
124
163
|
* anything non-numeric or negative degrades to the default. */
|
package/src/routes/models.ts
CHANGED
|
@@ -10,9 +10,11 @@ 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";
|
|
@@ -21,10 +23,12 @@ import {
|
|
|
21
23
|
getCustomOpenRouterModels,
|
|
22
24
|
getDefaultModelId,
|
|
23
25
|
getHiddenModelIds,
|
|
26
|
+
getThinkingOverrides,
|
|
24
27
|
setBillingOverrides,
|
|
25
28
|
setCustomOpenRouterModels,
|
|
26
29
|
setDefaultModelId,
|
|
27
30
|
setHiddenModelIds,
|
|
31
|
+
setThinkingOverrides,
|
|
28
32
|
} from "../db/settings.js";
|
|
29
33
|
|
|
30
34
|
export function createModelRoutes(deps: {
|
|
@@ -41,6 +45,7 @@ export function createModelRoutes(deps: {
|
|
|
41
45
|
const source = await registry.getModelSource();
|
|
42
46
|
const hidden = new Set(getHiddenModelIds(db));
|
|
43
47
|
const overrides = getBillingOverrides(db);
|
|
48
|
+
const thinking = getThinkingOverrides(db);
|
|
44
49
|
// `billingMode` already rides each provider entry (the registry applies the
|
|
45
50
|
// override last); the catalog additionally tags WHICH rows carry an explicit
|
|
46
51
|
// override, so the settings screen can render auto vs forced.
|
|
@@ -50,6 +55,10 @@ export function createModelRoutes(deps: {
|
|
|
50
55
|
...profile,
|
|
51
56
|
hidden: hidden.has(profile.id),
|
|
52
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] } : {}),
|
|
53
62
|
}));
|
|
54
63
|
|
|
55
64
|
const state = source?.state();
|
|
@@ -176,6 +185,39 @@ export function createModelRoutes(deps: {
|
|
|
176
185
|
return c.json(await buildCatalog());
|
|
177
186
|
})
|
|
178
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
|
+
|
|
179
221
|
.put("/models/billing", async (c) => {
|
|
180
222
|
const body = (await c.req.json().catch(() => null)) as unknown;
|
|
181
223
|
const billing = (body as { billing?: unknown } | null)?.billing;
|