@otto-code/brain 0.8.8 → 0.8.10
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/commands/bench.js +19 -5
- package/dist/commands/catalog.d.ts +1 -0
- package/dist/commands/catalog.js +1 -0
- package/dist/commands/pull.js +14 -33
- package/dist/commands/repo-download.d.ts +14 -0
- package/dist/commands/repo-download.js +29 -0
- package/dist/commands/search.js +6 -14
- package/dist/config/builtin-hosting-profiles.d.ts +8 -0
- package/dist/config/builtin-hosting-profiles.js +32 -0
- package/dist/config/hosting-profiles.d.ts +33 -0
- package/dist/config/hosting-profiles.js +71 -0
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.js +1 -0
- package/dist/config/paths.d.ts +2 -0
- package/dist/config/paths.js +1 -0
- package/dist/config/profile-edit.d.ts +4 -2
- package/dist/config/profile-edit.js +94 -4
- package/dist/config/profiles.js +25 -2
- package/dist/config/schema.d.ts +494 -24
- package/dist/config/schema.js +55 -0
- package/dist/config/store.js +12 -2
- package/dist/gguf.d.ts +1 -0
- package/dist/gguf.js +1 -0
- package/dist/models/download.js +5 -0
- package/dist/models/enrich.d.ts +6 -0
- package/dist/models/enrich.js +50 -2
- package/dist/ops/calibrate.d.ts +4 -1
- package/dist/ops/calibrate.js +10 -7
- package/dist/ops/sweep.d.ts +3 -1
- package/dist/ops/sweep.js +3 -3
- package/dist/runtime/args.d.ts +2 -2
- package/dist/runtime/args.js +13 -1
- package/dist/runtime/index.d.ts +7 -0
- package/dist/runtime/index.js +10 -0
- package/dist/service/host-api.d.ts +17 -1
- package/dist/service/host-api.js +196 -13
- package/dist/service/router.d.ts +18 -0
- package/dist/service/router.js +89 -4
- package/dist/service/serve.js +184 -12
- package/dist/service/supervisor.d.ts +32 -4
- package/dist/service/supervisor.js +30 -6
- package/dist/tui/app.js +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/vram.js +3 -3
- package/package.json +1 -1
package/dist/commands/bench.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `otto brain bench` - score a model's agentic coding ability on this machine.
|
|
3
3
|
* Either benchmarks an endpoint that is already serving (`--endpoint`) or loads
|
|
4
|
-
* each requested model itself.
|
|
5
|
-
*
|
|
4
|
+
* each requested model itself. Locally loaded runs use the router scheduler, so
|
|
5
|
+
* their requests receive the same profile system addendum as normal serving. A
|
|
6
|
+
* long streaming run, so it prints a formatted report directly rather than going
|
|
7
|
+
* through the output layer.
|
|
6
8
|
*/
|
|
7
9
|
import http from "node:http";
|
|
8
10
|
import path from "node:path";
|
|
9
|
-
import { forModel, getCalibration, loadBrainConfig, loadProfilesStore } from "../config/index.js";
|
|
11
|
+
import { forModel, getCalibration, loadBrainConfig, loadProfilesStore, resolveBrainPaths, } from "../config/index.js";
|
|
10
12
|
import { query as queryGpu } from "../gpu.js";
|
|
11
13
|
import { pickModel, scanModels } from "../models/index.js";
|
|
12
14
|
import { CommandError } from "../output/types.js";
|
|
@@ -190,9 +192,21 @@ async function runBenchSuite(options, _command) {
|
|
|
190
192
|
profile = fit.profile;
|
|
191
193
|
}
|
|
192
194
|
process.stdout.write(`\n${"=".repeat(74)}\n${model.displayName}\n${"=".repeat(74)}\n`);
|
|
193
|
-
const supervisor = new Supervisor({
|
|
195
|
+
const supervisor = new Supervisor({
|
|
196
|
+
runtime,
|
|
197
|
+
internalPort: port,
|
|
198
|
+
paths: resolveBrainPaths(),
|
|
199
|
+
getProfilesStore: () => store,
|
|
200
|
+
});
|
|
194
201
|
const telemetry = new Telemetry();
|
|
195
|
-
const server = http.createServer(createRouter({
|
|
202
|
+
const server = http.createServer(createRouter({
|
|
203
|
+
supervisor,
|
|
204
|
+
telemetry,
|
|
205
|
+
getCatalog: () => catalog,
|
|
206
|
+
loadModel: async (target) => {
|
|
207
|
+
await supervisor.start(target, forModel(store, target, config.defaults));
|
|
208
|
+
},
|
|
209
|
+
}));
|
|
196
210
|
await new Promise((resolve, reject) => {
|
|
197
211
|
server.once("error", reject);
|
|
198
212
|
server.listen(port + 1, "127.0.0.1", resolve);
|
package/dist/commands/catalog.js
CHANGED
|
@@ -29,6 +29,7 @@ export async function runCatalogCommand(_options, _command) {
|
|
|
29
29
|
const rows = catalog.models.map((model) => ({
|
|
30
30
|
id: model.id,
|
|
31
31
|
name: model.name,
|
|
32
|
+
family: model.family ?? null,
|
|
32
33
|
installed: installedCatalogIds.has(model.id),
|
|
33
34
|
publisher: model.publisher ?? "",
|
|
34
35
|
repo: model.hfRepo,
|
package/dist/commands/pull.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { loadBrainConfig, loadCatalog } from "../config/index.js";
|
|
2
|
-
import {
|
|
2
|
+
import { bundleDownloadPlan, listRepoQuants, managedModelsDir, pullModel, resolveHfToken, } from "../models/index.js";
|
|
3
3
|
import { CommandError } from "../output/types.js";
|
|
4
4
|
import { formatBytes } from "../models/scan.js";
|
|
5
5
|
import { withActivity } from "../service/activity.js";
|
|
6
|
+
import { downloadRepoFilesWithProgress } from "./repo-download.js";
|
|
6
7
|
const pullSchema = {
|
|
7
8
|
idField: "model",
|
|
8
9
|
columns: [
|
|
@@ -67,30 +68,16 @@ export async function runPullCommand(modelArg, options, _command) {
|
|
|
67
68
|
});
|
|
68
69
|
}
|
|
69
70
|
const plan = bundleDownloadPlan(model, options.component ?? [], choice?.files, choice?.sizeBytes);
|
|
70
|
-
|
|
71
|
-
const written = await
|
|
71
|
+
const progressLabel = `${model.name}${choice ? ` ${choice.quant}` : ""}`;
|
|
72
|
+
const written = await downloadRepoFilesWithProgress({
|
|
73
|
+
activityTarget: model.name,
|
|
74
|
+
progressLabel,
|
|
75
|
+
totalBytes: plan.totalBytes,
|
|
72
76
|
repo: plan.repo,
|
|
73
77
|
files: plan.files,
|
|
74
78
|
destRoot: managedModelsDir(config),
|
|
75
79
|
token,
|
|
76
|
-
|
|
77
|
-
activity.update(plan.totalBytes ? progress.receivedBytes / plan.totalBytes : null);
|
|
78
|
-
// The daemon owns the UI job record and only sees this child's
|
|
79
|
-
// stdout/stderr. Activity also feeds the host status, but emitting
|
|
80
|
-
// here is what keeps the Library's bundle progress ring live.
|
|
81
|
-
const pct = plan.totalBytes
|
|
82
|
-
? Math.floor((progress.receivedBytes / plan.totalBytes) * 100)
|
|
83
|
-
: 0;
|
|
84
|
-
// Network chunks do not land exactly on 5% boundaries. Emit every
|
|
85
|
-
// new integer so the daemon receives continuous progress instead of
|
|
86
|
-
// often seeing only the initial and final updates.
|
|
87
|
-
if (pct > lastPct) {
|
|
88
|
-
lastPct = pct;
|
|
89
|
-
process.stderr.write(` ${model.name}${choice ? ` ${choice.quant}` : ""}: ${pct}%\r`);
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
}));
|
|
93
|
-
process.stderr.write("\n");
|
|
80
|
+
});
|
|
94
81
|
return {
|
|
95
82
|
type: "single",
|
|
96
83
|
data: {
|
|
@@ -130,22 +117,15 @@ export async function runPullCommand(modelArg, options, _command) {
|
|
|
130
117
|
}
|
|
131
118
|
const files = [...choice.files, ...(mmproj ? mmproj.files : [])];
|
|
132
119
|
const total = choice.sizeBytes + (mmproj?.sizeBytes ?? 0);
|
|
133
|
-
|
|
134
|
-
|
|
120
|
+
const written = await downloadRepoFilesWithProgress({
|
|
121
|
+
activityTarget: `${model.name} (${choice.quant})`,
|
|
122
|
+
progressLabel: `${model.name} ${choice.quant}`,
|
|
123
|
+
totalBytes: total,
|
|
135
124
|
repo: model.hfRepo,
|
|
136
125
|
files,
|
|
137
126
|
destRoot: managedModelsDir(config),
|
|
138
127
|
token,
|
|
139
|
-
|
|
140
|
-
activity.update(total ? p.receivedBytes / total : null);
|
|
141
|
-
const pct = total ? Math.floor((p.receivedBytes / total) * 100) : 0;
|
|
142
|
-
if (pct > lastPct) {
|
|
143
|
-
lastPct = pct;
|
|
144
|
-
process.stderr.write(` ${model.name} ${choice.quant}: ${pct}%\r`);
|
|
145
|
-
}
|
|
146
|
-
},
|
|
147
|
-
}));
|
|
148
|
-
process.stderr.write("\n");
|
|
128
|
+
});
|
|
149
129
|
return {
|
|
150
130
|
type: "single",
|
|
151
131
|
data: {
|
|
@@ -177,6 +157,7 @@ export async function runPullCommand(modelArg, options, _command) {
|
|
|
177
157
|
}
|
|
178
158
|
},
|
|
179
159
|
}));
|
|
160
|
+
process.stderr.write(` ${model.name}: 100%\r`);
|
|
180
161
|
process.stderr.write("\n");
|
|
181
162
|
return {
|
|
182
163
|
type: "single",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Shared CLI progress and activity reporting for Hugging Face repo downloads. */
|
|
2
|
+
import { type DownloadFilesOptions } from "../models/index.js";
|
|
3
|
+
export interface RepoDownloadOptions extends DownloadFilesOptions {
|
|
4
|
+
activityTarget: string;
|
|
5
|
+
progressLabel: string;
|
|
6
|
+
totalBytes: number | null;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Download a repo selection while keeping the daemon job ring and host-status
|
|
10
|
+
* activity record in lockstep. Repository size metadata is advisory, so a
|
|
11
|
+
* successful transfer is the only authoritative completion signal.
|
|
12
|
+
*/
|
|
13
|
+
export declare function downloadRepoFilesWithProgress({ activityTarget, progressLabel, totalBytes, ...options }: RepoDownloadOptions): Promise<string[]>;
|
|
14
|
+
//# sourceMappingURL=repo-download.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Shared CLI progress and activity reporting for Hugging Face repo downloads. */
|
|
2
|
+
import { downloadRepoFiles, } from "../models/index.js";
|
|
3
|
+
import { withActivity } from "../service/activity.js";
|
|
4
|
+
/**
|
|
5
|
+
* Download a repo selection while keeping the daemon job ring and host-status
|
|
6
|
+
* activity record in lockstep. Repository size metadata is advisory, so a
|
|
7
|
+
* successful transfer is the only authoritative completion signal.
|
|
8
|
+
*/
|
|
9
|
+
export async function downloadRepoFilesWithProgress({ activityTarget, progressLabel, totalBytes, ...options }) {
|
|
10
|
+
let lastPct = -1;
|
|
11
|
+
const written = await withActivity("download", { target: activityTarget }, (activity) => downloadRepoFiles({
|
|
12
|
+
...options,
|
|
13
|
+
onProgress: (progress) => {
|
|
14
|
+
activity.update(totalBytes ? progress.receivedBytes / totalBytes : null);
|
|
15
|
+
const pct = totalBytes ? Math.floor((progress.receivedBytes / totalBytes) * 100) : 0;
|
|
16
|
+
// A download chunk can jump straight over a five-percent boundary.
|
|
17
|
+
// Report each new integer percent so the UI ring never stalls until
|
|
18
|
+
// completion simply because no chunk hit an exact multiple of five.
|
|
19
|
+
if (pct > lastPct) {
|
|
20
|
+
lastPct = pct;
|
|
21
|
+
process.stderr.write(` ${progressLabel}: ${pct}%\r`);
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
}));
|
|
25
|
+
process.stderr.write(` ${progressLabel}: 100%\r`);
|
|
26
|
+
process.stderr.write("\n");
|
|
27
|
+
return written;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=repo-download.js.map
|
package/dist/commands/search.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { loadBrainConfig } from "../config/index.js";
|
|
2
|
-
import {
|
|
2
|
+
import { listRepoQuants, managedModelsDir, repoOfModel, resolveHfToken, scanModels, searchModels, } from "../models/index.js";
|
|
3
3
|
import { formatBytes } from "../models/scan.js";
|
|
4
4
|
import { CommandError } from "../output/types.js";
|
|
5
|
+
import { downloadRepoFilesWithProgress } from "./repo-download.js";
|
|
5
6
|
/** What is already on disk, so search + quant listings can mark installed rows. */
|
|
6
7
|
function installedIndex(config) {
|
|
7
8
|
const repos = new Set();
|
|
@@ -132,24 +133,15 @@ export async function runAddCommand(repo, options, _command) {
|
|
|
132
133
|
(!options.primaryOnly && options.component === undefined));
|
|
133
134
|
const files = [...choice.files, ...(includeProjector ? mmproj.files : [])];
|
|
134
135
|
const total = choice.sizeBytes + (includeProjector ? mmproj.sizeBytes : 0);
|
|
135
|
-
|
|
136
|
-
|
|
136
|
+
const written = await downloadRepoFilesWithProgress({
|
|
137
|
+
activityTarget: `${repo} (${choice.quant})`,
|
|
138
|
+
progressLabel: `${repo} ${choice.quant}`,
|
|
139
|
+
totalBytes: total,
|
|
137
140
|
repo,
|
|
138
141
|
files,
|
|
139
142
|
destRoot: managedModelsDir(config),
|
|
140
143
|
token,
|
|
141
|
-
onProgress: (p) => {
|
|
142
|
-
const pct = total ? Math.floor((p.receivedBytes / total) * 100) : 0;
|
|
143
|
-
// A download chunk can jump straight over a five-percent boundary.
|
|
144
|
-
// Report each new integer percent so the UI ring never stalls until
|
|
145
|
-
// completion simply because no chunk hit an exact multiple of five.
|
|
146
|
-
if (pct > lastPct) {
|
|
147
|
-
lastPct = pct;
|
|
148
|
-
process.stderr.write(` ${repo} ${choice.quant}: ${pct}%\r`);
|
|
149
|
-
}
|
|
150
|
-
},
|
|
151
144
|
});
|
|
152
|
-
process.stderr.write("\n");
|
|
153
145
|
return {
|
|
154
146
|
type: "single",
|
|
155
147
|
data: {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ProfilesStore } from "./schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* Product-owned ids are upgraded wholesale, while records with unknown ids remain
|
|
4
|
+
* user-owned. Do not merge profile fields: stale template bytes or prose must not
|
|
5
|
+
* survive an Otto upgrade.
|
|
6
|
+
*/
|
|
7
|
+
export declare function seedBuiltinHostingProfiles(store: ProfilesStore): boolean;
|
|
8
|
+
//# sourceMappingURL=builtin-hosting-profiles.d.ts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curated Brain-owned hosting profiles. Source: Apache-2.0
|
|
3
|
+
* https://huggingface.co/peculiar-ragdoll/Qwen-Sharp-Chat-Templates
|
|
4
|
+
*/
|
|
5
|
+
import { Buffer } from "node:buffer";
|
|
6
|
+
const QWEN_SHARP_TEMPLATE_BASE64 = "eyUtIHNldCB0ZW1wbGF0ZV92ZXJzaW9uID0gInF3ZW4zLjYtZnJvZ2dlcmljLXYyMS4zIiAlfQp7JS0gc2V0IF90b29sX2Zvcm1hdCA9IHRvb2xfY2FsbF9mb3JtYXQgaWYgdG9vbF9jYWxsX2Zvcm1hdCBpcyBkZWZpbmVkIGVsc2UgJ3htbCcgJX0KeyUtIHNldCBpbWFnZV9jb3VudCA9IG5hbWVzcGFjZSh2YWx1ZT0wKSAlfQp7JS0gc2V0IHZpZGVvX2NvdW50ID0gbmFtZXNwYWNlKHZhbHVlPTApICV9CnslLSBzZXQgYWRkX3Zpc2lvbl9pZCA9IGFkZF92aXNpb25faWQgaWYgYWRkX3Zpc2lvbl9pZCBpcyBkZWZpbmVkIGVsc2UgZmFsc2UgJX0KeyUtIHNldCBlbmFibGVfdGhpbmtpbmcgPSBlbmFibGVfdGhpbmtpbmcgaWYgZW5hYmxlX3RoaW5raW5nIGlzIGRlZmluZWQgZWxzZSB0cnVlICV9CnslLSBzZXQgYXV0b19kaXNhYmxlX3RoaW5raW5nX3dpdGhfdG9vbHMgPSBhdXRvX2Rpc2FibGVfdGhpbmtpbmdfd2l0aF90b29scyBpZiBhdXRvX2Rpc2FibGVfdGhpbmtpbmdfd2l0aF90b29scyBpcyBkZWZpbmVkIGVsc2UgZmFsc2UgJX0KeyUtIHNldCBfcHJlc2VydmVfdGhpbmtpbmcgPSBwcmVzZXJ2ZV90aGlua2luZyBpZiBwcmVzZXJ2ZV90aGlua2luZyBpcyBkZWZpbmVkIGVsc2UgdHJ1ZSAlfQp7JS0gc2V0IG1heF90b29sX2FyZ19jaGFycyA9IG1heF90b29sX2FyZ19jaGFycyBpZiBtYXhfdG9vbF9hcmdfY2hhcnMgaXMgZGVmaW5lZCBlbHNlIDAgJX0KeyUtIHNldCBtYXhfdG9vbF9yZXNwb25zZV9jaGFycyA9IG1heF90b29sX3Jlc3BvbnNlX2NoYXJzIGlmIG1heF90b29sX3Jlc3BvbnNlX2NoYXJzIGlzIGRlZmluZWQgZWxzZSAwICV9CnslLSBzZXQgX2hhc190b29scyA9ICh0b29scyBpcyBkZWZpbmVkIGFuZCB0b29scyBhbmQgdG9vbHMgaXMgaXRlcmFibGUgYW5kIHRvb2xzIGlzIG5vdCBtYXBwaW5nKSAlfQp7JS0gc2V0IG5zX3N0YXRlID0gbmFtZXNwYWNlKHRoaW5raW5nPWVuYWJsZV90aGlua2luZykgJX0KeyUtIGlmIGF1dG9fZGlzYWJsZV90aGlua2luZ193aXRoX3Rvb2xzIGFuZCBfaGFzX3Rvb2xzICV9CiAgICB7JS0gc2V0IG5zX3N0YXRlLnRoaW5raW5nID0gZmFsc2UgJX0KeyUtIGVuZGlmICV9CnslLSBtYWNybyByZW5kZXJfY29udGVudChjb250ZW50LCBkb192aXNpb25fY291bnQsIGlzX3N5c3RlbV9jb250ZW50PWZhbHNlKSAlfQogICAgeyUtIGlmIGNvbnRlbnQgaXMgc3RyaW5nICV9CiAgICAgICAge3stIGNvbnRlbnQgfX0KICAgIHslLSBlbGlmIGNvbnRlbnQgaXMgaXRlcmFibGUgYW5kIGNvbnRlbnQgaXMgbm90IG1hcHBpbmcgJX0KICAgICAgICB7JS0gZm9yIGl0ZW0gaW4gY29udGVudCAlfQogICAgICAgICAgICB7JS0gaWYgaXRlbSBpcyBtYXBwaW5nICV9CiAgICAgICAgICAgICAgICB7JS0gaWYgaXRlbS50eXBlID09ICdpbWFnZScgb3IgJ2ltYWdlJyBpbiBpdGVtIG9yICdpbWFnZV91cmwnIGluIGl0ZW0gJX0KICAgICAgICAgICAgICAgICAgICB7JS0gaWYgaXNfc3lzdGVtX2NvbnRlbnQgJX0KICAgICAgICAgICAgICAgICAgICAgICAge3stIHJhaXNlX2V4Y2VwdGlvbignU3lzdGVtIG1lc3NhZ2UgY2Fubm90IGNvbnRhaW4gaW1hZ2VzLicpIH19CiAgICAgICAgICAgICAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgICAgICAgICAgICAgeyUtIGlmIGRvX3Zpc2lvbl9jb3VudCAlfQogICAgICAgICAgICAgICAgICAgICAgICB7JS0gc2V0IGltYWdlX2NvdW50LnZhbHVlID0gaW1hZ2VfY291bnQudmFsdWUgKyAxICV9CiAgICAgICAgICAgICAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgICAgICAgICAgICAgeyUtIGlmIGFkZF92aXNpb25faWQgJX0KICAgICAgICAgICAgICAgICAgICAgICAge3stICdQaWN0dXJlICcgfiBpbWFnZV9jb3VudC52YWx1ZSB+ICc6ICcgfX0KICAgICAgICAgICAgICAgICAgICB7JS0gZW5kaWYgJX0KICAgICAgICAgICAgICAgICAgICB7ey0gJzx8dmlzaW9uX3N0YXJ0fD48fGltYWdlX3BhZHw+PHx2aXNpb25fZW5kfD4nIH19CiAgICAgICAgICAgICAgICB7JS0gZWxpZiBpdGVtLnR5cGUgPT0gJ3ZpZGVvJyBvciAndmlkZW8nIGluIGl0ZW0gJX0KICAgICAgICAgICAgICAgICAgICB7JS0gaWYgaXNfc3lzdGVtX2NvbnRlbnQgJX0KICAgICAgICAgICAgICAgICAgICAgICAge3stIHJhaXNlX2V4Y2VwdGlvbignU3lzdGVtIG1lc3NhZ2UgY2Fubm90IGNvbnRhaW4gdmlkZW9zLicpIH19CiAgICAgICAgICAgICAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgICAgICAgICAgICAgeyUtIGlmIGRvX3Zpc2lvbl9jb3VudCAlfQogICAgICAgICAgICAgICAgICAgICAgICB7JS0gc2V0IHZpZGVvX2NvdW50LnZhbHVlID0gdmlkZW9fY291bnQudmFsdWUgKyAxICV9CiAgICAgICAgICAgICAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgICAgICAgICAgICAgeyUtIGlmIGFkZF92aXNpb25faWQgJX0KICAgICAgICAgICAgICAgICAgICAgICAge3stICdWaWRlbyAnIH4gdmlkZW9fY291bnQudmFsdWUgfiAnOiAnIH19CiAgICAgICAgICAgICAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgICAgICAgICAgICAge3stICc8fHZpc2lvbl9zdGFydHw+PHx2aWRlb19wYWR8Pjx8dmlzaW9uX2VuZHw+JyB9fQogICAgICAgICAgICAgICAgeyUtIGVsaWYgJ3RleHQnIGluIGl0ZW0gJX0KICAgICAgICAgICAgICAgICAgICB7ey0gaXRlbS50ZXh0IH19CiAgICAgICAgICAgICAgICB7JS0gZWxzZSAlfQogICAgICAgICAgICAgICAgICAgIHt7LSByYWlzZV9leGNlcHRpb24oJ1VuZXhwZWN0ZWQgaXRlbSB0eXBlIGluIGNvbnRlbnQuJykgfX0KICAgICAgICAgICAgICAgIHslLSBlbmRpZiAlfQogICAgICAgICAgICB7JS0gZWxzZSAlfQogICAgICAgICAgICAgICAge3stIGl0ZW0gfCBzdHJpbmcgfX0KICAgICAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgeyUtIGVuZGZvciAlfQogICAgeyUtIGVsaWYgY29udGVudCBpcyBub25lIG9yIGNvbnRlbnQgaXMgdW5kZWZpbmVkICV9CiAgICAgICAge3stICcnIH19CiAgICB7JS0gZWxzZSAlfQogICAgICAgIHt7LSByYWlzZV9leGNlcHRpb24oJ1VuZXhwZWN0ZWQgY29udGVudCB0eXBlLicpIH19CiAgICB7JS0gZW5kaWYgJX0KeyUtIGVuZG1hY3JvICV9CnslLSBpZiBub3QgbWVzc2FnZXMgJX0KICAgIHt7LSByYWlzZV9leGNlcHRpb24oJ05vIG1lc3NhZ2VzIHByb3ZpZGVkLicpIH19CnslLSBlbmRpZiAlfQp7JS0gc2V0IF9maXJzdF9yb2xlID0gbWVzc2FnZXNbMF0ucm9sZSAlfQp7JS0gaWYgX2ZpcnN0X3JvbGUgPT0gJ3N5c3RlbScgb3IgX2ZpcnN0X3JvbGUgPT0gJ2RldmVsb3BlcicgJX0KICAgIHslLSBzZXQgX3N5c19tc2cgPSBtZXNzYWdlc1swXSAlfQogICAgeyUtIHNldCBfbXNncyA9IG1lc3NhZ2VzWzE6XSAlfQp7JS0gZWxzZSAlfQogICAgeyUtIHNldCBfc3lzX21zZyA9IG5vbmUgJX0KICAgIHslLSBzZXQgX21zZ3MgPSBtZXNzYWdlcyAlfQp7JS0gZW5kaWYgJX0KeyUtIHNldCBfc2MgPSAnJyAlfQp7JS0gaWYgX3N5c19tc2cgaXMgbm90IG5vbmUgJX0KICAgIHslLSBzZXQgX3NjID0gcmVuZGVyX2NvbnRlbnQoX3N5c19tc2cuY29udGVudCwgZmFsc2UsIHRydWUpIHwgdHJpbSAlfQogICAgeyUtIGlmICc8fHRoaW5rX29mZnw+JyBpbiBfc2MgJX0KICAgICAgICB7JS0gc2V0IG5zX3N0YXRlLnRoaW5raW5nID0gZmFsc2UgJX0KICAgICAgICB7JS0gc2V0IF9zYyA9IF9zYy5zcGxpdCgnPHx0aGlua19vZmZ8PicpIHwgam9pbignJykgfCB0cmltICV9CiAgICB7JS0gZWxpZiAnPHx0aGlua19vbnw+JyBpbiBfc2MgJX0KICAgICAgICB7JS0gc2V0IG5zX3N0YXRlLnRoaW5raW5nID0gdHJ1ZSAlfQogICAgICAgIHslLSBzZXQgX3NjID0gX3NjLnNwbGl0KCc8fHRoaW5rX29ufD4nKSB8IGpvaW4oJycpIHwgdHJpbSAlfQogICAgeyUtIGVuZGlmICV9CnslLSBlbmRpZiAlfQp7JS0gc2V0IF90ZXJzZSAlfQpBbnN3ZXIgZGlyZWN0bHksIGFmdGVyIHRoaW5raW5nLiBMZWFkIHdpdGggdGhlIGFuc3dlciwgdGhlbiBvbmx5IHdoYXQgaXQgbmVlZHMgdG8gYmUgY29ycmVjdCBhbmQgdXNhYmxlLgpOZXZlcjogb3BlbiB3aXRoIHByZWFtYmxlIG9yIHBsZWFzYW50cmllczsgcmVzdGF0ZSB0aGUgcXVlc3Rpb247IGFkZCBmaWxsZXIgdHJhbnNpdGlvbnM7IGhlZGdlIHdpdGggbmljZXRpZXM7IG9yIHJlcGVhdCBhIHBvaW50IHlvdSd2ZSBhbHJlYWR5IG1hZGUuCkFsd2F5czoga2VlcCBlc3NlbnRpYWwgc3RlcHMsIGNhdmVhdHMsIHVuY2VydGFpbnRpZXMsIGFuZCBzcGVjaWZpY3Mg4oCUIG5ldmVyIGRyb3AgY29ycmVjdG5lc3Mgb3IgYSBuZWVkZWQgd2FybmluZyBmb3IgYnJldml0eS4gS2VlcCB0aGUgZmluYWwgYW5zd2VyIGxlYW4uIFVzZSB0aGUgbGVhc3Qgc3RydWN0dXJlIHRoYXQgY29udmV5cyBpdCAocGxhaW4gcHJvc2Ugd2hlbiBzaG9ydDsgbGlzdHMgb3IgY29kZSBvbmx5IHdoZW4gdGhleSBlYXJuIHRoZWlyIHBsYWNlKS4gSWYgZ2VudWluZWx5IHVuY2VydGFpbiwgc2F5IHNvIGFuZCBleHBsYWluIHdoeSDigJQgbmV2ZXIgb21pdCB1bmNlcnRhaW50eSBmb3IgdGhlIHNha2Ugb2YgYnJldml0eS4KSWYgYSB1c2VyIHJlcXVlc3QgaXMgZ2VudWluZWx5IGFtYmlndW91cywgYXNrIGEgc2hhcnAgcXVlc3Rpb24sIGRvbid0IGd1ZXNzLgp7JS0gZW5kc2V0ICV9CnslLSBpZiBub3QgX3NjICV9CiAgICB7JS0gc2V0IF9zYyA9IF90ZXJzZSB8IHRyaW0gJX0KeyUtIGVsc2UgJX0KICAgIHslLSBzZXQgX3NjID0gKF9zYyB8IHRyaW0pIH4gJ1xuXG4nIH4gKF90ZXJzZSB8IHRyaW0pICV9CnslLSBlbmRpZiAlfQp7JS0gaWYgX2hhc190b29scyAlfQogICAge3stICc8fGltX3N0YXJ0fD5zeXN0ZW1cbicgfX0KICAgIHt7LSAnIyBUb29sc1xuXG5Zb3UgaGF2ZSBhY2Nlc3MgdG8gdGhlIGZvbGxvd2luZyBmdW5jdGlvbnM6XG5cbjx0b29scz4nIH19CiAgICB7JS0gZm9yIHRvb2wgaW4gdG9vbHMgJX0KICAgICAgICB7ey0gJ1xuJyB9fQogICAgICAgIHt7LSB0b29sIHwgdG9qc29uIH19CiAgICB7JS0gZW5kZm9yICV9CiAgICB7ey0gJ1xuPC90b29scz4nIH19CiAgICB7JS0gc2V0IHRvb2xfaW5zdHJ1Y3Rpb25zICV9CklmIHlvdSBjaG9vc2UgdG8gY2FsbCBhIGZ1bmN0aW9uIE9OTFkgcmVwbHkgaW4gdGhlIGZvbGxvd2luZyBmb3JtYXQgd2l0aCBOTyBzdWZmaXg6Cgp7JS0gaWYgX3Rvb2xfZm9ybWF0ID09ICdqc29uJyAlfQo8dGhpbms+CkJyaWVmIGV4cGxhbmF0aW9uIG9mIHRvb2wgY2FsbAo8L3RoaW5rPgo8dG9vbF9jYWxsPgp7Im5hbWUiOiAiZXhhbXBsZV9mdW5jdGlvbl9uYW1lIiwgImFyZ3VtZW50cyI6IHsiZXhhbXBsZV9wYXJhbWV0ZXJfMSI6ICJ2YWx1ZV8xIiwgImV4YW1wbGVfcGFyYW1ldGVyXzIiOiAiVGhpcyBpcyB0aGUgdmFsdWUgZm9yIHRoZSBzZWNvbmQgcGFyYW1ldGVyIn19CjwvdG9vbF9jYWxsPgp7JS0gZWxzZSAlfQo8dGhpbms+CkJyaWVmIGV4cGxhbmF0aW9uIG9mIHRvb2wgY2FsbAo8L3RoaW5rPgo8dG9vbF9jYWxsPgo8ZnVuY3Rpb249ZXhhbXBsZV9mdW5jdGlvbl9uYW1lPgo8cGFyYW1ldGVyPWV4YW1wbGVfcGFyYW1ldGVyXzE+CnZhbHVlXzEKPC9wYXJhbWV0ZXI+CjxwYXJhbWV0ZXI9ZXhhbXBsZV9wYXJhbWV0ZXJfMj4KVGhpcyBpcyB0aGUgdmFsdWUgZm9yIHRoZSBzZWNvbmQgcGFyYW1ldGVyCnRoYXQgY2FuIHNwYW4KbXVsdGlwbGUgbGluZXMKPC9wYXJhbWV0ZXI+CjwvZnVuY3Rpb24+CjwvdG9vbF9jYWxsPgp7JS0gZW5kaWYgJX0KCjxJTVBPUlRBTlQ+ClJlbWluZGVyOgotIFlvdSBjYW4gdXNlIHRoZSA8dGhpbms+PC90aGluaz4gYmxvY2sgdG8gcGxhbiB5b3VyIG5leHQgdG9vbCBjYWxsIE9SIHRvIHN5bnRoZXNpemUgZGF0YSBhbmQgZm9ybXVsYXRlIHlvdXIgZmluYWwgcmVzcG9uc2UgdG8gdGhlIHVzZXIuCi0gQUxMIGV4cGxhbmF0aW9uIGFuZCByZWFzb25pbmcgTVVTVCBiZSBwbGFjZWQgc3RyaWN0bHkgaW5zaWRlIHRoZSA8dGhpbms+PC90aGluaz4gYmxvY2suCnslLSBpZiBfdG9vbF9mb3JtYXQgPT0gJ2pzb24nICV9Ci0gRnVuY3Rpb24gY2FsbHMgTVVTVCBmb2xsb3cgdGhlIHNwZWNpZmllZCBmb3JtYXQ6IGEgc2luZ2xlIEpTT04gb2JqZWN0IHdpdGggIm5hbWUiIGFuZCAiYXJndW1lbnRzIiBrZXlzIGluc2lkZSA8dG9vbF9jYWxsPjwvdG9vbF9jYWxsPiBYTUwgdGFncy4KeyUtIGVsc2UgJX0KLSBGdW5jdGlvbiBjYWxscyBNVVNUIGZvbGxvdyB0aGUgc3BlY2lmaWVkIGZvcm1hdDogYW4gaW5uZXIgPGZ1bmN0aW9uPS4uLj48L2Z1bmN0aW9uPiBibG9jayBtdXN0IGJlIG5lc3RlZCB3aXRoaW4gPHRvb2xfY2FsbD48L3Rvb2xfY2FsbD4gWE1MIHRhZ3MuCnslLSBlbmRpZiAlfQotIElmIHlvdSBjaG9vc2UgdG8gY2FsbCBhIHRvb2wsIHlvdSBNVVNUIG91dHB1dCB0aGUgPHRvb2xfY2FsbD4gYmxvY2sgSU1NRURJQVRFTFkgYWZ0ZXIgdGhpbmtpbmcsIHdpdGggTk8gY29udmVyc2F0aW9uYWwgdGV4dCBiZWZvcmUgaXQuCnslLSBpZiBfdG9vbF9mb3JtYXQgPT0gJ2pzb24nICV9Ci0gVGhlIDx0b29sX2NhbGw+IHRhZyBNVVNUIGJlIGF0IHRoZSB2ZXJ5IGJlZ2lubmluZyBvZiBhIG5ldyBsaW5lLCB3aXRoIE5PIHNwYWNlcyBvciBpbmRlbnRhdGlvbiBiZWZvcmUgaXQuCnslLSBlbHNlICV9Ci0gVGhlIDx0b29sX2NhbGw+IGFuZCA8ZnVuY3Rpb24+IHRhZ3MgTVVTVCBiZSBhdCB0aGUgdmVyeSBiZWdpbm5pbmcgb2YgYSBuZXcgbGluZSwgd2l0aCBOTyBzcGFjZXMgb3IgaW5kZW50YXRpb24gYmVmb3JlIHRoZW0uCnslLSBlbmRpZiAlfQotIFRvIGNhbGwgbXVsdGlwbGUgZnVuY3Rpb25zLCBvdXRwdXQgYSBzZXBhcmF0ZSwgY29tcGxldGVseSBjbG9zZWQgPHRvb2xfY2FsbD48L3Rvb2xfY2FsbD4gYmxvY2sgZm9yIEVBQ0ggZnVuY3Rpb24uIERvIE5PVCBuZXN0IDx0b29sX2NhbGw+IGJsb2Nrcy4KLSBJZiB5b3UgaGF2ZSBhbGwgbmVjZXNzYXJ5IGRhdGEsIHByb3ZpZGUgeW91ciBmaW5hbCBhbnN3ZXIgZGlyZWN0bHkgdG8gdGhlIHVzZXIgd2l0aG91dCBhbnkgdG9vbCBjYWxsLgo8L0lNUE9SVEFOVD4KICAgIHslLSBlbmRzZXQgJX0KICAgIHt7LSAnXG5cbicgfiB0b29sX2luc3RydWN0aW9ucyB8IHRyaW0gfX0KICAgIHslLSBpZiBfc2MgJX0KICAgICAgICB7ey0gJ1xuXG4nICsgX3NjIH19CiAgICB7JS0gZW5kaWYgJX0KICAgIHt7LSAnPHxpbV9lbmR8PlxuJyB9fQp7JS0gZWxzZSAlfQogICAgeyUtIGlmIF9zYyAlfQogICAgICAgIHt7LSAnPHxpbV9zdGFydHw+c3lzdGVtXG4nICsgX3NjICsgJzx8aW1fZW5kfD5cbicgfX0KICAgIHslLSBlbmRpZiAlfQp7JS0gZW5kaWYgJX0KeyUtIHNldCBfbGFzdF9pZHggPSBfbXNncyB8IGxlbmd0aCAtIDEgJX0KeyUtIHNldCBucyA9IG5hbWVzcGFjZShtdWx0aV9zdGVwX3Rvb2w9dHJ1ZSwgbGFzdF9xdWVyeV9pbmRleD1fbGFzdF9pZHgpICV9CnslLSBmb3IgbWVzc2FnZSBpbiBfbXNnc1s6Oi0xXSAlfQogICAgeyUtIHNldCBpbmRleCA9IChfbXNncyB8IGxlbmd0aCAtIDEpIC0gbG9vcC5pbmRleDAgJX0KICAgIHslLSBpZiBucy5tdWx0aV9zdGVwX3Rvb2wgYW5kIG1lc3NhZ2Uucm9sZSA9PSAndXNlcicgJX0KICAgICAgICB7JS0gc2V0IF9yYyA9IHJlbmRlcl9jb250ZW50KG1lc3NhZ2UuY29udGVudCwgZmFsc2UpIHwgdHJpbSAlfQogICAgICAgIHslLSBpZiBub3QgKF9yYy5zdGFydHN3aXRoKCc8dG9vbF9yZXNwb25zZT4nKSBhbmQgX3JjLmVuZHN3aXRoKCc8L3Rvb2xfcmVzcG9uc2U+JykpICV9CiAgICAgICAgICAgIHslLSBzZXQgbnMubXVsdGlfc3RlcF90b29sID0gZmFsc2UgJX0KICAgICAgICAgICAgeyUtIHNldCBucy5sYXN0X3F1ZXJ5X2luZGV4ID0gaW5kZXggJX0KICAgICAgICB7JS0gZW5kaWYgJX0KICAgIHslLSBlbmRpZiAlfQp7JS0gZW5kZm9yICV9CnslLSBpZiBucy5tdWx0aV9zdGVwX3Rvb2wgJX0KICAgIHslLSBpZiBfbGFzdF9pZHggPiA1MCAlfQogICAgICAgIHslLSBzZXQgbnMubGFzdF9xdWVyeV9pbmRleCA9IF9sYXN0X2lkeCAlfQogICAgeyUtIGVsc2UgJX0KICAgICAgICB7JS0gc2V0IG5zLmxhc3RfcXVlcnlfaW5kZXggPSAwICV9CiAgICB7JS0gZW5kaWYgJX0KeyUtIGVuZGlmICV9CnslLSBzZXQgbnMyID0gbmFtZXNwYWNlKHByZXZfcm9sZT0nJywgY29uc2VjdXRpdmVfZmFpbHVyZXM9MCkgJX0KeyUtIGZvciBtZXNzYWdlIGluIF9tc2dzICV9CiAgICB7JS0gc2V0IGlzX3N5c3RlbSA9IChtZXNzYWdlLnJvbGUgPT0gInN5c3RlbSIgb3IgbWVzc2FnZS5yb2xlID09ICJkZXZlbG9wZXIiKSAlfQogICAgeyUtIHNldCBjb250ZW50ID0gcmVuZGVyX2NvbnRlbnQobWVzc2FnZS5jb250ZW50LCB0cnVlLCBpc19zeXN0ZW0pIHwgdHJpbSAlfQogICAgeyUtIGlmIGlzX3N5c3RlbSBvciBtZXNzYWdlLnJvbGUgPT0gJ3VzZXInICV9CiAgICAgICAgeyUtIGlmICc8fHRoaW5rX29mZnw+JyBpbiBjb250ZW50ICV9CiAgICAgICAgICAgIHslLSBzZXQgbnNfc3RhdGUudGhpbmtpbmcgPSBmYWxzZSAlfQogICAgICAgICAgICB7JS0gc2V0IGNvbnRlbnQgPSBjb250ZW50LnNwbGl0KCc8fHRoaW5rX29mZnw+JykgfCBqb2luKCcnKSB8IHRyaW0gJX0KICAgICAgICB7JS0gZWxpZiAnPHx0aGlua19vbnw+JyBpbiBjb250ZW50ICV9CiAgICAgICAgICAgIHslLSBzZXQgbnNfc3RhdGUudGhpbmtpbmcgPSB0cnVlICV9CiAgICAgICAgICAgIHslLSBzZXQgY29udGVudCA9IGNvbnRlbnQuc3BsaXQoJzx8dGhpbmtfb258PicpIHwgam9pbignJykgfCB0cmltICV9CiAgICAgICAgeyUtIGVuZGlmICV9CiAgICB7JS0gZW5kaWYgJX0KICAgIHslLSBpZiBpc19zeXN0ZW0gJX0KICAgICAgICB7ey0gJzx8aW1fc3RhcnR8PnN5c3RlbVxuJyArIGNvbnRlbnQgKyAnPHxpbV9lbmR8PlxuJyB9fQogICAgeyUtIGVsaWYgbWVzc2FnZS5yb2xlID09ICd1c2VyJyAlfQogICAgICAgIHslLSBzZXQgbnMyLmNvbnNlY3V0aXZlX2ZhaWx1cmVzID0gMCAlfQogICAgICAgIHt7LSAnPHxpbV9zdGFydHw+dXNlclxuJyArIGNvbnRlbnQgKyAnPHxpbV9lbmR8PlxuJyB9fQogICAgeyUtIGVsaWYgbWVzc2FnZS5yb2xlID09ICdhc3Npc3RhbnQnICV9CiAgICAgICAgeyUtIHNldCByZWFzb25pbmdfY29udGVudCA9ICcnICV9CiAgICAgICAgeyUtIGlmIG1lc3NhZ2UucmVhc29uaW5nX2NvbnRlbnQgaXMgZGVmaW5lZCBhbmQgbWVzc2FnZS5yZWFzb25pbmdfY29udGVudCBpcyBub3Qgbm9uZSAlfQogICAgICAgICAgICB7JS0gaWYgbWVzc2FnZS5yZWFzb25pbmdfY29udGVudCBpcyBzdHJpbmcgJX0KICAgICAgICAgICAgICAgIHslLSBzZXQgcmVhc29uaW5nX2NvbnRlbnQgPSBtZXNzYWdlLnJlYXNvbmluZ19jb250ZW50ICV9CiAgICAgICAgICAgIHslLSBlbHNlICV9CiAgICAgICAgICAgICAgICB7JS0gc2V0IHJlYXNvbmluZ19jb250ZW50ID0gbWVzc2FnZS5yZWFzb25pbmdfY29udGVudCB8IHN0cmluZyAlfQogICAgICAgICAgICB7JS0gZW5kaWYgJX0KICAgICAgICB7JS0gZWxpZiBtZXNzYWdlLnRoaW5raW5nIGlzIGRlZmluZWQgYW5kIG1lc3NhZ2UudGhpbmtpbmcgaXMgbm90IG5vbmUgJX0KICAgICAgICAgICAgeyUtIGlmIG1lc3NhZ2UudGhpbmtpbmcgaXMgc3RyaW5nICV9CiAgICAgICAgICAgICAgICB7JS0gc2V0IHJlYXNvbmluZ19jb250ZW50ID0gbWVzc2FnZS50aGlua2luZyAlfQogICAgICAgICAgICB7JS0gZWxzZSAlfQogICAgICAgICAgICAgICAgeyUtIHNldCByZWFzb25pbmdfY29udGVudCA9IG1lc3NhZ2UudGhpbmtpbmcgfCBzdHJpbmcgJX0KICAgICAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgeyUtIGVsc2UgJX0KICAgICAgICAgICAgeyUtIHNldCBfdGhpbmtfZW5kID0gJycgJX0KICAgICAgICAgICAgeyUtIGlmIGNvbnRlbnQuc3RhcnRzd2l0aCgnPC90aGluaz4nKSAlfQogICAgICAgICAgICAgICAgeyUtIHNldCBfdGhpbmtfZW5kID0gJzwvdGhpbms+JyAlfQogICAgICAgICAgICB7JS0gZWxpZiBjb250ZW50LnN0YXJ0c3dpdGgoJzwvdGhpbmtpbmc+JykgJX0KICAgICAgICAgICAgICAgIHslLSBzZXQgX3RoaW5rX2VuZCA9ICc8L3RoaW5raW5nPicgJX0KICAgICAgICAgICAgeyUtIGVsaWYgJ1xuPC90aGluaz4nIGluIGNvbnRlbnQgJX0KICAgICAgICAgICAgICAgIHslLSBzZXQgX3RoaW5rX2VuZCA9ICdcbjwvdGhpbms+JyAlfQogICAgICAgICAgICB7JS0gZWxpZiAnXG48L3RoaW5raW5nPicgaW4gY29udGVudCAlfQogICAgICAgICAgICAgICAgeyUtIHNldCBfdGhpbmtfZW5kID0gJ1xuPC90aGlua2luZz4nICV9CiAgICAgICAgICAgIHslLSBlbGlmICdcbjwvIHRoaW5rPicgaW4gY29udGVudCAlfQogICAgICAgICAgICAgICAgeyUtIHNldCBfdGhpbmtfZW5kID0gJ1xuPC8gdGhpbms+JyAlfQogICAgICAgICAgICB7JS0gZWxpZiAnXG48L3RoaW5rID4nIGluIGNvbnRlbnQgJX0KICAgICAgICAgICAgICAgIHslLSBzZXQgX3RoaW5rX2VuZCA9ICdcbjwvdGhpbmsgPicgJX0KICAgICAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgICAgIHslLSBpZiBfdGhpbmtfZW5kICV9CiAgICAgICAgICAgICAgICB7JS0gaWYgJ3RoaW5raW5nJyBpbiBfdGhpbmtfZW5kICV9CiAgICAgICAgICAgICAgICAgICAgeyUtIHNldCBfdGhpbmtfc3RhcnQgPSAnPHRoaW5raW5nPicgJX0KICAgICAgICAgICAgICAgIHslLSBlbHNlICV9CiAgICAgICAgICAgICAgICAgICAgeyUtIHNldCBfdGhpbmtfc3RhcnQgPSAnPHRoaW5rPicgJX0KICAgICAgICAgICAgICAgIHslLSBlbmRpZiAlfQogICAgICAgICAgICAgICAgeyUtIHNldCByZWFzb25pbmdfY29udGVudCA9IGNvbnRlbnQuc3BsaXQoX3RoaW5rX2VuZClbMF0ucnN0cmlwKCdcbicpICV9CiAgICAgICAgICAgICAgICB7JS0gaWYgX3RoaW5rX3N0YXJ0IGluIHJlYXNvbmluZ19jb250ZW50ICV9CiAgICAgICAgICAgICAgICAgICAgeyUtIHNldCByZWFzb25pbmdfY29udGVudCA9IHJlYXNvbmluZ19jb250ZW50LnNwbGl0KF90aGlua19zdGFydClbLTFdLmxzdHJpcCgnXG4nKSAlfQogICAgICAgICAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgICAgICAgICB7JS0gc2V0IGNvbnRlbnQgPSBjb250ZW50LnNwbGl0KF90aGlua19lbmQpWy0xXS5sc3RyaXAoJ1xuJykgJX0KICAgICAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgeyUtIHNldCByZWFzb25pbmdfY29udGVudCA9IHJlYXNvbmluZ19jb250ZW50IHwgdHJpbSAlfQogICAgICAgIHslLSBpZiAoX3ByZXNlcnZlX3RoaW5raW5nIG9yIGxvb3AuaW5kZXgwID4gbnMubGFzdF9xdWVyeV9pbmRleCkgYW5kIHJlYXNvbmluZ19jb250ZW50ICV9CiAgICAgICAgICAgIHt7LSAnPHxpbV9zdGFydHw+YXNzaXN0YW50XG48dGhpbms+XG4nICsgcmVhc29uaW5nX2NvbnRlbnQgKyAnXG48L3RoaW5rPlxuXG4nICsgY29udGVudCB9fQogICAgICAgIHslLSBlbHNlICV9CiAgICAgICAgICAgIHt7LSAnPHxpbV9zdGFydHw+YXNzaXN0YW50XG4nICsgY29udGVudCB9fQogICAgICAgIHslLSBlbmRpZiAlfQogICAgICAgIHslLSBpZiBtZXNzYWdlLnRvb2xfY2FsbHMgaXMgZGVmaW5lZCBhbmQgbWVzc2FnZS50b29sX2NhbGxzIGFuZCBtZXNzYWdlLnRvb2xfY2FsbHMgaXMgaXRlcmFibGUgYW5kIG1lc3NhZ2UudG9vbF9jYWxscyBpcyBub3QgbWFwcGluZyAlfQogICAgICAgICAgICB7JS0gZm9yIHRvb2xfY2FsbCBpbiBtZXNzYWdlLnRvb2xfY2FsbHMgJX0KICAgICAgICAgICAgICAgIHslLSBpZiB0b29sX2NhbGwuZnVuY3Rpb24gaXMgZGVmaW5lZCBhbmQgdG9vbF9jYWxsLmZ1bmN0aW9uIGlzIG5vdCBub25lICV9CiAgICAgICAgICAgICAgICAgICAgeyUtIHNldCB0YyA9IHRvb2xfY2FsbC5mdW5jdGlvbiAlfQogICAgICAgICAgICAgICAgeyUtIGVsc2UgJX0KICAgICAgICAgICAgICAgICAgICB7JS0gc2V0IHRjID0gdG9vbF9jYWxsICV9CiAgICAgICAgICAgICAgICB7JS0gZW5kaWYgJX0KICAgICAgICAgICAgICAgIHslLSBpZiBfdG9vbF9mb3JtYXQgPT0gJ2pzb24nICV9CiAgICAgICAgICAgICAgICAgICAgeyUtIGlmIG5vdCBsb29wLmZpcnN0IG9yIGNvbnRlbnQgfCB0cmltICV9CiAgICAgICAgICAgICAgICAgICAgICAgIHt7LSAnXG5cbicgfX0KICAgICAgICAgICAgICAgICAgICB7JS0gZW5kaWYgJX0KICAgICAgICAgICAgICAgICAgICB7JS0gc2V0IF9hcmdzID0gJ3t9JyAlfQogICAgICAgICAgICAgICAgICAgIHslLSBpZiB0Yy5hcmd1bWVudHMgaXMgZGVmaW5lZCBhbmQgdGMuYXJndW1lbnRzIGlzIG5vdCBub25lICV9CiAgICAgICAgICAgICAgICAgICAgICAgIHslLSBpZiB0Yy5hcmd1bWVudHMgaXMgbWFwcGluZyAlfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgeyUtIHNldCBfYXJncyA9IHRjLmFyZ3VtZW50cyB8IHRvanNvbiAlfQogICAgICAgICAgICAgICAgICAgICAgICB7JS0gZWxpZiB0Yy5hcmd1bWVudHMgaXMgc3RyaW5nIGFuZCB0Yy5hcmd1bWVudHMgJX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIHslLSBzZXQgX2FyZ3MgPSB0Yy5hcmd1bWVudHMgJX0KICAgICAgICAgICAgICAgICAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgICAgICAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgICAgICAgICAgICAge3stICc8dG9vbF9jYWxsPlxueyJuYW1lIjogJyB9fXt7LSB0Yy5uYW1lIHwgdG9qc29uIH19e3stICcsICJhcmd1bWVudHMiOiAnIH19e3stIF9hcmdzIH19e3stICd9XG48L3Rvb2xfY2FsbD4nIH19CiAgICAgICAgICAgICAgICB7JS0gZWxzZSAlfQogICAgICAgICAgICAgICAgICAgIHslLSBpZiBsb29wLmZpcnN0ICV9CiAgICAgICAgICAgICAgICAgICAgICAgIHslLSBpZiBjb250ZW50IHwgdHJpbSAlfQogICAgICAgICAgICAgICAgICAgICAgICAgICAge3stICdcblxuPHRvb2xfY2FsbD5cbjxmdW5jdGlvbj0nICsgdGMubmFtZSArICc+XG4nIH19CiAgICAgICAgICAgICAgICAgICAgICAgIHslLSBlbHNlICV9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICB7ey0gJzx0b29sX2NhbGw+XG48ZnVuY3Rpb249JyArIHRjLm5hbWUgKyAnPlxuJyB9fQogICAgICAgICAgICAgICAgICAgICAgICB7JS0gZW5kaWYgJX0KICAgICAgICAgICAgICAgICAgICB7JS0gZWxzZSAlfQogICAgICAgICAgICAgICAgICAgICAgICB7ey0gJ1xuXG48dG9vbF9jYWxsPlxuPGZ1bmN0aW9uPScgKyB0Yy5uYW1lICsgJz5cbicgfX0KICAgICAgICAgICAgICAgICAgICB7JS0gZW5kaWYgJX0KICAgICAgICAgICAgICAgICAgICB7JS0gaWYgdGMuYXJndW1lbnRzIGlzIGRlZmluZWQgYW5kIHRjLmFyZ3VtZW50cyBpcyBub3Qgbm9uZSAlfQogICAgICAgICAgICAgICAgICAgICAgICB7JS0gaWYgdGMuYXJndW1lbnRzIGlzIG1hcHBpbmcgJX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIHslLSBmb3IgYXJnc19uYW1lLCBhcmdzX3ZhbHVlIGluIHRjLmFyZ3VtZW50cy5pdGVtcygpICV9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAge3stICc8cGFyYW1ldGVyPScgKyBhcmdzX25hbWUgKyAnPlxuJyB9fQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHslLSBpZiBhcmdzX3ZhbHVlIGlzIG1hcHBpbmcgb3IgKGFyZ3NfdmFsdWUgaXMgc2VxdWVuY2UgYW5kIGFyZ3NfdmFsdWUgaXMgbm90IHN0cmluZykgJX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgeyUtIHNldCBfYXYgPSBhcmdzX3ZhbHVlIHwgdG9qc29uICV9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgeyUtIGVsc2UgJX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgeyUtIHNldCBfYXYgPSBhcmdzX3ZhbHVlIHwgc3RyaW5nICV9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgeyUtIGlmIG1heF90b29sX2FyZ19jaGFycyA+IDAgYW5kIF9hdiB8IGxlbmd0aCA+IG1heF90b29sX2FyZ19jaGFycyAlfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB7ey0gX2F2WzptYXhfdG9vbF9hcmdfY2hhcnNdICsgJ1xuW1RSVU5DQVRFRCDigJQgb3JpZ2luYWwgbGVuZ3RoICcgfiAoX2F2IHwgbGVuZ3RoIHwgc3RyaW5nKSB+ICcgY2hhcnNdJyB9fQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHslLSBlbHNlICV9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHt7LSBfYXYgfX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB7JS0gZW5kaWYgJX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB7ey0gJ1xuPC9wYXJhbWV0ZXI+XG4nIH19CiAgICAgICAgICAgICAgICAgICAgICAgICAgICB7JS0gZW5kZm9yICV9CiAgICAgICAgICAgICAgICAgICAgICAgIHslLSBlbGlmIHRjLmFyZ3VtZW50cyBpcyBzdHJpbmcgYW5kIHRjLmFyZ3VtZW50cyAlfQogICAgICAgICAgICAgICAgICAgICAgICAgICAge3stIHRjLmFyZ3VtZW50cyB9fQogICAgICAgICAgICAgICAgICAgICAgICB7JS0gZW5kaWYgJX0KICAgICAgICAgICAgICAgICAgICB7JS0gZW5kaWYgJX0KICAgICAgICAgICAgICAgICAgICB7ey0gJzwvZnVuY3Rpb24+XG48L3Rvb2xfY2FsbD4nIH19CiAgICAgICAgICAgICAgICB7JS0gZW5kaWYgJX0KICAgICAgICAgICAgeyUtIGVuZGZvciAlfQogICAgICAgIHslLSBlbmRpZiAlfQogICAgICAgIHt7LSAnPHxpbV9lbmR8PlxuJyB9fQogICAgeyUtIGVsaWYgbWVzc2FnZS5yb2xlID09ICd0b29sJyAlfQogICAgICAgIHslLSBzZXQgX2NvbnRlbnRfbG93ZXIgPSBjb250ZW50IHwgbG93ZXIgJX0KICAgICAgICB7JS0gc2V0IF9jb250ZW50X2hlYWQgPSBfY29udGVudF9sb3dlcls6ODBdICV9CiAgICAgICAgeyUtIGlmIGNvbnRlbnQgfCBsZW5ndGggPCA1MDAgYW5kICckICcgbm90IGluIGNvbnRlbnQgYW5kICd0b29rICcgbm90IGluIF9jb250ZW50X2xvd2VyIGFuZCAoJyJlcnJvciI6JyBpbiBfY29udGVudF9oZWFkIG9yICdlcnJvcjonIGluIF9jb250ZW50X2hlYWQgb3IgJ2VyciEnIGluIF9jb250ZW50X2hlYWQgb3IgJ2ZhdGFsOicgaW4gX2NvbnRlbnRfaGVhZCBvciAnZXhjZXB0aW9uOicgaW4gX2NvbnRlbnRfaGVhZCBvciAndHJhY2ViYWNrJyBpbiBfY29udGVudF9oZWFkIG9yICdjb21tYW5kIG5vdCBmb3VuZCcgaW4gX2NvbnRlbnRfaGVhZCBvciAnaW52YWxpZCBzeW50YXgnIGluIF9jb250ZW50X2hlYWQgb3IgJ2ZhaWxlZCB0bycgaW4gX2NvbnRlbnRfaGVhZCkgJX0KICAgICAgICAgICAgeyUtIHNldCBuczIuY29uc2VjdXRpdmVfZmFpbHVyZXMgPSBuczIuY29uc2VjdXRpdmVfZmFpbHVyZXMgKyAxICV9CiAgICAgICAgeyUtIGVsc2UgJX0KICAgICAgICAgICAgeyUtIHNldCBuczIuY29uc2VjdXRpdmVfZmFpbHVyZXMgPSAwICV9CiAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgeyUtIGlmIG5zMi5wcmV2X3JvbGUgIT0gJ3Rvb2wnICV9CiAgICAgICAgICAgIHt7LSAnPHxpbV9zdGFydHw+dXNlcicgfX0KICAgICAgICB7JS0gZW5kaWYgJX0KICAgICAgICB7JS0gaWYgbWF4X3Rvb2xfcmVzcG9uc2VfY2hhcnMgPiAwIGFuZCBjb250ZW50IHwgbGVuZ3RoID4gbWF4X3Rvb2xfcmVzcG9uc2VfY2hhcnMgJX0KICAgICAgICAgICAgeyUtIHNldCBjb250ZW50ID0gY29udGVudFs6bWF4X3Rvb2xfcmVzcG9uc2VfY2hhcnNdICsgJ1xuW1RSVU5DQVRFRCDigJQgb3JpZ2luYWwgbGVuZ3RoICcgfiAoY29udGVudCB8IGxlbmd0aCB8IHN0cmluZykgfiAnIGNoYXJzXScgJX0KICAgICAgICB7JS0gZW5kaWYgJX0KICAgICAgICB7ey0gJ1xuPHRvb2xfcmVzcG9uc2U+XG4nICsgY29udGVudCB9fQogICAgICAgIHslLSBpZiBuczIuY29uc2VjdXRpdmVfZmFpbHVyZXMgPj0gMiAlfQogICAgICAgICAgICB7ey0gJ1xuXG7imqDvuI8gU1lTVEVNIFdBUk5JTkc6ICcgfiBuczIuY29uc2VjdXRpdmVfZmFpbHVyZXMgfiAnIGNvbnNlY3V0aXZlIHRvb2wgZXJyb3JzIGRldGVjdGVkLiBZb3VyIHByZXZpb3VzIGFwcHJvYWNoIGlzIGluY29ycmVjdC4gWW91IE1VU1QgdXNlIGEgZnVuZGFtZW50YWxseSBkaWZmZXJlbnQgYXBwcm9hY2ggb3IgY29ycmVjdGVkIGFyZ3VtZW50cy4nIH19CiAgICAgICAgeyUtIGVsaWYgbnMyLmNvbnNlY3V0aXZlX2ZhaWx1cmVzID09IDEgJX0KICAgICAgICAgICAge3stICdcblxu4pqg77iPIFNZU1RFTSBXQVJOSU5HOiBUaGUgcHJldmlvdXMgdG9vbCBjYWxsIHJldHVybmVkIGFuIGVycm9yLiBEaWFnbm9zZSB0aGUgZmFpbHVyZSBhbmQgcmV0cnkgd2l0aCBjb21wbGV0ZWx5IGNvcnJlY3RlZCBhcmd1bWVudHMuJyB9fQogICAgICAgIHslLSBlbmRpZiAlfQogICAgICAgIHt7LSAnXG48L3Rvb2xfcmVzcG9uc2U+JyB9fQogICAgICAgIHslLSBpZiBsb29wLmxhc3QgJX0KICAgICAgICAgICAge3stICc8fGltX2VuZHw+XG4nIH19CiAgICAgICAgeyUtIGVsc2UgJX0KICAgICAgICAgICAgeyUtIHNldCBfbmV4dF9yb2xlID0gX21zZ3NbbG9vcC5pbmRleDAgKyAxXS5yb2xlICV9CiAgICAgICAgICAgIHslLSBpZiBfbmV4dF9yb2xlICE9ICd0b29sJyAlfQogICAgICAgICAgICAgICAge3stICc8fGltX2VuZHw+XG4nIH19CiAgICAgICAgICAgIHslLSBlbmRpZiAlfQogICAgICAgIHslLSBlbmRpZiAlfQogICAgeyUtIGVsc2UgJX0KICAgICAgICB7ey0gJzx8aW1fc3RhcnR8PnVzZXJcblsnICsgbWVzc2FnZS5yb2xlICsgJ106ICcgKyBjb250ZW50ICsgJzx8aW1fZW5kfD5cbicgfX0KICAgIHslLSBlbmRpZiAlfQogICAgeyUtIHNldCBuczIucHJldl9yb2xlID0gbWVzc2FnZS5yb2xlICV9CnslLSBlbmRmb3IgJX0KeyUtIGlmIGFkZF9nZW5lcmF0aW9uX3Byb21wdCAlfQogICAge3stICc8fGltX3N0YXJ0fD5hc3Npc3RhbnRcbicgfX0KICAgIHslLSBpZiBub3QgbnNfc3RhdGUudGhpbmtpbmcgJX0KICAgICAgICB7ey0gJzx0aGluaz5cblxuPC90aGluaz5cblxuJyB9fQogICAgeyUtIGVsaWYgbnMyLmNvbnNlY3V0aXZlX2ZhaWx1cmVzID49IDIgJX0KICAgICAgICB7ey0gJzx0aGluaz5cblxuPC90aGluaz5cblxuJyB9fQogICAgeyUtIGVsc2UgJX0KICAgICAgICB7ey0gJzx0aGluaz5cbicgfX0KICAgIHslLSBlbmRpZiAlfQp7JS0gZW5kaWYgJX0=";
|
|
7
|
+
const QWEN_SHARP = {
|
|
8
|
+
id: "qwen-sharp-v21.3",
|
|
9
|
+
name: "Qwen Sharp — concise, tool-safe",
|
|
10
|
+
family: "qwen",
|
|
11
|
+
description: "froggeric fixed Qwen v21.3 template with peculiar-ragdoll's terse system prompt. Agentic default disables retained thinking to avoid multi-turn context/prefill growth.",
|
|
12
|
+
template: Buffer.from(QWEN_SHARP_TEMPLATE_BASE64, "base64").toString("utf8"),
|
|
13
|
+
systemPromptAddendum: null,
|
|
14
|
+
templateKwargs: { preserve_thinking: false },
|
|
15
|
+
};
|
|
16
|
+
const BUILTIN_HOSTING_PROFILES = [QWEN_SHARP];
|
|
17
|
+
/**
|
|
18
|
+
* Product-owned ids are upgraded wholesale, while records with unknown ids remain
|
|
19
|
+
* user-owned. Do not merge profile fields: stale template bytes or prose must not
|
|
20
|
+
* survive an Otto upgrade.
|
|
21
|
+
*/
|
|
22
|
+
export function seedBuiltinHostingProfiles(store) {
|
|
23
|
+
let changed = false;
|
|
24
|
+
for (const profile of BUILTIN_HOSTING_PROFILES) {
|
|
25
|
+
if (JSON.stringify(store.hostingProfiles[profile.id]) === JSON.stringify(profile))
|
|
26
|
+
continue;
|
|
27
|
+
store.hostingProfiles[profile.id] = profile;
|
|
28
|
+
changed = true;
|
|
29
|
+
}
|
|
30
|
+
return changed;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=builtin-hosting-profiles.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { BrainPaths } from "./paths.js";
|
|
2
|
+
import type { HostingProfile, Profile, ProfilesStore } from "./schema.js";
|
|
3
|
+
/**
|
|
4
|
+
* The bucket a model's hosting profiles live in. Families are catalog-authoritative
|
|
5
|
+
* when available, otherwise discovery derives them from GGUF metadata; models
|
|
6
|
+
* with neither use one real bucket rather than each being unaddressable. This
|
|
7
|
+
* must be the single definition: a writer that stored under "generic" while a
|
|
8
|
+
* reader looked up `null` was why a family default could be set on an unfamilied
|
|
9
|
+
* model and never take effect.
|
|
10
|
+
*/
|
|
11
|
+
export declare const GENERIC_HOSTING_FAMILY = "generic";
|
|
12
|
+
export declare function hostingFamily(family: string | null | undefined): string;
|
|
13
|
+
export declare function familyHostingProfileId(store: ProfilesStore, family: string | null | undefined): string | null;
|
|
14
|
+
export declare function effectiveHostingProfile(store: ProfilesStore, profile: Profile, family: string | null | undefined): HostingProfile | null;
|
|
15
|
+
/**
|
|
16
|
+
* Materialize a selected profile immediately before launch: the template as a
|
|
17
|
+
* file llama-server reads, the system addendum as a string the router injects
|
|
18
|
+
* into each completion request.
|
|
19
|
+
*
|
|
20
|
+
* The addendum is deliberately *not* spliced into the Jinja template, which was
|
|
21
|
+
* the first design. Doing that means rewriting the `messages` list from inside
|
|
22
|
+
* the template, and there is no portable way to do it: minja's support for list
|
|
23
|
+
* mutation differs from Jinja2's, every model family's template consumes the
|
|
24
|
+
* system turn differently, and a message's `content` is an array of parts for
|
|
25
|
+
* multimodal requests rather than a string to concatenate onto. Injecting into
|
|
26
|
+
* the parsed request body is the same composition (it appends to the agent's
|
|
27
|
+
* existing system message instead of replacing it) done where the shape is
|
|
28
|
+
* known and testable.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveHostingProfileForLaunch(paths: BrainPaths, store: ProfilesStore, profile: Profile, family: string | null | undefined): Profile;
|
|
31
|
+
/** Remove a deleted profile's generated template. Missing or locked files are non-fatal. */
|
|
32
|
+
export declare function removeHostingProfileMaterialization(paths: BrainPaths, id: string): void;
|
|
33
|
+
//# sourceMappingURL=hosting-profiles.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Brain-owned named chat-template compositions for llama-server. */
|
|
2
|
+
import { rmSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { writePrivateFileAtomicSync } from "./private-files.js";
|
|
5
|
+
const SAFE_FILE = /[^a-zA-Z0-9_-]/gu;
|
|
6
|
+
function materializedTemplatePath(paths, id) {
|
|
7
|
+
return path.join(paths.templatesDir, `${id.replace(SAFE_FILE, "_")}.jinja`);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The bucket a model's hosting profiles live in. Families are catalog-authoritative
|
|
11
|
+
* when available, otherwise discovery derives them from GGUF metadata; models
|
|
12
|
+
* with neither use one real bucket rather than each being unaddressable. This
|
|
13
|
+
* must be the single definition: a writer that stored under "generic" while a
|
|
14
|
+
* reader looked up `null` was why a family default could be set on an unfamilied
|
|
15
|
+
* model and never take effect.
|
|
16
|
+
*/
|
|
17
|
+
export const GENERIC_HOSTING_FAMILY = "generic";
|
|
18
|
+
export function hostingFamily(family) {
|
|
19
|
+
return family || GENERIC_HOSTING_FAMILY;
|
|
20
|
+
}
|
|
21
|
+
export function familyHostingProfileId(store, family) {
|
|
22
|
+
return store.familyHostingProfileIds[hostingFamily(family)] ?? null;
|
|
23
|
+
}
|
|
24
|
+
export function effectiveHostingProfile(store, profile, family) {
|
|
25
|
+
const selected = profile.hostingProfileMode === "inherit"
|
|
26
|
+
? familyHostingProfileId(store, family)
|
|
27
|
+
: profile.hostingProfileMode === "custom"
|
|
28
|
+
? profile.hostingProfileId
|
|
29
|
+
: null;
|
|
30
|
+
return selected ? (store.hostingProfiles[selected] ?? null) : null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Materialize a selected profile immediately before launch: the template as a
|
|
34
|
+
* file llama-server reads, the system addendum as a string the router injects
|
|
35
|
+
* into each completion request.
|
|
36
|
+
*
|
|
37
|
+
* The addendum is deliberately *not* spliced into the Jinja template, which was
|
|
38
|
+
* the first design. Doing that means rewriting the `messages` list from inside
|
|
39
|
+
* the template, and there is no portable way to do it: minja's support for list
|
|
40
|
+
* mutation differs from Jinja2's, every model family's template consumes the
|
|
41
|
+
* system turn differently, and a message's `content` is an array of parts for
|
|
42
|
+
* multimodal requests rather than a string to concatenate onto. Injecting into
|
|
43
|
+
* the parsed request body is the same composition (it appends to the agent's
|
|
44
|
+
* existing system message instead of replacing it) done where the shape is
|
|
45
|
+
* known and testable.
|
|
46
|
+
*/
|
|
47
|
+
export function resolveHostingProfileForLaunch(paths, store, profile, family) {
|
|
48
|
+
const selected = effectiveHostingProfile(store, profile, family);
|
|
49
|
+
const chatSystemAddendum = selected?.systemPromptAddendum?.trim() || null;
|
|
50
|
+
if (!selected?.template) {
|
|
51
|
+
return { ...profile, chatTemplateFile: null, chatTemplateKwargs: {}, chatSystemAddendum };
|
|
52
|
+
}
|
|
53
|
+
const file = materializedTemplatePath(paths, selected.id);
|
|
54
|
+
writePrivateFileAtomicSync(file, selected.template);
|
|
55
|
+
return {
|
|
56
|
+
...profile,
|
|
57
|
+
chatTemplateFile: file,
|
|
58
|
+
chatTemplateKwargs: selected.templateKwargs,
|
|
59
|
+
chatSystemAddendum,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Remove a deleted profile's generated template. Missing or locked files are non-fatal. */
|
|
63
|
+
export function removeHostingProfileMaterialization(paths, id) {
|
|
64
|
+
try {
|
|
65
|
+
rmSync(materializedTemplatePath(paths, id), { force: true });
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Profile deletion must not fail because a previous launch left a file locked.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=hosting-profiles.js.map
|
package/dist/config/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { resolveBrainPaths, packageRoot, type BrainPaths } from "./paths.js";
|
|
|
5
5
|
export { parseBooleanEnv, applyEnvOverrides } from "./env.js";
|
|
6
6
|
export { loadBrainConfig, loadPersistedConfig, saveBrainConfig, loadProfilesStore, saveProfilesStore, loadCatalog, } from "./store.js";
|
|
7
7
|
export { defaultProfile, forModel, put, calibrationKey, geometryKey, getCalibration, putCalibration, hasStaleCalibration, } from "./profiles.js";
|
|
8
|
+
export { effectiveHostingProfile, resolveHostingProfileForLaunch } from "./hosting-profiles.js";
|
|
8
9
|
export { calibrationInfo, nativeContextLimit, profileFieldDescriptors, profileWarnings, sanitizeProfilePatch, formatReasoningBudget, CACHE_TYPE_CYCLE, REASONING_BUDGET_CYCLE, UNRESTRICTED_REASONING_BUDGET, type CalibrationInfo, type CalibrationState, type ProfileFieldDescriptor, type ProfileWarning, } from "./profile-edit.js";
|
|
9
10
|
export * from "./schema.js";
|
|
10
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/config/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { resolveBrainPaths, packageRoot } from "./paths.js";
|
|
|
5
5
|
export { parseBooleanEnv, applyEnvOverrides } from "./env.js";
|
|
6
6
|
export { loadBrainConfig, loadPersistedConfig, saveBrainConfig, loadProfilesStore, saveProfilesStore, loadCatalog, } from "./store.js";
|
|
7
7
|
export { defaultProfile, forModel, put, calibrationKey, geometryKey, getCalibration, putCalibration, hasStaleCalibration, } from "./profiles.js";
|
|
8
|
+
export { effectiveHostingProfile, resolveHostingProfileForLaunch } from "./hosting-profiles.js";
|
|
8
9
|
export { calibrationInfo, nativeContextLimit, profileFieldDescriptors, profileWarnings, sanitizeProfilePatch, formatReasoningBudget, CACHE_TYPE_CYCLE, REASONING_BUDGET_CYCLE, UNRESTRICTED_REASONING_BUDGET, } from "./profile-edit.js";
|
|
9
10
|
export * from "./schema.js";
|
|
10
11
|
//# sourceMappingURL=index.js.map
|
package/dist/config/paths.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface BrainPaths {
|
|
|
12
12
|
logFile: string;
|
|
13
13
|
logsDir: string;
|
|
14
14
|
resultsDir: string;
|
|
15
|
+
/** Brain-owned Jinja files materialized from named hosting profiles. */
|
|
16
|
+
templatesDir: string;
|
|
15
17
|
}
|
|
16
18
|
export declare function resolveBrainPaths(env?: NodeJS.ProcessEnv): BrainPaths;
|
|
17
19
|
/**
|
package/dist/config/paths.js
CHANGED
|
@@ -45,10 +45,12 @@ export declare const MAX_PARALLEL_SLOTS = 16;
|
|
|
45
45
|
export declare const MAX_GPU_LAYERS = 999;
|
|
46
46
|
export declare const MIN_CONTEXT_SIZE = 1024;
|
|
47
47
|
export declare const CONTEXT_STEP = 8192;
|
|
48
|
+
export declare const CONTEXT_MULTIPLIERS: number[];
|
|
48
49
|
/** The context ceiling: the model's native window, or a generous bound if unknown. */
|
|
49
50
|
export declare function nativeContextLimit(model: Model | null): number;
|
|
51
|
+
export declare function contextLimit(model: Model | null, multiplier?: number): number;
|
|
50
52
|
/** The editable fields, resolved against one model's capabilities. */
|
|
51
|
-
export declare function profileFieldDescriptors(model: Model | null): ProfileFieldDescriptor[];
|
|
53
|
+
export declare function profileFieldDescriptors(model: Model | null, profile?: Profile | null): ProfileFieldDescriptor[];
|
|
52
54
|
/** A note attached to a field, or to the profile as a whole when `field` is null. */
|
|
53
55
|
export interface ProfileWarning {
|
|
54
56
|
field: string | null;
|
|
@@ -90,5 +92,5 @@ export interface SanitizeResult {
|
|
|
90
92
|
* CLI-only: they have no measured effect worth exposing and `extraArgs` is
|
|
91
93
|
* arbitrary process arguments.
|
|
92
94
|
*/
|
|
93
|
-
export declare function sanitizeProfilePatch(current: Profile, patch: unknown, model: Model | null): SanitizeResult;
|
|
95
|
+
export declare function sanitizeProfilePatch(current: Profile, patch: unknown, model: Model | null, runtimeBuild?: number | null): SanitizeResult;
|
|
94
96
|
//# sourceMappingURL=profile-edit.d.ts.map
|